merge stage/3-retinue → main: moons + rings, gate green (opus)

This commit is contained in:
monsterrobotparty 2026-07-16 00:19:28 +10:00
commit fedf05d8a2
3 changed files with 110 additions and 1 deletions

43
js/layers/moons.js Normal file
View File

@ -0,0 +1,43 @@
// SOLARGOD moons layer (Stage 3) — every moon in the registry as a lit sphere,
// positioned by ctx.bodyWorld (main.js computes the local compressed offset in the
// parent's equatorial plane; retrograde via negative period). Moons draw at the
// SAME exaggeration as their parent, so the true radius ratio (e.g. Io/Jupiter
// ≈ 0.026) is preserved. Labels appear only when the parent (or the moon) is the
// focus, to keep the wide view clean (brief §5).
export default function create(ctx) {
const { THREE, scene, CONFIG, ui, scale, bodies, focus, toLocal, effectiveE, makeLabel, registerPick } = ctx;
const entries = [];
for (const id in bodies) {
const b = bodies[id];
if (b.type !== 'moon') continue;
const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color(b.color), roughness: 1, metalness: 0 });
const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 24, 16), mat);
mesh.visible = true;
scene.add(mesh);
registerPick(mesh, id);
const lbl = makeLabel(mesh, b.name, 'body-label moon');
lbl.obj.visible = false; // CSS2DRenderer honors obj.visible (not div.style.display)
entries.push({ id, b, mesh, lbl });
}
let show = true;
ui.addLayer('moons', 'Moons', true, (on) => { show = on; for (const e of entries) e.mesh.visible = on; });
ui.setStatus('moons', `${entries.length} moons`, 'ok');
return {
id: 'moons',
onClockTick(simMs, jd) {
for (const e of entries) {
const abs = ctx.bodyWorld[e.id];
if (!abs) continue;
toLocal(abs, e.mesh.position);
e.mesh.scale.setScalar(scale.drawRadius(e.b.radiusKm, effectiveE(e.b.parent)));
const near = focus.id === e.b.parent || focus.id === e.id;
e.lbl.obj.visible = show && near;
e.lbl.div.classList.toggle('focused', e.id === focus.id);
}
},
};
}

66
js/layers/rings.js Normal file
View File

@ -0,0 +1,66 @@
// SOLARGOD rings layer (Stage 3) — Saturn's textured annulus + Uranus's thin
// schematic ring. Sized in true km scaled by the parent's draw scale, tilted with
// the parent's axial tilt, DoubleSide + transparent (brief §7). Texture optional:
// a flat color fallback keeps the ring visible if the PNG is missing.
export default function create(ctx) {
const { THREE, scene, lib, ui, scale, bodies, toLocal, effectiveE, CONFIG } = ctx;
const AU_KM = lib.AU_KM;
const RING_FALLBACK = { saturn: '#c9b98f', uranus: '#8fa3a8' };
const entries = [];
for (const id of ['saturn', 'uranus']) {
const b = bodies[id];
if (!b.rings) continue;
const ratio = b.rings.innerKm / b.rings.outerKm;
const geo = new THREE.RingGeometry(ratio, 1, 128, 1);
radialUV(geo); // remap UV so a radial ring texture maps across width
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color(b.rings.color || RING_FALLBACK[id] || '#bbb'),
side: THREE.DoubleSide, transparent: true, opacity: id === 'saturn' ? 0.9 : 0.45, depthWrite: false,
});
if (b.rings.texture) {
new THREE.TextureLoader().load(CONFIG.textures.path + b.rings.texture, (tex) => {
tex.colorSpace = THREE.SRGBColorSpace; mat.map = tex; mat.color.set(0xffffff); mat.needsUpdate = true;
}, undefined, () => { /* keep flat color */ });
}
const group = new THREE.Group();
group.rotation.z = (b.axialTiltDeg || 0) * lib.DEG; // same tilt as the planet group
const mesh = new THREE.Mesh(geo, mat);
mesh.rotation.x = -Math.PI / 2; // lay flat into the equatorial (XZ) plane
group.add(mesh);
scene.add(group);
entries.push({ id, b, group, mesh });
}
let show = true;
ui.addLayer('rings', 'Rings', true, (on) => { show = on; for (const e of entries) e.group.visible = on; });
ui.setStatus('rings', 'Saturn · Uranus', 'ok');
return {
id: 'rings',
onClockTick() {
const K = scale.getK();
for (const e of entries) {
const abs = ctx.bodyWorld[e.id];
if (!abs) continue;
toLocal(abs, e.group.position);
const E = effectiveE(e.id);
e.mesh.scale.setScalar((e.b.rings.outerKm / AU_KM) * K * E); // outer km → view units
}
},
};
}
// Remap RingGeometry UVs so u runs 0→1 across the radius (for a 1-D ring texture).
function radialUV(geo) {
const pos = geo.attributes.position, uv = geo.attributes.uv;
const inner = Math.hypot(pos.getX(0), pos.getY(0));
let outer = inner;
for (let i = 0; i < pos.count; i++) outer = Math.max(outer, Math.hypot(pos.getX(i), pos.getY(i)));
for (let i = 0; i < pos.count; i++) {
const r = Math.hypot(pos.getX(i), pos.getY(i));
uv.setXY(i, (r - inner) / (outer - inner || 1), 1);
}
uv.needsUpdate = true;
}

View File

@ -339,7 +339,7 @@ const ctx = {
moonsOf, PLANET_ORDER,
};
const LAYER_MODULES = ['./layers/planets.js'];
const LAYER_MODULES = ['./layers/planets.js', './layers/moons.js', './layers/rings.js'];
const activeLayers = [];
async function loadLayers() {
const results = await Promise.allSettled(LAYER_MODULES.map((p) => import(p)));