@principal-ai/principal-view-react 0.16.57 → 0.16.59
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/subsystem/SubsystemComponentGraph.d.ts +14 -5
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +436 -50
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +73 -7
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +91 -11
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +38 -3
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +95 -4
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +26 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +152 -13
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +181 -0
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +68 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +600 -50
- package/src/subsystem/model.test.ts +56 -0
- package/src/subsystem/model.ts +156 -12
- package/src/subsystem/nodes.test.ts +56 -0
- package/src/subsystem/nodes.tsx +122 -6
- package/src/utils/elkLayout.ts +164 -13
package/src/utils/elkLayout.ts
CHANGED
|
@@ -71,6 +71,14 @@ export interface ElkLayoutOptions {
|
|
|
71
71
|
* @default 'RIGHT'
|
|
72
72
|
*/
|
|
73
73
|
direction?: 'RIGHT' | 'LEFT' | 'DOWN' | 'UP';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compound groups — each becomes a nested ELK parent whose `memberIds`
|
|
77
|
+
* are laid out inside it. Members reference the group via React Flow
|
|
78
|
+
* `parentId`; ELK returns parent-relative child positions which this
|
|
79
|
+
* module flattens back to absolute flow coordinates.
|
|
80
|
+
*/
|
|
81
|
+
groups?: Array<{ id: string; memberIds: string[] }>;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/** Result of ELK layout computation */
|
|
@@ -83,6 +91,8 @@ export interface ElkLayoutResult {
|
|
|
83
91
|
edgeLabelPositions: Map<string, { x: number; y: number }>;
|
|
84
92
|
/** Raw ELK path points per edge (for debugging). */
|
|
85
93
|
edgePathPoints: Map<string, Point[]>;
|
|
94
|
+
/** Compound parent bounds from ELK (absolute flow coords), keyed by group id. */
|
|
95
|
+
groupBounds: Map<string, { x: number; y: number; width: number; height: number }>;
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
/** Point in 2D space */
|
|
@@ -102,6 +112,8 @@ interface ElkEdgeSection {
|
|
|
102
112
|
/** Extended ELK edge with sections */
|
|
103
113
|
interface ElkEdgeWithSections extends ElkExtendedEdge {
|
|
104
114
|
sections?: ElkEdgeSection[];
|
|
115
|
+
/** Id of the compound node whose coordinate frame sections/labels use. */
|
|
116
|
+
container?: string;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
// Create ELK instance lazily to avoid issues in test environments
|
|
@@ -228,6 +240,47 @@ export function calculatePathMidpoint(points: Point[]): Point {
|
|
|
228
240
|
return points[points.length - 1];
|
|
229
241
|
}
|
|
230
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Closest point on a polyline to a target (for snapping labels onto the stroke).
|
|
245
|
+
*/
|
|
246
|
+
export function closestPointOnPath(points: Point[], target: Point): Point {
|
|
247
|
+
if (points.length === 0) return { x: 0, y: 0 };
|
|
248
|
+
if (points.length === 1) return points[0];
|
|
249
|
+
|
|
250
|
+
let best = points[0];
|
|
251
|
+
let bestDist = Infinity;
|
|
252
|
+
|
|
253
|
+
for (let i = 1; i < points.length; i++) {
|
|
254
|
+
const a = points[i - 1];
|
|
255
|
+
const b = points[i];
|
|
256
|
+
const dx = b.x - a.x;
|
|
257
|
+
const dy = b.y - a.y;
|
|
258
|
+
const lenSq = dx * dx + dy * dy;
|
|
259
|
+
const t = lenSq === 0 ? 0 : Math.max(0, Math.min(1, ((target.x - a.x) * dx + (target.y - a.y) * dy) / lenSq));
|
|
260
|
+
const px = a.x + dx * t;
|
|
261
|
+
const py = a.y + dy * t;
|
|
262
|
+
const dist = Math.hypot(target.x - px, target.y - py);
|
|
263
|
+
if (dist < bestDist) {
|
|
264
|
+
bestDist = dist;
|
|
265
|
+
best = { x: px, y: py };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return best;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Polyline length in flow-space units (sum of segment lengths).
|
|
274
|
+
* @public Exported for testing
|
|
275
|
+
*/
|
|
276
|
+
export function calculatePathLength(points: Point[]): number {
|
|
277
|
+
let total = 0;
|
|
278
|
+
for (let i = 1; i < points.length; i++) {
|
|
279
|
+
total += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
|
|
280
|
+
}
|
|
281
|
+
return total;
|
|
282
|
+
}
|
|
283
|
+
|
|
231
284
|
/**
|
|
232
285
|
* Get ELK layout options based on configuration
|
|
233
286
|
*/
|
|
@@ -447,22 +500,103 @@ export async function computeElkLayout(
|
|
|
447
500
|
return elkEdge;
|
|
448
501
|
});
|
|
449
502
|
|
|
503
|
+
// Partition leaf nodes into compound parents when groups are given.
|
|
504
|
+
// Group shells themselves are NOT part of `nodes` — they are reconstructed
|
|
505
|
+
// by the caller from `groupBounds`. Only leaf ids in `memberIds` nest.
|
|
506
|
+
const groupDefs = (options.groups ?? []).filter((g) => g.memberIds.length > 0);
|
|
507
|
+
const memberToGroup = new Map<string, string>();
|
|
508
|
+
const groupedLeafIds = new Set<string>();
|
|
509
|
+
for (const g of groupDefs) {
|
|
510
|
+
for (const mid of g.memberIds) {
|
|
511
|
+
if (!memberToGroup.has(mid)) {
|
|
512
|
+
memberToGroup.set(mid, g.id);
|
|
513
|
+
groupedLeafIds.add(mid);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const elkById = new Map(elkNodes.map((n) => [n.id, n]));
|
|
518
|
+
const ungroupedElkNodes: ElkNode[] = [];
|
|
519
|
+
for (const n of elkNodes) {
|
|
520
|
+
if (!groupedLeafIds.has(n.id)) ungroupedElkNodes.push(n);
|
|
521
|
+
}
|
|
522
|
+
const elkParents: ElkNode[] = [];
|
|
523
|
+
for (const g of groupDefs) {
|
|
524
|
+
const children = g.memberIds
|
|
525
|
+
.map((mid) => elkById.get(mid))
|
|
526
|
+
.filter((n): n is ElkNode => !!n);
|
|
527
|
+
// Skip groups with <2 real members — a single-child frame adds noise;
|
|
528
|
+
// the caller drops the shell and leaves the node top-level.
|
|
529
|
+
if (children.length < 2) {
|
|
530
|
+
for (const c of children) ungroupedElkNodes.push(c);
|
|
531
|
+
memberToGroup.delete(g.memberIds[0]);
|
|
532
|
+
groupedLeafIds.delete(g.memberIds[0]);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
elkParents.push({
|
|
536
|
+
id: g.id,
|
|
537
|
+
children,
|
|
538
|
+
layoutOptions: {
|
|
539
|
+
'elk.algorithm': 'layered',
|
|
540
|
+
'elk.direction': direction,
|
|
541
|
+
'elk.padding': '[top=48,left=24,bottom=24,right=24]',
|
|
542
|
+
'elk.spacing.nodeNode': '40',
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
450
547
|
// Create ELK graph
|
|
548
|
+
const rootOptions = getElkOptions(options);
|
|
549
|
+
if (elkParents.length > 0) {
|
|
550
|
+
rootOptions['elk.hierarchyHandling'] = 'INCLUDE_CHILDREN';
|
|
551
|
+
}
|
|
451
552
|
const elkGraph: ElkNode = {
|
|
452
553
|
id: 'root',
|
|
453
|
-
layoutOptions:
|
|
454
|
-
children:
|
|
554
|
+
layoutOptions: rootOptions,
|
|
555
|
+
children: [...ungroupedElkNodes, ...elkParents],
|
|
455
556
|
edges: elkEdges,
|
|
456
557
|
};
|
|
457
558
|
|
|
458
559
|
// Run ELK layout
|
|
459
560
|
const layoutedGraph = await getElkInstance().layout(elkGraph);
|
|
460
561
|
|
|
461
|
-
// Build
|
|
562
|
+
// Build maps of ELK-computed positions. Nested children report
|
|
563
|
+
// parent-relative coords — flatten to absolute for edges, and keep the
|
|
564
|
+
// relative form for React Flow children (whose position is parent-relative).
|
|
565
|
+
// Absolute offset of every ELK node (parents included), accumulated down
|
|
566
|
+
// the ancestor chain — edge sections/labels are relative to their
|
|
567
|
+
// `container`, so each edge needs its container's absolute offset.
|
|
568
|
+
const elkAbsOffsets = new Map<string, { x: number; y: number }>();
|
|
462
569
|
const elkPositions = new Map<string, { x: number; y: number }>();
|
|
570
|
+
const elkRelativePositions = new Map<string, { x: number; y: number }>();
|
|
571
|
+
const groupBounds = new Map<string, { x: number; y: number; width: number; height: number }>();
|
|
572
|
+
const walkElk = (n: ElkNode, ox: number, oy: number) => {
|
|
573
|
+
const ax = ox + (n.x ?? 0);
|
|
574
|
+
const ay = oy + (n.y ?? 0);
|
|
575
|
+
elkAbsOffsets.set(n.id, { x: ax, y: ay });
|
|
576
|
+
for (const c of n.children ?? []) walkElk(c, ax, ay);
|
|
577
|
+
};
|
|
578
|
+
walkElk(layoutedGraph, 0, 0);
|
|
463
579
|
if (layoutedGraph.children) {
|
|
464
580
|
for (const child of layoutedGraph.children) {
|
|
465
|
-
|
|
581
|
+
if (child.children && child.children.length > 0 && groupDefs.some((g) => g.id === child.id)) {
|
|
582
|
+
const gx = child.x ?? 0;
|
|
583
|
+
const gy = child.y ?? 0;
|
|
584
|
+
groupBounds.set(child.id, {
|
|
585
|
+
x: gx,
|
|
586
|
+
y: gy,
|
|
587
|
+
width: child.width ?? 0,
|
|
588
|
+
height: child.height ?? 0,
|
|
589
|
+
});
|
|
590
|
+
for (const grand of child.children) {
|
|
591
|
+
const rx = grand.x ?? 0;
|
|
592
|
+
const ry = grand.y ?? 0;
|
|
593
|
+
elkRelativePositions.set(grand.id, { x: rx, y: ry });
|
|
594
|
+
elkPositions.set(grand.id, { x: gx + rx, y: gy + ry });
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
elkPositions.set(child.id, { x: child.x ?? 0, y: child.y ?? 0 });
|
|
598
|
+
elkRelativePositions.set(child.id, { x: child.x ?? 0, y: child.y ?? 0 });
|
|
599
|
+
}
|
|
466
600
|
}
|
|
467
601
|
}
|
|
468
602
|
|
|
@@ -486,7 +620,10 @@ export async function computeElkLayout(
|
|
|
486
620
|
const targetOriginal = targetId ? originalPositions.get(targetId) : null;
|
|
487
621
|
const targetElk = targetId ? elkPositions.get(targetId) : null;
|
|
488
622
|
|
|
489
|
-
// Collect all points from sections
|
|
623
|
+
// Collect all points from sections. Sections (and labels) are
|
|
624
|
+
// relative to the edge's `container` — intra-group edges live in
|
|
625
|
+
// the parent's frame, so translate to root-absolute flow coords.
|
|
626
|
+
const containerOffset = elkAbsOffsets.get(edge.container ?? 'root') ?? { x: 0, y: 0 };
|
|
490
627
|
const allPoints: Point[] = [];
|
|
491
628
|
|
|
492
629
|
for (const section of edge.sections) {
|
|
@@ -496,6 +633,12 @@ export async function computeElkLayout(
|
|
|
496
633
|
}
|
|
497
634
|
allPoints.push(section.endPoint);
|
|
498
635
|
}
|
|
636
|
+
if (containerOffset.x !== 0 || containerOffset.y !== 0) {
|
|
637
|
+
for (const p of allPoints) {
|
|
638
|
+
p.x += containerOffset.x;
|
|
639
|
+
p.y += containerOffset.y;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
499
642
|
|
|
500
643
|
// If preserving positions, we need to offset the edge points
|
|
501
644
|
// The edge path is relative to ELK's layout, so we translate it
|
|
@@ -529,9 +672,10 @@ export async function computeElkLayout(
|
|
|
529
672
|
// and apply the same coordinate offset.
|
|
530
673
|
if (edge.labels && edge.labels.length > 0) {
|
|
531
674
|
const elkLabel = edge.labels[0];
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
let
|
|
675
|
+
// ELK reports the label's top-left; convert to center so screen-space
|
|
676
|
+
// overlays can anchor with translate(-50%, -50%) at any zoom.
|
|
677
|
+
let lx = (elkLabel.x ?? 0) + (elkLabel.width ?? 0) / 2 + containerOffset.x;
|
|
678
|
+
let ly = (elkLabel.y ?? 0) + (elkLabel.height ?? 0) / 2 + containerOffset.y;
|
|
535
679
|
if (preserveNodePositions && sourceOriginal && sourceElk && targetOriginal && targetElk) {
|
|
536
680
|
const sourceOffset = {
|
|
537
681
|
x: sourceOriginal.x - sourceElk.x,
|
|
@@ -549,7 +693,11 @@ export async function computeElkLayout(
|
|
|
549
693
|
lx += sourceOffset.x + (targetOffset.x - sourceOffset.x) * t;
|
|
550
694
|
ly += sourceOffset.y + (targetOffset.y - sourceOffset.y) * t;
|
|
551
695
|
}
|
|
552
|
-
|
|
696
|
+
// Snap onto the polyline stroke. ELK's label box can sit slightly
|
|
697
|
+
// off the route (side selection / reserved label space); we keep
|
|
698
|
+
// its along-edge placement but center on the actual path.
|
|
699
|
+
const onPath = closestPointOnPath(allPoints, { x: lx, y: ly });
|
|
700
|
+
edgeLabelPositions.set(edge.id, onPath);
|
|
553
701
|
}
|
|
554
702
|
|
|
555
703
|
// For orthogonal routing with preserved positions, the offset can distort
|
|
@@ -598,15 +746,17 @@ export async function computeElkLayout(
|
|
|
598
746
|
}
|
|
599
747
|
}
|
|
600
748
|
|
|
601
|
-
// Process nodes (update positions if not preserving)
|
|
749
|
+
// Process nodes (update positions if not preserving). Grouped children use
|
|
750
|
+
// parent-relative coords (React Flow child semantics); everything else uses
|
|
751
|
+
// absolute coords.
|
|
602
752
|
const resultNodes = preserveNodePositions
|
|
603
753
|
? nodes
|
|
604
754
|
: nodes.map((node) => {
|
|
605
|
-
const
|
|
606
|
-
if (
|
|
755
|
+
const rel = elkRelativePositions.get(node.id);
|
|
756
|
+
if (rel) {
|
|
607
757
|
return {
|
|
608
758
|
...node,
|
|
609
|
-
position: { x:
|
|
759
|
+
position: { x: rel.x, y: rel.y },
|
|
610
760
|
};
|
|
611
761
|
}
|
|
612
762
|
return node;
|
|
@@ -617,6 +767,7 @@ export async function computeElkLayout(
|
|
|
617
767
|
edgePaths,
|
|
618
768
|
edgeLabelPositions,
|
|
619
769
|
edgePathPoints,
|
|
770
|
+
groupBounds,
|
|
620
771
|
};
|
|
621
772
|
}
|
|
622
773
|
|