fix(globe): gate bathymetry fill by zoom to avoid ocean tearing

This commit is contained in:
htlee 2026-02-15 18:47:52 +09:00
부모 3497b8c7e2
커밋 15d5d5ad23

파일 보기

@ -997,14 +997,20 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
} as unknown as LayerSpecification;
// Insert before the first symbol layer (keep labels on top), otherwise append.
const rawLayers = Array.isArray(style.layers) ? style.layers : [];
const layers = rawLayers.filter((layer): layer is LayerSpecification => {
if (!layer || typeof layer !== "object") return false;
return typeof (layer as { id?: unknown }).id === "string";
});
const symbolIndex = layers.findIndex((l) => l.type === "symbol");
const layers = Array.isArray(style.layers) ? (style.layers as unknown as LayerSpecification[]) : [];
if (!Array.isArray(style.layers)) {
style.layers = layers as unknown as StyleSpecification["layers"];
}
const symbolIndex = layers.findIndex((l) => (l as { type?: unknown } | null)?.type === "symbol");
const insertAt = symbolIndex >= 0 ? symbolIndex : layers.length;
const existingIds = new Set<string>();
for (const layer of layers) {
const id = getLayerId(layer);
if (id) existingIds.add(id);
}
const toInsert = [
bathyFill,
bathyBandBorders,
@ -1013,12 +1019,44 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
bathyLinesMajor,
bathyLabels,
landformLabels,
].filter(
(l) => !layers.some((x) => x.id === l.id),
);
].filter((l) => !existingIds.has(l.id));
if (toInsert.length > 0) layers.splice(insertAt, 0, ...toInsert);
}
type BathyZoomRange = {
id: string;
mercator: [number, number];
globe: [number, number];
};
const BATHY_ZOOM_RANGES: BathyZoomRange[] = [
{ id: "bathymetry-fill", mercator: [6, 12], globe: [8, 12] },
{ id: "bathymetry-borders", mercator: [6, 14], globe: [8, 14] },
{ id: "bathymetry-borders-major", mercator: [4, 14], globe: [8, 14] },
];
function applyBathymetryZoomProfile(map: maplibregl.Map, baseMap: BaseMapId, projection: MapProjectionId) {
if (!map || !map.isStyleLoaded()) return;
if (baseMap !== "enhanced") return;
const isGlobe = projection === "globe";
for (const range of BATHY_ZOOM_RANGES) {
if (!map.getLayer(range.id)) continue;
const [minzoom, maxzoom] = isGlobe ? range.globe : range.mercator;
try {
// Safety: ensure heavy layers aren't stuck hidden from a previous session.
map.setLayoutProperty(range.id, "visibility", "visible");
} catch {
// ignore
}
try {
map.setLayerZoomRange(range.id, minzoom, maxzoom);
} catch {
// ignore
}
}
}
async function resolveInitialMapStyle(signal: AbortSignal): Promise<string | StyleSpecification> {
const key = getMapTilerKey();
if (!key) return "/map/styles/osm-seamark.json";
@ -1223,6 +1261,7 @@ export function Map3D({
const projectionBusyTokenRef = useRef(0);
const projectionBusyTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
const projectionPrevRef = useRef<MapProjectionId>(projection);
const bathyZoomProfileKeyRef = useRef<string>("");
const mapTooltipRef = useRef<maplibregl.Popup | null>(null);
const deckHoverRafRef = useRef<number | null>(null);
const deckHoverHasHitRef = useRef(false);
@ -2081,33 +2120,25 @@ export function Map3D({
}, [baseMap]);
// Globe rendering + bathymetry tuning.
// Some terrain/hillshade/extrusion effects look unstable under globe and can occlude Deck overlays.
// Under globe projection, low-zoom bathymetry polygons can exceed MapLibre's per-segment 16-bit vertex
// limit (65535) due to projection subdivision. Keep globe stable by gating heavy bathymetry fills/borders
// to higher zoom levels rather than toggling them on every frame.
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const apply = () => {
if (!map.isStyleLoaded()) return;
const disableBathyHeavy = projection === "globe" && baseMap === "enhanced";
const visHeavy = disableBathyHeavy ? "none" : "visible";
const seaVisibility = "visible" as const;
const seaRegex = /(water|sea|ocean|river|lake|coast|bay)/i;
// Globe + our injected bathymetry fill polygons can exceed MapLibre's per-segment vertex limit
// (65535), causing broken ocean rendering. Keep globe mode stable by disabling the heavy fill.
const heavyIds = [
"bathymetry-fill",
"bathymetry-borders",
"bathymetry-borders-major",
"bathymetry-extrusion",
"bathymetry-hillshade",
];
for (const id of heavyIds) {
try {
if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", visHeavy);
} catch {
// ignore
}
// Apply zoom gating for heavy bathymetry layers once per (baseMap, projection) combination.
// This avoids repeatedly mutating layer zoom ranges on hover/mapSyncEpoch pulses.
const nextProfileKey = `bathyZoomV1|${baseMap}|${projection}`;
if (bathyZoomProfileKeyRef.current !== nextProfileKey) {
applyBathymetryZoomProfile(map, baseMap, projection);
bathyZoomProfileKeyRef.current = nextProfileKey;
kickRepaint(map);
}
// Vector basemap water layers can be tuned per-style. Keep visible by default,