🔭 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();
|
||||
}
|
||||
|
||||
let lastTextMs = 0;
|
||||
return {
|
||||
id: 'comets',
|
||||
onClockTick(simMs, jd) {
|
||||
if (!on) return;
|
||||
const transitioning = scale.isTransitioning();
|
||||
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) {
|
||||
ephem.smallBodyEcl(c.el, jd, _e);
|
||||
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[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.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)
|
||||
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); }
|
||||
|
||||
@ -118,10 +118,15 @@ export default function create(ctx) {
|
||||
return out;
|
||||
}
|
||||
|
||||
let lastTextMs = 0;
|
||||
return {
|
||||
id: 'spacecraft',
|
||||
onClockTick(simMs, jd) {
|
||||
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;
|
||||
for (const cr of crafts) {
|
||||
const live = show && cr.samples && interp(cr.samples, jd, _p);
|
||||
@ -143,10 +148,12 @@ export default function create(ctx) {
|
||||
cr.tgeo.setDrawRange(0, n);
|
||||
cr.tgeo.attributes.position.needsUpdate = true;
|
||||
cr.trail.visible = n > 1;
|
||||
if (updateText) {
|
||||
const rSun = Math.hypot(_p.x, _p.y, _p.z);
|
||||
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) {
|
||||
const markers = crafts.filter((c) => c.spr.visible).map((c) => c.spr);
|
||||
|
||||
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);
|
||||
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.dampingFactor = 0.08;
|
||||
controls.minDistance = CONFIG.camera.minDistance;
|
||||
@ -168,6 +170,7 @@ const focus = {
|
||||
|
||||
// bodyWorld[id] = absolute view-world position, plain float64 {x,y,z}.
|
||||
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 _ecl = {};
|
||||
|
||||
@ -176,7 +179,7 @@ function computeBodyWorld() {
|
||||
for (const id in BODIES) {
|
||||
const b = BODIES[id];
|
||||
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);
|
||||
else if (b.elements) ephem.smallBodyEcl(b.elements, clock.jd, _ecl);
|
||||
else continue;
|
||||
@ -464,7 +467,15 @@ wireControls();
|
||||
if (DEBUG) document.getElementById('debug').hidden = false;
|
||||
try { applyHash(); } catch (err) { console.warn('bad hash', err); }
|
||||
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(() => {
|
||||
if (pendingLayers !== null) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user