11import ELK from "elkjs/lib/elk.bundled.js" ;
2- import type { ElkNode , ElkExtendedEdge , LayoutOptions as ElkLayoutOptions } from "elkjs/lib/elk-api" ;
32import type {
3+ ElkNode ,
4+ ElkExtendedEdge ,
5+ LayoutOptions as ElkLayoutOptions ,
6+ } from "elkjs/lib/elk-api" ;
7+ import type {
8+ LayoutAlgorithm ,
9+ LayoutDirection ,
410 LayoutInput ,
511 LayoutNodeInput ,
612 LayoutOptions ,
@@ -20,6 +26,38 @@ const DEFAULT_SPACING = {
2026const DEFAULT_GROUP_PADDING = 40 ;
2127const GROUP_HEADER_PADDING = 60 ;
2228
29+ /**
30+ * Build ELK layoutOptions for a given algorithm. `rectpacking` and `layered`
31+ * share `elk.spacing.nodeNode` but otherwise take disjoint option keys — ELK
32+ * silently ignores irrelevant keys but we keep the output narrow for clarity.
33+ */
34+ function buildAlgorithmLayoutOptions ( params : {
35+ algorithm : LayoutAlgorithm ;
36+ direction : LayoutDirection ;
37+ spacing : { nodeNode : number ; nodeNodeBetweenLayers : number ; edgeNode : number } ;
38+ aspectRatio ?: number ;
39+ includeHierarchyHandling ?: "INCLUDE_CHILDREN" | "SEPARATE_CHILDREN" ;
40+ } ) : ElkLayoutOptions {
41+ const options : ElkLayoutOptions = {
42+ "elk.algorithm" : params . algorithm ,
43+ "elk.spacing.nodeNode" : String ( params . spacing . nodeNode ) ,
44+ } ;
45+ if ( params . algorithm === "layered" ) {
46+ options [ "elk.direction" ] = params . direction ;
47+ options [ "elk.layered.spacing.nodeNodeBetweenLayers" ] = String (
48+ params . spacing . nodeNodeBetweenLayers ,
49+ ) ;
50+ options [ "elk.spacing.edgeNode" ] = String ( params . spacing . edgeNode ) ;
51+ }
52+ if ( params . algorithm === "rectpacking" && params . aspectRatio !== undefined ) {
53+ options [ "elk.aspectRatio" ] = String ( params . aspectRatio ) ;
54+ }
55+ if ( params . includeHierarchyHandling ) {
56+ options [ "elk.hierarchyHandling" ] = params . includeHierarchyHandling ;
57+ }
58+ return options ;
59+ }
60+
2361/**
2462 * Lay out a Ryzome canvas with elkjs. Groups become compound (parent) nodes so
2563 * members stay spatially clustered; the result includes both individual node
@@ -36,6 +74,7 @@ export async function computeCanvasLayout(
3674
3775 const spacing = { ...DEFAULT_SPACING , ...options . spacing } ;
3876 const direction = options . direction ?? "DOWN" ;
77+ const algorithm : LayoutAlgorithm = options . algorithm ?? "layered" ;
3978 const groupPadding = options . groupPadding ?? DEFAULT_GROUP_PADDING ;
4079
4180 const measureNode =
@@ -100,36 +139,47 @@ export async function computeCanvasLayout(
100139 if ( members . length === 0 ) continue ;
101140
102141 const pad = group . padding ?? groupPadding ;
142+ const groupDirection = group . direction ?? direction ;
143+ const groupAlgorithm = group . algorithm ?? algorithm ;
144+ // When the group's direction or algorithm differs from the root, the
145+ // root's INCLUDE_CHILDREN hierarchy handling would otherwise let the
146+ // parent layout dominate. Force SEPARATE_CHILDREN so the override
147+ // actually takes effect on this group's members.
148+ const needsLocalLayout =
149+ groupDirection !== direction || groupAlgorithm !== algorithm ;
150+ const groupLayoutOptions = buildAlgorithmLayoutOptions ( {
151+ algorithm : groupAlgorithm ,
152+ direction : groupDirection ,
153+ spacing,
154+ aspectRatio : group . aspectRatio ,
155+ includeHierarchyHandling : needsLocalLayout
156+ ? "SEPARATE_CHILDREN"
157+ : undefined ,
158+ } ) ;
159+ groupLayoutOptions [ "elk.padding" ] =
160+ `[top=${ GROUP_HEADER_PADDING } ,left=${ pad } ,bottom=${ pad } ,right=${ pad } ]` ;
103161 const groupNode : ElkNode = {
104162 id : group . id ,
105163 children : members . map ( makeLeafNode ) ,
106- layoutOptions : {
107- "elk.padding" : `[top=${ GROUP_HEADER_PADDING } ,left=${ pad } ,bottom=${ pad } ,right=${ pad } ]` ,
108- // Still layer members inside the group.
109- "elk.algorithm" : "layered" ,
110- "elk.direction" : direction ,
111- "elk.layered.spacing.nodeNodeBetweenLayers" : String (
112- spacing . nodeNodeBetweenLayers ,
113- ) ,
114- "elk.spacing.nodeNode" : String ( spacing . nodeNode ) ,
115- } ,
164+ layoutOptions : groupLayoutOptions ,
116165 } ;
117166 rootChildren . push ( groupNode ) ;
118167 }
119168
120- const rootLayoutOptions : ElkLayoutOptions = {
121- "elk.algorithm" : "layered" ,
122- "elk.direction" : direction ,
123- "elk.layered.spacing.nodeNodeBetweenLayers" : String (
124- spacing . nodeNodeBetweenLayers ,
125- ) ,
126- "elk.spacing.nodeNode" : String ( spacing . nodeNode ) ,
127- "elk.spacing.edgeNode" : String ( spacing . edgeNode ) ,
128- // Place disconnected components side-by-side instead of overlapping them at the origin.
129- "elk.separateConnectedComponents" : "true" ,
169+ const rootLayoutOptions = buildAlgorithmLayoutOptions ( {
170+ algorithm,
171+ direction,
172+ spacing,
173+ aspectRatio : options . aspectRatio ,
130174 // Consider node size when routing; hierarchical edges cross group boundaries.
131- "elk.hierarchyHandling" : "INCLUDE_CHILDREN" ,
132- } ;
175+ // Only meaningful for `layered`.
176+ includeHierarchyHandling :
177+ algorithm === "layered" ? "INCLUDE_CHILDREN" : undefined ,
178+ } ) ;
179+ if ( algorithm === "layered" ) {
180+ // Place disconnected components side-by-side instead of overlapping at origin.
181+ rootLayoutOptions [ "elk.separateConnectedComponents" ] = "true" ;
182+ }
133183
134184 const root : ElkNode = {
135185 id : "__root__" ,
0 commit comments