@@ -91,9 +91,10 @@ function buildRLut(flo: number, fhi: number): Float32Array {
9191interface Decoded {
9292 nVerts : number ; nTris : number ;
9393 positions : Float32Array ; index : Uint32Array ;
94- colors : Uint8Array ; normals : Int8Array ; layer : Float32Array ;
95- concepts : number ;
94+ colors : Uint8Array ; normals : Int8Array ; layer : Float32Array ; vrow : Uint32Array ;
95+ concepts : number ; conceptList : ConceptMeta [ ] ;
9696}
97+ interface ConceptMeta { row : number ; name : string ; layer : number ; cx : number ; cy : number ; cz : number ; }
9798
9899function decode ( buf : ArrayBuffer ) : Decoded {
99100 const dv = new DataView ( buf ) ;
@@ -106,8 +107,8 @@ function decode(buf: ArrayBuffer): Decoded {
106107 o += 16 * nC ; // guid
107108 const matOff = o ; o += nC ; // material u8 (unused here)
108109 const layerOff = o ; o += nC ; // LAYER u8
109- o += 4 * nC ; // label idx
110- o += 12 * nC ; // centroid
110+ const labelOff = o ; o += 4 * nC ; // label idx (u32 → name in labels_json)
111+ const cenOff = o ; o += 12 * nC ; // centroid 3f
111112 o += 8 * nC ; // vrange
112113 const posOff = o ; o += posBytes * nV ;
113114 const helixOff = o ; o += 6 * nV ; // pos3 | nrm3 — we read the nrm half
@@ -205,7 +206,20 @@ function decode(buf: ArrayBuffer): Decoded {
205206 }
206207 const index = kept . slice ( 0 , w ) ;
207208 if ( nOut ) console . log ( `/helix max-diameter clamp: ${ nOut } stray verts (worst ${ worst . toFixed ( 1 ) } × p95), dropped ${ ( nT - w / 3 ) . toLocaleString ( ) } tris` ) ;
208- return { nVerts : nV , nTris : w / 3 , positions, index, colors, normals, layer, concepts : nC } ;
209+
210+ // per-concept metadata for the browser: name (label→labels_json), layer, display centroid.
211+ let to = idxOff + 12 * nT ;
212+ const labLen = dv . getUint32 ( to , true ) ; to += 4 ;
213+ let names : string [ ] = [ ] ;
214+ try { const lj = JSON . parse ( new TextDecoder ( ) . decode ( new Uint8Array ( buf . slice ( to , to + labLen ) ) ) ) ; names = lj . names ?? lj ; } catch { /* names optional */ }
215+ const labelIdx = new Uint32Array ( buf . slice ( labelOff , labelOff + 4 * nC ) ) ;
216+ const cen = new Float32Array ( buf . slice ( cenOff , cenOff + 12 * nC ) ) ;
217+ const conceptList : ConceptMeta [ ] = [ ] ;
218+ for ( let c = 0 ; c < nC ; c ++ ) {
219+ conceptList . push ( { row : c , name : names [ labelIdx [ c ] ] ?? `concept ${ c } ` , layer : cLayer [ c ] || 8 ,
220+ cx : - cen [ c * 3 ] , cy : cen [ c * 3 + 2 ] , cz : cen [ c * 3 + 1 ] } ) ; // source → display (-x,z,y)
221+ }
222+ return { nVerts : nV , nTris : w / 3 , positions, index, colors, normals, layer, vrow : rowArr , concepts : nC , conceptList } ;
209223}
210224
211225const VERT = `
@@ -225,11 +239,13 @@ void main(){
225239}` ;
226240const FRAG = `
227241precision mediump float;
242+ uniform float uAlpha; // 1 = solid · <1 = x-ray (whole-body translucent)
228243varying vec3 vColor;
229- void main(){ gl_FragColor = vec4(vColor, 1.0 ); }` ; // visible layers are pre-filtered into the
244+ void main(){ gl_FragColor = vec4(vColor, uAlpha ); }` ; // visible layers are pre-filtered into the
230245// draw range (NOT a discard) → early-Z survives; the GPU never touches hidden triangles.
246+ type Focus = { x : number ; y : number ; z : number ; d : number } ;
231247
232- function mount ( container : HTMLDivElement , d : Decoded , enabled : Float32Array , dirty : { current : boolean } ) : ( ) => void {
248+ function mount ( container : HTMLDivElement , d : Decoded , enabled : Float32Array , dirty : { current : boolean } , focus : { current : Focus | null } , xray : { current : boolean } , lod : { current : boolean } ) : ( ) => void {
233249 let w = container . clientWidth || window . innerWidth , h = container . clientHeight || window . innerHeight ;
234250 const scene = new THREE . Scene ( ) ; scene . background = new THREE . Color ( PAGE_BG ) ;
235251 const camera = new THREE . PerspectiveCamera ( 45 , w / h , 0.01 , 100 ) ; camera . position . set ( 0 , 0.05 , 3.0 ) ;
@@ -250,27 +266,33 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir
250266 const fullIdx = d . index ;
251267 const nTriAll = fullIdx . length / 3 ;
252268 const triLayer = new Uint8Array ( nTriAll ) ;
253- for ( let t = 0 ; t < nTriAll ; t ++ ) triLayer [ t ] = d . layer [ fullIdx [ t * 3 ] ] ;
269+ const triConcept = new Uint32Array ( nTriAll ) ; // concept (row) of each triangle → server-LOD gate
270+ for ( let t = 0 ; t < nTriAll ; t ++ ) { triLayer [ t ] = d . layer [ fullIdx [ t * 3 ] ] ; triConcept [ t ] = d . vrow [ fullIdx [ t * 3 ] ] ; }
271+ // server-LOD action per concept: 255 = show (the default until the cascade answers), 0 = the
272+ // HHTL depth-cascade rejected this concept as off-frustum. Folded into the index rebuild below.
273+ const lodAction = new Uint8Array ( d . concepts ) . fill ( 255 ) ;
254274 const active = new Uint32Array ( fullIdx . length ) ;
255275 const rebuild = ( ) : number => {
256276 let n = 0 ;
257277 for ( let t = 0 ; t < nTriAll ; t ++ ) {
258- if ( enabled [ triLayer [ t ] ] >= 0.5 ) { const o = t * 3 ; active [ n ++ ] = fullIdx [ o ] ; active [ n ++ ] = fullIdx [ o + 1 ] ; active [ n ++ ] = fullIdx [ o + 2 ] ; }
278+ if ( enabled [ triLayer [ t ] ] >= 0.5 && lodAction [ triConcept [ t ] ] !== 0 ) { const o = t * 3 ; active [ n ++ ] = fullIdx [ o ] ; active [ n ++ ] = fullIdx [ o + 1 ] ; active [ n ++ ] = fullIdx [ o + 2 ] ; }
259279 }
260280 return n ;
261281 } ;
262282 const idxAttr = new THREE . BufferAttribute ( active , 1 ) ;
263283 idxAttr . setUsage ( THREE . DynamicDrawUsage ) ;
264284 geom . setIndex ( idxAttr ) ;
285+ const applyIndex = ( ) => { geom . setDrawRange ( 0 , rebuild ( ) ) ; idxAttr . needsUpdate = true ; } ;
265286 geom . setDrawRange ( 0 , rebuild ( ) ) ;
266287
267- const mat = new THREE . ShaderMaterial ( { vertexShader : VERT , fragmentShader : FRAG , side : THREE . FrontSide } ) ;
288+ const uniforms = { uAlpha : { value : 1 } } ;
289+ const mat = new THREE . ShaderMaterial ( { uniforms, vertexShader : VERT , fragmentShader : FRAG , side : THREE . FrontSide } ) ;
268290 const mesh = new THREE . Mesh ( geom , mat ) ; scene . add ( mesh ) ;
269291
270292 // minimal orbit: drag = rotate, wheel = dolly.
271293 let az = 0 , el = 0.1 , dist = 3.0 , dragging = false , px = 0 , py = 0 ;
272294 const target = new THREE . Vector3 ( 0 , 0 , 0 ) ;
273- const onDown = ( e : PointerEvent ) => { dragging = true ; px = e . clientX ; py = e . clientY ; dirty . current = true ; } ;
295+ const onDown = ( e : PointerEvent ) => { dragging = true ; px = e . clientX ; py = e . clientY ; focus . current = null ; dirty . current = true ; } ;
274296 const onUp = ( ) => { dragging = false ; dirty . current = true ; } ;
275297 const onMove = ( e : PointerEvent ) => {
276298 if ( ! dragging ) return ;
@@ -282,6 +304,36 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir
282304 el2 . addEventListener ( 'pointerdown' , onDown ) ; window . addEventListener ( 'pointerup' , onUp ) ;
283305 window . addEventListener ( 'pointermove' , onMove ) ; el2 . addEventListener ( 'wheel' , onWheel , { passive : false } ) ;
284306
307+ // server HHTL LOD (opt-in): post the live camera to /api/body/lod; the depth-cascade returns
308+ // a per-concept action (0 = off-frustum reject). We fold the cull into the SAME geometry index
309+ // rebuild as the layer toggles — NOT a fragment discard — so early-Z survives and the GPU draws
310+ // strictly fewer triangles when zoomed in (the mobile lever, working WITH the database). Absent
311+ // endpoint (old deploy) → silently keep the full render. This is the living DB reasoning the view.
312+ let lodNext = 0 , lodInflight = false , lodFail = false , lodDirty = false , lodWasOn = false ;
313+ const postLod = ( now : number ) => {
314+ if ( lodFail || lodInflight || now < lodNext ) return ;
315+ lodInflight = true ; lodNext = now + 220 ;
316+ camera . updateMatrixWorld ( ) ;
317+ const e = camera . matrixWorldInverse . elements ; // column-major → row-major view rows
318+ const view = [
319+ [ e [ 0 ] , e [ 4 ] , e [ 8 ] , e [ 12 ] ] , [ e [ 1 ] , e [ 5 ] , e [ 9 ] , e [ 13 ] ] ,
320+ [ e [ 2 ] , e [ 6 ] , e [ 10 ] , e [ 14 ] ] , [ e [ 3 ] , e [ 7 ] , e [ 11 ] , e [ 15 ] ] ,
321+ ] ;
322+ const fy = ( h / 2 ) / Math . tan ( ( camera . fov * Math . PI ) / 360 ) ;
323+ const body = { view, fx : fy , fy, cx : w / 2 , cy : h / 2 , near : camera . near , far : camera . far , width : w , height : h , position : [ camera . position . x , camera . position . y , camera . position . z ] } ;
324+ fetch ( '/api/body/lod' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( body ) } )
325+ . then ( ( r ) => ( r . ok ? r . json ( ) : Promise . reject ( new Error ( `HTTP ${ r . status } ` ) ) ) )
326+ . then ( ( j : { actions : number [ ] ; n_concepts ?: number ; tally ?: number [ ] } ) => {
327+ const a = j . actions ;
328+ const visible = ( j . n_concepts ?? a . length ) - ( j . tally ?. [ 0 ] ?? 0 ) ;
329+ const degenerate = visible <= Math . max ( 1 , a . length * 0.02 ) ; // cascade culled ~all ⇒ camera map suspect → show all
330+ for ( let i = 0 ; i < d . concepts && i < a . length ; i ++ ) lodAction [ i ] = degenerate ? 255 : a [ i ] ;
331+ lodDirty = true ; dirty . current = true ;
332+ } )
333+ . catch ( ( ) => { lodFail = true ; } ) // endpoint absent (old deploy) → keep full render
334+ . finally ( ( ) => { lodInflight = false ; } ) ;
335+ } ;
336+
285337 let raf = 0 , ema = 16.6 , last = performance . now ( ) , sig = enabled . join ( ',' ) ;
286338 const onResize = ( ) => {
287339 w = container . clientWidth || window . innerWidth ; h = container . clientHeight || window . innerHeight ;
@@ -290,6 +342,11 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir
290342 window . addEventListener ( 'resize' , onResize ) ;
291343 const tick = ( ) => {
292344 raf = requestAnimationFrame ( tick ) ;
345+ // server-LOD lifecycle runs even on idle frames (the cascade tracks the static view too);
346+ // turning LOD off restores the full geometry. Both are cheap and bounded by the 220 ms poll.
347+ const tnow = performance . now ( ) ;
348+ if ( lod . current ) { postLod ( tnow ) ; lodWasOn = true ; }
349+ else if ( lodWasOn ) { lodWasOn = false ; lodFail = false ; lodAction . fill ( 255 ) ; lodDirty = true ; dirty . current = true ; }
293350 // render ON DEMAND: a static body (no drag/zoom/toggle) costs nothing — 6.8 M tris are
294351 // only redrawn when something actually changes, which is what makes idle + heat sane.
295352 if ( ! dirty . current && ! dragging ) { last = performance . now ( ) ; return ; }
@@ -298,9 +355,22 @@ function mount(container: HTMLDivElement, d: Decoded, enabled: Float32Array, dir
298355 // this is the single biggest lever — quarters/ninths the fragment load while dragging.
299356 const pr = ema > 33 ? 1 : Math . min ( window . devicePixelRatio , 2 ) ;
300357 if ( renderer . getPixelRatio ( ) !== pr ) renderer . setPixelRatio ( pr ) ;
301- // layer toggled → rebuild the active index (geometry exclusion, not discard)
358+ // layer toggled OR server-LOD answered → rebuild the active index (geometry exclusion, not
359+ // discard): one linear pass folds both the layer mask and the per-concept LOD action.
302360 const ns = enabled . join ( ',' ) ;
303- if ( ns !== sig ) { sig = ns ; geom . setDrawRange ( 0 , rebuild ( ) ) ; idxAttr . needsUpdate = true ; }
361+ if ( ns !== sig || lodDirty ) { sig = ns ; lodDirty = false ; applyIndex ( ) ; }
362+ // x-ray: whole-body translucency (depthWrite off; cheap, unsorted-blend is fine here)
363+ const wantX = xray . current ;
364+ if ( mat . transparent !== wantX ) { mat . transparent = wantX ; mat . depthWrite = ! wantX ; mat . needsUpdate = true ; }
365+ uniforms . uAlpha . value = wantX ? 0.4 : 1.0 ;
366+ // browser pick → glide the orbit target + dolly onto the chosen concept
367+ if ( focus . current ) {
368+ const f = focus . current ;
369+ target . lerp ( new THREE . Vector3 ( f . x , f . y , f . z ) , 0.12 ) ;
370+ dist += ( f . d - dist ) * 0.12 ;
371+ if ( Math . abs ( dist - f . d ) < 0.02 ) focus . current = null ;
372+ dirty . current = true ;
373+ }
304374 camera . position . set ( target . x + dist * Math . cos ( el ) * Math . sin ( az ) , target . y + dist * Math . sin ( el ) , target . z + dist * Math . cos ( el ) * Math . cos ( az ) ) ;
305375 camera . lookAt ( target ) ;
306376 renderer . render ( scene , camera ) ;
@@ -347,8 +417,15 @@ export default function BodyHelix() {
347417 const [ d , setD ] = useState < Decoded | null > ( null ) ;
348418 const [ error , setError ] = useState ( '' ) ;
349419 const [ on , setOn ] = useState < Record < number , boolean > > ( { 1 : false , 2 : false , 3 : true , 4 : true , 5 : true , 6 : true , 7 : true , 8 : true } ) ;
420+ const [ xray , setXray ] = useState ( false ) ;
421+ const [ lod , setLod ] = useState ( false ) ; // server HHTL LOD — opt-in (off = full render)
422+ const [ query , setQuery ] = useState ( '' ) ;
423+ const [ open , setOpen ] = useState < Record < number , boolean > > ( { 4 : true } ) ; // expanded layer groups
350424 const enabledRef = useRef ( new Float32Array ( [ 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] ) ) ;
351425 const dirtyRef = useRef ( true ) ; // request a redraw (the render loop is on-demand)
426+ const focusRef = useRef < Focus | null > ( null ) ;
427+ const xrayRef = useRef ( false ) ;
428+ const lodRef = useRef ( false ) ;
352429
353430 useEffect ( ( ) => {
354431 let cancelled = false ;
@@ -361,33 +438,77 @@ export default function BodyHelix() {
361438 for ( let i = 1 ; i <= 8 ; i ++ ) enabledRef . current [ i ] = on [ i ] ? 1 : 0 ;
362439 dirtyRef . current = true ;
363440 } , [ on ] ) ;
364- useEffect ( ( ) => { const c = ref . current ; if ( ! c || ! d ) return ; return mount ( c , d , enabledRef . current , dirtyRef ) ; } , [ d ] ) ;
441+ useEffect ( ( ) => { xrayRef . current = xray ; dirtyRef . current = true ; } , [ xray ] ) ;
442+ useEffect ( ( ) => { lodRef . current = lod ; dirtyRef . current = true ; } , [ lod ] ) ;
443+ useEffect ( ( ) => { const c = ref . current ; if ( ! c || ! d ) return ; return mount ( c , d , enabledRef . current , dirtyRef , focusRef , xrayRef , lodRef ) ; } , [ d ] ) ;
444+
445+ const focusOn = ( c : ConceptMeta ) => {
446+ focusRef . current = { x : c . cx , y : c . cy , z : c . cz , d : 0.6 } ;
447+ if ( ! enabledRef . current [ c . layer ] ) setOn ( ( p ) => ( { ...p , [ c . layer ] : true } ) ) ; // reveal its layer
448+ dirtyRef . current = true ;
449+ } ;
365450
366451 const btn = ( active : boolean ) : React . CSSProperties => ( {
367452 padding : '5px 10px' , borderRadius : 6 , cursor : 'pointer' , border : '1px solid #2a3242' ,
368453 background : active ? '#1c2738' : '#0e1219' , color : active ? '#cdd9e5' : '#6b7686' , font : '12px ui-monospace, monospace' ,
369454 } ) ;
455+ const q = query . trim ( ) . toLowerCase ( ) ;
456+ const groups = LAYERS . map ( ( l ) => ( {
457+ l, items : d ? d . conceptList . filter ( ( c ) => c . layer === l . id && ( ! q || c . name . toLowerCase ( ) . includes ( q ) ) ) : [ ] ,
458+ } ) ) . filter ( ( g ) => g . items . length > 0 || ! q ) ;
370459
371460 return (
372461 < div style = { { position : 'fixed' , inset : 0 , background : `#${ PAGE_BG . toString ( 16 ) . padStart ( 6 , '0' ) } ` } } >
373462 < div ref = { ref } style = { { position : 'absolute' , inset : 0 } } />
374- < div style = { { position : 'absolute' , top : 12 , left : 16 , color : '#cdd9e5' , font : '13px ui-monospace, monospace' } } >
375- < div style = { { color : '#fff' , fontSize : 15 } } > /helix — surfel-normal viewer (experimental) </ div >
376- < div style = { { opacity : 0.65 , marginTop : 2 , maxWidth : 440 } } >
463+ < div style = { { position : 'absolute' , top : 12 , left : 16 , color : '#cdd9e5' , font : '13px ui-monospace, monospace' , pointerEvents : 'none' } } >
464+ < div style = { { color : '#fff' , fontSize : 15 } } > /helix — living anatomy browser </ div >
465+ < div style = { { opacity : 0.6 , marginTop : 2 , maxWidth : 300 } } >
377466 { error ? < span style = { { color : '#e06c6c' } } > { error } </ span >
378- : d ? `${ d . nVerts . toLocaleString ( ) } verts · ${ d . concepts . toLocaleString ( ) } concepts — canonical helix::Signed360 normals: Fisher-Z rim → r=sinθ decoded once into a normalized int8 normal; Gouraud shading (per-vertex), trivial fragment shader. `
379- : 'loading canonical helix bake (Signed360 normals) …' }
467+ : d ? `${ d . nVerts . toLocaleString ( ) } verts · ${ d . concepts . toLocaleString ( ) } structures · helix::Signed360 normals ( Fisher-Z rim) `
468+ : 'loading canonical helix bake…' }
380469 </ div >
381470 </ div >
382471 { d && (
383- < div style = { { position : 'absolute' , top : 12 , right : 16 , display : 'flex' , gap : 6 , flexWrap : 'wrap' , maxWidth : 360 , justifyContent : 'flex-end' } } >
472+ < div style = { { position : 'absolute' , top : 12 , right : 16 , display : 'flex' , gap : 6 , flexWrap : 'wrap' , maxWidth : 380 , justifyContent : 'flex-end' } } >
473+ < button style = { btn ( xray ) } onClick = { ( ) => setXray ( ( x ) => ! x ) } title = "x-ray: make the whole body translucent so deeper structures show through" > x-ray</ button >
474+ < button style = { btn ( lod ) } onClick = { ( ) => setLod ( ( v ) => ! v ) } title = "LOD: the HHTL depth-cascade culls off-frustum structures as you zoom in — the living database deciding what's worth drawing" > LOD { lod ? 'on' : 'off' } </ button >
384475 { LAYERS . map ( ( l ) => (
385476 < button key = { l . id } style = { btn ( on [ l . id ] ) } onClick = { ( ) => setOn ( ( p ) => ( { ...p , [ l . id ] : ! p [ l . id ] } ) ) } >
386477 < span style = { { display : 'inline-block' , width : 8 , height : 8 , borderRadius : 4 , background : l . color , marginRight : 5 , verticalAlign : 'middle' } } /> { l . name }
387478 </ button >
388479 ) ) }
389480 </ div >
390481 ) }
482+ { d && (
483+ < div style = { { position : 'absolute' , left : 16 , top : 66 , bottom : 16 , width : 290 , display : 'flex' , flexDirection : 'column' , background : 'rgba(11,15,22,0.92)' , border : '1px solid #1c2530' , borderRadius : 10 , overflow : 'hidden' } } >
484+ < input value = { query } onChange = { ( e ) => setQuery ( e . target . value ) } placeholder = { `search ${ d . concepts . toLocaleString ( ) } structures…` }
485+ style = { { margin : 10 , padding : '8px 10px' , borderRadius : 7 , border : '1px solid #243244' , background : '#0e1219' , color : '#cdd9e5' , font : '13px ui-monospace, monospace' , outline : 'none' } } />
486+ < div style = { { overflowY : 'auto' , padding : '0 6px 8px' } } >
487+ { groups . map ( ( { l, items } ) => {
488+ const expanded = ! ! open [ l . id ] || ! ! q ;
489+ return (
490+ < div key = { l . id } >
491+ < div onClick = { ( ) => setOpen ( ( p ) => ( { ...p , [ l . id ] : ! expanded } ) ) }
492+ style = { { display : 'flex' , alignItems : 'center' , gap : 7 , padding : '7px 8px' , cursor : 'pointer' , color : '#cdd9e5' , font : '12px ui-monospace, monospace' , userSelect : 'none' } } >
493+ < span style = { { width : 8 , opacity : 0.7 } } > { expanded ? '▾' : '▸' } </ span >
494+ < span style = { { display : 'inline-block' , width : 9 , height : 9 , borderRadius : 5 , background : l . color } } />
495+ < span style = { { flex : 1 } } > { l . name } </ span >
496+ < span style = { { opacity : 0.45 } } > { items . length } </ span >
497+ </ div >
498+ { expanded && items . slice ( 0 , 500 ) . map ( ( c ) => (
499+ < div key = { c . row } onClick = { ( ) => focusOn ( c ) } title = { c . name }
500+ style = { { padding : '4px 8px 4px 30px' , cursor : 'pointer' , color : '#9fb0c2' , font : '12px ui-monospace, monospace' , whiteSpace : 'nowrap' , overflow : 'hidden' , textOverflow : 'ellipsis' , borderRadius : 5 } }
501+ onMouseEnter = { ( e ) => { e . currentTarget . style . background = '#152030' ; e . currentTarget . style . color = '#dce6f0' ; } }
502+ onMouseLeave = { ( e ) => { e . currentTarget . style . background = 'transparent' ; e . currentTarget . style . color = '#9fb0c2' ; } } >
503+ { c . name }
504+ </ div >
505+ ) ) }
506+ </ div >
507+ ) ;
508+ } ) }
509+ </ div >
510+ </ div >
511+ ) }
391512 </ div >
392513 ) ;
393514}
0 commit comments