@@ -278,6 +278,14 @@ export class GraphManager {
278278 . on ( 'tick' , ( ) => this . onTick ( ) ) ;
279279 }
280280
281+ private usesGuidedTreeLayout ( ) {
282+ return this . layoutMode !== 'web' ;
283+ }
284+
285+ private isStructuredMapMode ( ) {
286+ return this . layoutMode === 'structured' ;
287+ }
288+
281289 private getMetadata ( nodeId : string ) {
282290 return this . nodeMetadata . get ( nodeId ) || createDefaultNodeMetadata ( ) ;
283291 }
@@ -317,6 +325,7 @@ export class GraphManager {
317325 height : this . height ,
318326 treeSpacing : this . treeSpacing ,
319327 branchSpread : this . branchSpread ,
328+ layoutMode : this . layoutMode ,
320329 } ) ;
321330
322331 this . childrenByParent = forestLayout . childrenByParent ;
@@ -343,25 +352,33 @@ export class GraphManager {
343352 ( this . simulation . force ( 'y' ) as d3 . ForceY < Node > ) . strength ( ( node ) => this . getTargetStrength ( node ) ) ;
344353
345354 if ( reheat ) {
346- this . simulation . alpha ( this . layoutMode === 'forest' ? 0.55 : 0.3 ) . restart ( ) ;
355+ const nextAlpha = this . layoutMode === 'forest' ? 0.55 : this . isStructuredMapMode ( ) ? 0.44 : 0.3 ;
356+ this . simulation . alpha ( nextAlpha ) . restart ( ) ;
347357 }
348358 }
349359
350360 private getCollisionRadius ( node : Node ) {
351361 const connections = this . degreeById . get ( node . id ) || 0 ;
352362 const meta = this . getMetadata ( node . id ) ;
353- const layoutBoost = this . layoutMode === 'forest' && meta . colorRole === 'root' ? 6 : 0 ;
363+ const layoutBoost = this . usesGuidedTreeLayout ( ) && meta . colorRole === 'root'
364+ ? ( this . isStructuredMapMode ( ) ? 8 : 6 )
365+ : 0 ;
354366 return ( Math . min ( 30 + connections * 0.5 , 60 ) * this . nodeSizeScale ) + layoutBoost ;
355367 }
356368
357369 private getChargeStrength ( node : Node ) {
358- if ( this . layoutMode !== 'forest ') return - 500 ;
370+ if ( this . layoutMode === 'web ') return - 500 ;
359371 const meta = this . getMetadata ( node . id ) ;
372+ if ( this . isStructuredMapMode ( ) ) {
373+ if ( meta . colorRole === 'root' ) return - 340 ;
374+ if ( meta . layoutDepth !== undefined && meta . layoutDepth > 2 ) return - 190 ;
375+ return - 235 ;
376+ }
360377 return meta . colorRole === 'root' ? - 360 : - 220 ;
361378 }
362379
363380 private getLayoutTarget ( node : Node ) {
364- if ( this . layoutMode !== 'forest' ) {
381+ if ( ! this . usesGuidedTreeLayout ( ) ) {
365382 return { x : this . width / 2 , y : this . height / 2 } ;
366383 }
367384
@@ -377,15 +394,24 @@ export class GraphManager {
377394 }
378395
379396 private getTargetStrength ( node : Node ) {
380- if ( this . layoutMode !== 'forest' ) return 0.02 ;
397+ if ( ! this . usesGuidedTreeLayout ( ) ) return 0.02 ;
381398 const meta = this . getMetadata ( node . id ) ;
382399 if ( meta . isPinned ) return 0.4 ;
400+ if ( this . isStructuredMapMode ( ) ) {
401+ if ( meta . colorRole === 'root' ) return 0.24 ;
402+ return 0.18 ;
403+ }
383404 if ( meta . colorRole === 'root' ) return 0.18 ;
384405 return 0.12 ;
385406 }
386407
387408 private getLinkDistance ( link : Link ) {
388- if ( this . layoutMode !== 'forest' ) return this . nodeSpacing ;
409+ if ( ! this . usesGuidedTreeLayout ( ) ) return this . nodeSpacing ;
410+ if ( this . isStructuredMapMode ( ) ) {
411+ return link . layoutRole === 'cross'
412+ ? Math . max ( this . nodeSpacing * 0.6 , 96 )
413+ : Math . max ( this . treeSpacing * 0.74 , 118 ) ;
414+ }
389415 return link . layoutRole === 'cross'
390416 ? Math . max ( this . nodeSpacing * 0.75 , 120 )
391417 : Math . max ( this . treeSpacing * 0.58 , 92 ) ;
@@ -400,7 +426,7 @@ export class GraphManager {
400426 const targetId = typeof link . target === 'object' ? link . target . id : link . target ;
401427
402428 if ( this . hiddenNodeIds . has ( sourceId ) || this . hiddenNodeIds . has ( targetId ) ) return false ;
403- if ( this . layoutMode === 'forest' && ! this . showCrossLinks && link . layoutRole === 'cross' ) return false ;
429+ if ( this . usesGuidedTreeLayout ( ) && ! this . showCrossLinks && link . layoutRole === 'cross' ) return false ;
404430 return true ;
405431 }
406432
@@ -781,7 +807,7 @@ export class GraphManager {
781807 const node = this . nodes . find ( ( candidate ) => candidate . id === nodeId ) ;
782808 if ( ! node || node . x === undefined || node . y === undefined ) return ;
783809 this . updatePinnedNodePosition ( nodeId , { x : node . x , y : node . y } ) ;
784- this . refreshDerivedState ( { reheat : this . layoutMode === 'forest' } ) ;
810+ this . refreshDerivedState ( { reheat : this . usesGuidedTreeLayout ( ) } ) ;
785811 this . updateDOM ( ) ;
786812 }
787813
@@ -796,7 +822,7 @@ export class GraphManager {
796822 } ) ;
797823 node . fx = null ;
798824 node . fy = null ;
799- this . refreshDerivedState ( { reheat : this . layoutMode === 'forest' } ) ;
825+ this . refreshDerivedState ( { reheat : this . usesGuidedTreeLayout ( ) } ) ;
800826 this . updateDOM ( ) ;
801827 }
802828
@@ -811,7 +837,7 @@ export class GraphManager {
811837 ...meta ,
812838 isCollapsed : nextValue ,
813839 } ) ;
814- this . refreshDerivedState ( { reheat : this . layoutMode === 'forest' } ) ;
840+ this . refreshDerivedState ( { reheat : this . usesGuidedTreeLayout ( ) } ) ;
815841 this . updateDOM ( ) ;
816842 return nextValue ;
817843 }
@@ -834,7 +860,7 @@ export class GraphManager {
834860 node . fx = null ;
835861 node . fy = null ;
836862 } ) ;
837- this . refreshDerivedState ( { reheat : this . layoutMode === 'forest' } ) ;
863+ this . refreshDerivedState ( { reheat : this . usesGuidedTreeLayout ( ) } ) ;
838864 this . updateDOM ( ) ;
839865 }
840866
@@ -1221,6 +1247,7 @@ export class GraphManager {
12211247 const isBacklink = typeof d . type === 'string' && d . type . includes ( 'backlink' ) ;
12221248 const isCrossLink = d . layoutRole === 'cross' ;
12231249 const isForestPrimary = this . layoutMode === 'forest' && ! isCrossLink ;
1250+ const isStructuredPrimary = this . isStructuredMapMode ( ) && ! isCrossLink ;
12241251
12251252 const focusActive = this . focusNodeId !== null ;
12261253 const isIncidentToFocus = focusActive && ( sourceId === this . focusNodeId || targetId === this . focusNodeId ) ;
@@ -1236,6 +1263,9 @@ export class GraphManager {
12361263 const baseStroke = ( ( ) => {
12371264 if ( isDimmed ) return '#555' ;
12381265 if ( isPathLink ) return '#00ff88' ;
1266+ if ( isStructuredPrimary && originSeed ) {
1267+ return this . hashColor ( originSeed , 0.7 , 0.64 , Math . max ( 0 , Math . min ( 12 , originDepth ) ) * 7 ) ;
1268+ }
12391269 if ( isForestPrimary && originSeed ) {
12401270 return this . hashColor ( originSeed , 0.84 , 0.66 , Math . max ( 0 , Math . min ( 12 , originDepth ) ) * 8 ) ;
12411271 }
@@ -1248,6 +1278,7 @@ export class GraphManager {
12481278 const baseStrokeWidth = ( ( ) => {
12491279 if ( isDimmed ) return 1 ;
12501280 if ( isPathLink ) return 4 ;
1281+ if ( isStructuredPrimary ) return 3.2 ;
12511282 if ( isForestPrimary ) return 3.8 ;
12521283 if ( isCrossLink ) return 1.8 ;
12531284 return isBacklink ? 2 : 3 ;
@@ -1256,6 +1287,7 @@ export class GraphManager {
12561287 const baseStrokeOpacity = ( ( ) => {
12571288 if ( isDimmed ) return 0.12 ;
12581289 if ( isPathLink ) return 0.85 ;
1290+ if ( isStructuredPrimary ) return 0.72 ;
12591291 if ( isForestPrimary ) return 0.9 ;
12601292 if ( isCrossLink ) return 0.28 ;
12611293 return isBacklink ? 0.5 : 0.6 ;
@@ -1407,7 +1439,7 @@ export class GraphManager {
14071439
14081440 group . attr ( 'opacity' , this . getNodeOpacity ( meta ) ) ;
14091441
1410- const radius = this . getCollisionRadius ( d ) - ( this . layoutMode === 'forest' && meta . colorRole === 'root' ? 6 : 0 ) ;
1442+ const radius = this . getCollisionRadius ( d ) - ( this . usesGuidedTreeLayout ( ) && meta . colorRole === 'root' ? 6 : 0 ) ;
14111443 const focusScale = this . getFocusScale ( meta ) ;
14121444 inner . attr ( 'transform' , `scale(${ focusScale } )` ) ;
14131445
@@ -1534,11 +1566,11 @@ export class GraphManager {
15341566 if ( meta . isBulkSelected ) return '#ff8800' ; // Orange for bulk-selected
15351567 if ( meta . originSeed ) {
15361568 const depth = Math . max ( 0 , Math . min ( 12 , meta . layoutDepth ?? meta . originDepth ?? 0 ) ) ;
1537- const hueOffset = depth * ( this . layoutMode === 'forest' ? 11 : 14 ) ;
1538- const saturation = this . layoutMode === 'forest'
1569+ const hueOffset = depth * ( this . usesGuidedTreeLayout ( ) ? 11 : 14 ) ;
1570+ const saturation = this . usesGuidedTreeLayout ( )
15391571 ? Math . max ( 0.48 , 0.86 - depth * 0.04 )
15401572 : Math . max ( 0.42 , 0.78 - depth * 0.045 ) ;
1541- const lightness = this . layoutMode === 'forest'
1573+ const lightness = this . usesGuidedTreeLayout ( )
15421574 ? Math . max ( 0.32 , 0.62 - depth * 0.03 )
15431575 : Math . max ( 0.34 , 0.56 - depth * 0.02 ) ;
15441576 return this . hashColor ( meta . originSeed , saturation , lightness , hueOffset ) ;
@@ -1586,8 +1618,8 @@ export class GraphManager {
15861618 if ( meta . isBulkSelected ) return '#ffff00' ; // Yellow stroke for bulk-selected
15871619 if ( meta . isCurrentlyExploring ) return '#ff6600' ; // Orange stroke for exploring
15881620 if ( meta . isExpanded ) return '#00ffff' ; // Cyan for expanded
1589- if ( this . layoutMode === 'forest' && meta . isPinned ) return '#f8fafc' ;
1590- if ( this . layoutMode === 'forest' && meta . colorRole === 'root' ) return '#e2e8f0' ;
1621+ if ( this . usesGuidedTreeLayout ( ) && meta . isPinned ) return '#f8fafc' ;
1622+ if ( this . usesGuidedTreeLayout ( ) && meta . colorRole === 'root' ) return '#e2e8f0' ;
15911623 return '#fff' ;
15921624 }
15931625
@@ -1598,8 +1630,8 @@ export class GraphManager {
15981630 if ( meta . isPathEndpoint ) return 5 ;
15991631 if ( meta . isBulkSelected ) return 3 ;
16001632 if ( meta . isExpanded ) return 3 ;
1601- if ( this . layoutMode === 'forest' && meta . isPinned ) return 3 ;
1602- if ( this . layoutMode === 'forest' && meta . colorRole === 'root' ) return 2.5 ;
1633+ if ( this . usesGuidedTreeLayout ( ) && meta . isPinned ) return 3 ;
1634+ if ( this . usesGuidedTreeLayout ( ) && meta . colorRole === 'root' ) return 2.5 ;
16031635 if ( meta . isDimmed ) return 1 ;
16041636 return 2 ;
16051637 }
@@ -1621,7 +1653,7 @@ export class GraphManager {
16211653 . attr ( 'text-anchor' , 'middle' )
16221654 . attr ( 'fill' , '#ffffff' )
16231655 . attr ( 'font-size' , `${ Math . max ( 7 , 9 * this . nodeSizeScale ) } px` )
1624- . attr ( 'font-weight' , this . layoutMode === 'forest' ? 700 : 'bold' )
1656+ . attr ( 'font-weight' , this . usesGuidedTreeLayout ( ) ? 700 : 'bold' )
16251657 . attr ( 'pointer-events' , 'none' ) ;
16261658
16271659 // Simple text wrapping
@@ -1688,7 +1720,7 @@ export class GraphManager {
16881720 event . sourceEvent . preventDefault ( ) ;
16891721 d . fx = event . x ;
16901722 d . fy = event . y ;
1691- if ( this . layoutMode === 'forest' ) {
1723+ if ( this . usesGuidedTreeLayout ( ) ) {
16921724 this . updatePinnedNodePosition ( d . id , { x : event . x , y : event . y } ) ;
16931725 }
16941726 }
@@ -1701,7 +1733,7 @@ export class GraphManager {
17011733 const distance = Math . sqrt ( dx * dx + dy * dy ) ;
17021734
17031735 if ( distance > this . dragThreshold ) {
1704- if ( this . layoutMode === 'forest' ) {
1736+ if ( this . usesGuidedTreeLayout ( ) ) {
17051737 this . updatePinnedNodePosition ( d . id , { x : event . x , y : event . y } ) ;
17061738 this . refreshDerivedState ( { reheat : true } ) ;
17071739 this . updateDOM ( ) ;
@@ -1768,7 +1800,8 @@ export class GraphManager {
17681800 const screenX = transform . applyX ( n . x ) ;
17691801 const screenY = transform . applyY ( n . y ) ;
17701802 const degree = this . degreeById . get ( n . id ) || 0 ;
1771- const mass = ( 0.8 + Math . min ( 10 , degree ) * 0.25 ) * this . nodeSizeScale * ( this . layoutMode === 'forest' ? 0.82 : 1 ) ;
1803+ const modeScale = this . layoutMode === 'forest' ? 0.82 : this . isStructuredMapMode ( ) ? 0.88 : 1 ;
1804+ const mass = ( 0.8 + Math . min ( 10 , degree ) * 0.25 ) * this . nodeSizeScale * modeScale ;
17721805 result . push ( { x : screenX , y : screenY , mass } ) ;
17731806 }
17741807 return result ;
0 commit comments