44// · its part-of position (basin-local: organ → chamber → wall → structure)
55// · its leaf-limited global TYPE (the 0xFFFF ceiling pole — cross-cutting,
66// the same "Cardiac muscle tissue" shared by every chamber).
7+ //
8+ // This is the `Cascade` (ontology / part-of) reading of OGAR PR #116's HhtlMode
9+ // FMA tier model: each node is a stack of 8:8 [container:identity] tiers —
10+ // HEEL=[Organ:Heart], HIP=[Chamber:id], TWIG=[Wall:id], LEAF=[Tissue:id] — where
11+ // the container byte is the KIND mixin node and the identity the instance, so the
12+ // partonomy IS the key and the layout reads straight off it. OGAR's
13+ // ogar-fma-skeleton is the `Located` (spatial) sibling (the same 8:8 tiers carry
14+ // coronal x:y / depth z Morton cells). classid 0x0A01 = anatomical_structure in
15+ // OGAR's ConceptDomain::Anatomy (0x0A).
716import { useEffect , useMemo , useRef , useState } from 'react' ;
817import { Network , type Options } from 'vis-network' ;
918import { DataSet } from 'vis-data' ;
@@ -27,6 +36,44 @@ const classColor = (c: number) => FMA_CLASS[c]?.color ?? '#8899aa';
2736const REL = [ 'member-of' , 'interfaces' , 'part-of' , 'is-a' ] ;
2837const REL_COLOR = [ '#223040' , '#223040' , '#7fa6c4' , CEILING_COLOR ] ;
2938
39+ // ── 8:8 [container:identity] HHTL-tier layout ────────────────────────────────
40+ // The bake addresses each node as a stack of 8:8 tiers (see src/bin/fma.rs):
41+ // HEEL=[Organ:Heart] HIP=[Chamber:id] TWIG=[Wall:id] LEAF=[Tissue:id]
42+ // family=[Cell:id]. The container (high byte) is the KIND mixin node, the
43+ // identity (low byte) is the instance. The non-zero tier identities ARE the
44+ // partonomy path — so position is read straight off the tiers, no Morton decode:
45+ // y = depth (class), x = a nested slot that subdivides under each parent.
46+ const COL = 1500 ; // total layout width in vis units
47+ const ROW = 210 ; // vertical gap per depth level
48+ const POLE_Y = - 1.7 * ROW ; // the cross-cutting global types hover above the body
49+
50+ /// the instance (low) byte of an 8:8 tier.
51+ const inst = ( t : number ) => t & 0xff ;
52+
53+ // Nested horizontal slot from the [Chamber][Wall][Tissue][Cell] instance path:
54+ // each level subdivides its parent's slot (max-siblings per level: 4/3/2/2), so
55+ // children cluster under their parent. y is the depth band (class).
56+ function tierPos (
57+ soa : Soa ,
58+ i : number ,
59+ ) : { x : number ; y : number } {
60+ const path : Array < [ number , number ] > = [
61+ [ inst ( soa . hip [ i ] ) , 4 ] , // chamber 1..4
62+ [ inst ( soa . twig [ i ] ) , 3 ] , // wall 1..3
63+ [ inst ( soa . leaf [ i ] ) , 2 ] , // tissue 1..2
64+ [ inst ( soa . family [ i ] ) , 2 ] , // cell 1..2
65+ ] ;
66+ let lo = 0 ;
67+ let hi = 1 ;
68+ for ( const [ id , n ] of path ) {
69+ if ( id <= 0 ) break ;
70+ const w = ( hi - lo ) / n ;
71+ lo += ( id - 1 ) * w ;
72+ hi = lo + w ;
73+ }
74+ return { x : ( ( lo + hi ) / 2 ) * COL , y : soa . cls [ i ] * ROW } ;
75+ }
76+
3077const OPTIONS : Options = {
3178 nodes : { shape : 'dot' , borderWidth : 2.5 , font : { color : '#d9e9f9' , size : 13 , strokeWidth : 3 , strokeColor : PAGE_BG } } ,
3279 edges : {
@@ -36,11 +83,8 @@ const OPTIONS: Options = {
3683 smooth : { enabled : true , type : 'continuous' , roundness : 0.2 } ,
3784 arrows : { to : { enabled : true , scaleFactor : 0.45 } } ,
3885 } ,
39- physics : {
40- solver : 'forceAtlas2Based' ,
41- forceAtlas2Based : { gravitationalConstant : - 70 , centralGravity : 0.008 , springLength : 130 , springConstant : 0.04 , damping : 0.5 , avoidOverlap : 0.5 } ,
42- stabilization : { iterations : 180 , fit : true } ,
43- } ,
86+ // positions are fixed 8:8-tier slots (see tierPos) — no force simulation.
87+ physics : { enabled : false } ,
4488 interaction : { hover : true , tooltipDelay : 90 , dragNodes : true } ,
4589 layout : { improvedLayout : false } ,
4690} ;
@@ -93,18 +137,40 @@ export function FmaGraph() {
93137 useEffect ( ( ) => {
94138 if ( ! hostRef . current || ! soa || ! rel ) return ;
95139 const ceiling = ( i : number ) => soa . ceiling [ i ] === 1 || soa . cls [ i ] === 5 ;
96- const baseNode = ( i : number ) => ( {
97- id : i ,
98- label : soa . labels [ i ] || `#${ i } ` ,
99- shape : ceiling ( i ) ? 'diamond' : 'dot' ,
100- color : {
101- background : ceiling ( i ) ? 'rgba(255,209,102,0.14)' : 'rgba(10,14,23,0.88)' ,
102- border : ceiling ( i ) ? CEILING_COLOR : classColor ( soa . cls [ i ] ) ,
103- } ,
104- size : ceiling ( i ) ? 22 : 13 ,
105- font : { color : ceiling ( i ) ? '#ffe9b0' : '#d9e9f9' } ,
106- title : `${ soa . labels [ i ] } \n${ ceiling ( i ) ? '◈ global type (leaf-limited, cross-cutting)' : FMA_CLASS [ soa . cls [ i ] ] ?. name } ` ,
107- } ) ;
140+
141+ // Fixed position per node, read straight off the 8:8 [container:identity]
142+ // HHTL tiers: part-of nodes nest by their [Chamber][Wall][Tissue][Cell]
143+ // instance path (y = depth = class); the cross-cutting global types line up
144+ // along the pole above the body, spread across the same width.
145+ const poleNodes = Array . from ( { length : soa . nodeCount } , ( _ , i ) => i ) . filter ( ceiling ) ;
146+ const posOf = ( i : number ) : { x : number ; y : number ; size : number } => {
147+ if ( ceiling ( i ) ) {
148+ const k = poleNodes . indexOf ( i ) ;
149+ const x = ( ( k + 0.5 ) / Math . max ( poleNodes . length , 1 ) ) * COL ;
150+ return { x, y : POLE_Y , size : 22 } ;
151+ }
152+ const { x, y } = tierPos ( soa , i ) ;
153+ return { x, y, size : 30 - soa . cls [ i ] * 4 } ; // coarser tier → larger dot
154+ } ;
155+
156+ const baseNode = ( i : number ) => {
157+ const p = posOf ( i ) ;
158+ return {
159+ id : i ,
160+ label : soa . labels [ i ] || `#${ i } ` ,
161+ x : p . x ,
162+ y : p . y ,
163+ fixed : { x : true , y : true } , // the address is the layout — pin it
164+ shape : ceiling ( i ) ? 'diamond' : 'dot' ,
165+ color : {
166+ background : ceiling ( i ) ? 'rgba(255,209,102,0.14)' : 'rgba(10,14,23,0.88)' ,
167+ border : ceiling ( i ) ? CEILING_COLOR : classColor ( soa . cls [ i ] ) ,
168+ } ,
169+ size : p . size ,
170+ font : { color : ceiling ( i ) ? '#ffe9b0' : '#d9e9f9' } ,
171+ title : `${ soa . labels [ i ] } \n${ ceiling ( i ) ? '◈ global type (leaf-limited, cross-cutting)' : FMA_CLASS [ soa . cls [ i ] ] ?. name } ` ,
172+ } ;
173+ } ;
108174 const baseEdge = ( e : { s : number ; t : number ; r : number } , id : number ) => ( {
109175 id,
110176 from : e . s ,
@@ -117,10 +183,9 @@ export function FmaGraph() {
117183 const visNodes = new DataSet < any > ( Array . from ( { length : soa . nodeCount } , ( _ , i ) => baseNode ( i ) ) ) ;
118184 const visEdges = new DataSet < any > ( soa . edges . map ( ( e , id ) => baseEdge ( e , id ) ) ) ;
119185 const net = new Network ( hostRef . current , { nodes : visNodes , edges : visEdges } , OPTIONS ) ;
120- net . once ( 'stabilizationIterationsDone' , ( ) => {
121- net . setOptions ( { physics : { enabled : false } } ) ;
122- setStatus ( `${ soa . nodeCount } nodes · ${ soa . edgeCount } edges — click a tissue to see its dual membership` ) ;
123- } ) ;
186+ // fixed 8:8-tier slots, no simulation — just frame the nested cascade.
187+ net . once ( 'afterDrawing' , ( ) => net . fit ( { animation : false } ) ) ;
188+ setStatus ( `${ soa . nodeCount } nodes · ${ soa . edgeCount } edges — Z-order tile pyramid; click a tissue for its dual membership` ) ;
124189
125190 const dim = ( ) => {
126191 visNodes . update ( Array . from ( { length : soa . nodeCount } , ( _ , i ) => ( { id : i , color : { background : 'rgba(10,14,23,0.5)' , border : '#26323f' } , font : { color : '#566779' } } ) ) ) ;
0 commit comments