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;
}
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 {
const v = value + offset;
return ((v % 360) + 360) % 360;
@ -3674,8 +3701,10 @@ export function Map3D({
);
}
const normalizedLayers = sanitizeDeckLayerList(layers);
const deckProps = {
layers,
layers: normalizedLayers,
getTooltip:
projection === "globe"
? undefined
@ -3771,8 +3800,40 @@ export function Map3D({
},
} as const;
if (projection === "globe") globeDeckLayerRef.current?.setProps(deckProps);
else overlayRef.current?.setProps(deckProps as unknown as never);
const safeDeckProps = { ...deckProps, layers: normalizedLayers };
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,
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];
type DeckLayerList = NonNullable<DeckProps<MatrixView[]>["layers"]>;
function readMat4(m: ArrayLike<number>): number[] {
const out = new Array<number>(16);
@ -40,6 +41,25 @@ function mat4Changed(a: number[] | undefined, b: ArrayLike<number>): boolean {
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 {
id: string;
type = "custom" as const;
@ -67,7 +87,8 @@ export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface
}
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[]>);
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
// finalized Layer objects (Deck does not allow that).
this._lastMvp = undefined;
this._deck.setProps({
const nextDeckProps = {
...this._deckProps,
layers: sanitizeDeckLayers(this._deckProps.layers),
canvas: map.getCanvas(),
// Ensure any pending redraw requests trigger a map repaint again.
_customRender: () => map.triggerRepaint(),
} as DeckProps<MatrixView[]>);
};
this._deck.setProps(nextDeckProps as DeckProps<MatrixView[]>);
return;
}
const deck = new Deck<MatrixView[]>({
...this._deckProps,
layers: sanitizeDeckLayers(this._deckProps.layers),
// Share MapLibre's WebGL context + canvas (single context).
gl: gl as WebGL2RenderingContext,
canvas: map.getCanvas(),