fix: guard deck layer arrays against null ids

This commit is contained in:
htlee 2026-02-15 15:30:09 +09:00
부모 b883c4113b
커밋 ccf3f2361f
2개의 변경된 파일91개의 추가작업 그리고 6개의 파일을 삭제

파일 보기

@ -195,6 +195,33 @@ function getLayerId(value: unknown): string | null {
return typeof candidate === "string" ? candidate : null; return typeof candidate === "string" ? candidate : null;
} }
function sanitizeDeckLayerList(value: unknown): unknown[] {
if (!Array.isArray(value)) return [];
const seen = new Set<string>();
const out: unknown[] = [];
let dropped = 0;
for (const layer of value) {
const layerId = getLayerId(layer);
if (!layerId) {
dropped += 1;
continue;
}
if (seen.has(layerId)) {
dropped += 1;
continue;
}
seen.add(layerId);
out.push(layer);
}
if (dropped > 0 && import.meta.env.DEV) {
console.warn(`Sanitized deck layer list: dropped ${dropped} invalid/duplicate entries`);
}
return out;
}
function normalizeAngleDeg(value: number, offset = 0): number { function normalizeAngleDeg(value: number, offset = 0): number {
const v = value + offset; const v = value + offset;
return ((v % 360) + 360) % 360; return ((v % 360) + 360) % 360;
@ -3674,8 +3701,10 @@ export function Map3D({
); );
} }
const normalizedLayers = sanitizeDeckLayerList(layers);
const deckProps = { const deckProps = {
layers, layers: normalizedLayers,
getTooltip: getTooltip:
projection === "globe" projection === "globe"
? undefined ? undefined
@ -3771,8 +3800,40 @@ export function Map3D({
}, },
} as const; } as const;
if (projection === "globe") globeDeckLayerRef.current?.setProps(deckProps); const safeDeckProps = { ...deckProps, layers: normalizedLayers };
else overlayRef.current?.setProps(deckProps as unknown as never); const fallbackDeckProps = { ...safeDeckProps, layers: [] as unknown[] };
const applyDeckProps = () => {
if (projection === "globe") {
const target = globeDeckLayerRef.current;
if (!target) return;
try {
target.setProps(safeDeckProps as never);
} catch (e) {
console.error("Failed to apply deck props on globe overlay. Falling back to empty deck layer set.", e);
try {
target.setProps(fallbackDeckProps as never);
} catch {
// Ignore secondary failure; rendering will recover on next update.
}
}
return;
}
const target = overlayRef.current;
if (!target) return;
try {
target.setProps(safeDeckProps as unknown as never);
} catch (e) {
console.error("Failed to apply deck props on mercator overlay. Falling back to empty deck layer set.", e);
try {
target.setProps(fallbackDeckProps as unknown as never);
} catch {
// Ignore secondary failure.
}
}
};
applyDeckProps();
}, [ }, [
projection, projection,
shipData, shipData,

파일 보기

@ -24,6 +24,7 @@ class MatrixView extends View<MatrixViewState> {
} }
const IDENTITY_4x4: number[] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; const IDENTITY_4x4: number[] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
type DeckLayerList = NonNullable<DeckProps<MatrixView[]>["layers"]>;
function readMat4(m: ArrayLike<number>): number[] { function readMat4(m: ArrayLike<number>): number[] {
const out = new Array<number>(16); const out = new Array<number>(16);
@ -40,6 +41,25 @@ function mat4Changed(a: number[] | undefined, b: ArrayLike<number>): boolean {
return false; return false;
} }
function getDeckLayerId(value: unknown): string | null {
if (!value || typeof value !== "object") return null;
const candidate = (value as { id?: unknown }).id;
return typeof candidate === "string" ? candidate : null;
}
function sanitizeDeckLayers(value: unknown): DeckLayerList {
if (!Array.isArray(value)) return [] as DeckLayerList;
const out: DeckLayerList = [];
const seen = new Set<string>();
for (const item of value) {
const layerId = getDeckLayerId(item);
if (!layerId || seen.has(layerId)) continue;
seen.add(layerId);
out.push(item as DeckLayerList[number]);
}
return out;
}
export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface { export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface {
id: string; id: string;
type = "custom" as const; type = "custom" as const;
@ -67,7 +87,8 @@ export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface
} }
setProps(next: Partial<DeckProps<MatrixView[]>>) { setProps(next: Partial<DeckProps<MatrixView[]>>) {
this._deckProps = { ...this._deckProps, ...next }; const normalized = next.layers ? { ...next, layers: sanitizeDeckLayers(next.layers) } : next;
this._deckProps = { ...this._deckProps, ...normalized };
if (this._deck) this._deck.setProps(this._deckProps as DeckProps<MatrixView[]>); if (this._deck) this._deck.setProps(this._deckProps as DeckProps<MatrixView[]>);
this._map?.triggerRepaint(); this._map?.triggerRepaint();
} }
@ -80,17 +101,20 @@ export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface
// Re-attached after a style change; keep the existing Deck instance so we don't reuse // Re-attached after a style change; keep the existing Deck instance so we don't reuse
// finalized Layer objects (Deck does not allow that). // finalized Layer objects (Deck does not allow that).
this._lastMvp = undefined; this._lastMvp = undefined;
this._deck.setProps({ const nextDeckProps = {
...this._deckProps, ...this._deckProps,
layers: sanitizeDeckLayers(this._deckProps.layers),
canvas: map.getCanvas(), canvas: map.getCanvas(),
// Ensure any pending redraw requests trigger a map repaint again. // Ensure any pending redraw requests trigger a map repaint again.
_customRender: () => map.triggerRepaint(), _customRender: () => map.triggerRepaint(),
} as DeckProps<MatrixView[]>); };
this._deck.setProps(nextDeckProps as DeckProps<MatrixView[]>);
return; return;
} }
const deck = new Deck<MatrixView[]>({ const deck = new Deck<MatrixView[]>({
...this._deckProps, ...this._deckProps,
layers: sanitizeDeckLayers(this._deckProps.layers),
// Share MapLibre's WebGL context + canvas (single context). // Share MapLibre's WebGL context + canvas (single context).
gl: gl as WebGL2RenderingContext, gl: gl as WebGL2RenderingContext,
canvas: map.getCanvas(), canvas: map.getCanvas(),