🔭 review: hash restores full camera pose; tick-loop hygiene (fable)
- pendingCam now restores heading+pitch (inverse of serializeHash spherical), not just distance — pasted URLs reproduce the exact view (Stage 2 gate) - bodyWorld.sun allocated once, not per frame (§11 zero-alloc tick rule) - spacecraft/comet label DOM writes throttled ≤5 Hz (§11); geometry still per-frame Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4352394a56
commit
228caf2270
@ -78,12 +78,17 @@ export default function create(ctx) {
|
|||||||
c.geo.computeBoundingSphere();
|
c.geo.computeBoundingSphere();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastTextMs = 0;
|
||||||
return {
|
return {
|
||||||
id: 'comets',
|
id: 'comets',
|
||||||
onClockTick(simMs, jd) {
|
onClockTick(simMs, jd) {
|
||||||
if (!on) return;
|
if (!on) return;
|
||||||
const transitioning = scale.isTransitioning();
|
const transitioning = scale.isTransitioning();
|
||||||
const camDist = ctx.camera.position.length();
|
const camDist = ctx.camera.position.length();
|
||||||
|
// label DOM writes throttled ≤5 Hz (brief §11)
|
||||||
|
const nowMs = performance.now();
|
||||||
|
const updateText = nowMs - lastTextMs > 200;
|
||||||
|
if (updateText) lastTextMs = nowMs;
|
||||||
for (const c of comets) {
|
for (const c of comets) {
|
||||||
ephem.smallBodyEcl(c.el, jd, _e);
|
ephem.smallBodyEcl(c.el, jd, _e);
|
||||||
const r = Math.hypot(_e.x, _e.y, _e.z);
|
const r = Math.hypot(_e.x, _e.y, _e.z);
|
||||||
@ -98,7 +103,7 @@ export default function create(ctx) {
|
|||||||
tp[0] = c.spr.position.x; tp[1] = c.spr.position.y; tp[2] = c.spr.position.z;
|
tp[0] = c.spr.position.x; tp[1] = c.spr.position.y; tp[2] = c.spr.position.z;
|
||||||
tp[3] = c.spr.position.x + _dir.x * len; tp[4] = c.spr.position.y + _dir.y * len; tp[5] = c.spr.position.z + _dir.z * len;
|
tp[3] = c.spr.position.x + _dir.x * len; tp[4] = c.spr.position.y + _dir.y * len; tp[5] = c.spr.position.z + _dir.z * len;
|
||||||
c.tgeo.attributes.position.needsUpdate = true;
|
c.tgeo.attributes.position.needsUpdate = true;
|
||||||
c.lbl.div.textContent = `${c.name} · ${lib.fmtAU(r)}`;
|
if (updateText) c.lbl.div.textContent = `${c.name} · ${lib.fmtAU(r)}`;
|
||||||
// orbit rebuild on T-drift or scale transition (brief §11)
|
// orbit rebuild on T-drift or scale transition (brief §11)
|
||||||
const needAu = Math.abs(lib.centuriesSinceJ2000(jd) - c.lastT) > CONFIG.orbitRebuildCy;
|
const needAu = Math.abs(lib.centuriesSinceJ2000(jd) - c.lastT) > CONFIG.orbitRebuildCy;
|
||||||
if (needAu) { c.auPath = ephem.orbitPathFromElements(c.el, N); c.lastT = lib.centuriesSinceJ2000(jd); }
|
if (needAu) { c.auPath = ephem.orbitPathFromElements(c.el, N); c.lastT = lib.centuriesSinceJ2000(jd); }
|
||||||
|
|||||||
@ -118,10 +118,15 @@ export default function create(ctx) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastTextMs = 0;
|
||||||
return {
|
return {
|
||||||
id: 'spacecraft',
|
id: 'spacecraft',
|
||||||
onClockTick(simMs, jd) {
|
onClockTick(simMs, jd) {
|
||||||
const camDist = ctx.camera.position.length();
|
const camDist = ctx.camera.position.length();
|
||||||
|
// label DOM writes throttled ≤5 Hz (brief §11); geometry still updates per frame
|
||||||
|
const nowMs = performance.now();
|
||||||
|
const updateText = nowMs - lastTextMs > 200;
|
||||||
|
if (updateText) lastTextMs = nowMs;
|
||||||
let earthDone = false;
|
let earthDone = false;
|
||||||
for (const cr of crafts) {
|
for (const cr of crafts) {
|
||||||
const live = show && cr.samples && interp(cr.samples, jd, _p);
|
const live = show && cr.samples && interp(cr.samples, jd, _p);
|
||||||
@ -143,9 +148,11 @@ export default function create(ctx) {
|
|||||||
cr.tgeo.setDrawRange(0, n);
|
cr.tgeo.setDrawRange(0, n);
|
||||||
cr.tgeo.attributes.position.needsUpdate = true;
|
cr.tgeo.attributes.position.needsUpdate = true;
|
||||||
cr.trail.visible = n > 1;
|
cr.trail.visible = n > 1;
|
||||||
const rSun = Math.hypot(_p.x, _p.y, _p.z);
|
if (updateText) {
|
||||||
const rEarth = Math.hypot(_p.x - _earth.x, _p.y - _earth.y, _p.z - _earth.z);
|
const rSun = Math.hypot(_p.x, _p.y, _p.z);
|
||||||
cr.lbl.div.textContent = `${cr.c.name} · ${lib.fmtAU(rSun)} · ${lib.fmtLightTime(rEarth)} lt`;
|
const rEarth = Math.hypot(_p.x - _earth.x, _p.y - _earth.y, _p.z - _earth.z);
|
||||||
|
cr.lbl.div.textContent = `${cr.c.name} · ${lib.fmtAU(rSun)} · ${lib.fmtLightTime(rEarth)} lt`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handlePick(raycaster) {
|
handlePick(raycaster) {
|
||||||
|
|||||||
17
js/main.js
17
js/main.js
@ -41,7 +41,9 @@ const scene = new THREE.Scene();
|
|||||||
const camera = new THREE.PerspectiveCamera(CONFIG.camera.fov, innerWidth / innerHeight, CONFIG.camera.near, CONFIG.camera.far);
|
const camera = new THREE.PerspectiveCamera(CONFIG.camera.fov, innerWidth / innerHeight, CONFIG.camera.near, CONFIG.camera.far);
|
||||||
camera.position.set(0, 26, 78);
|
camera.position.set(0, 26, 78);
|
||||||
|
|
||||||
const controls = new OrbitControls(camera, labelRenderer.domElement);
|
// Bind to the WebGL canvas, NOT labelRenderer.domElement — that div lives inside
|
||||||
|
// #labels {pointer-events:none}, so OrbitControls would never receive input.
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true;
|
||||||
controls.dampingFactor = 0.08;
|
controls.dampingFactor = 0.08;
|
||||||
controls.minDistance = CONFIG.camera.minDistance;
|
controls.minDistance = CONFIG.camera.minDistance;
|
||||||
@ -168,6 +170,7 @@ const focus = {
|
|||||||
|
|
||||||
// bodyWorld[id] = absolute view-world position, plain float64 {x,y,z}.
|
// bodyWorld[id] = absolute view-world position, plain float64 {x,y,z}.
|
||||||
const bodyWorld = Object.create(null);
|
const bodyWorld = Object.create(null);
|
||||||
|
bodyWorld.sun = { x: 0, y: 0, z: 0 }; // heliocentric origin, never moves
|
||||||
const focusOffset = { x: 0, y: 0, z: 0 };
|
const focusOffset = { x: 0, y: 0, z: 0 };
|
||||||
const _ecl = {};
|
const _ecl = {};
|
||||||
|
|
||||||
@ -176,7 +179,7 @@ function computeBodyWorld() {
|
|||||||
for (const id in BODIES) {
|
for (const id in BODIES) {
|
||||||
const b = BODIES[id];
|
const b = BODIES[id];
|
||||||
if (b.type === 'moon') continue;
|
if (b.type === 'moon') continue;
|
||||||
if (id === 'sun') { bodyWorld.sun = { x: 0, y: 0, z: 0 }; continue; }
|
if (id === 'sun') continue;
|
||||||
if (b.ephemId) ephem.helioEcl(b.ephemId, clock.jd, _ecl);
|
if (b.ephemId) ephem.helioEcl(b.ephemId, clock.jd, _ecl);
|
||||||
else if (b.elements) ephem.smallBodyEcl(b.elements, clock.jd, _ecl);
|
else if (b.elements) ephem.smallBodyEcl(b.elements, clock.jd, _ecl);
|
||||||
else continue;
|
else continue;
|
||||||
@ -464,7 +467,15 @@ wireControls();
|
|||||||
if (DEBUG) document.getElementById('debug').hidden = false;
|
if (DEBUG) document.getElementById('debug').hidden = false;
|
||||||
try { applyHash(); } catch (err) { console.warn('bad hash', err); }
|
try { applyHash(); } catch (err) { console.warn('bad hash', err); }
|
||||||
setFocus(focus.id, false);
|
setFocus(focus.id, false);
|
||||||
if (pendingCam) { camera.position.setLength(pendingCam[0]); } // heading/pitch restored loosely via length; orbit drag refines
|
if (pendingCam) {
|
||||||
|
// restore the full spherical pose — inverse of serializeHash's
|
||||||
|
// heading = atan2(x, z), pitch = asin(y / dist)
|
||||||
|
const [d, heading, pitch] = pendingCam;
|
||||||
|
camera.position.set(
|
||||||
|
d * Math.cos(pitch) * Math.sin(heading),
|
||||||
|
d * Math.sin(pitch),
|
||||||
|
d * Math.cos(pitch) * Math.cos(heading));
|
||||||
|
}
|
||||||
|
|
||||||
loadLayers().then(() => {
|
loadLayers().then(() => {
|
||||||
if (pendingLayers !== null) {
|
if (pendingLayers !== null) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user