mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-09 04:44:01 +00:00
fix(cockpit): a new ride's TRACK ACQUIRED keeps its own time; an arrow during the ease back tilts on from the view; a site beyond the reach ends a focus; the horizon uses the WGS84 surface normal; a hop to the opposite side goes round the Earth; drop the unused in-frame listing
This commit is contained in:
1 parent
7d6314b981
commit
6be8af17f1
10 files changed
+142
-170
No files matched your search
@@ -1708,7 +1708,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) that face the satellite and lie within the off-nadir
|
||||
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
|
||||
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,
|
||||
@@ -1727,7 +1728,10 @@ time. TRACK ACQUIRED stays first. Then:
|
||||
back to nadir, and the camera hops to the new satellite along the great
|
||||
circle over 1.2 s (cut under reduced motion), keeping the zoom. Until the
|
||||
camera is on the new satellite the page shows only TRACK ACQUIRED and
|
||||
RETURN, then rebuilds at once.
|
||||
RETURN (and SITES STANDBY with Mapped Installations off), then rebuilds
|
||||
at once. The new satellite's TRACK ACQUIRED carries the time it was
|
||||
ridden. The hop starts from where the camera was, which does not follow
|
||||
the old satellite on.
|
||||
|
||||
Sites take two rows while satellites fill the rest, more when there are
|
||||
fewer satellites. The Contact card stays hidden in orbit.
|
||||
@@ -1741,7 +1745,8 @@ with the arrows' easing on a slower clock (250 ms time constant, about
|
||||
0.75 s to 95%), then holds the site exactly, and cuts under
|
||||
`prefers-reduced-motion`. A single click on a site's marker focuses it too:
|
||||
the pick is resolved through the layer's own pick ids (`findByPick`: a named
|
||||
marker's `{ installationId }` or a site entity). A click anywhere else, the
|
||||
marker's `{ installationId }` or a site entity). A click anywhere else
|
||||
(a site beyond the reach included), the
|
||||
focused site's row, NADIR, Home or `0` ends the focus and the view eases
|
||||
back to nadir on the same clock; an arrow ends it (or the ease back) and
|
||||
tilts on from where the view is. The layer reloads its thinned view as the
|
||||
|
||||
+20
-80
@@ -24,8 +24,9 @@ export const ORBITAL_FOCUS_EASE_TAU_MS = 250;
|
||||
/** How often the orbital Live Signals rows are rebuilt, ms. */
|
||||
export const ORBITAL_REACH_REFRESH_MS = 1000;
|
||||
|
||||
/** The most mapped sites offered for the rows, nearest to nadir first. */
|
||||
export const ORBITAL_REACH_LIST_MAX = 6;
|
||||
/** WGS84 semi-axes squared, m²: the surface normal for the horizon test. */
|
||||
const WGS84_A2 = 6378137 ** 2;
|
||||
const WGS84_B2 = 6356752.314245179 ** 2;
|
||||
|
||||
/** Slack on the reach limit so a target held exactly at 45° stays. */
|
||||
const REACH_EPSILON_DEG = 1e-6;
|
||||
@@ -71,14 +72,15 @@ export function focusLook(nadir, satellite, target) {
|
||||
const along = dot(v, nadir.direction);
|
||||
const up = dot(v, nadir.up);
|
||||
const right = Math.min(1, Math.max(-1, dot(v, nadir.right)));
|
||||
// The target's own zenith: the satellite is above its horizon when the
|
||||
// line from the target to it points up.
|
||||
const targetRadius = Math.hypot(target.x, target.y, target.z);
|
||||
// The target's own zenith, the WGS84 surface normal there: the satellite
|
||||
// is above its horizon when the line from the target to it points up.
|
||||
const normal = {
|
||||
x: target.x / WGS84_A2,
|
||||
y: target.y / WGS84_A2,
|
||||
z: target.z / WGS84_B2,
|
||||
};
|
||||
const visible =
|
||||
targetRadius > 0 &&
|
||||
-(line.x * target.x + line.y * target.y + line.z * target.z) /
|
||||
targetRadius >
|
||||
0;
|
||||
Math.hypot(normal.x, normal.y, normal.z) > 0 && -dot(line, normal) > 0;
|
||||
const yawDeg = toDegrees(Math.asin(right));
|
||||
const pitchDeg = toDegrees(Math.atan2(up, along));
|
||||
return {
|
||||
@@ -106,38 +108,6 @@ export function focusInReach(look) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `target` is inside the picture as it is now framed.
|
||||
* @param {{direction: object, up: object, right: object}} view The camera's
|
||||
* orientation (the nadir frame tilted by the eased look).
|
||||
* @param {{x: number, y: number, z: number}} satellite ECEF, metres.
|
||||
* @param {{x: number, y: number, z: number}} target ECEF, metres.
|
||||
* @param {{verticalFovRad: number, aspect?: number}} frame
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function inFrame(
|
||||
view,
|
||||
satellite,
|
||||
target,
|
||||
{ verticalFovRad, aspect = 1 },
|
||||
) {
|
||||
if (!view || !finiteVector(satellite) || !finiteVector(target)) return false;
|
||||
if (!(verticalFovRad > 0)) return false;
|
||||
const line = {
|
||||
x: target.x - satellite.x,
|
||||
y: target.y - satellite.y,
|
||||
z: target.z - satellite.z,
|
||||
};
|
||||
const along = dot(line, view.direction);
|
||||
if (!(along > 0)) return false;
|
||||
const halfTan = Math.tan(verticalFovRad / 2);
|
||||
const ratio = Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
|
||||
return (
|
||||
Math.abs(dot(line, view.up)) / along <= halfTan &&
|
||||
Math.abs(dot(line, view.right)) / along <= halfTan * ratio
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ease what is left of the turn onto a focus target toward nothing: the
|
||||
* look's easing (`easeLook`) on the focus's slower clock. The view is the
|
||||
@@ -171,53 +141,23 @@ export function focusEaseDtMs(dtMs) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The mapped points the view shows now or can be turned to: those facing
|
||||
* the satellite and either within the off-nadir reach (`reachable`) or in
|
||||
* the frame as it is. Nearest to nadir first, at most `max`.
|
||||
* The mapped points the view can be turned to: those facing the satellite
|
||||
* and within the off-nadir reach, nearest to nadir first.
|
||||
* @param {Iterable<{key: string, position: {x: number, y: number,
|
||||
* z: number}}>} candidates
|
||||
* @param {{nadir: object, view: object, satellite: object,
|
||||
* verticalFovRad: number, aspect?: number, max?: number}} frame
|
||||
* @returns {{count: number, entries: Array<{key: string,
|
||||
* offNadirDeg: number, reachable: boolean, look: object}>}} `count` is
|
||||
* every point in reach, `entries` the nearest.
|
||||
* @param {{nadir: object, satellite: object}} frame
|
||||
* @returns {Array<{key: string, offNadirDeg: number, look: object}>}
|
||||
*/
|
||||
export function pointsInReach(
|
||||
candidates,
|
||||
{
|
||||
nadir,
|
||||
view,
|
||||
satellite,
|
||||
verticalFovRad,
|
||||
aspect = 1,
|
||||
max = ORBITAL_REACH_LIST_MAX,
|
||||
},
|
||||
) {
|
||||
export function pointsInReach(candidates, { nadir, satellite }) {
|
||||
const entries = [];
|
||||
if (!nadir || !finiteVector(satellite)) return { count: 0, entries };
|
||||
if (!nadir || !finiteVector(satellite)) return entries;
|
||||
for (const candidate of candidates) {
|
||||
const look = focusLook(nadir, satellite, candidate?.position);
|
||||
if (!look?.visible) continue;
|
||||
const reachable = focusInReach(look);
|
||||
if (
|
||||
!reachable &&
|
||||
!inFrame(view, satellite, candidate.position, { verticalFovRad, aspect })
|
||||
)
|
||||
continue;
|
||||
entries.push({
|
||||
key: candidate.key,
|
||||
offNadirDeg: look.offNadirDeg,
|
||||
reachable,
|
||||
look,
|
||||
});
|
||||
if (!focusInReach(look)) continue;
|
||||
entries.push({ key: candidate.key, offNadirDeg: look.offNadirDeg, look });
|
||||
}
|
||||
entries.sort(
|
||||
return entries.sort(
|
||||
(a, b) =>
|
||||
a.offNadirDeg - b.offNadirDeg || String(a.key).localeCompare(b.key),
|
||||
);
|
||||
const limit =
|
||||
max === Infinity || (Number.isInteger(max) && max >= 0)
|
||||
? max
|
||||
: ORBITAL_REACH_LIST_MAX;
|
||||
return { count: entries.length, entries: entries.slice(0, limit) };
|
||||
}
|
||||
@@ -6,12 +6,10 @@ import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
ORBITAL_FOCUS_EASE_TAU_MS,
|
||||
ORBITAL_REACH_LIST_MAX,
|
||||
easeFocusRemainder,
|
||||
focusEaseDtMs,
|
||||
focusInReach,
|
||||
focusLook,
|
||||
inFrame,
|
||||
pointsInReach,
|
||||
} from './cockpitOrbitalFocus.js';
|
||||
import {
|
||||
@@ -200,50 +198,30 @@ test('the reach list keeps points in reach or in frame, nearest first, bounded',
|
||||
for (let i = 0; i < 80; i += 1)
|
||||
candidates.push({ key: `east-${i}`, position: geodeticToEcef(0, i * 0.1) });
|
||||
candidates.push({ key: 'far-side', position: geodeticToEcef(0, 180) });
|
||||
const frame = {
|
||||
nadir,
|
||||
view: nadir,
|
||||
satellite,
|
||||
verticalFovRad: (60 * Math.PI) / 180,
|
||||
aspect: 16 / 9,
|
||||
};
|
||||
const all = pointsInReach(candidates, { ...frame, max: 100 });
|
||||
assert.ok(all.entries.every((entry) => entry.key !== 'far-side'));
|
||||
const angles = all.entries.map((entry) => entry.offNadirDeg);
|
||||
const entries = pointsInReach(candidates, { nadir, satellite });
|
||||
assert.ok(entries.every((entry) => entry.key !== 'far-side'));
|
||||
const angles = entries.map((entry) => entry.offNadirDeg);
|
||||
assert.deepEqual(angles, [...angles].sort((a, b) => a - b), 'nearest first');
|
||||
assert.equal(all.entries[0].key, 'east-0');
|
||||
assert.equal(all.entries[0].offNadirDeg, 0);
|
||||
// The widest frame (16:9 at 60° tall) reaches past 45° across: those
|
||||
// points are listed, but cannot be focused.
|
||||
const beyond = all.entries.filter((entry) => !entry.reachable);
|
||||
assert.ok(beyond.length > 0, 'some only in frame');
|
||||
for (const entry of beyond) assert.ok(Math.abs(entry.look.yawDeg) > 45);
|
||||
for (const entry of all.entries.filter((e) => e.reachable))
|
||||
assert.ok(Math.abs(entry.look.yawDeg) <= 45);
|
||||
// Out of frame and out of reach: not listed.
|
||||
assert.ok(all.count < 80);
|
||||
|
||||
const bounded = pointsInReach(candidates, frame);
|
||||
assert.equal(bounded.entries.length, ORBITAL_REACH_LIST_MAX);
|
||||
assert.equal(bounded.count, all.count, 'the count is all of them');
|
||||
assert.deepEqual(
|
||||
bounded.entries.map((entry) => entry.key),
|
||||
all.entries.slice(0, ORBITAL_REACH_LIST_MAX).map((entry) => entry.key),
|
||||
);
|
||||
assert.deepEqual(pointsInReach([], frame), { count: 0, entries: [] });
|
||||
assert.equal(entries[0].key, 'east-0');
|
||||
assert.equal(entries[0].offNadirDeg, 0);
|
||||
// Only those within the 45° tilt are listed.
|
||||
assert.ok(entries.length > 0 && entries.length < 80);
|
||||
for (const entry of entries) assert.ok(Math.abs(entry.look.yawDeg) <= 45);
|
||||
const beyond = focusLook(nadir, satellite, candidates[entries.length].position);
|
||||
assert.ok(Math.abs(beyond.yawDeg) > 45, 'the next one east is past the reach');
|
||||
assert.deepEqual(pointsInReach([], { nadir, satellite }), []);
|
||||
});
|
||||
|
||||
test('in frame follows the view, its height and its width', () => {
|
||||
const satellite = satelliteAt(0, 0);
|
||||
const nadir = nadirOrientation(satellite);
|
||||
const target = geodeticToEcef(0, 3);
|
||||
const narrow = { verticalFovRad: (5 * Math.PI) / 180, aspect: 1 };
|
||||
assert.equal(inFrame(nadir, satellite, target, narrow), false);
|
||||
const look = focusLook(nadir, satellite, target);
|
||||
const tilted = offNadirOrientation(nadir, {
|
||||
northDeg: look.pitchDeg,
|
||||
eastDeg: look.yawDeg,
|
||||
});
|
||||
assert.equal(inFrame(tilted, satellite, target, narrow), true);
|
||||
assert.equal(inFrame(tilted, satellite, target, { verticalFovRad: 0 }), false);
|
||||
test('the horizon is the ground’s own: the WGS84 surface normal, not the centre line', () => {
|
||||
const target = geodeticToEcef(45, 0);
|
||||
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(target);
|
||||
const satelliteAtElevation = (elevationDeg) => {
|
||||
const e = Cesium.Math.toRadians(elevationDeg);
|
||||
const local = new Cesium.Cartesian3(0, -Math.cos(e) * 4e7, Math.sin(e) * 4e7);
|
||||
return Cesium.Matrix4.multiplyByPoint(enu, local, new Cesium.Cartesian3());
|
||||
};
|
||||
const below = satelliteAtElevation(-0.1);
|
||||
assert.equal(focusLook(nadirOrientation(below), below, target).visible, false);
|
||||
const above = satelliteAtElevation(0.1);
|
||||
assert.equal(focusLook(nadirOrientation(above), above, target).visible, true);
|
||||
});
|
||||
@@ -22,8 +22,8 @@ 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. Both ends may move during the hop; pass
|
||||
* their positions now.
|
||||
* centre interpolated linearly. Opposite ends turn about a fixed axis
|
||||
* square to them, so even then the path stays off the Earth.
|
||||
* @param {{x: number, y: number, z: number}} from
|
||||
* @param {{x: number, y: number, z: number}} to
|
||||
* @param {number} progress Eased, 0..1.
|
||||
@@ -39,6 +39,27 @@ export function hopPosition(from, to, progress) {
|
||||
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 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 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 = {
|
||||
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) {
|
||||
@@ -46,7 +67,6 @@ export function hopPosition(from, to, progress) {
|
||||
ka = Math.sin((1 - p) * omega) / sine;
|
||||
kb = Math.sin(p * omega) / sine;
|
||||
}
|
||||
const r = ra + (rb - ra) * p;
|
||||
return {
|
||||
x: (a.x * ka + b.x * kb) * r,
|
||||
y: (a.y * ka + b.y * kb) * r,
|
||||
|
||||
@@ -26,3 +26,16 @@ test('the hop follows the great circle and lands on the satellite', () => {
|
||||
assert.ok(Math.abs(Math.atan2(mid.y, mid.x) - Math.PI / 4) < 1e-9);
|
||||
assert.equal(hopPosition(null, to, 0.5), null);
|
||||
});
|
||||
|
||||
test('a hop to the opposite side goes round the Earth, not through it', () => {
|
||||
const from = at(0, 0, 400_000);
|
||||
const to = at(0, 180, 400_000);
|
||||
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}`);
|
||||
}
|
||||
const end = hopPosition(from, to, 1);
|
||||
for (const axis of ['x', 'y', 'z'])
|
||||
assert.ok(Math.abs(end[axis] - to[axis]) < 1e-3, axis);
|
||||
});
|
||||
Vendored
+7
-3
@@ -389,10 +389,14 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
{ excludeIds = [], excludeClasses = [] } = {},
|
||||
) {
|
||||
const nearest = [];
|
||||
if (!center || !layerState._points || layerState._points.size === 0)
|
||||
const cap = Number.isFinite(maxCount) ? Math.floor(maxCount) : 8;
|
||||
if (
|
||||
cap < 1 ||
|
||||
!center ||
|
||||
!layerState._points ||
|
||||
layerState._points.size === 0
|
||||
)
|
||||
return nearest;
|
||||
const cap =
|
||||
Number.isFinite(maxCount) && maxCount > 0 ? Math.floor(maxCount) : 8;
|
||||
const excluded = new Set(excludeIds.map(String));
|
||||
const classes = new Set(excludeClasses);
|
||||
const dense = layerState._denseIds.length > 0;
|
||||
|
||||
@@ -64,4 +64,7 @@ test('the nearest shown satellites come first, bounded, with their distance', ()
|
||||
assert.equal(nearest[0].satelliteClass, 'imaging');
|
||||
assert.equal(methods.getNearestPositions(CENTRE, 1, { excludeIds: [6] })[0].label, 'DEB');
|
||||
assert.deepEqual(methods.getNearestPositions(null, 3), []);
|
||||
assert.deepEqual(methods.getNearestPositions(CENTRE, 0), []);
|
||||
assert.deepEqual(methods.getNearestPositions(CENTRE, 0.5), []);
|
||||
assert.equal(methods.getNearestPositions(CENTRE, 2.5).length, 2);
|
||||
});
|
||||
@@ -10,10 +10,7 @@
|
||||
*/
|
||||
import * as Cesium from 'cesium';
|
||||
import { clampOrbitalLook, easeLook } from '../cockpitLook.js';
|
||||
import {
|
||||
ORBITAL_EARTH_RADIUS_M,
|
||||
offNadirOrientation,
|
||||
} from '../cockpitOrbital.js';
|
||||
import { ORBITAL_EARTH_RADIUS_M } from '../cockpitOrbital.js';
|
||||
import {
|
||||
easeFocusRemainder,
|
||||
focusEaseDtMs,
|
||||
@@ -181,8 +178,12 @@ function startFocus(site) {
|
||||
export function releaseOrbitalFocus({ handOver = false } = {}) {
|
||||
const state = this.orbital;
|
||||
if (!state?.focus) {
|
||||
// An arrow during the ease back takes the arrows' own clock again.
|
||||
if (handOver && state) state.focusReleasing = false;
|
||||
// An arrow during the ease back takes the arrows' own clock again and
|
||||
// tilts on from where the view is, not from the nadir it was easing to.
|
||||
if (handOver && state?.focusReleasing) {
|
||||
state.lookTarget = clampOrbitalLook(state.lookView);
|
||||
state.focusReleasing = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
state.focus = null;
|
||||
@@ -249,8 +250,7 @@ export function onOrbitalClick(position) {
|
||||
if (site) {
|
||||
if (state.focus?.key === String(site.id)) return true;
|
||||
if (startFocus.call(this, site)) return true;
|
||||
// A mapped site beyond the reach leaves the view as it is.
|
||||
return false;
|
||||
// A mapped site beyond the reach is a click elsewhere: the focus ends.
|
||||
}
|
||||
releaseOrbitalFocus.call(this);
|
||||
return false;
|
||||
@@ -283,8 +283,9 @@ export function routeSceneClick() {
|
||||
/**
|
||||
* The orbital view's look for one frame, while a focus holds or is ending.
|
||||
* A held focus aims at its target: the live look plus whatever is left of
|
||||
* the turn onto it. The focus ends, easing back, when its site is gone (its
|
||||
* layer off, the site no longer loaded) or beyond the reach. Also rebuilds
|
||||
* the turn onto it. The focus ends, easing back, when its layer is turned
|
||||
* off or its site is beyond the reach (a site the layer stops loading is
|
||||
* held from its last record). Also rebuilds
|
||||
* the orbital Live Signals rows on their own clock.
|
||||
* @param {object} state The orbital session.
|
||||
* @param {{direction: object, up: object, right: object}} nadir This
|
||||
@@ -389,9 +390,8 @@ export function orbitalFocusReadout() {
|
||||
* with the layer off.
|
||||
* @param {{latitude: number, longitude: number, altitudeM: number}} info
|
||||
* The ridden satellite.
|
||||
* @returns {{count: number, entries: Array<{site: object,
|
||||
* offNadirDeg: number, focused: boolean}>}} `count` also counts sites in
|
||||
* the frame but beyond the reach.
|
||||
* @returns {{entries: Array<{site: object, offNadirDeg: number,
|
||||
* focused: boolean}>}}
|
||||
*/
|
||||
export function orbitalSitesInReach(info) {
|
||||
const state = this.orbital;
|
||||
@@ -401,9 +401,9 @@ export function orbitalSitesInReach(info) {
|
||||
// then, and no row is left to focus from.
|
||||
if (state && !sitesLayerOn(this)) state.listedSites = new Map();
|
||||
if (!frame || typeof layer?.getNearby !== 'function' || !sitesLayerOn(this))
|
||||
return { count: 0, entries: [] };
|
||||
return { entries: [] };
|
||||
if (!Number.isFinite(info?.latitude) || !Number.isFinite(info?.longitude))
|
||||
return { count: 0, entries: [] };
|
||||
return { entries: [] };
|
||||
const rangeM = reachGroundRangeM(info.altitudeM);
|
||||
let sites = [];
|
||||
try {
|
||||
@@ -449,27 +449,15 @@ export function orbitalSitesInReach(info) {
|
||||
seenMs: nowMs,
|
||||
});
|
||||
state.listedSites = remembered;
|
||||
const frustum = this.viewer.camera?.frustum;
|
||||
const reach = pointsInReach(candidates, {
|
||||
nadir: frame.nadir,
|
||||
view: offNadirOrientation(frame.nadir, {
|
||||
northDeg: state.lookView.pitchDeg,
|
||||
eastDeg: state.lookView.yawDeg,
|
||||
}),
|
||||
satellite: frame.position,
|
||||
verticalFovRad: state.verticalFov,
|
||||
aspect: Number.isFinite(frustum?.aspectRatio) ? frustum.aspectRatio : 1,
|
||||
max: Infinity,
|
||||
});
|
||||
const focusKey = state.focus?.key ?? null;
|
||||
return {
|
||||
count: reach.count,
|
||||
entries: reach.entries
|
||||
.filter((entry) => entry.reachable)
|
||||
.map(({ key, offNadirDeg }) => ({
|
||||
site: byId.get(key),
|
||||
offNadirDeg,
|
||||
focused: key === focusKey,
|
||||
})),
|
||||
entries: pointsInReach(candidates, {
|
||||
nadir: frame.nadir,
|
||||
satellite: frame.position,
|
||||
}).map(({ key, offNadirDeg }) => ({
|
||||
site: byId.get(key),
|
||||
offNadirDeg,
|
||||
focused: key === focusKey,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -573,9 +573,14 @@ test('a click on a site marker focuses it through the layer; a click elsewhere e
|
||||
const largest = run(scene, LAND_FRAMES, { drift: 0 });
|
||||
assert.ok(largest < MAX_TURN_PER_FRAME_DEG);
|
||||
assert.ok(offNadir(scene.shots.at(-1)) < 1e-6);
|
||||
// A site beyond the reach is not focused.
|
||||
// A site beyond the reach is not focused, and ends a focus as a click
|
||||
// elsewhere does.
|
||||
scene.click(SITES.far.at);
|
||||
assert.equal(cockpit.orbital.focus ?? null, null);
|
||||
scene.click(SITES.alpha.at);
|
||||
assert.equal(cockpit.orbital.focus.key, SITES.alpha.id);
|
||||
scene.click(SITES.far.at);
|
||||
assert.equal(cockpit.orbital.focus, null);
|
||||
});
|
||||
|
||||
test('a click a tool owns is left alone', async (t) => {
|
||||
@@ -676,7 +681,12 @@ test('a satellite row rides it, staying in orbit; RETURN goes back', async (t) =
|
||||
run(scene, 5);
|
||||
const before = Cesium.Cartesian3.clone(scene.shots.at(-1).destination);
|
||||
|
||||
const firstTrack = cockpit.signalItems.find((item) => item.key === 'track');
|
||||
firstTrack.timestamp -= 60_000;
|
||||
satelliteRow(scene, 49260).click();
|
||||
const newTrack = cockpit.signalItems.filter((item) => item.key === 'track');
|
||||
assert.equal(newTrack.length, 1);
|
||||
assert.ok(newTrack[0].timestamp > firstTrack.timestamp, 'the new ride’s own time');
|
||||
assert.deepEqual(satellites.calls.slice(-2), [['track', 49260], ['subject', 49260]]);
|
||||
assert.equal(viewer.trackedEntity, undefined, 'the Cockpit keeps the camera');
|
||||
assert.equal(cockpit.subjectId, 49260);
|
||||
@@ -793,6 +803,8 @@ test('an arrow during the ease back from a focus turns on the arrows’ clock',
|
||||
scene.click(EMPTY_GROUND);
|
||||
scene.frame();
|
||||
assert.equal(cockpit.orbital.focusReleasing, true);
|
||||
const view = { ...cockpit.orbital.lookView };
|
||||
assert.ok(Math.abs(view.pitchDeg) > 1 || Math.abs(view.yawDeg) > 1, 'still turned');
|
||||
cockpit.onKeyDown({
|
||||
key: 'ArrowUp',
|
||||
target: document.body,
|
||||
@@ -800,6 +812,12 @@ test('an arrow during the ease back from a focus turns on the arrows’ clock',
|
||||
stopImmediatePropagation() {},
|
||||
});
|
||||
assert.equal(cockpit.orbital.focusReleasing, false);
|
||||
// The arrow tilts on from where the view is, not from nadir.
|
||||
assert.ok(
|
||||
Math.abs(cockpit.orbital.lookTarget.yawDeg - view.yawDeg) < 1e-6,
|
||||
'across kept',
|
||||
);
|
||||
assert.ok(cockpit.orbital.lookTarget.pitchDeg > view.pitchDeg, 'tilted on up');
|
||||
});
|
||||
|
||||
test('leaving orbit leaves no orbital rows to act on', async (t) => {
|
||||
|
||||
@@ -150,6 +150,9 @@ export function switchOrbitalSatellite(noradId) {
|
||||
this.lastHudUpdateMs = 0;
|
||||
this.lastContextUpdateMs = 0;
|
||||
this.dispatchCockpitModeChanged(true, info);
|
||||
// The new satellite's TRACK ACQUIRED replaces the old one, time and all.
|
||||
this.signalItems = this.signalItems.filter((item) => item.key !== 'track');
|
||||
this.signalSignatures.delete('track');
|
||||
this.pushCockpitSignal(
|
||||
'track',
|
||||
'track',
|
||||
|
||||
Reference in new issue
Block a user