Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 171 additions & 25 deletions cockpit/src/BodyHelix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ import * as THREE from 'three';
const PAGE_BG = 0x0a0d12;
const REL = 'https://github.com/AdaWorldAPI/q2/releases/download/fma-body-soa-v3-v1';

// The scene name implied by the PATH alone (no `?scene=` override). This is the SINGLE
// source of truth for path→scene mapping — fetchSoa() (which bake to load) and isGeoScene
// (whether to disable server LOD + light the geo beautification) BOTH read it, so they can
// never disagree. `?scene=<name>` still overrides in the callers. /geo → the OSM bake,
// /ice → the Iceland DEM bake; any other path → null (the anatomy body, helix_latest).
function pathScene(): string | null {
const p = window.location.pathname;
if (p === '/geo') return 'osm';
if (p === '/ice') return 'iceland';
return null;
}

const LAYERS = [
{ id: 1, name: 'skin', color: '#dba88a' }, { id: 2, name: 'muscle', color: '#bd5c57' },
{ id: 3, name: 'organ', color: '#cc9484' }, { id: 4, name: 'skeleton', color: '#ebe0c7' },
Expand Down Expand Up @@ -225,7 +237,60 @@ function decode(buf: ArrayBuffer): Decoded {
const VERT = `
precision highp float;
attribute vec3 aColor; attribute vec3 aNormal;
uniform float uGeo; // 1 = geo scene → height-profile terrain palette · 0 = anatomy → aColor (byte-identical)
uniform float uYMin; // decoded height range (display.y), measured once at load from the position buffer
uniform float uYMax;
uniform float uExag; // geo relief exaggeration: the Iceland bake is true-scale (span ~0.0074 in the
// [-1,1] frame), so raise the geometry to read as terrain. 1 for anatomy (untouched).
uniform float uTime; // seconds — drives the Kurvenlineal breathing (0 for anatomy).
uniform float uRuler; // Kurvenlineal displacement amplitude (0 for anatomy).
varying vec3 vColor;
// THE KURVENLINEAL (helix::CurveRuler) — a GLSL port of the sculpt crate's deterministic
// stride-4-over-17 phase (gcd(4,17)=1 → a full 17-residue permutation). Not 100% bit-exact to
// the Rust CurveRuler (that walks a u64 place with integer mixing; here we hash floats), but
// faithful in spirit: a deterministic bipolar phase in ~[-1,1] regenerated from the vertex's
// lattice address — "phase is convention, not data" (OGAR D-QUANTGATE). It synthesises relief
// detail where the sparse DEM bake has gaps, and — animated by uTime — makes the terrain BREATHE.
float rhash(vec3 p){ return fract(sin(dot(floor(p), vec3(127.1, 311.7, 74.7))) * 43758.5453); }
float kurvenlineal(vec3 pos, float detail){
vec3 cell = floor(pos * detail); // the lattice cell = the address
vec3 sub = floor(pos * detail * 3.0); // finer sub-lattice picks the step k
float place = floor(rhash(cell) * 65536.0);
float k = mod(floor(rhash(sub) * 991.0), 17.0); // k ∈ [0,17)
float idx = mod(mod(place, 17.0) + 4.0 * k, 17.0); // stride-4-over-17 coprime walk
return idx / 8.0 - 1.0; // ~[-1, 1] bipolar
}
// Height-profile terrain palette for the geo bakes (Iceland DEM, OSM). Elevation is display.y.
// In the Iceland bake it is TRUE-SCALE (not vertically exaggerated) → a tiny span (~[0, 0.0074])
// with ~39% ocean at EXACTLY 0 and a heavily-quantized lowland plateau holding ~58% of verts,
// then a thin highland tail to the peaks/glaciers. Hence we normalize by the ACTUAL measured
// [uYMin,uYMax] (never the [-1,1] convention — that would flatten everything to water) and apply
// a sqrt curve to spread the skewed tail. Colour is driven by HEIGHT ONLY: the decoded Signed360
// normal is BIMODAL for the geo bake (77% |n.y|≈0 / 23% ≈1), so it is NOT a usable continuous
// slope — classifying rock/scree/volcano from it would give a binary patchwork. The shade
// lighting term below still uses the normal for form. HONEST NOTE: true lava-field / glacier /
// volcano feature classification needs a DEM re-bake carrying feature layers; the warm "volcano"
// band here is a height-derived accent approximation, deliberately subtle.
vec3 terrainColor(float h){
float hl = clamp((h - uYMin) / max(uYMax - uYMin, 1e-6), 0.0, 1.0); // linear normalized height
float hs = sqrt(hl); // spread the quantized tail
vec3 ocean = vec3(0.05, 0.15, 0.30);
vec3 coast = vec3(0.28, 0.42, 0.24); // lowest green coastal fringe
vec3 moss = vec3(0.34, 0.36, 0.22); // khaki moss/tundra — the dominant quantized lowland
vec3 rock = vec3(0.44, 0.38, 0.30); // brown highland rock
vec3 scree = vec3(0.56, 0.55, 0.55); // grey scree
vec3 ice = vec3(0.90, 0.93, 0.98); // snow / glacier cap
vec3 land = coast;
land = mix(land, moss, smoothstep(0.18, 0.30, hs));
land = mix(land, rock, smoothstep(0.34, 0.48, hs));
land = mix(land, scree, smoothstep(0.50, 0.64, hs));
land = mix(land, ice, smoothstep(0.66, 0.82, hs));
vec3 lava = vec3(0.42, 0.16, 0.10); // warm volcanic-highland accent (approximation — see note)
float volc = smoothstep(0.42, 0.52, hs) * (1.0 - smoothstep(0.60, 0.72, hs));
land = mix(land, lava, volc * 0.28);
float water = 1.0 - smoothstep(0.002, 0.010, hl); // ocean is EXACTLY 0; first land ≈ 0.0155 normalized
return mix(land, ocean, water);
}
void main(){
// GOURAUD: shade per-vertex from the cheap rim normal, interpolate the COLOUR across the
// face. At 6.8 M sub-pixel tris this matches per-fragment lighting visually but leaves the
Expand All @@ -234,15 +299,47 @@ void main(){
const vec3 L = vec3(-0.401, 0.783, 0.476);
float ndl = max(abs(dot(n, L)), 0.0);
float shade = min(0.34 + 0.20*(abs(n.y)*0.5+0.5) + 0.12*(-n.x*0.5+0.5) + 0.92*ndl, 1.3);
vColor = aColor * shade;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
// uGeo is a dynamically-uniform branch (a uniform, not a varying): coherent, no divergence.
// uGeo == 0 -> EXACTLY the pre-existing aColor*shade, so anatomy scenes are byte-identical.
vColor = (uGeo > 0.5) ? terrainColor(position.y) * shade : aColor * shade;
// Geo relief + the KURVENLINEAL: raise the true-scale island by uExag, then add the deterministic
// stride-4-over-17 phase as extra relief that BREATHES over uTime — it synthesises detail where the
// sparse DEM bake has gaps and gives the horizon a living, breathing motion (not 100% physical: a
// mental horizon that motivates the next, DEM-true, improvement). Anatomy (uGeo==0): untouched.
vec3 dpos = position;
if (uGeo > 0.5) {
float rp = kurvenlineal(position, 90.0);
float breathe = 0.55 + 0.45 * sin(uTime * 0.5 + rp * 6.2831);
dpos.y = position.y * uExag + rp * uRuler * breathe;
}
gl_Position = projectionMatrix * modelViewMatrix * vec4(dpos, 1.0);
}`;
const FRAG = `
precision mediump float;
uniform float uAlpha; // 1 = solid · <1 = x-ray (whole-body translucent)
varying vec3 vColor;
void main(){ gl_FragColor = vec4(vColor, uAlpha); }`; // visible layers are pre-filtered into the
// draw range (NOT a discard) → early-Z survives; the GPU never touches hidden triangles.

// ── procedural sky dome (geo scenes only) ──────────────────────────────────────────────
// A large BackSide sphere with a vertical gradient (pale horizon → blue zenith), rendered
// first (renderOrder −1, depthWrite off) so terrain always draws over it. No external asset
// (CSP blocks CDNs) — the gradient is computed in the fragment shader from the view direction.
// Added to the scene ONLY for geo scenes; anatomy scenes keep the flat PAGE_BG background.
const SKY_HORIZON = new THREE.Color(0.74, 0.80, 0.85);
const SKY_ZENITH = new THREE.Color(0.24, 0.42, 0.64);
const SKY_VERT = `
precision highp float;
varying vec3 vDir;
void main(){ vDir = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`;
const SKY_FRAG = `
precision mediump float;
varying vec3 vDir;
uniform vec3 uHorizon; uniform vec3 uZenith;
void main(){
float t = clamp(normalize(vDir).y * 0.5 + 0.5, 0.0, 1.0);
gl_FragColor = vec4(mix(uHorizon, uZenith, smoothstep(0.12, 0.92, t)), 1.0);
}`;
type Focus = { x: number; y: number; z: number; d: number };

function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dirty: { current: boolean }, focus: { current: Focus | null }, xray: { current: boolean }, lod: { current: boolean }): () => void {
Expand All @@ -253,6 +350,21 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir
renderer.setSize(w, h); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);

// Geo scene? (scene=osm, scene=iceland, /geo, /ice, …). Resolved via pathScene() so this
// matches fetchSoa() EXACTLY (they share the one helper). Gates three things: server LOD is
// disabled (the /api/body/lod cascade runs over the BODY's block-bounds; a geo scene's
// concepts must render in full), the height-profile terrain palette is lit (uGeo), and the
// sky dome is added. An empty `?scene=` resolves falsy → anatomy body → all three stay off.
const sceneParam = new URLSearchParams(window.location.search).get('scene');
const isGeoScene = Boolean(sceneParam ?? pathScene());

// Height range for the geo palette: measured ONCE from the decoded position buffer (display.y).
// The Iceland bake is true-scale so the span is tiny — normalizing against [-1,1] would flatten
// it to all-water; the actual [min,max] is what the shader needs. Only consumed when uGeo == 1.
let yMin = Infinity, yMax = -Infinity;
for (let i = 0; i < d.nVerts; i++) { const y = d.positions[i * 3 + 1]; if (y < yMin) yMin = y; if (y > yMax) yMax = y; }
if (!(yMax > yMin)) { yMin = 0; yMax = 1; } // degenerate/flat guard → avoid divide-by-zero

const geom = new THREE.BufferGeometry();
geom.setAttribute('position', new THREE.BufferAttribute(d.positions, 3));
geom.setAttribute('aColor', new THREE.Uint8BufferAttribute(d.colors, 3, true));
Expand Down Expand Up @@ -285,13 +397,32 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir
const applyIndex = () => { geom.setDrawRange(0, rebuild()); idxAttr.needsUpdate = true; };
geom.setDrawRange(0, rebuild());

const uniforms = { uAlpha: { value: 1 } };
const uniforms = { uAlpha: { value: 1 }, uGeo: { value: isGeoScene ? 1 : 0 }, uYMin: { value: yMin }, uYMax: { value: yMax }, uExag: { value: isGeoScene ? 10 : 1 }, uTime: { value: 0 }, uRuler: { value: isGeoScene ? 0.03 : 0 } };
const mat = new THREE.ShaderMaterial({ uniforms, vertexShader: VERT, fragmentShader: FRAG, side: THREE.FrontSide });
const mesh = new THREE.Mesh(geom, mat); scene.add(mesh);

// Sky dome — geo scenes only (anatomy keeps the flat PAGE_BG set above, byte-identical).
let skyGeom: THREE.SphereGeometry | null = null;
let skyMat: THREE.ShaderMaterial | null = null;
if (isGeoScene) {
scene.background = SKY_HORIZON.clone(); // horizon tint behind the dome (any gap reads as sky)
skyGeom = new THREE.SphereGeometry(40, 32, 16); // radius 40 < camera far (100), surrounds the orbit
skyMat = new THREE.ShaderMaterial({
side: THREE.BackSide, depthWrite: false,
uniforms: { uHorizon: { value: SKY_HORIZON.clone() }, uZenith: { value: SKY_ZENITH.clone() } },
vertexShader: SKY_VERT, fragmentShader: SKY_FRAG,
});
const sky = new THREE.Mesh(skyGeom, skyMat);
sky.renderOrder = -1; // draw first → terrain (with depth) always paints over it
scene.add(sky);
}

// minimal orbit: drag = rotate, wheel = dolly.
let az = 0, el = 0.1, dist = 3.0, dragging = false, px = 0, py = 0;
const target = new THREE.Vector3(0, 0, 0);
// Geo scenes open on an AERIAL oblique view (el ~35°) so the terrain reads as a landscape under
// the sky dome, not edge-on; anatomy keeps the near-level body orbit. Target lifts slightly so
// the raised relief sits in frame. Drag/wheel still free-orbit from there.
let az = 0, el = isGeoScene ? 0.62 : 0.1, dist = isGeoScene ? 2.6 : 3.0, dragging = false, px = 0, py = 0;
const target = new THREE.Vector3(0, isGeoScene ? 0.08 : 0, 0);
const onDown = (e: PointerEvent) => { dragging = true; px = e.clientX; py = e.clientY; focus.current = null; dirty.current = true; };
const onUp = () => { dragging = false; dirty.current = true; };
const onMove = (e: PointerEvent) => {
Expand All @@ -310,17 +441,13 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir
// strictly fewer triangles when zoomed in (the mobile lever, working WITH the database). Absent
// endpoint (old deploy) → silently keep the full render. This is the living DB reasoning the view.
let lodNext = 0, lodInflight = false, lodFail = false, lodDirty = false, lodWasOn = false;
// Any geo scene (scene=osm, scene=iceland, /geo, …) shares the /api/body/lod
// endpoint, but that cascade runs over the BODY's compile-time block-bounds —
// it would cull the OSM/geo concepts with anatomy bounds. Disable server LOD
// for EVERY non-body scene (full render), not just osm; a geo LOD that reads
// the scene's own .blocks sidecar is future work. Mirror the artifact resolver
// below EXACTLY (same `?? (/geo→osm)` + truthy test) so the two never disagree:
// an empty `?scene=` resolves falsy → anatomy body (helix_latest), so LOD stays ON.
const sceneParam = new URLSearchParams(window.location.search).get('scene');
const isGeoScene = Boolean(
sceneParam ?? (window.location.pathname === '/geo' ? 'osm' : null),
);
// Any geo scene (scene=osm, scene=iceland, /geo, /ice, …) shares the /api/body/lod
// endpoint, but that cascade runs over the BODY's compile-time block-bounds — it would
// cull the geo concepts with anatomy bounds. Server LOD is therefore disabled for EVERY
// geo scene (full render); a geo LOD that reads the scene's own .blocks sidecar is future
// work. The gate is `isGeoScene`, computed once near the top of mount() via the SAME
// pathScene() helper fetchSoa() uses, so the artifact resolver and the LOD gate can never
// disagree (an empty `?scene=` → anatomy body → helix_latest → LOD stays ON).
const postLod = (now: number) => {
if (isGeoScene || lodFail || lodInflight || now < lodNext) return;
lodInflight = true; lodNext = now + 220;
Expand Down Expand Up @@ -353,6 +480,9 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir
window.addEventListener('resize', onResize);
const tick = () => {
raf = requestAnimationFrame(tick);
// Kurvenlineal breathing: geo scenes advance uTime and render every frame so the terrain
// breathes; anatomy stays fully on-demand (uTime 0, dirty gating untouched — no idle cost).
if (isGeoScene) { uniforms.uTime.value = performance.now() / 1000; dirty.current = true; }
// server-LOD lifecycle runs even on idle frames (the cascade tracks the static view too);
// turning LOD off restores the full geometry. Both are cheap and bounded by the 220 ms poll.
const tnow = performance.now();
Expand Down Expand Up @@ -393,7 +523,10 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir
el2.removeEventListener('pointerdown', onDown); window.removeEventListener('pointerup', onUp);
window.removeEventListener('pointermove', onMove); el2.removeEventListener('wheel', onWheel);
window.removeEventListener('resize', onResize);
geom.dispose(); mat.dispose(); renderer.dispose();
geom.dispose(); mat.dispose();
if (skyGeom) skyGeom.dispose();
if (skyMat) skyMat.dispose();
renderer.dispose();
if (el2.parentElement === container) container.removeChild(el2);
};
}
Expand All @@ -414,11 +547,12 @@ async function fetchSoa(): Promise<ArrayBuffer> {
const man = await fetch('/body.manifest.json').then((r) => (r.ok ? r.json() : null)).catch(() => null);
// /helix → anatomy body (helix_latest). /helix?scene=<name> → that scene's
// bake (<name>_latest, e.g. osm_latest, iceland_latest) through the SAME
// Signed360 decoder. /geo is shorthand for scene=osm. The body's data slot
// and the separate /osm slippy-map page are both untouched.
// Signed360 decoder. /geo is shorthand for scene=osm, /ice for scene=iceland
// (via pathScene() — the SAME helper isGeoScene reads, so the bake choice and
// the LOD/beautification gate can never disagree). The body's data slot and the
// separate /osm slippy-map page are both untouched.
const scene =
new URLSearchParams(window.location.search).get('scene') ??
(window.location.pathname === '/geo' ? 'osm' : null);
new URLSearchParams(window.location.search).get('scene') ?? pathScene();
const key = scene ? `${scene}_latest` : 'helix_latest';
const stamped: string | undefined = man?.[key];
if (!stamped) {
Expand Down Expand Up @@ -476,15 +610,27 @@ export default function BodyHelix() {
l, items: d ? d.conceptList.filter((c) => c.layer === l.id && (!q || c.name.toLowerCase().includes(q))) : [],
})).filter((g) => g.items.length > 0 || !q);

// Scene label (same resolution as fetchSoa/isGeoScene). Anatomy keeps the exact original
// title/subtitle; geo scenes get a scene-appropriate heading so /ice doesn't read "anatomy".
const scene = new URLSearchParams(window.location.search).get('scene') ?? pathScene();
const title =
scene === 'iceland' ? '/ice — Iceland height-profile terrain'
: scene === 'osm' ? '/geo — OSM terrain'
: scene ? `/helix?scene=${scene}`
: '/helix — living anatomy browser';
const subtitle = d
? scene
? `${d.nVerts.toLocaleString()} verts · ${d.concepts.toLocaleString()} structures · height-profile palette + sky`
: `${d.nVerts.toLocaleString()} verts · ${d.concepts.toLocaleString()} structures · helix::Signed360 normals (Fisher-Z rim)`
: 'loading canonical helix bake…';

return (
<div style={{ position: 'fixed', inset: 0, background: `#${PAGE_BG.toString(16).padStart(6, '0')}` }}>
<div ref={ref} style={{ position: 'absolute', inset: 0 }} />
<div style={{ position: 'absolute', top: 12, left: 16, color: '#cdd9e5', font: '13px ui-monospace, monospace', pointerEvents: 'none' }}>
<div style={{ color: '#fff', fontSize: 15 }}>/helix — living anatomy browser</div>
<div style={{ color: '#fff', fontSize: 15 }}>{title}</div>
<div style={{ opacity: 0.6, marginTop: 2, maxWidth: 300 }}>
{error ? <span style={{ color: '#e06c6c' }}>{error}</span>
: d ? `${d.nVerts.toLocaleString()} verts · ${d.concepts.toLocaleString()} structures · helix::Signed360 normals (Fisher-Z rim)`
: 'loading canonical helix bake…'}
{error ? <span style={{ color: '#e06c6c' }}>{error}</span> : subtitle}
</div>
</div>
{d && (
Expand Down
5 changes: 5 additions & 0 deletions cockpit/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ createRoot(document.getElementById('root')!).render(
Equivalent to /helix?scene=osm; a dedicated address so the map has a
home without touching /helix's body slot or the /osm slippy map. */}
<Route path="/geo" element={<BodyHelix />} />
{/* /ice — the Iceland DEM bake (iceland_latest) through the same BodyHelix
decoder; equivalent to /helix?scene=iceland, a dedicated home with
height-profile beautification (ocean/moss/rock/ice terrain palette +
procedural sky dome). Client-side render only — no re-bake. */}
<Route path="/ice" element={<BodyHelix />} />
{/* /cpic — CPIC pharmacogenomics cockpit (gene-first): {gene, diplotype, drug}
→ phenotype → recommendation, 2-hop NARS deduction over the real CPIC tables
via POST /api/cpic/reason (the standalone cpic crate). Additive, gene-first
Expand Down