From c00cb169888c246ecf19762a6bb36cf592284ae7 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 29 Jul 2026 18:52:28 +0300 Subject: [PATCH] fix(map): keep ghost pin stems terrain-relative --- docs/MAP_CESIUM_REACT_ADAPTER.md | 5 ++ docs/MAP_TEMPLATE.md | 4 + .../src/CesiumMapRenderer.tsx | 90 +++++++++++-------- .../map-cesium-react/test/ghostPins.test.mjs | 12 +++ 4 files changed, 76 insertions(+), 35 deletions(-) diff --git a/docs/MAP_CESIUM_REACT_ADAPTER.md b/docs/MAP_CESIUM_REACT_ADAPTER.md index 0e071ab..2cd97f4 100644 --- a/docs/MAP_CESIUM_REACT_ADAPTER.md +++ b/docs/MAP_CESIUM_REACT_ADAPTER.md @@ -88,5 +88,10 @@ positions. The adapter only: - returns the current positions through `snapshotGhostPins()` for an explicit product save. +The elevated head and stem use non-constant Cesium properties so their ground +height is resolved again after asynchronous terrain tiles arrive. The stem +also keeps the same material as its depth-fail fallback; an early ellipsoid +height must not leave a visible head with its stem hidden below terrain. + Motion never recreates the Cesium viewer or any provider. A renderer teardown stops its single animation frame loop. diff --git a/docs/MAP_TEMPLATE.md b/docs/MAP_TEMPLATE.md index add0c2c..7621fac 100644 --- a/docs/MAP_TEMPLATE.md +++ b/docs/MAP_TEMPLATE.md @@ -152,6 +152,10 @@ billboard до label-слоя, поэтому непрозрачная плаш лежащий под ней маркер вместо «рентгеновского» проступания его обводки. Text outline плашки отключён и не создаёт дополнительный halo. Контуры `surface-fill` остаются частью отдельного контракта геозон. +Высота elevated target и его стержня остаётся callback-свойством Cesium: +после асинхронной загрузки terrain она пересчитывается относительно уже +доступной поверхности, а depth-fail material не даёт стержню исчезнуть при +пересечении рельефа. ## Acceptance fixtures diff --git a/packages/map-cesium-react/src/CesiumMapRenderer.tsx b/packages/map-cesium-react/src/CesiumMapRenderer.tsx index 771476c..54c5b27 100644 --- a/packages/map-cesium-react/src/CesiumMapRenderer.tsx +++ b/packages/map-cesium-react/src/CesiumMapRenderer.tsx @@ -605,33 +605,25 @@ function rebuildGhostPinEntities( presentation.label_hide_camera_height_meters, ); for (const pin of runtime.currentGhostPins) { - const groundHeight = ghostPinGroundHeight(runtime, pin); - const headHeight = groundHeight + presentation.stem_height_meters; const label = presentation.label_mode === "subject_id" ? pin.id : pin.label; ghostPins.entities.add({ id: pin.id, - position: Cesium.Cartesian3.fromDegrees( - pin.longitude, - pin.latitude, - headHeight, + position: ghostPinTopPosition( + runtime, + pin, + presentation.stem_height_meters, ), polyline: { - positions: [ - Cesium.Cartesian3.fromDegrees( - pin.longitude, - pin.latitude, - groundHeight, - ), - Cesium.Cartesian3.fromDegrees( - pin.longitude, - pin.latitude, - headHeight, - ), - ], + positions: ghostPinStemPositions( + runtime, + pin, + presentation.stem_height_meters, + ), width: presentation.stem_width_px, material: fillColor, + depthFailMaterial: fillColor, distanceDisplayCondition: targetDistance, }, billboard: { @@ -731,29 +723,57 @@ function updateGhostPinPositions( for (const pin of runtime.currentGhostPins) { const entity = ghostPins.entities.getById(pin.id); if (!entity) continue; - const groundHeight = ghostPinGroundHeight(runtime, pin); - const headHeight = - groundHeight + layer.presentation.stem_height_meters; - entity.position = new Cesium.ConstantPositionProperty( - Cesium.Cartesian3.fromDegrees(pin.longitude, pin.latitude, headHeight), + entity.position = ghostPinTopPosition( + runtime, + pin, + layer.presentation.stem_height_meters, ); if (entity.polyline) { - entity.polyline.positions = new Cesium.ConstantProperty([ - Cesium.Cartesian3.fromDegrees( - pin.longitude, - pin.latitude, - groundHeight, - ), - Cesium.Cartesian3.fromDegrees( - pin.longitude, - pin.latitude, - headHeight, - ), - ]); + entity.polyline.positions = ghostPinStemPositions( + runtime, + pin, + layer.presentation.stem_height_meters, + ); } } } +function ghostPinTopPosition( + runtime: RendererRuntime, + pin: MapGhostPin, + stemHeightMeters: number, +): CesiumModule.CallbackPositionProperty { + return new runtime.Cesium.CallbackPositionProperty(() => ( + runtime.Cesium.Cartesian3.fromDegrees( + pin.longitude, + pin.latitude, + ghostPinGroundHeight(runtime, pin) + stemHeightMeters, + ) + ), false); +} + +function ghostPinStemPositions( + runtime: RendererRuntime, + pin: MapGhostPin, + stemHeightMeters: number, +): CesiumModule.CallbackProperty { + return new runtime.Cesium.CallbackProperty(() => { + const groundHeight = ghostPinGroundHeight(runtime, pin); + return [ + runtime.Cesium.Cartesian3.fromDegrees( + pin.longitude, + pin.latitude, + groundHeight, + ), + runtime.Cesium.Cartesian3.fromDegrees( + pin.longitude, + pin.latitude, + groundHeight + stemHeightMeters, + ), + ]; + }, false); +} + function ghostPinGroundHeight( runtime: RendererRuntime, pin: MapGhostPin, diff --git a/packages/map-cesium-react/test/ghostPins.test.mjs b/packages/map-cesium-react/test/ghostPins.test.mjs index b97f8bb..5e625bc 100644 --- a/packages/map-cesium-react/test/ghostPins.test.mjs +++ b/packages/map-cesium-react/test/ghostPins.test.mjs @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; import test from "node:test"; import { @@ -30,3 +31,14 @@ test("advanceGhostPins turns an escaping pin back toward its anchor", () => { assert.ok(geodesicDistanceMeters(anchor, advanced) < 990); assert.ok(advanced.heading_degrees > 260 && advanced.heading_degrees < 280); }); + +test("ghost pin stems stay terrain-relative after asynchronous tile loading", async () => { + const source = await readFile( + new URL("../src/CesiumMapRenderer.tsx", import.meta.url), + "utf8", + ); + + assert.match(source, /new runtime\.Cesium\.CallbackPositionProperty/); + assert.match(source, /new runtime\.Cesium\.CallbackProperty/); + assert.match(source, /depthFailMaterial: fillColor/); +});