From 15378ed7ffac2ad9187ab33da4e7fdfc356dbb74 Mon Sep 17 00:00:00 2001 From: htlee Date: Sun, 15 Feb 2026 14:24:00 +0900 Subject: [PATCH] fix(map): scale flat icons and prioritize relation layers --- apps/web/src/widgets/map3d/Map3D.tsx | 142 +++++++++++++++------------ 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/apps/web/src/widgets/map3d/Map3D.tsx b/apps/web/src/widgets/map3d/Map3D.tsx index c8acd5b..fa1846c 100644 --- a/apps/web/src/widgets/map3d/Map3D.tsx +++ b/apps/web/src/widgets/map3d/Map3D.tsx @@ -182,6 +182,11 @@ const DEPTH_DISABLED_PARAMS = { depthWriteEnabled: false, } as const; +const FLAT_SHIP_ICON_SIZE = 19; +const FLAT_SHIP_ICON_SIZE_SELECTED = 28; +const FLAT_LEGACY_HALO_RADIUS = 14; +const FLAT_LEGACY_HALO_RADIUS_SELECTED = 18; + const GLOBE_OVERLAY_PARAMS = { // In globe mode we want depth-testing against the globe so features on the far side don't draw through. // Still disable depth writes so our overlays don't interfere with each other. @@ -717,7 +722,12 @@ export function Map3D({ if (!map) return; let cancelled = false; let retries = 0; - const maxRetries = 6; + const maxRetries = 18; + + const getCurrentProjection = () => { + const projectionConfig = map.getProjection?.(); + return projectionConfig && "type" in projectionConfig ? (projectionConfig.type as MapProjectionId | undefined) : undefined; + }; const syncProjectionAndDeck = () => { if (cancelled) return; @@ -732,7 +742,9 @@ export function Map3D({ const next = projection; try { - map.setProjection({ type: next }); + if (getCurrentProjection() !== next) { + map.setProjection({ type: next }); + } map.setRenderWorldCopies(next !== "globe"); } catch (e) { if (!cancelled && retries < maxRetries) { @@ -781,6 +793,11 @@ export function Map3D({ } catch { // ignore } + if (!map.getLayer(layer.id) && !cancelled && retries < maxRetries) { + retries += 1; + window.requestAnimationFrame(() => syncProjectionAndDeck()); + return; + } } } else { // Tear down globe custom layer (if present), restore MapboxOverlay interleaved. @@ -1433,7 +1450,7 @@ export function Map3D({ return; } - const before = map.getLayer("ships-globe-halo") ? "ships-globe-halo" : undefined; + const before = undefined; if (!map.getLayer(layerId)) { try { @@ -1525,7 +1542,7 @@ export function Map3D({ return; } - const before = map.getLayer("ships-globe-halo") ? "ships-globe-halo" : undefined; + const before = undefined; if (!map.getLayer(layerId)) { try { @@ -1613,7 +1630,7 @@ export function Map3D({ return; } - const before = map.getLayer("ships-globe-halo") ? "ships-globe-halo" : undefined; + const before = undefined; if (!map.getLayer(layerId)) { try { @@ -1706,7 +1723,7 @@ export function Map3D({ return; } - const before = map.getLayer("ships-globe-halo") ? "ships-globe-halo" : undefined; + const before = undefined; if (!map.getLayer(layerId)) { try { @@ -1805,22 +1822,61 @@ export function Map3D({ ); } - if (overlays.fleetCircles && projection !== "globe" && (fleetCircles?.length ?? 0) > 0) { + if (settings.showShips && projection !== "globe") { layers.push( - new ScatterplotLayer({ - id: "fleet-circles", - data: fleetCircles, + new IconLayer({ + id: "ships", + data: shipData, + pickable: true, + // Keep icons horizontal on the sea surface when view is pitched/rotated. + billboard: false, + parameters: overlayParams, + iconAtlas: "/assets/ship.svg", + iconMapping: SHIP_ICON_MAPPING, + getIcon: () => "ship", + getPosition: (d) => + [d.lon, d.lat] as [number, number], + getAngle: (d) => (isFiniteNumber(d.cog) ? d.cog : 0), + sizeUnits: "pixels", + getSize: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? FLAT_SHIP_ICON_SIZE_SELECTED : FLAT_SHIP_ICON_SIZE), + getColor: (d) => getShipColor(d, selectedMmsi, legacyHits?.get(d.mmsi)?.shipCode ?? null), + alphaCutoff: 0.05, + updateTriggers: { + getSize: [selectedMmsi], + getColor: [selectedMmsi, legacyHits], + }, + }), + ); + } + + if (settings.showShips && projection !== "globe" && legacyTargets.length > 0) { + layers.push( + new ScatterplotLayer({ + id: "legacy-halo", + data: legacyTargets, pickable: false, billboard: false, + // This ring is most prone to z-fighting, so force it into pure painter's-order rendering. parameters: overlayParams, filled: false, stroked: true, - radiusUnits: "meters", - getRadius: (d) => d.radiusNm * 1852, + radiusUnits: "pixels", + getRadius: (d) => + (selectedMmsi && d.mmsi === selectedMmsi ? FLAT_LEGACY_HALO_RADIUS_SELECTED : FLAT_LEGACY_HALO_RADIUS), lineWidthUnits: "pixels", - getLineWidth: 1, - getLineColor: () => [245, 158, 11, 140], - getPosition: (d) => d.center, + getLineWidth: 2, + getLineColor: (d) => { + const l = legacyHits?.get(d.mmsi); + const rgb = l ? LEGACY_CODE_COLORS[l.shipCode] : null; + if (!rgb) return [245, 158, 11, 200]; + return [rgb[0], rgb[1], rgb[2], 200]; + }, + getPosition: (d) => + [d.lon, d.lat] as [number, number], + updateTriggers: { + getRadius: [selectedMmsi], + getLineColor: [legacyHits], + }, }), ); } @@ -1878,60 +1934,22 @@ export function Map3D({ ); } - if (settings.showShips && projection !== "globe" && legacyTargets.length > 0) { + if (overlays.fleetCircles && projection !== "globe" && (fleetCircles?.length ?? 0) > 0) { layers.push( - new ScatterplotLayer({ - id: "legacy-halo", - data: legacyTargets, + new ScatterplotLayer({ + id: "fleet-circles", + data: fleetCircles, pickable: false, billboard: false, - // This ring is most prone to z-fighting, so force it into pure painter's-order rendering. parameters: overlayParams, filled: false, stroked: true, - radiusUnits: "pixels", - getRadius: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 22 : 16), + radiusUnits: "meters", + getRadius: (d) => d.radiusNm * 1852, lineWidthUnits: "pixels", - getLineWidth: 2, - getLineColor: (d) => { - const l = legacyHits?.get(d.mmsi); - const rgb = l ? LEGACY_CODE_COLORS[l.shipCode] : null; - if (!rgb) return [245, 158, 11, 200]; - return [rgb[0], rgb[1], rgb[2], 200]; - }, - getPosition: (d) => - [d.lon, d.lat] as [number, number], - updateTriggers: { - getRadius: [selectedMmsi], - getLineColor: [legacyHits], - }, - }), - ); - } - - if (settings.showShips && projection !== "globe") { - layers.push( - new IconLayer({ - id: "ships", - data: shipData, - pickable: true, - // Keep icons horizontal on the sea surface when view is pitched/rotated. - billboard: false, - parameters: overlayParams, - iconAtlas: "/assets/ship.svg", - iconMapping: SHIP_ICON_MAPPING, - getIcon: () => "ship", - getPosition: (d) => - [d.lon, d.lat] as [number, number], - getAngle: (d) => (isFiniteNumber(d.cog) ? d.cog : 0), - sizeUnits: "pixels", - getSize: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 34 : 22), - getColor: (d) => getShipColor(d, selectedMmsi, legacyHits?.get(d.mmsi)?.shipCode ?? null), - alphaCutoff: 0.05, - updateTriggers: { - getSize: [selectedMmsi], - getColor: [selectedMmsi, legacyHits], - }, + getLineWidth: 1, + getLineColor: () => [245, 158, 11, 140], + getPosition: (d) => d.center, }), ); }