fix(cockpit): the hop's circle runs through both ends, so it moves smoothly as a nearly opposite destination does and lands on it

This commit is contained in:
Sameh Khamis committed 2026-11-08 17:12:04 -08:00
1 parent 6be8af17f1
commit 577170ee8e
3 files changed
+60 -29

No files matched your search

+2 -2
View File
@@ -1709,8 +1709,8 @@ time. TRACK ACQUIRED stays first. Then:
- **Sites**: the Mapped Installations layer's loaded records around the
sub-satellite point (`getNearby`, asked only as far as the reach extends
over the ground, and never past 3,000 km, so from a high orbit only the
sites within 3,000 km of the point below are listed) that face the satellite and lie within the off-nadir
reach, nearest to nadir first, the focused site always among them. A row
sites within 3,000 km of the point below are listed) that face the
satellite and lie within the off-nadir reach, nearest to nadir first, the focused site always among them. A row
reads the site's name over its class and off-nadir angle; its button
(`Keep the camera on …`) focuses the site, and on the focused site's row,
marked as the subject is (`FOCUS · …`, `Release the camera from …`), ends
+25 -26
View File
@@ -22,8 +22,10 @@ function finiteVector(v) {
/**
* Where the camera is part-way through a hop: along the great circle
* between the two positions (never through the Earth), its distance from the
* centre interpolated linearly. Opposite ends turn about a fixed axis
* square to them, so even then the path stays off the Earth.
* centre interpolated linearly. The circle is the one through both ends, so
* the path moves smoothly as the destination does, however far round it
* lies; only exactly opposite ends, which no single circle joins, turn about
* a fixed axis square to them.
* @param {{x: number, y: number, z: number}} from
* @param {{x: number, y: number, z: number}} to
* @param {number} progress Eased, 0..1.
@@ -37,39 +39,36 @@ export function hopPosition(from, to, progress) {
if (!(ra > 0) || !(rb > 0)) return null;
const a = { x: from.x / ra, y: from.y / ra, z: from.z / ra };
const b = { x: to.x / rb, y: to.y / rb, z: to.z / rb };
const cosine = Math.min(1, Math.max(-1, a.x * b.x + a.y * b.y + a.z * b.z));
const omega = Math.acos(cosine);
const cosine = a.x * b.x + a.y * b.y + a.z * b.z;
// The direction the circle leaves `a` in: `b` less its part along `a`.
let c = {
x: b.x - cosine * a.x,
y: b.y - cosine * a.y,
z: b.z - cosine * a.z,
};
let rc = Math.hypot(c.x, c.y, c.z);
const r = ra + (rb - ra) * p;
if (Math.PI - omega < 1e-6) {
// No single great circle joins opposite points: turn about the axis
// square to `a` that is least aligned with the Earth's.
const omega = Math.atan2(rc, cosine);
if (!(rc > 1e-12)) {
if (cosine > 0) return { x: a.x * r, y: a.y * r, z: a.z * r };
// Opposite ends: set off square to `a`, northward (or, near a pole,
// toward the prime meridian).
const pole =
Math.abs(a.z) < 0.9 ? { x: 0, y: 0, z: 1 } : { x: 1, y: 0, z: 0 };
const along = pole.x * a.x + pole.y * a.y + pole.z * a.z;
const c = {
c = {
x: pole.x - along * a.x,
y: pole.y - along * a.y,
z: pole.z - along * a.z,
};
const rc = Math.hypot(c.x, c.y, c.z);
const cos = Math.cos(p * Math.PI);
const sin = Math.sin(p * Math.PI);
return {
x: (a.x * cos + (c.x / rc) * sin) * r,
y: (a.y * cos + (c.y / rc) * sin) * r,
z: (a.z * cos + (c.z / rc) * sin) * r,
};
}
let ka = 1 - p;
let kb = p;
if (omega > 1e-9) {
const sine = Math.sin(omega);
ka = Math.sin((1 - p) * omega) / sine;
kb = Math.sin(p * omega) / sine;
rc = Math.hypot(c.x, c.y, c.z);
}
const angle = p * omega;
const cos = Math.cos(angle);
const sin = Math.sin(angle) / rc;
return {
x: (a.x * ka + b.x * kb) * r,
y: (a.y * ka + b.y * kb) * r,
z: (a.z * ka + b.z * kb) * r,
x: (a.x * cos + c.x * sin) * r,
y: (a.y * cos + c.y * sin) * r,
z: (a.z * cos + c.z * sin) * r,
};
}
+33 -1
View File
@@ -33,9 +33,41 @@ test('a hop to the opposite side goes round the Earth, not through it', () => {
const radius = Cesium.Cartesian3.magnitude(from);
for (const p of [0.25, 0.5, 0.75]) {
const point = hopPosition(from, to, p);
assert.ok(Math.abs(Math.hypot(point.x, point.y, point.z) - radius) < 1e-3, `p=${p}`);
assert.ok(
Math.abs(Math.hypot(point.x, point.y, point.z) - radius) < 1e-3,
`p=${p}`,
);
}
const end = hopPosition(from, to, 1);
for (const axis of ['x', 'y', 'z'])
assert.ok(Math.abs(end[axis] - to[axis]) < 1e-3, axis);
});
test('a hop to nearly the opposite side moves smoothly as the destination does', () => {
const r = 6_778_137;
const from = { x: r, y: 0, z: 0 };
const near = (delta) => ({
x: -r * Math.cos(delta),
y: r * Math.sin(delta),
z: 0,
});
const mid = (delta) => hopPosition(from, near(delta), 0.5);
const [a, b] = [mid(0.999e-6), mid(1.001e-6)];
assert.ok(
Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z) < 1,
'no jump between neighbours',
);
for (const delta of [1e-3, 1e-6, 1e-9]) {
const to = near(delta);
const end = hopPosition(from, to, 1);
assert.ok(
Math.hypot(end.x - to.x, end.y - to.y, end.z - to.z) < 1e-3,
`lands at ${delta}`,
);
const half = mid(delta);
assert.ok(
Math.abs(Math.hypot(half.x, half.y, half.z) - r) < 1e-3,
`round at ${delta}`,
);
}
});