mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Settle the draw decisions in the renderer contract
- lodGroup links items across sets as one thing at different detail; picks name the member shown and report the group. displayRange is half-open. - An orbits capability: the renderer propagates two-line elements with SGP4 on the set's motion clock. The stub propagates with satellite.js. - Height queries see draw sets only with includeInHeightQueries (was excludeFromHeightQueries); HeightSample.source gains 'set'. - courseGlide is a capability; courseBlendMs eases the course on every renderer. - Arc segments climb at climbMps; a negative minDtS stays for the aircraft warm-up. - The star catalogue is optional, a hidden column row keeps its motion, and draw.clampState() reports the drape fallback. Checks cover the climb and warm-up, lodGroup ranges, orbits against SGP4 (with and without motionSegments), hidden rows resuming, and sets not answering height queries by default.
This commit is contained in:
1 parent
2fc99e7c19
commit
14f06df735
10 files changed
+597
-21
No files matched your search
@@ -154,5 +154,12 @@ Browser checks use the chrome-devtools MCP against a page in its own window.
|
|||||||
The Cesium scene renderer (`src/render/cesium/sceneRenderer.js`) draws the
|
The Cesium scene renderer (`src/render/cesium/sceneRenderer.js`) draws the
|
||||||
icon, point, line, shape, model and city scenes, and the references were
|
icon, point, line, shape, model and city scenes, and the references were
|
||||||
recorded with it. It has only the members the scenes need. The quad, mesh,
|
recorded with it. It has only the members the scenes need. The quad, mesh,
|
||||||
field and agent scenes have no reference yet. Each family's reference is
|
field, agent and orbit scenes have no reference yet. Each family's reference
|
||||||
recorded when the Cesium renderer gains that family.
|
is recorded when the Cesium renderer gains that family.
|
||||||
|
|
||||||
|
Some behaviour is only drawn, so the logic checks cannot see it: course
|
||||||
|
easing (`courseBlendMs`, `courseGlide`), and a hidden row keeping its eased
|
||||||
|
course. The stub keeps no drawn course. These wait for scenes that capture
|
||||||
|
an icon mid-turn. Height queries that count a set
|
||||||
|
(`includeInHeightQueries`) are not checked either: the stub's heights are
|
||||||
|
scripted, so it does not model them.
|
||||||
@@ -5144,9 +5144,10 @@
|
|||||||
"src/render/stub/camera.js",
|
"src/render/stub/camera.js",
|
||||||
"src/render/stub/geodesic.js",
|
"src/render/stub/geodesic.js",
|
||||||
"src/render/stub/viewRectangle.js",
|
"src/render/stub/viewRectangle.js",
|
||||||
|
"src/render/stub/orbits.js",
|
||||||
"src/render/conformance/goldens/camera.json",
|
"src/render/conformance/goldens/camera.json",
|
||||||
"src/render/conformance/checks/camera.js"
|
"src/render/conformance/checks/camera.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": ["satellite.js"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,21 @@ import {
|
|||||||
checkRendererShape,
|
checkRendererShape,
|
||||||
} from '../../contract/index.js';
|
} from '../../contract/index.js';
|
||||||
import { AUSTIN, SCRIPT_VIEWPORT } from '../cameraScripts.js';
|
import { AUSTIN, SCRIPT_VIEWPORT } from '../cameraScripts.js';
|
||||||
|
import {
|
||||||
|
eciToEcf,
|
||||||
|
eciToGeodetic,
|
||||||
|
gstime,
|
||||||
|
propagate,
|
||||||
|
twoline2satrec,
|
||||||
|
} from 'satellite.js';
|
||||||
import { deepEqual, equal, near, nearVec, ok, throws } from '../expect.js';
|
import { deepEqual, equal, near, nearVec, ok, throws } from '../expect.js';
|
||||||
|
|
||||||
|
/** Elements for the orbit check: a station-like orbit, epoch 2026-01-04. */
|
||||||
|
const ISS_TLE = Object.freeze([
|
||||||
|
'1 25544U 98067A 26004.50000000 .00016717 00000-0 10270-3 0 9992',
|
||||||
|
'2 25544 51.6400 208.9163 0006317 69.9862 25.2906 15.49560532 10009',
|
||||||
|
]);
|
||||||
|
|
||||||
const DOT = {
|
const DOT = {
|
||||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><circle cx="8" cy="8" r="8" fill="#fff"/></svg>',
|
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><circle cx="8" cy="8" r="8" fill="#fff"/></svg>',
|
||||||
};
|
};
|
||||||
@@ -125,6 +138,7 @@ const SET_KINDS = [
|
|||||||
['quads', 'quads'],
|
['quads', 'quads'],
|
||||||
['meshes', 'meshes'],
|
['meshes', 'meshes'],
|
||||||
['agents', 'agents'],
|
['agents', 'agents'],
|
||||||
|
['orbits', 'orbits'],
|
||||||
];
|
];
|
||||||
|
|
||||||
export function portChecks() {
|
export function portChecks() {
|
||||||
@@ -690,6 +704,311 @@ export function portChecks() {
|
|||||||
},
|
},
|
||||||
{ capability: 'motionSegments' },
|
{ capability: 'motionSegments' },
|
||||||
),
|
),
|
||||||
|
check(
|
||||||
|
'draw.motion.climb',
|
||||||
|
'an arc climbs at climbMps, and a negative minDtS runs it backward',
|
||||||
|
async (h, r) => {
|
||||||
|
await overAustin(h, r, 30000);
|
||||||
|
const t0 = r.frame.time();
|
||||||
|
const fixAt = { ...AUSTIN, height: 12000 };
|
||||||
|
const fix = h.toEcef(fixAt);
|
||||||
|
const lon = (fixAt.lon * Math.PI) / 180;
|
||||||
|
const lat = (fixAt.lat * Math.PI) / 180;
|
||||||
|
const east = { x: -Math.sin(lon), y: Math.cos(lon), z: 0 };
|
||||||
|
const north = {
|
||||||
|
x: -Math.sin(lat) * Math.cos(lon),
|
||||||
|
y: -Math.sin(lat) * Math.sin(lon),
|
||||||
|
z: Math.cos(lat),
|
||||||
|
};
|
||||||
|
const up = {
|
||||||
|
x: Math.cos(lat) * Math.cos(lon),
|
||||||
|
y: Math.cos(lat) * Math.sin(lon),
|
||||||
|
z: Math.sin(lat),
|
||||||
|
};
|
||||||
|
// The contract's formula: the turn in the tangent plane, the climb up.
|
||||||
|
const at = (seg, dtS, climbs = true) => {
|
||||||
|
const track = (seg.trackDeg * Math.PI) / 180;
|
||||||
|
const w = (seg.turnRateDps * Math.PI) / 180;
|
||||||
|
const v = seg.speedMps;
|
||||||
|
const straight = Math.abs(w) < 1e-4;
|
||||||
|
const e = straight
|
||||||
|
? v * dtS * Math.sin(track)
|
||||||
|
: (v / w) * (Math.cos(track) - Math.cos(track + w * dtS));
|
||||||
|
const n = straight
|
||||||
|
? v * dtS * Math.cos(track)
|
||||||
|
: (v / w) * (Math.sin(track + w * dtS) - Math.sin(track));
|
||||||
|
const u = climbs ? seg.climbMps * dtS : 0;
|
||||||
|
return {
|
||||||
|
x: fix.x + east.x * e + north.x * n + up.x * u,
|
||||||
|
y: fix.y + east.y * e + north.y * n + up.y * u,
|
||||||
|
z: fix.z + east.z * e + north.z * n + up.z * u,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const segments = {
|
||||||
|
straight: {
|
||||||
|
kind: 'arc',
|
||||||
|
fix,
|
||||||
|
t0,
|
||||||
|
speedMps: 200,
|
||||||
|
trackDeg: 90,
|
||||||
|
turnRateDps: 0,
|
||||||
|
climbMps: 1000,
|
||||||
|
minDtS: -10,
|
||||||
|
maxDtS: 10,
|
||||||
|
},
|
||||||
|
turning: {
|
||||||
|
kind: 'arc',
|
||||||
|
fix,
|
||||||
|
t0,
|
||||||
|
speedMps: 150,
|
||||||
|
trackDeg: 0,
|
||||||
|
turnRateDps: 3,
|
||||||
|
climbMps: 1500,
|
||||||
|
minDtS: 0,
|
||||||
|
maxDtS: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const set = r.draw.icons({
|
||||||
|
id: 'conf:climb',
|
||||||
|
motionClock: { kind: 'scene' },
|
||||||
|
});
|
||||||
|
const find = (id, p) => {
|
||||||
|
const s = r.query.project(p);
|
||||||
|
return r.query
|
||||||
|
.drillPick(s.x, s.y)
|
||||||
|
.find((x) => x.set === 'conf:climb' && x.id === id);
|
||||||
|
};
|
||||||
|
const apart = (a, b) => {
|
||||||
|
const sa = r.query.project(a);
|
||||||
|
const sb = r.query.project(b);
|
||||||
|
return Math.hypot(sa.x - sb.x, sa.y - sb.y);
|
||||||
|
};
|
||||||
|
for (const [id, seg, sceneDtS, dtS, name] of [
|
||||||
|
['straight', segments.straight, 30, 10, 'climbed, clamped to maxDtS'],
|
||||||
|
['straight', segments.straight, -30, -10, 'run backward to minDtS'],
|
||||||
|
['turning', segments.turning, 8, 8, 'turning and climbing at 8 s'],
|
||||||
|
]) {
|
||||||
|
set.set([{ id, pos: fix, image: DOT, size: [16, 16], motion: seg }]);
|
||||||
|
r.frame.setTime(t0 + sceneDtS * 1000);
|
||||||
|
await drawn(h, r);
|
||||||
|
const want = at(seg, dtS);
|
||||||
|
const flat = at(seg, dtS, false);
|
||||||
|
ok(apart(want, flat) > 20, `${name}: the test can tell`);
|
||||||
|
const hit = find(id, want);
|
||||||
|
ok(hit, `${name}: picked where the formula puts it`);
|
||||||
|
if (hit?.position)
|
||||||
|
nearVec(
|
||||||
|
hit.position,
|
||||||
|
want,
|
||||||
|
50,
|
||||||
|
`${name}: at the formula's position`,
|
||||||
|
);
|
||||||
|
ok(!find(id, flat), `${name}: not at the fix's height`);
|
||||||
|
}
|
||||||
|
set.dispose();
|
||||||
|
},
|
||||||
|
{ capability: 'motionSegments' },
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'draw.lodGroup',
|
||||||
|
'members of a lodGroup show over their own ranges and picks report the group',
|
||||||
|
async (h, r) => {
|
||||||
|
const pos = h.toEcef({ ...AUSTIN, height: 0 });
|
||||||
|
const near = r.draw.points({ id: 'conf:lod-near' });
|
||||||
|
const far = r.draw.icons({ id: 'conf:lod-far' });
|
||||||
|
const members = (nearRange, farRange) => {
|
||||||
|
near.set([
|
||||||
|
{
|
||||||
|
id: 'a',
|
||||||
|
pos,
|
||||||
|
pixelSize: 20,
|
||||||
|
color: '#f00',
|
||||||
|
lodGroup: 'thing',
|
||||||
|
displayRange: nearRange,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
far.set([
|
||||||
|
{
|
||||||
|
id: 'a',
|
||||||
|
pos,
|
||||||
|
image: DOT,
|
||||||
|
size: [20, 20],
|
||||||
|
lodGroup: 'thing',
|
||||||
|
displayRange: farRange,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
const shown = () => {
|
||||||
|
const s = r.query.project(pos);
|
||||||
|
return r.query
|
||||||
|
.drillPick(s.x, s.y)
|
||||||
|
.filter(
|
||||||
|
(x) => x.set === 'conf:lod-near' || x.set === 'conf:lod-far',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
members([0, 10000], [10000, Infinity]);
|
||||||
|
for (const [height, set] of [
|
||||||
|
[2000, 'conf:lod-near'],
|
||||||
|
[50000, 'conf:lod-far'],
|
||||||
|
]) {
|
||||||
|
await overAustin(h, r, height);
|
||||||
|
await drawn(h, r);
|
||||||
|
const hits = shown();
|
||||||
|
deepEqual(
|
||||||
|
hits.map((x) => x.set),
|
||||||
|
[set],
|
||||||
|
`at ${height} m only ${set} is drawn`,
|
||||||
|
);
|
||||||
|
equal(hits[0]?.id, 'a', `at ${height} m the hit names the member`);
|
||||||
|
equal(hits[0]?.lodGroup, 'thing', `at ${height} m the group`);
|
||||||
|
}
|
||||||
|
members([0, 100000], [1000, Infinity]);
|
||||||
|
await drawn(h, r);
|
||||||
|
deepEqual(
|
||||||
|
shown()
|
||||||
|
.map((x) => x.set)
|
||||||
|
.sort(),
|
||||||
|
['conf:lod-far', 'conf:lod-near'],
|
||||||
|
'overlapping ranges show both members',
|
||||||
|
);
|
||||||
|
const plain = r.draw.points({ id: 'conf:lod-none' });
|
||||||
|
plain.set([{ id: 'b', pos, pixelSize: 20, color: '#0f0' }]);
|
||||||
|
await drawn(h, r);
|
||||||
|
const s = r.query.project(pos);
|
||||||
|
const hit = r.query.drillPick(s.x, s.y).find((x) => x.id === 'b');
|
||||||
|
ok(hit && !('lodGroup' in hit), 'an item without a group has none');
|
||||||
|
near.dispose();
|
||||||
|
far.dispose();
|
||||||
|
plain.dispose();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'draw.orbits',
|
||||||
|
'an orbit is drawn where SGP4 puts it on its set’s motion clock',
|
||||||
|
async (h, r) => {
|
||||||
|
const t = Date.UTC(2026, 0, 4, 12);
|
||||||
|
const expected = (ms) => {
|
||||||
|
const date = new Date(ms);
|
||||||
|
const satrec = twoline2satrec(ISS_TLE[0], ISS_TLE[1]);
|
||||||
|
const eci = propagate(satrec, date).position;
|
||||||
|
const gmst = gstime(date);
|
||||||
|
const ecf = eciToEcf(eci, gmst);
|
||||||
|
const geo = eciToGeodetic(eci, gmst);
|
||||||
|
return {
|
||||||
|
ecef: { x: ecf.x * 1000, y: ecf.y * 1000, z: ecf.z * 1000 },
|
||||||
|
lon: (geo.longitude * 180) / Math.PI,
|
||||||
|
lat: (geo.latitude * 180) / Math.PI,
|
||||||
|
height: geo.height * 1000,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const set = r.draw.orbits({
|
||||||
|
id: 'conf:orbits',
|
||||||
|
motionClock: { kind: 'scene' },
|
||||||
|
});
|
||||||
|
set.set([
|
||||||
|
{
|
||||||
|
id: 'iss',
|
||||||
|
tle: ISS_TLE,
|
||||||
|
marker: { pixelSize: 16, color: '#fff' },
|
||||||
|
meta: { n: 1 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const hitAt = (p) => {
|
||||||
|
const s = r.query.project(p);
|
||||||
|
return s
|
||||||
|
? r.query
|
||||||
|
.drillPick(s.x, s.y)
|
||||||
|
.find((x) => x.set === 'conf:orbits' && x.id === 'iss')
|
||||||
|
: undefined;
|
||||||
|
};
|
||||||
|
const now = expected(t);
|
||||||
|
r.camera.setView({
|
||||||
|
destination: {
|
||||||
|
lon: now.lon,
|
||||||
|
lat: now.lat,
|
||||||
|
height: now.height + 2000000,
|
||||||
|
},
|
||||||
|
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||||
|
});
|
||||||
|
r.frame.setTime(t);
|
||||||
|
await drawn(h, r);
|
||||||
|
const hit = hitAt(now.ecef);
|
||||||
|
ok(hit, 'picked at its propagated position');
|
||||||
|
deepEqual(hit?.meta, { n: 1 }, 'Hit.meta');
|
||||||
|
if (hit?.position)
|
||||||
|
nearVec(hit.position, now.ecef, 1000, 'within a kilometre of SGP4');
|
||||||
|
r.frame.setTime(t + 60000);
|
||||||
|
await drawn(h, r);
|
||||||
|
const later = expected(t + 60000);
|
||||||
|
const laterHit = hitAt(later.ecef);
|
||||||
|
ok(laterHit, 'a minute on, where SGP4 puts it then');
|
||||||
|
if (laterHit?.position)
|
||||||
|
nearVec(
|
||||||
|
laterHit.position,
|
||||||
|
later.ecef,
|
||||||
|
1000,
|
||||||
|
'a minute on, within a kilometre',
|
||||||
|
);
|
||||||
|
ok(!hitAt(now.ecef), 'no longer where it was');
|
||||||
|
set.dispose();
|
||||||
|
// `orbits` alone, without `motionSegments`, still reads the clock.
|
||||||
|
const alone = await h.create({
|
||||||
|
viewport: SCRIPT_VIEWPORT,
|
||||||
|
capabilities: [...r.capabilities].filter(
|
||||||
|
(c) => c !== 'motionSegments',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (alone)
|
||||||
|
try {
|
||||||
|
const only = alone.draw.orbits({
|
||||||
|
id: 'conf:orbits-alone',
|
||||||
|
motionClock: { kind: 'scene' },
|
||||||
|
});
|
||||||
|
only.set([
|
||||||
|
{
|
||||||
|
id: 'iss',
|
||||||
|
tle: ISS_TLE,
|
||||||
|
marker: { pixelSize: 16, color: '#fff' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
alone.camera.setView({
|
||||||
|
destination: {
|
||||||
|
lon: now.lon,
|
||||||
|
lat: now.lat,
|
||||||
|
height: now.height + 2000000,
|
||||||
|
},
|
||||||
|
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||||
|
});
|
||||||
|
alone.frame.setTime(t);
|
||||||
|
await drawn(h, alone);
|
||||||
|
const s = alone.query.project(now.ecef);
|
||||||
|
ok(
|
||||||
|
alone.query
|
||||||
|
.drillPick(s.x, s.y)
|
||||||
|
.some((x) => x.set === 'conf:orbits-alone'),
|
||||||
|
'without motionSegments, on the scene clock it was given',
|
||||||
|
);
|
||||||
|
only.dispose();
|
||||||
|
} finally {
|
||||||
|
alone.destroy();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ capability: 'orbits' },
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'draw.clampState',
|
||||||
|
'clampState says whether clamped items drape or overlay',
|
||||||
|
(h, r) => {
|
||||||
|
ok(
|
||||||
|
['draped', 'overlay'].includes(r.draw.clampState()),
|
||||||
|
'draped or overlay',
|
||||||
|
);
|
||||||
|
const off = r.draw.on('clampState', () => {});
|
||||||
|
equal(typeof off, 'function', 'on returns an Off');
|
||||||
|
off();
|
||||||
|
off();
|
||||||
|
},
|
||||||
|
),
|
||||||
check(
|
check(
|
||||||
'draw.columns',
|
'draw.columns',
|
||||||
'column updates are rows: sized, picked, removed',
|
'column updates are rows: sized, picked, removed',
|
||||||
@@ -729,6 +1048,58 @@ export function portChecks() {
|
|||||||
},
|
},
|
||||||
{ capability: 'columns' },
|
{ capability: 'columns' },
|
||||||
),
|
),
|
||||||
|
check(
|
||||||
|
'draw.columns.shown',
|
||||||
|
'a row hidden by its column keeps its motion and resumes where it would be',
|
||||||
|
async (h, r) => {
|
||||||
|
if (!r.capabilities.has('motionSegments'))
|
||||||
|
return { skip: 'no motionSegments' };
|
||||||
|
await overAustin(h, r, 8000);
|
||||||
|
const t0 = r.frame.time();
|
||||||
|
const from = h.toEcef({ ...AUSTIN, lon: AUSTIN.lon - 0.02, height: 0 });
|
||||||
|
const to = h.toEcef({ ...AUSTIN, lon: AUSTIN.lon + 0.02, height: 0 });
|
||||||
|
const along = (f) => ({
|
||||||
|
x: from.x + (to.x - from.x) * f,
|
||||||
|
y: from.y + (to.y - from.y) * f,
|
||||||
|
z: from.z + (to.z - from.z) * f,
|
||||||
|
});
|
||||||
|
const set = r.draw.icons({
|
||||||
|
id: 'conf:shown',
|
||||||
|
motionClock: { kind: 'scene' },
|
||||||
|
});
|
||||||
|
set.set([
|
||||||
|
{
|
||||||
|
id: 'mover',
|
||||||
|
pos: from,
|
||||||
|
image: DOT,
|
||||||
|
size: [16, 16],
|
||||||
|
motion: { kind: 'lerp', from, to, t0, t1: t0 + 10000 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const pickAt = (p) => {
|
||||||
|
const s = r.query.project(p);
|
||||||
|
return r.query
|
||||||
|
.drillPick(s.x, s.y)
|
||||||
|
.some((x) => x.set === 'conf:shown');
|
||||||
|
};
|
||||||
|
const shown = (on) =>
|
||||||
|
set.setColumns({ ids: ['mover'], shown: new Uint8Array([on]) });
|
||||||
|
r.frame.setTime(t0 + 2000);
|
||||||
|
await drawn(h, r);
|
||||||
|
ok(pickAt(along(0.2)), 'moving before it is hidden');
|
||||||
|
shown(0);
|
||||||
|
r.frame.setTime(t0 + 4000);
|
||||||
|
await drawn(h, r);
|
||||||
|
ok(!pickAt(along(0.4)), 'hidden');
|
||||||
|
r.frame.setTime(t0 + 7000);
|
||||||
|
shown(1);
|
||||||
|
await drawn(h, r);
|
||||||
|
ok(pickAt(along(0.7)), 'shown again where its segment puts it now');
|
||||||
|
ok(!pickAt(from), 'not restarted');
|
||||||
|
set.dispose();
|
||||||
|
},
|
||||||
|
{ capability: 'columns' },
|
||||||
|
),
|
||||||
check(
|
check(
|
||||||
'draw.models',
|
'draw.models',
|
||||||
'a model set says when a model is ready',
|
'a model set says when a model is ready',
|
||||||
@@ -882,6 +1253,51 @@ export function portChecks() {
|
|||||||
},
|
},
|
||||||
{ capability: 'sampleHeight' },
|
{ capability: 'sampleHeight' },
|
||||||
),
|
),
|
||||||
|
check(
|
||||||
|
'query.heights.sets',
|
||||||
|
'draw sets are not ground to height queries unless they ask to be',
|
||||||
|
async (h, r) => {
|
||||||
|
await overAustin(h, r);
|
||||||
|
await drawn(h, r);
|
||||||
|
const point = { ...AUSTIN };
|
||||||
|
const before = await r.query.sampleHeight([point]);
|
||||||
|
const shapes = r.draw.shapes({ id: 'conf:not-ground' });
|
||||||
|
shapes.set([
|
||||||
|
{
|
||||||
|
id: 'tower',
|
||||||
|
geometry: {
|
||||||
|
kind: 'box',
|
||||||
|
center: { ...AUSTIN, height: 500 },
|
||||||
|
dimensions: { x: 200, y: 200, z: 1000 },
|
||||||
|
},
|
||||||
|
fill: '#888',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const points = r.draw.points({ id: 'conf:not-ground-points' });
|
||||||
|
points.set([
|
||||||
|
{
|
||||||
|
id: 'p',
|
||||||
|
pos: { ...AUSTIN, height: 800 },
|
||||||
|
pixelSize: 40,
|
||||||
|
color: '#f00',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await drawn(h, r);
|
||||||
|
const after = await r.query.sampleHeight([point]);
|
||||||
|
equal(after[0] === null, before[0] === null, 'answered as before');
|
||||||
|
if (after[0] && before[0])
|
||||||
|
near(
|
||||||
|
after[0].height,
|
||||||
|
before[0].height,
|
||||||
|
0.5,
|
||||||
|
'the same height with sets drawn over the point',
|
||||||
|
);
|
||||||
|
ok(after[0]?.source !== 'set', 'not answered from a set');
|
||||||
|
shapes.dispose();
|
||||||
|
points.dispose();
|
||||||
|
},
|
||||||
|
{ capability: 'sampleHeight' },
|
||||||
|
),
|
||||||
check(
|
check(
|
||||||
'query.withoutCapability',
|
'query.withoutCapability',
|
||||||
'without sampleHeight the height members answer null',
|
'without sampleHeight the height members answer null',
|
||||||
|
|||||||
Vendored
+4
@@ -186,6 +186,10 @@ export type Capability =
|
|||||||
| 'celestial'
|
| 'celestial'
|
||||||
/** Agents moving along polylines (traffic flow). */
|
/** Agents moving along polylines (traffic flow). */
|
||||||
| 'agents'
|
| 'agents'
|
||||||
|
/** Satellites propagated by the renderer from their elements (`draw.orbits`). */
|
||||||
|
| 'orbits'
|
||||||
|
/** Rate-limited course easing (`IconItem.courseGlide`). */
|
||||||
|
| 'courseGlide'
|
||||||
/** Screen-space clustering of icons. */
|
/** Screen-space clustering of icons. */
|
||||||
| 'cluster'
|
| 'cluster'
|
||||||
/** Post-processing styles. */
|
/** Post-processing styles. */
|
||||||
|
|||||||
Vendored
+78
-11
@@ -55,12 +55,16 @@ export interface SetSpec {
|
|||||||
drawOrder?: number;
|
drawOrder?: number;
|
||||||
/** `false`: the set never takes part in picks. */
|
/** `false`: the set never takes part in picks. */
|
||||||
pickable?: boolean;
|
pickable?: boolean;
|
||||||
/** Height queries see through this set. */
|
/**
|
||||||
excludeFromHeightQueries?: boolean;
|
* Height queries (`query.sampleHeight` and the rest) see this set as
|
||||||
|
* ground. Off by default: queries answer from tiles and terrain alone.
|
||||||
|
*/
|
||||||
|
includeInHeightQueries?: boolean;
|
||||||
/** What clamped items clamp to. */
|
/** What clamped items clamp to. */
|
||||||
clampTo?: ClampTarget;
|
clampTo?: ClampTarget;
|
||||||
/**
|
/**
|
||||||
* `motionSegments`: the clock motion segments are evaluated on.
|
* `motionSegments` or `orbits`: the clock motion segments and orbits are
|
||||||
|
* evaluated on.
|
||||||
* `live` (the default) is wall time minus `delayMs`, as a live feed is
|
* `live` (the default) is wall time minus `delayMs`, as a live feed is
|
||||||
* drawn a little behind real time; `scene` is the frame port's scene time.
|
* drawn a little behind real time; `scene` is the frame port's scene time.
|
||||||
*/
|
*/
|
||||||
@@ -79,8 +83,16 @@ export interface Placed {
|
|||||||
pickable?: boolean;
|
pickable?: boolean;
|
||||||
scaleByDistance?: NearFar;
|
scaleByDistance?: NearFar;
|
||||||
fadeByDistance?: NearFar;
|
fadeByDistance?: NearFar;
|
||||||
/** Shown only between these eye distances, metres. */
|
/** Shown only at eye distances in `[near, far)`, metres. */
|
||||||
displayRange?: [near: number, far: number];
|
displayRange?: [near: number, far: number];
|
||||||
|
/**
|
||||||
|
* Items in different sets that share a `lodGroup` are one thing drawn at
|
||||||
|
* different detail (a model near, an icon far), each shown over its own
|
||||||
|
* `displayRange`. A renderer may draw the group as one entity; a pick on
|
||||||
|
* it names the member shown at that distance (`Hit.set`, `Hit.id`) and
|
||||||
|
* reports the group (`Hit.lodGroup`).
|
||||||
|
*/
|
||||||
|
lodGroup?: string;
|
||||||
/** Draw over depth within this many metres of the eye (`Infinity`: always). */
|
/** Draw over depth within this many metres of the eye (`Infinity`: always). */
|
||||||
disableDepthTest?: number;
|
disableDepthTest?: number;
|
||||||
}
|
}
|
||||||
@@ -102,12 +114,14 @@ export type ImageRef =
|
|||||||
* at both ends; the course runs from `course0Deg` by `courseSlopeDeg`
|
* at both ends; the course runs from `course0Deg` by `courseSlopeDeg`
|
||||||
* across the segment.
|
* across the segment.
|
||||||
* - arc: a constant-rate turn in the plane tangent to the ellipsoid at `fix`
|
* - arc: a constant-rate turn in the plane tangent to the ellipsoid at `fix`
|
||||||
* (its east and north axes), at the fix's height, from `fix` at `t0`, at
|
* (its east and north axes), from `fix` at `t0`, at `speedMps` along
|
||||||
* `speedMps` along `trackDeg` turning at `turnRateDps` (under 1e-4 rad/s is
|
* `trackDeg` turning at `turnRateDps` (under 1e-4 rad/s is straight), and
|
||||||
* straight). The elapsed time is clamped to `[minDtS, maxDtS]`; a negative
|
* climbing along the fix's up axis at `climbMps` (default 0). The elapsed
|
||||||
* `minDtS` runs the fix backward before `t0` (a warm-up), `maxDtS` is how
|
* time is clamped to `[minDtS, maxDtS]`; a negative `minDtS` runs the same
|
||||||
* long it coasts.
|
* path backward before `t0` (a warm-up), `maxDtS` is how long it coasts.
|
||||||
* `courseGlide` eases the displayed course onto the segment's at `rateDps`.
|
* The drawn course eases onto a new one over the item's `courseBlendMs`; an
|
||||||
|
* item that also has `courseGlide` eases at its rate instead, on a renderer
|
||||||
|
* with the `courseGlide` capability.
|
||||||
*/
|
*/
|
||||||
export type MotionSegment =
|
export type MotionSegment =
|
||||||
| { kind: 'hold'; courseDeg?: number }
|
| { kind: 'hold'; courseDeg?: number }
|
||||||
@@ -129,6 +143,8 @@ export type MotionSegment =
|
|||||||
speedMps: number;
|
speedMps: number;
|
||||||
trackDeg: number;
|
trackDeg: number;
|
||||||
turnRateDps: number;
|
turnRateDps: number;
|
||||||
|
/** Metres per second along the fix's up axis. */
|
||||||
|
climbMps?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CourseGlide {
|
export interface CourseGlide {
|
||||||
@@ -147,7 +163,14 @@ export interface IconItem extends Placed {
|
|||||||
scale?: number;
|
scale?: number;
|
||||||
/** `motionSegments`: motion on the renderer's clock. */
|
/** `motionSegments`: motion on the renderer's clock. */
|
||||||
motion?: MotionSegment;
|
motion?: MotionSegment;
|
||||||
|
/** `courseGlide`: ease the drawn course at a rate, from `fromCourseDeg`. */
|
||||||
courseGlide?: CourseGlide;
|
courseGlide?: CourseGlide;
|
||||||
|
/**
|
||||||
|
* Ease the drawn course onto a new one over this long; 0 or omitted turns
|
||||||
|
* at once. Every renderer has it; with the `courseGlide` capability an
|
||||||
|
* item's `courseGlide`, when it has one, is used instead.
|
||||||
|
*/
|
||||||
|
courseBlendMs?: number;
|
||||||
/** Degrees clockwise from north. */
|
/** Degrees clockwise from north. */
|
||||||
course?: number;
|
course?: number;
|
||||||
/**
|
/**
|
||||||
@@ -389,7 +412,12 @@ export type CelestialSpec =
|
|||||||
| {
|
| {
|
||||||
kind: 'stars';
|
kind: 'stars';
|
||||||
id: string;
|
id: string;
|
||||||
catalogue: Float32Array;
|
/**
|
||||||
|
* Packed stars; omitted draws the renderer's own catalogue (one it
|
||||||
|
* streams, say), down to `magnitudeLimit`, or no stars when it has
|
||||||
|
* none.
|
||||||
|
*/
|
||||||
|
catalogue?: Float32Array;
|
||||||
magnitudeLimit?: number;
|
magnitudeLimit?: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -401,6 +429,29 @@ export type CelestialSpec =
|
|||||||
widthPx: number;
|
widthPx: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `orbits`: a satellite from its elements. The renderer propagates it with
|
||||||
|
* SGP4 on its set's `motionClock` and draws the marker and, optionally, its
|
||||||
|
* ring. Without the capability Core propagates and draws the positions
|
||||||
|
* itself as icons and lines.
|
||||||
|
*/
|
||||||
|
export interface OrbitItem {
|
||||||
|
id: string;
|
||||||
|
/** A two-line element set. */
|
||||||
|
tle: [line1: string, line2: string];
|
||||||
|
marker:
|
||||||
|
| { image: ImageRef; size: [width: number, height: number] }
|
||||||
|
| { pixelSize: number; color: Color };
|
||||||
|
/** The orbit's ring over `periods` orbits (default 1), in the inertial frame. */
|
||||||
|
ring?: { color: Color; widthPx: number; periods?: number } | null;
|
||||||
|
meta?: Meta;
|
||||||
|
show?: boolean;
|
||||||
|
pickable?: boolean;
|
||||||
|
/** As on `Placed`. */
|
||||||
|
displayRange?: [near: number, far: number];
|
||||||
|
lodGroup?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** `agents`: dots moving along polylines (traffic flow). */
|
/** `agents`: dots moving along polylines (traffic flow). */
|
||||||
export interface AgentPathItem {
|
export interface AgentPathItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -426,6 +477,10 @@ export interface Columns {
|
|||||||
positions?: Float64Array;
|
positions?: Float64Array;
|
||||||
/** Degrees clockwise from north. */
|
/** Degrees clockwise from north. */
|
||||||
courses?: Float32Array;
|
courses?: Float32Array;
|
||||||
|
/**
|
||||||
|
* 0 hides a row. A hidden row keeps its motion state (where it is on its
|
||||||
|
* segment, its eased course), so showing it again resumes, not restarts.
|
||||||
|
*/
|
||||||
shown?: Uint8Array;
|
shown?: Uint8Array;
|
||||||
scales?: Float32Array;
|
scales?: Float32Array;
|
||||||
alphas?: Float32Array;
|
alphas?: Float32Array;
|
||||||
@@ -472,6 +527,18 @@ export interface DrawPort {
|
|||||||
celestial?(spec: CelestialSpec): Handle;
|
celestial?(spec: CelestialSpec): Handle;
|
||||||
/** `agents`. */
|
/** `agents`. */
|
||||||
agents?(spec: SetSpec): DrawSet<AgentPathItem>;
|
agents?(spec: SetSpec): DrawSet<AgentPathItem>;
|
||||||
|
/** `orbits`. */
|
||||||
|
orbits?(spec: SetSpec): DrawSet<OrbitItem>;
|
||||||
|
/**
|
||||||
|
* Whether clamped items (`clampTo: 'tiles' | 'both'`, draped lines and
|
||||||
|
* shapes) are draped onto the 3D tiles now (`draped`), or drawn over them
|
||||||
|
* and hidden past the ellipsoid's horizon (`overlay`), the fallback a
|
||||||
|
* renderer uses where it cannot drape. Without tiles in the base it is
|
||||||
|
* `draped`: clamping to terrain or the ellipsoid always drapes.
|
||||||
|
*/
|
||||||
|
clampState(): 'draped' | 'overlay';
|
||||||
|
/** Fires when `clampState()` changes (not on subscribing). */
|
||||||
|
on(event: 'clampState', cb: (state: 'draped' | 'overlay') => void): Off;
|
||||||
/** Register an atlas of images icons refer to by slot. */
|
/** Register an atlas of images icons refer to by slot. */
|
||||||
atlas(id: string, images: Record<string, ImageRef>): void;
|
atlas(id: string, images: Record<string, ImageRef>): void;
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,8 @@ export const CAPABILITIES = Object.freeze([
|
|||||||
'fields',
|
'fields',
|
||||||
'celestial',
|
'celestial',
|
||||||
'agents',
|
'agents',
|
||||||
|
'orbits',
|
||||||
|
'courseGlide',
|
||||||
'cluster',
|
'cluster',
|
||||||
'styles',
|
'styles',
|
||||||
'bloom',
|
'bloom',
|
||||||
@@ -149,13 +151,22 @@ export const PORTS = deepFreeze({
|
|||||||
['imagery.setSplit', 'splitSwipe'],
|
['imagery.setSplit', 'splitSwipe'],
|
||||||
]),
|
]),
|
||||||
draw: Object.fromEntries([
|
draw: Object.fromEntries([
|
||||||
...required('icons', 'points', 'lines', 'shapes', 'atlas'),
|
...required(
|
||||||
|
'icons',
|
||||||
|
'points',
|
||||||
|
'lines',
|
||||||
|
'shapes',
|
||||||
|
'atlas',
|
||||||
|
'clampState',
|
||||||
|
'on',
|
||||||
|
),
|
||||||
['models', 'models'],
|
['models', 'models'],
|
||||||
['quads', 'quads'],
|
['quads', 'quads'],
|
||||||
['meshes', 'meshes'],
|
['meshes', 'meshes'],
|
||||||
['field', 'fields'],
|
['field', 'fields'],
|
||||||
['celestial', 'celestial'],
|
['celestial', 'celestial'],
|
||||||
['agents', 'agents'],
|
['agents', 'agents'],
|
||||||
|
['orbits', 'orbits'],
|
||||||
]),
|
]),
|
||||||
camera: Object.fromEntries(
|
camera: Object.fromEntries(
|
||||||
required(
|
required(
|
||||||
|
|||||||
Vendored
+10
-3
@@ -15,6 +15,8 @@ export interface Hit {
|
|||||||
id: string;
|
id: string;
|
||||||
/** The item's `meta`, as it was given; absent when the item had none. */
|
/** The item's `meta`, as it was given; absent when the item had none. */
|
||||||
meta?: Meta;
|
meta?: Meta;
|
||||||
|
/** The item's `lodGroup`, when it has one: the thing it is a detail level of. */
|
||||||
|
lodGroup?: string;
|
||||||
/** The map itself (tiles or globe), rather than a drawn item. */
|
/** The map itself (tiles or globe), rather than a drawn item. */
|
||||||
surface: boolean;
|
surface: boolean;
|
||||||
/** Where the hit is, when the renderer knows it. */
|
/** Where the hit is, when the renderer knows it. */
|
||||||
@@ -33,9 +35,13 @@ export interface PickWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HeightOptions {
|
export interface HeightOptions {
|
||||||
/** `surface`: tiles where they are drawn, otherwise terrain. */
|
/**
|
||||||
|
* `surface`: tiles where they are drawn, otherwise terrain. Sets drawn
|
||||||
|
* with `includeInHeightQueries` count under `surface` and `tiles`, never
|
||||||
|
* under `terrain`.
|
||||||
|
*/
|
||||||
against?: 'surface' | 'terrain' | 'tiles';
|
against?: 'surface' | 'terrain' | 'tiles';
|
||||||
/** Set ids to see through. */
|
/** Set ids to see through, of those drawn with `includeInHeightQueries`. */
|
||||||
exclude?: string[];
|
exclude?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +50,8 @@ export interface HeightSample {
|
|||||||
height: number;
|
height: number;
|
||||||
/** 1 for the most detailed data, lower for a coarser level. */
|
/** 1 for the most detailed data, lower for a coarser level. */
|
||||||
confidence: number;
|
confidence: number;
|
||||||
source: 'tiles' | 'terrain' | 'ellipsoid';
|
/** `set`: a draw set drawn with `includeInHeightQueries`. */
|
||||||
|
source: 'tiles' | 'terrain' | 'ellipsoid' | 'set';
|
||||||
/** The detail answered from: what is drawn now, or (`finestHeights`) the finest available. */
|
/** The detail answered from: what is drawn now, or (`finestHeights`) the finest available. */
|
||||||
lod: 'visible' | 'finest';
|
lod: 'visible' | 'finest';
|
||||||
/** Edge length of the tile answered from, metres, when known. */
|
/** Edge length of the tile answered from, metres, when known. */
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import { CAPABILITIES, CONTRACT_VERSION } from '../contract/index.js';
|
import { CAPABILITIES, CONTRACT_VERSION } from '../contract/index.js';
|
||||||
import { createStubCamera } from './camera.js';
|
import { createStubCamera } from './camera.js';
|
||||||
import { motionPosition } from './motion.js';
|
import { motionPosition } from './motion.js';
|
||||||
|
import { orbitPosition } from './orbits.js';
|
||||||
import { computeViewRectangle } from './viewRectangle.js';
|
import { computeViewRectangle } from './viewRectangle.js';
|
||||||
import {
|
import {
|
||||||
add,
|
add,
|
||||||
@@ -408,6 +409,7 @@ export function createStubRenderer(options = {}) {
|
|||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clampListeners = [];
|
||||||
const draw = {
|
const draw = {
|
||||||
icons: (spec) => makeSet('icons', spec),
|
icons: (spec) => makeSet('icons', spec),
|
||||||
points: (spec) => makeSet('points', spec),
|
points: (spec) => makeSet('points', spec),
|
||||||
@@ -417,6 +419,18 @@ export function createStubRenderer(options = {}) {
|
|||||||
atlases.set(id, { ...images });
|
atlases.set(id, { ...images });
|
||||||
log('draw', 'atlas', [id]);
|
log('draw', 'atlas', [id]);
|
||||||
},
|
},
|
||||||
|
// The stub drapes wherever it is asked to: it never falls back.
|
||||||
|
clampState: () => 'draped',
|
||||||
|
on(event, cb) {
|
||||||
|
if (event !== 'clampState') return () => {};
|
||||||
|
const entry = { cb, live: true };
|
||||||
|
clampListeners.push(entry);
|
||||||
|
return () => {
|
||||||
|
entry.live = false;
|
||||||
|
const i = clampListeners.indexOf(entry);
|
||||||
|
if (i >= 0) clampListeners.splice(i, 1);
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
if (capabilities.has('models'))
|
if (capabilities.has('models'))
|
||||||
draw.models = (spec) => makeSet('models', spec);
|
draw.models = (spec) => makeSet('models', spec);
|
||||||
@@ -425,6 +439,10 @@ export function createStubRenderer(options = {}) {
|
|||||||
draw.meshes = (spec) => makeSet('meshes', spec);
|
draw.meshes = (spec) => makeSet('meshes', spec);
|
||||||
if (capabilities.has('agents'))
|
if (capabilities.has('agents'))
|
||||||
draw.agents = (spec) => makeSet('agents', spec);
|
draw.agents = (spec) => makeSet('agents', spec);
|
||||||
|
/** Parsed orbit elements, by line pair; cleared on destroy. */
|
||||||
|
const satrecs = new Map();
|
||||||
|
if (capabilities.has('orbits'))
|
||||||
|
draw.orbits = (spec) => makeSet('orbits', spec);
|
||||||
if (capabilities.has('fields'))
|
if (capabilities.has('fields'))
|
||||||
draw.field = (spec) => {
|
draw.field = (spec) => {
|
||||||
const handle = simpleHandle('field', spec);
|
const handle = simpleHandle('field', spec);
|
||||||
@@ -528,6 +546,10 @@ export function createStubRenderer(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function itemPosition(set, item, aheadMs = 0) {
|
function itemPosition(set, item, aheadMs = 0) {
|
||||||
|
if (set.kind === 'orbits')
|
||||||
|
return item.tle
|
||||||
|
? orbitPosition(satrecs, item.tle, motionTime(set) + aheadMs)
|
||||||
|
: null;
|
||||||
if (!item.pos) return null;
|
if (!item.pos) return null;
|
||||||
const base = toEcef(item.pos);
|
const base = toEcef(item.pos);
|
||||||
if (
|
if (
|
||||||
@@ -556,17 +578,25 @@ export function createStubRenderer(options = {}) {
|
|||||||
if (
|
if (
|
||||||
set.kind !== 'icons' &&
|
set.kind !== 'icons' &&
|
||||||
set.kind !== 'points' &&
|
set.kind !== 'points' &&
|
||||||
set.kind !== 'models'
|
set.kind !== 'models' &&
|
||||||
|
set.kind !== 'orbits'
|
||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
for (const item of set.items.values()) {
|
for (const item of set.items.values()) {
|
||||||
if (item.show === false || item.pickable === false) continue;
|
if (item.show === false || item.pickable === false) continue;
|
||||||
const p = itemPosition(set, item);
|
const p = itemPosition(set, item);
|
||||||
if (!p || isOccluded(p)) continue;
|
if (!p || isOccluded(p)) continue;
|
||||||
|
if (item.displayRange) {
|
||||||
|
const distance = magnitude(sub(p, eye().position));
|
||||||
|
const [nearM, farM] = item.displayRange;
|
||||||
|
if (distance < nearM || distance >= farM) continue;
|
||||||
|
}
|
||||||
const s = project(p);
|
const s = project(p);
|
||||||
if (!s) continue;
|
if (!s) continue;
|
||||||
|
const pixel = item.pixelSize ?? item.marker?.pixelSize ?? 10;
|
||||||
const size = item.pickProxy?.size ??
|
const size = item.pickProxy?.size ??
|
||||||
item.size ?? [item.pixelSize ?? 10, item.pixelSize ?? 10];
|
item.size ??
|
||||||
|
item.marker?.size ?? [pixel, pixel];
|
||||||
if (
|
if (
|
||||||
Math.abs(s.x - x) <= size[0] / 2 + halfW &&
|
Math.abs(s.x - x) <= size[0] / 2 + halfW &&
|
||||||
Math.abs(s.y - y) <= size[1] / 2 + halfH
|
Math.abs(s.y - y) <= size[1] / 2 + halfH
|
||||||
@@ -575,6 +605,7 @@ export function createStubRenderer(options = {}) {
|
|||||||
set: setId,
|
set: setId,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
meta: item.meta,
|
meta: item.meta,
|
||||||
|
...(item.lodGroup !== undefined ? { lodGroup: item.lodGroup } : {}),
|
||||||
surface: false,
|
surface: false,
|
||||||
position: p,
|
position: p,
|
||||||
depth: magnitude(sub(p, eye().position)),
|
depth: magnitude(sub(p, eye().position)),
|
||||||
@@ -1166,6 +1197,7 @@ export function createStubRenderer(options = {}) {
|
|||||||
for (const clear of onceTimers) clear();
|
for (const clear of onceTimers) clear();
|
||||||
onceTimers.clear();
|
onceTimers.clear();
|
||||||
sets.clear();
|
sets.clear();
|
||||||
|
satrecs.clear();
|
||||||
},
|
},
|
||||||
testing: {
|
testing: {
|
||||||
/** Run one frame of the loop; true when it drew. */
|
/** Run one frame of the loop; true when it drew. */
|
||||||
|
|||||||
@@ -44,8 +44,12 @@ export function motionPosition(segment, pos, t) {
|
|||||||
east = (v / w) * (Math.cos(track) - Math.cos(track + w * dt));
|
east = (v / w) * (Math.cos(track) - Math.cos(track + w * dt));
|
||||||
north = (v / w) * (Math.sin(track + w * dt) - Math.sin(track));
|
north = (v / w) * (Math.sin(track + w * dt) - Math.sin(track));
|
||||||
}
|
}
|
||||||
|
const up = (segment.climbMps ?? 0) * dt;
|
||||||
const enu = eastNorthUp(segment.fix);
|
const enu = eastNorthUp(segment.fix);
|
||||||
return add(segment.fix, add(scale(enu.x, east), scale(enu.y, north)));
|
return add(
|
||||||
|
segment.fix,
|
||||||
|
add(add(scale(enu.x, east), scale(enu.y, north)), scale(enu.z, up)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`unknown motion segment ${segment.kind}`);
|
throw new Error(`unknown motion segment ${segment.kind}`);
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* @module render/stub/orbits
|
||||||
|
* @description Where an orbit item is at a time: its two-line elements
|
||||||
|
* propagated with SGP4 and turned into Earth-fixed metres.
|
||||||
|
*/
|
||||||
|
import { eciToEcf, gstime, propagate, twoline2satrec } from 'satellite.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Map<string, object>} satrecs Parsed elements by line pair, the caller's cache.
|
||||||
|
* @param {[string, string]} tle
|
||||||
|
* @param {number} t Epoch ms.
|
||||||
|
* @returns {{x: number, y: number, z: number}|null} ECEF metres, or null when the elements cannot be propagated to `t`.
|
||||||
|
*/
|
||||||
|
export function orbitPosition(satrecs, tle, t) {
|
||||||
|
const key = `${tle[0]}\n${tle[1]}`;
|
||||||
|
let satrec = satrecs.get(key);
|
||||||
|
if (!satrec) {
|
||||||
|
satrec = twoline2satrec(tle[0], tle[1]);
|
||||||
|
satrecs.set(key, satrec);
|
||||||
|
}
|
||||||
|
const date = new Date(t);
|
||||||
|
const state = propagate(satrec, date);
|
||||||
|
const eci = state?.position;
|
||||||
|
if (!eci || typeof eci !== 'object') return null;
|
||||||
|
const ecf = eciToEcf(eci, gstime(date));
|
||||||
|
return { x: ecf.x * 1000, y: ecf.y * 1000, z: ecf.z * 1000 };
|
||||||
|
}
|
||||||
Reference in new issue
Block a user