Keep bounded imagery off Cesium's base layer and pins on drawn 3D tiles

Cesium treats the first imagery layer shown on the globe as its base
layer and stretches that layer's edge texels over the whole globe. With
no base map under it, a KMZ ground overlay turned the whole globe its
edge colour. The Cesium imagery adapter now keeps a floor under an
engine layer that covers less than the globe (a bounded rectangle, or
Web Mercator tiles, which stop at ±85.05°) whenever that layer would
otherwise be the base layer. The floor is one transparent pixel over the
globe at alpha 0, so Cesium draws nothing of it. It is placed when the
adapter adds or changes a layer, and again at each scene preUpdate,
before Cesium picks the base layer. When base imagery lies under the
layer, which is the app's normal state, there is no floor. A bounded
layer now draws only inside its rectangle, as the contract says.

When the globe is not the drawn surface, the import display now clamps
ground pins to the drawn scene, as Cesium's import did whenever the
globe was hidden. Before, it did this only for the map's own 3D tiles.
Each height query now leaves out the point sets of every imported file
on the engine, not only the file's own sets: Cesium's import kept all
imported pins out of height queries, so a higher pin from another file
never became the ground. The display also gives a footprint with its
points at the heights they are drawn at, and flying to a file frames
pins on raised tiles as Cesium did. Cesium's import clamped visible pins
with clampToHeightMostDetailed, which loads the finest tiles. The
display samples the tiles drawn now and samples them again after the
camera stops and while tiles load.

The KML reader's parity test against KmlDataSource now includes the
large-file QA's style case: a shared style with point, line and polygon
parts, inline overrides of each part, and named styles and an inline
StyleMap pair that are alike. The reader matches Cesium there.

QA scripts:
- qa-geo-import moves the camera over the files before it reads which
  pins show. Ground pins are hidden only beyond the horizon.
- qa-geo-import-large compares clamped pins by longitude and latitude.
  Cesium keeps a clamped billboard at its file height, while the import
  draws its pins on the sampled ground.
- qa-geo-import-regressions gives its 3D-tiles fixture a non-zero
  tileset geometricError, because Cesium never draws a tileset whose own
  error is 0. Its XYZ check now waits for the import to sample the ground
  again once the globe's tiles have loaded.

Known differences that remain: a KMZ ground overlay is now imagery, so
ground fills draw over it where the old ground rectangle drew over them;
right after an import on ellipsoid terrain, pins can sit on the coarse
globe mesh until the ground is sampled again.

The full unit suite and the boundary, format and coupling checks pass.
In the browser, keyless: qa-geo-import, qa-geo-import-large and
qa-navara-geo-import pass; qa-geo-import-regressions fails only its
portrait-flight check, which also fails before the refactor.
This commit is contained in:
German Khokhlov committed 2026-11-07 14:11:03 +04:00
1 parent f4b83a3a31
commit 910d693b94
12 files changed
+390 -61

No files matched your search

+10 -2
View File
@@ -141,9 +141,15 @@ try {
(value(b.verticalOrigin) === C.VerticalOrigin.BOTTOM
? height / 2
: 0);
// A clamped billboard keeps its file height and Cesium clamps it
// while drawing; the import draws its pins at the sampled ground.
// Both compare by longitude and latitude.
const clamped =
(value(b.heightReference) ?? C.HeightReference.NONE) !==
C.HeightReference.NONE;
rows.push({
kind: 'pin',
position: xyz(value(e.position)),
position: (clamped ? lonLat : xyz)(value(e.position)),
size:
2 *
Math.max(
@@ -184,7 +190,9 @@ try {
for (const item of r.items)
rows.push({
kind: 'pin',
position: xyz(item.position),
position: (r.options.depthTest === 'always' ? lonLat : xyz)(
item.position,
),
size: r.options.sprite.sizePx,
rotation: item.rotation,
color: item.color,
+18 -2
View File
@@ -19,7 +19,9 @@ const meshBytes = Buffer.alloc(60);
const fixture = {
'/__qa_import_surface/tileset.json': {
asset: { version: '1.1' },
geometricError: 0,
// Cesium draws no tileset whose own error is 0: the mesh must draw to be
// the rendered surface.
geometricError: 10000,
root: {
transform: Cesium.Matrix4.toArray(
Cesium.Transforms.eastNorthUpToFixedFrame(
@@ -186,14 +188,28 @@ try {
},
'xyz.geojson',
);
// The file's 100 m is dropped: the pin is on the ground sampled under
// it, the globe's tiles drawn (here the ellipsoid, 0 m, once they have
// loaded and the import has read them again).
const pinHeight = () =>
C.Cartographic.fromCartesian(pins(r)[0].position).height;
const groundDeadline = performance.now() + 15000;
while (
Math.abs(pinHeight()) >= 0.01 &&
performance.now() < groundDeadline
) {
viewer.scene.requestRender();
await new Promise((resolve) => setTimeout(resolve, 100));
}
const pin = pins(r)[0],
polygon = ground(r)[0];
check(
'XYZ ground footprint and isolated pick IDs',
r.outcome.ok &&
pin.pickId === undefined &&
Math.abs(C.Cartographic.fromCartesian(pin.position).height) < 0.01 &&
Math.abs(pinHeight()) < 0.01 &&
polygon.rings.flat().every((p) => p.length === 2),
{ pinHeight: pinHeight() },
);
check(
'ground pins use fixed positions and ignore scene depth',
+17 -14
View File
@@ -364,6 +364,21 @@ try {
)),
`${JSON.stringify(clockBefore)} → ${JSON.stringify(clockAfter)}`,
);
// Over the files: their pins on the ground show unless beyond the
// horizon, so all of them show here.
await page.evaluate((view) => {
const viewer = window.__godsEyeView.viewer;
viewer.camera.cancelFlight?.();
viewer.camera.setView({
destination: window.__CESIUM__.Cartesian3.fromDegrees(
view.lon,
view.lat,
view.height,
),
orientation: { heading: 0, pitch: (view.pitch * Math.PI) / 180, roll: 0 },
});
}, VIEW);
await wait(2500);
const inside = await page.evaluate(() => {
const groups = window.__qaImports.groups();
const pins = (group) =>
@@ -389,7 +404,8 @@ try {
].some((key) => key in r),
),
fallbackPins: pins(kml).length,
datedPinsShown: pins(kml).every((pin) => pin.show),
datedPinsShown:
pins(kml).length > 0 && pins(kml).every((pin) => pin.show),
kmlVertices: paths(kml).map((line) => line.positions.length),
gpxLines: paths(gpx).length,
gpxPoints: pins(gpx).length,
@@ -442,19 +458,6 @@ try {
'XYZ polygons and KML/KMZ lines use ground geometry',
inside.xyzPolygon && inside.groundedLines,
);
await page.evaluate((view) => {
const viewer = window.__godsEyeView.viewer;
viewer.camera.cancelFlight?.();
viewer.camera.setView({
destination: window.__CESIUM__.Cartesian3.fromDegrees(
view.lon,
view.lat,
view.height,
),
orientation: { heading: 0, pitch: (view.pitch * Math.PI) / 180, roll: 0 },
});
}, VIEW);
await wait(2500);
const cyanShown = await cyanPixels();
await shot('imported');
check(