@principal-ai/subsystems-react 0.43.1 → 0.44.0
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.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/subsystem/SubsystemModelTransition.d.ts +40 -0
- package/dist/subsystem/SubsystemModelTransition.d.ts.map +1 -0
- package/dist/subsystem/SubsystemModelTransition.js +232 -0
- package/dist/subsystem/SubsystemModelTransition.js.map +1 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +112 -82
- package/dist/subsystem/model.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +8 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +100 -0
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +5 -0
- package/src/stories/Subsystem/ComponentGraph/AnimatedProgression.stories.tsx +120 -0
- package/src/subsystem/SubsystemModelTransition.tsx +389 -0
- package/src/subsystem/model.test.ts +30 -0
- package/src/subsystem/model.ts +109 -79
- package/src/utils/elkLayout.ts +106 -0
package/src/subsystem/model.ts
CHANGED
|
@@ -1062,23 +1062,49 @@ export function buildBoundaryLayoutGroupsByPath(
|
|
|
1062
1062
|
keepRegion(r, showSingletons),
|
|
1063
1063
|
);
|
|
1064
1064
|
|
|
1065
|
-
// Directory
|
|
1066
|
-
//
|
|
1065
|
+
// Directory nesting is SCOPED to the enclosing process (else package, else
|
|
1066
|
+
// root), so module grouping stays subordinate to process boundaries. A
|
|
1067
|
+
// folder that spans two processes is split per process rather than becoming
|
|
1068
|
+
// a frame that belongs to neither — which would leave the process frames
|
|
1069
|
+
// childless and drop them. Composite key = `${scopeKey}\0${path}`.
|
|
1070
|
+
const scopeKeyFor = (c: SubsystemComponent): string => {
|
|
1071
|
+
const p = c.process?.trim();
|
|
1072
|
+
if (p && keptProcessKeys.has(p)) return `p:${p}`;
|
|
1073
|
+
const pkg = componentPackageKey(c);
|
|
1074
|
+
if (pkg && keptPackageKeys.has(pkg)) return `k:${pkg}`;
|
|
1075
|
+
return 'root';
|
|
1076
|
+
};
|
|
1077
|
+
const scopeParentId = (scopeKey: string): string | undefined => {
|
|
1078
|
+
if (scopeKey.startsWith('p:')) return processGroupNodeId(scopeKey.slice(2));
|
|
1079
|
+
if (scopeKey.startsWith('k:')) return packageGroupNodeId(scopeKey.slice(2));
|
|
1080
|
+
return undefined;
|
|
1081
|
+
};
|
|
1082
|
+
const dirGroupId = (scopeKey: string, path: string): string =>
|
|
1083
|
+
scopeKey === 'root'
|
|
1084
|
+
? directoryGroupNodeId(path)
|
|
1085
|
+
: `directory:${scopeKey}::${path}`;
|
|
1086
|
+
|
|
1067
1087
|
const dirMembers = new Map<string, Set<string>>();
|
|
1088
|
+
const dirPathOf = new Map<string, string>();
|
|
1089
|
+
const dirScopeOf = new Map<string, string>();
|
|
1068
1090
|
for (const c of doc.components) {
|
|
1069
1091
|
const m = c.module?.trim();
|
|
1070
1092
|
if (!m) continue;
|
|
1093
|
+
const scope = scopeKeyFor(c);
|
|
1071
1094
|
let d = parentDirectory(m);
|
|
1072
1095
|
while (d) {
|
|
1073
|
-
const
|
|
1096
|
+
const ck = `${scope}\0${d}`;
|
|
1097
|
+
const set = dirMembers.get(ck) ?? new Set<string>();
|
|
1074
1098
|
set.add(c.alias);
|
|
1075
|
-
dirMembers.set(
|
|
1099
|
+
dirMembers.set(ck, set);
|
|
1100
|
+
dirPathOf.set(ck, d);
|
|
1101
|
+
dirScopeOf.set(ck, scope);
|
|
1076
1102
|
d = parentDirectory(d);
|
|
1077
1103
|
}
|
|
1078
1104
|
}
|
|
1079
|
-
const
|
|
1080
|
-
for (const [
|
|
1081
|
-
if (members.size >= 2 || showSingletons)
|
|
1105
|
+
const keptDirs = new Set<string>();
|
|
1106
|
+
for (const [ck, members] of dirMembers) {
|
|
1107
|
+
if (members.size >= 2 || showSingletons) keptDirs.add(ck);
|
|
1082
1108
|
}
|
|
1083
1109
|
|
|
1084
1110
|
// Files sitting directly in each directory (drives chain compaction below).
|
|
@@ -1087,76 +1113,74 @@ export function buildBoundaryLayoutGroupsByPath(
|
|
|
1087
1113
|
const m = c.module?.trim();
|
|
1088
1114
|
if (!m) continue;
|
|
1089
1115
|
const d = parentDirectory(m);
|
|
1090
|
-
if (d)
|
|
1116
|
+
if (!d) continue;
|
|
1117
|
+
const ck = `${scopeKeyFor(c)}\0${d}`;
|
|
1118
|
+
directFileCount.set(ck, (directFileCount.get(ck) ?? 0) + 1);
|
|
1091
1119
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1120
|
+
const nearestKeptDirCk = (ck: string): string | undefined => {
|
|
1121
|
+
const scope = dirScopeOf.get(ck)!;
|
|
1122
|
+
let a = parentDirectory(dirPathOf.get(ck)!);
|
|
1123
|
+
while (a) {
|
|
1124
|
+
const cand = `${scope}\0${a}`;
|
|
1125
|
+
if (keptDirs.has(cand)) return cand;
|
|
1126
|
+
a = parentDirectory(a);
|
|
1127
|
+
}
|
|
1128
|
+
return undefined;
|
|
1129
|
+
};
|
|
1130
|
+
// Compact single-child directory chains within a scope, mirroring the file
|
|
1131
|
+
// tree: `app` → `app/book` renders one `app/book` frame, not `app` wrapping
|
|
1132
|
+
// `book`.
|
|
1096
1133
|
let compacting = true;
|
|
1097
1134
|
while (compacting) {
|
|
1098
1135
|
compacting = false;
|
|
1099
1136
|
const childDirsOf = new Map<string, string[]>();
|
|
1100
|
-
for (const
|
|
1101
|
-
|
|
1102
|
-
while (a && !keptDirKeys.has(a)) a = parentDirectory(a);
|
|
1137
|
+
for (const ck of keptDirs) {
|
|
1138
|
+
const a = nearestKeptDirCk(ck);
|
|
1103
1139
|
if (!a) continue;
|
|
1104
1140
|
const list = childDirsOf.get(a) ?? [];
|
|
1105
|
-
list.push(
|
|
1141
|
+
list.push(ck);
|
|
1106
1142
|
childDirsOf.set(a, list);
|
|
1107
1143
|
}
|
|
1108
|
-
for (const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1144
|
+
for (const ck of [...keptDirs]) {
|
|
1145
|
+
if (
|
|
1146
|
+
(childDirsOf.get(ck) ?? []).length === 1 &&
|
|
1147
|
+
(directFileCount.get(ck) ?? 0) === 0
|
|
1148
|
+
) {
|
|
1149
|
+
keptDirs.delete(ck);
|
|
1112
1150
|
compacting = true;
|
|
1113
1151
|
}
|
|
1114
1152
|
}
|
|
1115
1153
|
}
|
|
1154
|
+
const dirLabel = (ck: string): string => {
|
|
1155
|
+
const path = dirPathOf.get(ck)!;
|
|
1156
|
+
const ancestor = nearestKeptDirCk(ck);
|
|
1157
|
+
return ancestor ? path.slice(dirPathOf.get(ancestor)!.length + 1) : path;
|
|
1158
|
+
};
|
|
1116
1159
|
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1160
|
+
const moduleRegionByKey = new Map(modules.map((r) => [r.key, r]));
|
|
1161
|
+
// A module's scope is uniform across its members (else undefined → root), so
|
|
1162
|
+
// a module straddling processes is never forced into one.
|
|
1163
|
+
const moduleScope = new Map<string, string | undefined>();
|
|
1164
|
+
for (const r of modules) {
|
|
1165
|
+
const scopes = new Set<string>();
|
|
1166
|
+
for (const a of r.memberAliases) {
|
|
1167
|
+
const c = byAlias.get(a);
|
|
1168
|
+
if (c) scopes.add(scopeKeyFor(c));
|
|
1122
1169
|
}
|
|
1123
|
-
|
|
1124
|
-
}
|
|
1125
|
-
const
|
|
1126
|
-
.
|
|
1127
|
-
|
|
1128
|
-
// Label is the path relative to the enclosing kept directory (which
|
|
1129
|
-
// already carries the prefix): `book` under `app`, or the full collapsed
|
|
1130
|
-
// path `app/book` when the chain was compacted.
|
|
1131
|
-
const ancestor = nearestKeptDirAncestor(key);
|
|
1132
|
-
return {
|
|
1133
|
-
kind: 'directory' as const,
|
|
1134
|
-
key,
|
|
1135
|
-
label: ancestor ? key.slice(ancestor.length + 1) : key,
|
|
1136
|
-
memberAliases: [...(dirMembers.get(key) ?? [])],
|
|
1137
|
-
};
|
|
1138
|
-
});
|
|
1139
|
-
const moduleDirParent = (moduleKey: string): string | undefined => {
|
|
1170
|
+
moduleScope.set(r.key, scopes.size === 1 ? [...scopes][0] : undefined);
|
|
1171
|
+
}
|
|
1172
|
+
const moduleDirCk = (moduleKey: string): string | undefined => {
|
|
1173
|
+
const scope = moduleScope.get(moduleKey);
|
|
1174
|
+
if (scope === undefined) return undefined;
|
|
1140
1175
|
let d = parentDirectory(moduleKey);
|
|
1141
1176
|
while (d) {
|
|
1142
|
-
|
|
1177
|
+
const cand = `${scope}\0${d}`;
|
|
1178
|
+
if (keptDirs.has(cand)) return cand;
|
|
1143
1179
|
d = parentDirectory(d);
|
|
1144
1180
|
}
|
|
1145
1181
|
return undefined;
|
|
1146
1182
|
};
|
|
1147
1183
|
|
|
1148
|
-
const sharedProcessParent = (
|
|
1149
|
-
memberAliases: readonly string[],
|
|
1150
|
-
): string | undefined => {
|
|
1151
|
-
const seen = new Set<string>();
|
|
1152
|
-
for (const alias of memberAliases) {
|
|
1153
|
-
const p = byAlias.get(alias)?.process?.trim();
|
|
1154
|
-
if (p) seen.add(p);
|
|
1155
|
-
}
|
|
1156
|
-
if (seen.size !== 1) return undefined;
|
|
1157
|
-
const p = [...seen][0]!;
|
|
1158
|
-
return keptProcessKeys.has(p) ? processGroupNodeId(p) : undefined;
|
|
1159
|
-
};
|
|
1160
1184
|
const sharedPackageParent = (
|
|
1161
1185
|
memberAliases: readonly string[],
|
|
1162
1186
|
): string | undefined => {
|
|
@@ -1170,17 +1194,15 @@ export function buildBoundaryLayoutGroupsByPath(
|
|
|
1170
1194
|
const pkg = [...seen][0]!;
|
|
1171
1195
|
return keptPackageKeys.has(pkg) ? packageGroupNodeId(pkg) : undefined;
|
|
1172
1196
|
};
|
|
1173
|
-
const frameParent = (memberAliases: readonly string[]): string | undefined =>
|
|
1174
|
-
sharedProcessParent(memberAliases) ?? sharedPackageParent(memberAliases);
|
|
1175
1197
|
|
|
1176
1198
|
const moduleGroups: BoundaryLayoutGroup[] = modules.map((r) => {
|
|
1177
|
-
const
|
|
1178
|
-
const parentId =
|
|
1179
|
-
?
|
|
1180
|
-
:
|
|
1199
|
+
const ck = moduleDirCk(r.key);
|
|
1200
|
+
const parentId = ck
|
|
1201
|
+
? dirGroupId(dirScopeOf.get(ck)!, dirPathOf.get(ck)!)
|
|
1202
|
+
: scopeParentId(moduleScope.get(r.key) ?? 'root');
|
|
1181
1203
|
// Relative label when nested: `app/book/actions.ts` under `app/book`
|
|
1182
1204
|
// reads as `actions.ts`. At the root the full module path stays.
|
|
1183
|
-
const label =
|
|
1205
|
+
const label = ck ? r.key.slice(dirPathOf.get(ck)!.length + 1) : r.label;
|
|
1184
1206
|
return {
|
|
1185
1207
|
id: moduleGroupNodeId(r.key),
|
|
1186
1208
|
memberAliases: [...r.memberAliases],
|
|
@@ -1189,41 +1211,46 @@ export function buildBoundaryLayoutGroupsByPath(
|
|
|
1189
1211
|
};
|
|
1190
1212
|
});
|
|
1191
1213
|
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
const
|
|
1196
|
-
const ancestor = nearestKeptDirAncestor(r.key);
|
|
1214
|
+
const dirGroups: BoundaryLayoutGroup[] = [...keptDirs].map((ck) => {
|
|
1215
|
+
const scope = dirScopeOf.get(ck)!;
|
|
1216
|
+
const path = dirPathOf.get(ck)!;
|
|
1217
|
+
const ancestor = nearestKeptDirCk(ck);
|
|
1197
1218
|
const parentId = ancestor
|
|
1198
|
-
?
|
|
1199
|
-
:
|
|
1219
|
+
? dirGroupId(dirScopeOf.get(ancestor)!, dirPathOf.get(ancestor)!)
|
|
1220
|
+
: scopeParentId(scope);
|
|
1200
1221
|
// Immediate children: subdirectories and module frames parented here.
|
|
1201
1222
|
// Only child *group ids* go in the layout member list — a group's
|
|
1202
1223
|
// descendant leaves belong to its child groups alone, or ELK sees the
|
|
1203
1224
|
// same leaf in two compound parents and rejects the layout.
|
|
1204
|
-
const
|
|
1205
|
-
.filter((d) => nearestKeptDirAncestor(d.key) === r.key)
|
|
1206
|
-
.map((d) => d.key);
|
|
1225
|
+
const childDirCks = [...keptDirs].filter((d) => nearestKeptDirCk(d) === ck);
|
|
1207
1226
|
const childModuleKeys = modules
|
|
1208
|
-
.filter((m) =>
|
|
1227
|
+
.filter((m) => moduleDirCk(m.key) === ck)
|
|
1209
1228
|
.map((m) => m.key);
|
|
1210
1229
|
const claimed = new Set<string>();
|
|
1211
|
-
for (const
|
|
1212
|
-
for (const a of
|
|
1230
|
+
for (const d of childDirCks) {
|
|
1231
|
+
for (const a of dirMembers.get(d) ?? []) claimed.add(a);
|
|
1213
1232
|
}
|
|
1214
1233
|
for (const mk of childModuleKeys) {
|
|
1215
1234
|
for (const a of moduleRegionByKey.get(mk)?.memberAliases ?? []) claimed.add(a);
|
|
1216
1235
|
}
|
|
1217
|
-
const directLeaves =
|
|
1236
|
+
const directLeaves = [...(dirMembers.get(ck) ?? [])].filter(
|
|
1237
|
+
(a) => !claimed.has(a),
|
|
1238
|
+
);
|
|
1239
|
+
const region: SubsystemProcessRegion = {
|
|
1240
|
+
kind: 'directory',
|
|
1241
|
+
key: path,
|
|
1242
|
+
label: dirLabel(ck),
|
|
1243
|
+
memberAliases: [...(dirMembers.get(ck) ?? [])],
|
|
1244
|
+
};
|
|
1218
1245
|
return {
|
|
1219
|
-
id,
|
|
1246
|
+
id: dirGroupId(scope, path),
|
|
1220
1247
|
memberAliases: [
|
|
1221
|
-
...
|
|
1248
|
+
...childDirCks.map((d) => dirGroupId(dirScopeOf.get(d)!, dirPathOf.get(d)!)),
|
|
1222
1249
|
...childModuleKeys.map(moduleGroupNodeId),
|
|
1223
1250
|
...directLeaves,
|
|
1224
1251
|
],
|
|
1225
1252
|
parentId,
|
|
1226
|
-
region
|
|
1253
|
+
region,
|
|
1227
1254
|
};
|
|
1228
1255
|
});
|
|
1229
1256
|
|
|
@@ -2132,6 +2159,9 @@ export async function buildSubsystemGraph(
|
|
|
2132
2159
|
// layered pass place those sibling frames along the horizontal axis
|
|
2133
2160
|
// while each frame's members stack vertically.
|
|
2134
2161
|
direction: edges.length > 0 ? 'RIGHT' : 'DOWN',
|
|
2162
|
+
// Edge-less containment: fold the root frames into a column-major grid
|
|
2163
|
+
// so the shape reads closer to square than one long strip.
|
|
2164
|
+
reflowGrid: edges.length === 0,
|
|
2135
2165
|
nodeSpacing: 60,
|
|
2136
2166
|
edgeSpacing: 30,
|
|
2137
2167
|
edgeNodeSpacing: 60,
|
package/src/utils/elkLayout.ts
CHANGED
|
@@ -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,
|