fix(globe): document Cesium's default view angles and check them

FlightOptions and setView now state Cesium's defaults for missing angles
(heading 0, pitch -pi/2, roll 0), enforced by a contract-suite check. The
suite also checks a horizon-bounded view rectangle and an undefined one
when the camera looks away from the Earth.
This commit is contained in:
German Khokhlov committed 2026-10-15 17:21:31 +04:00
1 parent 708c96b6f6
commit cd94a26b9f
2 files changed
+90 -1

No files matched your search

+4 -1
View File
@@ -46,7 +46,9 @@
*
* @typedef {object} FlightOptions
* @property {Cartesian3} destination
* @property {Orientation} [orientation] Missing angles keep the current ones.
* @property {Orientation} [orientation] As in Cesium's setView and flyTo,
* missing angles default to heading 0, pitch −π/2 and roll 0 (they do not
* keep the current ones).
* @property {number} [duration] Seconds. 0 (or less) sets the view and calls
* `complete` synchronously, before flyTo returns, as Cesium does.
* @property {'cubicInOut'|'sinusoidalInOut'|'linear'} [easing]
@@ -70,6 +72,7 @@
* @typedef {object} GlobeCamera
* @property {() => CameraPose} pose Fresh values on every call.
* @property {(view: { destination: Cartesian3, orientation?: Orientation }) => void} setView
* Missing angles default to heading 0, pitch −π/2, roll 0.
* @property {(options: FlightOptions) => void} flyTo Cancels a running flight first.
* @property {(sphere: { center: Cartesian3, radius: number }, options?: SphereFlightOptions) => void} flyToSphere
* Ends unlocked (identity transform).
+86
View File
@@ -1064,6 +1064,92 @@ export function createContractSuite(
);
},
],
[
'camera.setView and flyTo: missing angles default to heading 0, pitch −π/2, roll 0',
async () => {
const destination = cartesian3.fromDegrees(40, 30, 20000);
const expectDefaults = (label) => {
const pose = camera.pose();
near(pose.pitch, -math.PI_OVER_TWO, 1e-6, `${label} pitch`);
near(angleBetween(pose.heading, 0), 0, 1e-6, `${label} heading`);
near(pose.roll, 0, 1e-6, `${label} roll`);
};
const tilted = { heading: 1, pitch: -0.3, roll: 0.2 };
camera.setView({ destination, orientation: tilted });
camera.setView({ destination });
await settle();
expectDefaults('setView without orientation');
camera.setView({ destination, orientation: tilted });
camera.setView({ destination, orientation: {} });
expectDefaults('setView with empty orientation');
camera.setView({ destination, orientation: tilted });
camera.flyTo({ destination, duration: 0 });
expectDefaults('flyTo without orientation');
camera.setView({ destination, orientation: tilted });
camera.setView({ destination, orientation: { heading: 1 } });
const partial = camera.pose();
near(angleBetween(partial.heading, 1), 0, 1e-6, 'partial heading');
near(partial.pitch, -math.PI_OVER_TWO, 1e-6, 'partial pitch default');
},
],
[
'camera.viewRectangle: a horizontal view is bounded by the horizon',
async () => {
camera.setView({
destination: cartesian3.fromDegrees(20, 10, 100000),
orientation: { heading: 0, pitch: 0, roll: 0 },
});
await settle();
const rectangle = camera.viewRectangle();
ensure(rectangle, 'no view rectangle looking at the horizon');
for (const side of ['west', 'south', 'east', 'north'])
ensure(Number.isFinite(rectangle[side]), `${side} is not finite`);
// Looking north, Cesium's rectangle is built from the canvas corners
// that hit the globe (the bottom ones) and horizon points for the
// rest; it need not contain the camera's own position.
const { width, height } = engine.host.size();
for (const x of [0, width]) {
const ground = camera.pickEllipsoid(cartesian2.create(x, height));
ensure(
isCartesian3(ground),
`the bottom corner at x=${x} missed the globe`,
);
const seen = cartographic.fromCartesian(ground, WGS84);
ensure(
rectangle.west <= seen.longitude + 1e-9 &&
seen.longitude - 1e-9 <= rectangle.east &&
rectangle.south <= seen.latitude + 1e-9 &&
seen.latitude - 1e-9 <= rectangle.north,
`the visible corner at x=${x} lies outside the rectangle`,
);
}
const lon = math.toRadians(20);
ensure(
rectangle.west < lon && lon < rectangle.east,
`camera longitude outside ${rectangle.west}..${rectangle.east}`,
);
ensure(
rectangle.east - rectangle.west < math.toRadians(30) &&
rectangle.north - rectangle.south < math.toRadians(30),
'the rectangle is not bounded by the horizon',
);
},
],
[
'camera.viewRectangle: undefined when looking away from the Earth',
async () => {
camera.setView({
destination: cartesian3.fromDegrees(20, 10, 1e6),
orientation: { heading: 0, pitch: math.PI_OVER_TWO, roll: 0 },
});
await settle();
const rectangle = camera.viewRectangle();
ensure(
rectangle === undefined,
`looking away from the Earth gave ${JSON.stringify(rectangle)}`,
);
},
],
[
'camera.viewRectangle: spans all longitudes over a pole',
async () => {