fix(map): keep ghost pin stems terrain-relative

This commit is contained in:
Codex
2026-07-29 18:52:28 +03:00
parent 08a3c47391
commit c00cb16988
4 changed files with 76 additions and 35 deletions
+5
View File
@@ -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.
+4
View File
@@ -152,6 +152,10 @@ billboard до label-слоя, поэтому непрозрачная плаш
лежащий под ней маркер вместо «рентгеновского» проступания его обводки. Text
outline плашки отключён и не создаёт дополнительный halo. Контуры
`surface-fill` остаются частью отдельного контракта геозон.
Высота elevated target и его стержня остаётся callback-свойством Cesium:
после асинхронной загрузки terrain она пересчитывается относительно уже
доступной поверхности, а depth-fail material не даёт стержню исчезнуть при
пересечении рельефа.
## Acceptance fixtures
@@ -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,
@@ -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/);
});