diff --git a/apps/web/src/widgets/map3d/Map3D.tsx b/apps/web/src/widgets/map3d/Map3D.tsx index 879ee26..8d9de7e 100644 --- a/apps/web/src/widgets/map3d/Map3D.tsx +++ b/apps/web/src/widgets/map3d/Map3D.tsx @@ -601,14 +601,21 @@ export function Map3D({ }); })(); - return () => { - cancelled = true; - controller.abort(); + return () => { + cancelled = true; + controller.abort(); - if (map) { - map.remove(); - map = null; - } + // If we are unmounting, ensure the globe Deck instance is finalized (style reload would keep it alive). + try { + globeDeckLayerRef.current?.requestFinalize(); + } catch { + // ignore + } + + if (map) { + map.remove(); + map = null; + } if (overlay) { overlay.finalize(); overlay = null; @@ -670,6 +677,7 @@ export function Map3D({ const globeLayer = globeDeckLayerRef.current; if (globeLayer && map.getLayer(globeLayer.id)) { try { + globeLayer.requestFinalize(); map.removeLayer(globeLayer.id); } catch { // ignore @@ -728,6 +736,36 @@ export function Map3D({ }; }, [baseMap]); + // Globe rendering + bathymetry tuning. + // Some terrain/hillshade/extrusion effects look unstable under globe and can occlude Deck overlays. + useEffect(() => { + const map = mapRef.current; + if (!map) return; + + const apply = () => { + if (!map.isStyleLoaded()) return; + const disableBathy3D = projection === "globe" && baseMap === "enhanced"; + const vis = disableBathy3D ? "none" : "visible"; + for (const id of ["bathymetry-extrusion", "bathymetry-hillshade"]) { + try { + if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", vis); + } catch { + // ignore + } + } + }; + + if (map.isStyleLoaded()) apply(); + map.on("style.load", apply); + return () => { + try { + map.off("style.load", apply); + } catch { + // ignore + } + }; + }, [projection, baseMap]); + // seamark toggle useEffect(() => { const map = mapRef.current; diff --git a/apps/web/src/widgets/map3d/MaplibreDeckCustomLayer.ts b/apps/web/src/widgets/map3d/MaplibreDeckCustomLayer.ts index c6baec1..0fae915 100644 --- a/apps/web/src/widgets/map3d/MaplibreDeckCustomLayer.ts +++ b/apps/web/src/widgets/map3d/MaplibreDeckCustomLayer.ts @@ -50,6 +50,7 @@ export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface private _deckProps: Partial> = {}; private _viewId: string; private _lastMvp: number[] | undefined; + private _finalizeOnRemove: boolean = false; constructor(opts: { id: string; viewId: string; deckProps?: Partial> }) { this.id = opts.id; @@ -61,6 +62,10 @@ export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface return this._deck; } + requestFinalize() { + this._finalizeOnRemove = true; + } + setProps(next: Partial>) { this._deckProps = { ...this._deckProps, ...next }; if (this._deck) this._deck.setProps(this._deckProps as DeckProps); @@ -68,8 +73,22 @@ export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface } onAdd(map: maplibregl.Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void { + this._finalizeOnRemove = false; this._map = map; + if (this._deck) { + // 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({ + ...this._deckProps, + canvas: map.getCanvas(), + // Ensure any pending redraw requests trigger a map repaint again. + _customRender: () => map.triggerRepaint(), + } as DeckProps); + return; + } + const deck = new Deck({ ...this._deckProps, // Share MapLibre's WebGL context + canvas (single context). @@ -104,15 +123,36 @@ export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface } onRemove(): void { - this._deck?.finalize(); - this._deck = null; + const deck = this._deck; + const map = this._map; this._map = null; this._lastMvp = undefined; + + if (!deck) return; + + if (this._finalizeOnRemove) { + deck.finalize(); + this._deck = null; + return; + } + + // Likely a base style swap; keep Deck instance alive and re-attach in onAdd(). + // Disable repaint requests until we get re-attached. + try { + deck.setProps({ _customRender: () => void 0 } as Partial>); + } catch { + // ignore + } + try { + map?.triggerRepaint(); + } catch { + // ignore + } } render(_gl: WebGLRenderingContext | WebGL2RenderingContext, options: maplibregl.CustomRenderMethodInput): void { const deck = this._deck; - if (!deck) return; + if (!deck || !deck.isInitialized) return; // MapLibre gives us a world->clip matrix for the current projection (mercator/globe). // For globe, this matrix expects unit-sphere world coordinates (see MapLibre's globe transform).