From 0ffadb2e66f28712a33deadda2b8d36d4baeb81a Mon Sep 17 00:00:00 2001 From: htlee Date: Sun, 15 Feb 2026 14:27:08 +0900 Subject: [PATCH] fix(map): harden globe projection switch and overlay teardown --- apps/web/src/widgets/map3d/Map3D.tsx | 86 ++++++++++++++++++---------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/apps/web/src/widgets/map3d/Map3D.tsx b/apps/web/src/widgets/map3d/Map3D.tsx index fa1846c..625d0cc 100644 --- a/apps/web/src/widgets/map3d/Map3D.tsx +++ b/apps/web/src/widgets/map3d/Map3D.tsx @@ -126,6 +126,16 @@ function onMapStyleReady(map: maplibregl.Map, callback: () => void) { }; } +function extractProjectionType(map: maplibregl.Map): MapProjectionId | undefined { + const projection = map.getProjection?.(); + if (!projection || typeof projection !== "object") return undefined; + + const rawType = (projection as { type?: unknown; name?: unknown }).type ?? (projection as { type?: unknown; name?: unknown }).name; + if (rawType === "globe") return "globe"; + if (rawType === "mercator") return "mercator"; + return undefined; +} + const DEG2RAD = Math.PI / 180; const clampNumber = (value: number, minValue: number, maxValue: number) => Math.max(minValue, Math.min(maxValue, value)); @@ -724,9 +734,38 @@ export function Map3D({ let retries = 0; const maxRetries = 18; - const getCurrentProjection = () => { - const projectionConfig = map.getProjection?.(); - return projectionConfig && "type" in projectionConfig ? (projectionConfig.type as MapProjectionId | undefined) : undefined; + const disposeMercatorOverlay = () => { + const current = overlayRef.current; + if (!current) return; + try { + map.removeControl(current as never); + } catch { + // ignore + } + try { + current.finalize(); + } catch { + // ignore + } + overlayRef.current = null; + }; + + const disposeGlobeDeckLayer = () => { + const current = globeDeckLayerRef.current; + if (!current) return; + if (map.getLayer(current.id)) { + try { + map.removeLayer(current.id); + } catch { + // ignore + } + } + try { + current.requestFinalize(); + } catch { + // ignore + } + globeDeckLayerRef.current = null; }; const syncProjectionAndDeck = () => { @@ -741,11 +780,21 @@ export function Map3D({ } const next = projection; + const currentProjection = extractProjectionType(map); + const shouldSwitchProjection = currentProjection !== next; + try { - if (getCurrentProjection() !== next) { + if (shouldSwitchProjection) { map.setProjection({ type: next }); } map.setRenderWorldCopies(next !== "globe"); + if (shouldSwitchProjection) { + if (!cancelled && retries < maxRetries) { + retries += 1; + window.requestAnimationFrame(() => syncProjectionAndDeck()); + return; + } + } } catch (e) { if (!cancelled && retries < maxRetries) { retries += 1; @@ -758,25 +807,12 @@ export function Map3D({ const oldOverlay = overlayRef.current; if (projection === "globe" && oldOverlay) { // Globe mode uses custom MapLibre deck layers and should fully replace Mercator overlays. - try { - oldOverlay.finalize(); - } catch { - // ignore - } - overlayRef.current = null; + disposeMercatorOverlay(); } if (projection === "globe") { - // Ensure any stale layer from old mode is dropped then re-added on this projection. - if (globeDeckLayerRef.current) { - try { - if (map.getLayer(globeDeckLayerRef.current.id)) { - map.removeLayer(globeDeckLayerRef.current.id); - } - } catch { - // ignore - } - } + // Start with a clean globe Deck layer state to avoid partially torn-down renders. + disposeGlobeDeckLayer(); if (!globeDeckLayerRef.current) { globeDeckLayerRef.current = new MaplibreDeckCustomLayer({ @@ -801,15 +837,7 @@ export function Map3D({ } } else { // Tear down globe custom layer (if present), restore MapboxOverlay interleaved. - const globeLayer = globeDeckLayerRef.current; - if (globeLayer && map.getLayer(globeLayer.id)) { - try { - globeLayer.requestFinalize(); - map.removeLayer(globeLayer.id); - } catch { - // ignore - } - } + disposeGlobeDeckLayer(); if (!overlayRef.current) { try {