@principal-ai/subsystems-react 0.43.2 → 0.44.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -94,6 +94,15 @@ export interface ElkLayoutOptions {
94
94
  */
95
95
  orderByLine?: boolean;
96
96
 
97
+ /**
98
+ * Reflow root-level compound frames into a column-major grid after ELK runs:
99
+ * fill each column top-to-bottom, then start the next column to the right.
100
+ * Gives an approximately square shape for edge-less containment graphs, which
101
+ * ELK otherwise packs into one long strip. No effect when edges exist (their
102
+ * routes would need re-routing).
103
+ */
104
+ reflowGrid?: boolean;
105
+
97
106
  /**
98
107
  * Compound groups — each becomes an ELK parent whose `memberIds` are laid
99
108
  * out inside it. `memberIds` may be leaf node ids or other group ids
@@ -529,6 +538,7 @@ export async function computeElkLayout(
529
538
  const edgeLabels = options.edgeLabels;
530
539
  const endpointInset = options.endpointInset ?? 0;
531
540
  const direction = options.direction ?? 'RIGHT';
541
+ const reflowGrid = options.reflowGrid === true;
532
542
  const orderByLine = options.orderByLine ?? false;
533
543
 
534
544
  // Build a map of original node positions BEFORE passing to ELK
@@ -1023,6 +1033,102 @@ export async function computeElkLayout(
1023
1033
  return node;
1024
1034
  });
1025
1035
 
1036
+ // Optional column-major grid reflow for edge-less containment graphs. ELK
1037
+ // packs disconnected root frames into one long strip; fold them into a grid
1038
+ // (fill a column top-to-bottom, then start the next column) so the shape
1039
+ // approaches a square. Root frames are moved; their descendants (relative
1040
+ // group bounds / leaf positions) ride along, and absoluteRects is rebased so
1041
+ // hit-testing stays correct.
1042
+ if (reflowGrid && edges.length === 0) {
1043
+ const rootGroupIds: string[] = [];
1044
+ for (const g of groupDefs) {
1045
+ if (!builtGroups.has(g.id)) continue;
1046
+ if (g.parentId && builtGroups.has(g.parentId)) continue;
1047
+ rootGroupIds.push(g.id);
1048
+ }
1049
+ type GridItem = {
1050
+ id: string;
1051
+ kind: 'group' | 'leaf';
1052
+ x: number;
1053
+ y: number;
1054
+ w: number;
1055
+ h: number;
1056
+ };
1057
+ const items: GridItem[] = [];
1058
+ for (const id of rootGroupIds) {
1059
+ const b = groupBounds.get(id);
1060
+ if (b) items.push({ id, kind: 'group', x: b.x, y: b.y, w: b.width, h: b.height });
1061
+ }
1062
+ for (const id of ungroupedElkNodes.map((nd) => nd.id)) {
1063
+ const rel = elkRelativePositions.get(id) ?? { x: 0, y: 0 };
1064
+ const r = absoluteRects.get(id);
1065
+ items.push({ id, kind: 'leaf', x: rel.x, y: rel.y, w: r?.width ?? 160, h: r?.height ?? 60 });
1066
+ }
1067
+ if (items.length > 1) {
1068
+ const count = items.length;
1069
+ const rows = Math.max(1, Math.ceil(Math.sqrt(count)));
1070
+ const cols = Math.ceil(count / rows);
1071
+ const GAP = 80;
1072
+ const PAD = 40;
1073
+ const colW: number[] = new Array(cols).fill(0);
1074
+ const rowH: number[] = new Array(rows).fill(0);
1075
+ items.forEach((it, i) => {
1076
+ const c = Math.floor(i / rows);
1077
+ const r = i % rows;
1078
+ colW[c] = Math.max(colW[c] ?? 0, it.w);
1079
+ rowH[r] = Math.max(rowH[r] ?? 0, it.h);
1080
+ });
1081
+ const colX: number[] = [];
1082
+ let acc = PAD;
1083
+ for (let c = 0; c < cols; c++) {
1084
+ colX[c] = acc;
1085
+ acc += (colW[c] ?? 0) + GAP;
1086
+ }
1087
+ const rowY: number[] = [];
1088
+ acc = PAD;
1089
+ for (let r = 0; r < rows; r++) {
1090
+ rowY[r] = acc;
1091
+ acc += (rowH[r] ?? 0) + GAP;
1092
+ }
1093
+ const shiftAbsSubtree = (id: string, dx: number, dy: number) => {
1094
+ const ar = absoluteRects.get(id);
1095
+ if (ar) absoluteRects.set(id, { ...ar, x: ar.x + dx, y: ar.y + dy });
1096
+ const g = groupById.get(id);
1097
+ if (!g) return;
1098
+ for (const mid of g.memberIds) {
1099
+ if (builtGroups.has(mid)) shiftAbsSubtree(mid, dx, dy);
1100
+ else {
1101
+ const mar = absoluteRects.get(mid);
1102
+ if (mar) absoluteRects.set(mid, { ...mar, x: mar.x + dx, y: mar.y + dy });
1103
+ }
1104
+ }
1105
+ };
1106
+ const movedLeaves = new Map<string, { x: number; y: number }>();
1107
+ items.forEach((it, i) => {
1108
+ const c = Math.floor(i / rows);
1109
+ const r = i % rows;
1110
+ const dx = (colX[c] ?? 0) - it.x;
1111
+ const dy = (rowY[r] ?? 0) - it.y;
1112
+ if (dx === 0 && dy === 0) return;
1113
+ if (it.kind === 'group') {
1114
+ const b = groupBounds.get(it.id);
1115
+ if (b) groupBounds.set(it.id, { ...b, x: b.x + dx, y: b.y + dy });
1116
+ shiftAbsSubtree(it.id, dx, dy);
1117
+ } else {
1118
+ shiftAbsSubtree(it.id, dx, dy);
1119
+ const rel = elkRelativePositions.get(it.id);
1120
+ if (rel) movedLeaves.set(it.id, { x: rel.x + dx, y: rel.y + dy });
1121
+ }
1122
+ });
1123
+ if (movedLeaves.size > 0) {
1124
+ for (const node of resultNodes) {
1125
+ const m = movedLeaves.get(node.id);
1126
+ if (m) (node as { position: { x: number; y: number } }).position = m;
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+
1026
1132
  return {
1027
1133
  nodes: resultNodes,
1028
1134
  edgePaths,