fix(map3d): sync mercator restore on globe toggle

This commit is contained in:
htlee 2026-02-15 14:33:50 +09:00
부모 6f7a82af4c
커밋 1225d5c54c

파일 보기

@ -9,7 +9,7 @@ import maplibregl, {
type StyleSpecification, type StyleSpecification,
type VectorSourceSpecification, type VectorSourceSpecification,
} from "maplibre-gl"; } from "maplibre-gl";
import { useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { AisTarget } from "../../entities/aisTarget/model/types"; import type { AisTarget } from "../../entities/aisTarget/model/types";
import type { LegacyVesselInfo } from "../../entities/legacyVessel/model/types"; import type { LegacyVesselInfo } from "../../entities/legacyVessel/model/types";
import type { ZonesGeoJson } from "../../entities/zone/api/useZones"; import type { ZonesGeoJson } from "../../entities/zone/api/useZones";
@ -580,6 +580,67 @@ export function Map3D({
projectionRef.current = projection; projectionRef.current = projection;
}, [projection]); }, [projection]);
const removeLayerIfExists = useCallback((map: maplibregl.Map, layerId: string) => {
try {
if (map.getLayer(layerId)) {
map.removeLayer(layerId);
}
} catch {
// ignore
}
}, []);
const removeSourceIfExists = useCallback((map: maplibregl.Map, sourceId: string) => {
try {
if (map.getSource(sourceId)) {
map.removeSource(sourceId);
}
} catch {
// ignore
}
}, []);
const ensureMercatorOverlay = useCallback(() => {
const map = mapRef.current;
if (!map) return null;
if (overlayRef.current) return overlayRef.current;
try {
const next = new MapboxOverlay({ interleaved: true, layers: [] } as unknown as never);
map.addControl(next);
overlayRef.current = next;
return next;
} catch (e) {
console.warn("Deck overlay create failed:", e);
return null;
}
}, []);
const clearGlobeNativeLayers = useCallback(() => {
const map = mapRef.current;
if (!map) return;
const layerIds = [
"ships-globe-halo",
"ships-globe-outline",
"ships-globe",
"pair-lines-ml",
"fc-lines-ml",
"fleet-circles-ml",
"pair-range-ml",
"deck-globe",
];
for (const id of layerIds) {
removeLayerIfExists(map, id);
}
const sourceIds = ["ships-globe-src", "pair-lines-ml-src", "fc-lines-ml-src", "fleet-circles-ml-src", "pair-range-ml-src"];
for (const id of sourceIds) {
removeSourceIfExists(map, id);
}
}, [removeLayerIfExists, removeSourceIfExists]);
// Init MapLibre + Deck.gl (single WebGL context via MapboxOverlay) // Init MapLibre + Deck.gl (single WebGL context via MapboxOverlay)
useEffect(() => { useEffect(() => {
if (!containerRef.current || mapRef.current) return; if (!containerRef.current || mapRef.current) return;
@ -758,13 +819,7 @@ export function Map3D({
const disposeGlobeDeckLayer = () => { const disposeGlobeDeckLayer = () => {
const current = globeDeckLayerRef.current; const current = globeDeckLayerRef.current;
if (!current) return; if (!current) return;
if (map.getLayer(current.id)) { removeLayerIfExists(map, current.id);
try {
map.removeLayer(current.id);
} catch {
// ignore
}
}
try { try {
current.requestFinalize(); current.requestFinalize();
} catch { } catch {
@ -790,8 +845,10 @@ export function Map3D({
if (projection === "globe") { if (projection === "globe") {
disposeMercatorOverlay(); disposeMercatorOverlay();
clearGlobeNativeLayers();
} else { } else {
disposeGlobeDeckLayer(); disposeGlobeDeckLayer();
clearGlobeNativeLayers();
} }
try { try {
@ -842,15 +899,7 @@ export function Map3D({
// Tear down globe custom layer (if present), restore MapboxOverlay interleaved. // Tear down globe custom layer (if present), restore MapboxOverlay interleaved.
disposeGlobeDeckLayer(); disposeGlobeDeckLayer();
if (!overlayRef.current) { ensureMercatorOverlay();
try {
const next = new MapboxOverlay({ interleaved: true, layers: [] } as unknown as never);
map.addControl(next);
overlayRef.current = next;
} catch (e) {
console.warn("Deck overlay create failed:", e);
}
}
} }
// MapLibre may not schedule a frame immediately after projection swaps if the map is idle. // MapLibre may not schedule a frame immediately after projection swaps if the map is idle.
@ -876,7 +925,7 @@ export function Map3D({
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [projection]); }, [projection, clearGlobeNativeLayers, ensureMercatorOverlay, removeLayerIfExists]);
// Base map toggle // Base map toggle
useEffect(() => { useEffect(() => {
@ -1333,7 +1382,7 @@ export function Map3D({
"icon-rotate": ["to-number", ["get", "cog"], 0], "icon-rotate": ["to-number", ["get", "cog"], 0],
// Keep the icon on the sea surface. // Keep the icon on the sea surface.
"icon-rotation-alignment": "map", "icon-rotation-alignment": "map",
"icon-pitch-alignment": "viewport", "icon-pitch-alignment": "map",
}, },
paint: { paint: {
"icon-color": ["coalesce", ["get", "shipColor"], "#64748b"] as never, "icon-color": ["coalesce", ["get", "shipColor"], "#64748b"] as never,
@ -1830,7 +1879,19 @@ export function Map3D({
useEffect(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map) return;
const deckTarget = projection === "globe" ? globeDeckLayerRef.current : overlayRef.current; let deckTarget = projection === "globe" ? globeDeckLayerRef.current : overlayRef.current;
if (projection === "mercator") {
if (!deckTarget) deckTarget = ensureMercatorOverlay();
if (!deckTarget) return;
try {
deckTarget.setProps({ layers: [] } as never);
} catch {
// ignore
}
} else if (!deckTarget && projection === "globe") {
return;
}
if (!deckTarget) return; if (!deckTarget) return;
const overlayParams = projection === "globe" ? GLOBE_OVERLAY_PARAMS : DEPTH_DISABLED_PARAMS; const overlayParams = projection === "globe" ? GLOBE_OVERLAY_PARAMS : DEPTH_DISABLED_PARAMS;
@ -2068,6 +2129,7 @@ export function Map3D({
fcDashed, fcDashed,
fleetCircles, fleetCircles,
mapSyncEpoch, mapSyncEpoch,
ensureMercatorOverlay,
]); ]);
return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />; return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;