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 - returns the current positions through `snapshotGhostPins()` for an explicit
product save. 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 Motion never recreates the Cesium viewer or any provider. A renderer teardown
stops its single animation frame loop. stops its single animation frame loop.
+4
View File
@@ -152,6 +152,10 @@ billboard до label-слоя, поэтому непрозрачная плаш
лежащий под ней маркер вместо «рентгеновского» проступания его обводки. Text лежащий под ней маркер вместо «рентгеновского» проступания его обводки. Text
outline плашки отключён и не создаёт дополнительный halo. Контуры outline плашки отключён и не создаёт дополнительный halo. Контуры
`surface-fill` остаются частью отдельного контракта геозон. `surface-fill` остаются частью отдельного контракта геозон.
Высота elevated target и его стержня остаётся callback-свойством Cesium:
после асинхронной загрузки terrain она пересчитывается относительно уже
доступной поверхности, а depth-fail material не даёт стержню исчезнуть при
пересечении рельефа.
## Acceptance fixtures ## Acceptance fixtures
@@ -605,33 +605,25 @@ function rebuildGhostPinEntities(
presentation.label_hide_camera_height_meters, presentation.label_hide_camera_height_meters,
); );
for (const pin of runtime.currentGhostPins) { 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" const label = presentation.label_mode === "subject_id"
? pin.id ? pin.id
: pin.label; : pin.label;
ghostPins.entities.add({ ghostPins.entities.add({
id: pin.id, id: pin.id,
position: Cesium.Cartesian3.fromDegrees( position: ghostPinTopPosition(
pin.longitude, runtime,
pin.latitude, pin,
headHeight, presentation.stem_height_meters,
), ),
polyline: { polyline: {
positions: [ positions: ghostPinStemPositions(
Cesium.Cartesian3.fromDegrees( runtime,
pin.longitude, pin,
pin.latitude, presentation.stem_height_meters,
groundHeight, ),
),
Cesium.Cartesian3.fromDegrees(
pin.longitude,
pin.latitude,
headHeight,
),
],
width: presentation.stem_width_px, width: presentation.stem_width_px,
material: fillColor, material: fillColor,
depthFailMaterial: fillColor,
distanceDisplayCondition: targetDistance, distanceDisplayCondition: targetDistance,
}, },
billboard: { billboard: {
@@ -731,29 +723,57 @@ function updateGhostPinPositions(
for (const pin of runtime.currentGhostPins) { for (const pin of runtime.currentGhostPins) {
const entity = ghostPins.entities.getById(pin.id); const entity = ghostPins.entities.getById(pin.id);
if (!entity) continue; if (!entity) continue;
const groundHeight = ghostPinGroundHeight(runtime, pin); entity.position = ghostPinTopPosition(
const headHeight = runtime,
groundHeight + layer.presentation.stem_height_meters; pin,
entity.position = new Cesium.ConstantPositionProperty( layer.presentation.stem_height_meters,
Cesium.Cartesian3.fromDegrees(pin.longitude, pin.latitude, headHeight),
); );
if (entity.polyline) { if (entity.polyline) {
entity.polyline.positions = new Cesium.ConstantProperty([ entity.polyline.positions = ghostPinStemPositions(
Cesium.Cartesian3.fromDegrees( runtime,
pin.longitude, pin,
pin.latitude, layer.presentation.stem_height_meters,
groundHeight, );
),
Cesium.Cartesian3.fromDegrees(
pin.longitude,
pin.latitude,
headHeight,
),
]);
} }
} }
} }
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( function ghostPinGroundHeight(
runtime: RendererRuntime, runtime: RendererRuntime,
pin: MapGhostPin, pin: MapGhostPin,
@@ -1,4 +1,5 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test"; import test from "node:test";
import { import {
@@ -30,3 +31,14 @@ test("advanceGhostPins turns an escaping pin back toward its anchor", () => {
assert.ok(geodesicDistanceMeters(anchor, advanced) < 990); assert.ok(geodesicDistanceMeters(anchor, advanced) < 990);
assert.ok(advanced.heading_degrees > 260 && advanced.heading_degrees < 280); 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/);
});