@principal-ai/subsystems-react 0.37.0 → 0.37.2
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 +1 -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/ComponentDeclaration.d.ts +16 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +75 -7
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +13 -3
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +273 -220
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +36 -28
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SymbolInspectionCard.d.ts.map +1 -1
- package/dist/subsystem/SymbolInspectionCard.js +1 -1
- package/dist/subsystem/SymbolInspectionCard.js.map +1 -1
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +7 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/utils/edgeLabel.d.ts +51 -0
- package/dist/utils/edgeLabel.d.ts.map +1 -0
- package/dist/utils/edgeLabel.js +81 -0
- package/dist/utils/edgeLabel.js.map +1 -0
- package/dist/utils/elkLayout.d.ts +6 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +27 -6
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +457 -0
- package/src/subsystem/ComponentDeclaration.tsx +130 -7
- package/src/subsystem/ConstructsCatalog.tsx +431 -341
- package/src/subsystem/SubsystemComponentGraph.tsx +74 -29
- package/src/subsystem/SymbolInspectionCard.tsx +10 -9
- package/src/subsystem/model.ts +7 -1
- package/src/utils/edgeLabel.test.ts +23 -0
- package/src/utils/edgeLabel.ts +95 -0
- package/src/utils/elkLayout.ts +41 -6
|
@@ -70,12 +70,13 @@ import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
|
|
|
70
70
|
import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
|
|
71
71
|
import { buildRepoGroups, repoAvatarUrl, type RepoGroup } from './paths';
|
|
72
72
|
import { WalkthroughsPanel, WALKTHROUGH_PLAY_PAUSE_MS } from './WalkthroughsPanel';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
import {
|
|
74
|
+
EDGE_LABEL_WIDTH,
|
|
75
|
+
EDGE_LABEL_HEIGHT,
|
|
76
|
+
EDGE_LABEL_FONT_SIZE,
|
|
77
|
+
EDGE_LABEL_CLOUD_PATH,
|
|
78
|
+
EDGE_LABEL_CLOUD_EXTRA_TOP,
|
|
79
|
+
} from '../utils/edgeLabel';
|
|
79
80
|
|
|
80
81
|
/** Context passed to `renderWalkthroughViewer` when a flow/step is focused. */
|
|
81
82
|
export interface WalkthroughViewerContext {
|
|
@@ -2036,14 +2037,6 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
|
|
|
2036
2037
|
const text = lbl.stepNos?.length
|
|
2037
2038
|
? `${lbl.stepNos.map((n) => `${n}:`).join(' ')} ${lbl.mechanism}`
|
|
2038
2039
|
: lbl.mechanism;
|
|
2039
|
-
// Labels stay readable at full size until they'd exceed a share of
|
|
2040
|
-
// the edge's screen length, then shrink with zoom.
|
|
2041
|
-
const estWidth = text.length * EDGE_LABEL_CHAR_PX + EDGE_LABEL_PAD_PX;
|
|
2042
|
-
const screenEdgeLen = lbl.pathLength * viewport.zoom;
|
|
2043
|
-
const scale =
|
|
2044
|
-
lbl.pathLength > 0 && estWidth > 0
|
|
2045
|
-
? Math.min(1, (screenEdgeLen * EDGE_LABEL_MAX_EDGE_FRACTION) / estWidth)
|
|
2046
|
-
: 1;
|
|
2047
2040
|
return (
|
|
2048
2041
|
<div
|
|
2049
2042
|
key={lbl.id}
|
|
@@ -2057,29 +2050,81 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
|
|
|
2057
2050
|
position: 'absolute',
|
|
2058
2051
|
left: screenX,
|
|
2059
2052
|
top: screenY,
|
|
2060
|
-
// Center on the flow-space midpoint
|
|
2061
|
-
// space
|
|
2062
|
-
//
|
|
2063
|
-
transform: `translate(-50%, -50%) scale(${
|
|
2053
|
+
// Center on the flow-space midpoint, and scale with the graph
|
|
2054
|
+
// (flow-space size): labels shrink as you zoom out instead of
|
|
2055
|
+
// staying fixed-screen-size and dominating the smaller graph.
|
|
2056
|
+
transform: `translate(-50%, -50%) scale(${viewport.zoom})`,
|
|
2064
2057
|
transformOrigin: 'center center',
|
|
2065
2058
|
display: 'flex',
|
|
2066
2059
|
alignItems: 'center',
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
background: 'rgba(21,21,21,0.9)',
|
|
2073
|
-
border: verifiable ? `0.5px solid ${color}` : `1px dashed ${color}`,
|
|
2074
|
-
borderRadius: verifiable ? 4 : '10px 14px 12px 16px / 14px 10px 16px 12px',
|
|
2075
|
-
padding: '3px 8px',
|
|
2060
|
+
justifyContent: 'center',
|
|
2061
|
+
width: EDGE_LABEL_WIDTH,
|
|
2062
|
+
height: EDGE_LABEL_HEIGHT,
|
|
2063
|
+
boxSizing: 'border-box',
|
|
2064
|
+
padding: '7px 8px',
|
|
2076
2065
|
cursor: 'pointer',
|
|
2077
2066
|
pointerEvents: 'auto',
|
|
2078
2067
|
opacity: lbl.dimmed ? 0.15 : 1,
|
|
2079
|
-
whiteSpace: 'nowrap',
|
|
2080
2068
|
}}
|
|
2081
2069
|
>
|
|
2082
|
-
{
|
|
2070
|
+
{/* Background layer. Opaque theme surface (matches the node fill)
|
|
2071
|
+
so the edge line behind is hidden. Verifiable mechanisms get a
|
|
2072
|
+
crisp rounded box; soft / ambiguous ones (uses, feeds,
|
|
2073
|
+
watches …) get a real lobed cloud silhouette — ambiguous, not
|
|
2074
|
+
a dashed proposal (or a pill, which border-radius can only
|
|
2075
|
+
ever make). */}
|
|
2076
|
+
{verifiable ? (
|
|
2077
|
+
<span
|
|
2078
|
+
aria-hidden
|
|
2079
|
+
style={{
|
|
2080
|
+
position: 'absolute',
|
|
2081
|
+
inset: 0,
|
|
2082
|
+
borderRadius: 4,
|
|
2083
|
+
border: `0.5px solid ${color}`,
|
|
2084
|
+
background:
|
|
2085
|
+
theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
2086
|
+
pointerEvents: 'none',
|
|
2087
|
+
}}
|
|
2088
|
+
/>
|
|
2089
|
+
) : (
|
|
2090
|
+
<svg
|
|
2091
|
+
aria-hidden
|
|
2092
|
+
width={EDGE_LABEL_WIDTH}
|
|
2093
|
+
height={EDGE_LABEL_HEIGHT + EDGE_LABEL_CLOUD_EXTRA_TOP}
|
|
2094
|
+
viewBox={`0 0 ${EDGE_LABEL_WIDTH} ${EDGE_LABEL_HEIGHT + EDGE_LABEL_CLOUD_EXTRA_TOP}`}
|
|
2095
|
+
style={{
|
|
2096
|
+
position: 'absolute',
|
|
2097
|
+
left: 0,
|
|
2098
|
+
top: -EDGE_LABEL_CLOUD_EXTRA_TOP,
|
|
2099
|
+
pointerEvents: 'none',
|
|
2100
|
+
}}
|
|
2101
|
+
>
|
|
2102
|
+
<path
|
|
2103
|
+
d={EDGE_LABEL_CLOUD_PATH}
|
|
2104
|
+
fill={theme.colors.backgroundSecondary ?? theme.colors.background}
|
|
2105
|
+
stroke={hexWithAlpha(color, 0.75)}
|
|
2106
|
+
strokeWidth={1.2}
|
|
2107
|
+
strokeLinejoin="round"
|
|
2108
|
+
/>
|
|
2109
|
+
</svg>
|
|
2110
|
+
)}
|
|
2111
|
+
<span
|
|
2112
|
+
style={{
|
|
2113
|
+
position: 'relative',
|
|
2114
|
+
display: 'block',
|
|
2115
|
+
maxWidth: '100%',
|
|
2116
|
+
overflow: 'hidden',
|
|
2117
|
+
textOverflow: 'ellipsis',
|
|
2118
|
+
whiteSpace: 'nowrap',
|
|
2119
|
+
fontSize: EDGE_LABEL_FONT_SIZE,
|
|
2120
|
+
lineHeight: 1,
|
|
2121
|
+
fontFamily: theme.fonts.monospace,
|
|
2122
|
+
fontWeight: 500,
|
|
2123
|
+
color,
|
|
2124
|
+
}}
|
|
2125
|
+
>
|
|
2126
|
+
{text}
|
|
2127
|
+
</span>
|
|
2083
2128
|
</div>
|
|
2084
2129
|
);
|
|
2085
2130
|
})}
|
|
@@ -337,7 +337,7 @@ export function SymbolInspectionCard({
|
|
|
337
337
|
×
|
|
338
338
|
</button>
|
|
339
339
|
|
|
340
|
-
{/* Top: the declaration itself, full-bleed (or
|
|
340
|
+
{/* Top: the declaration itself, full-bleed (or just the name on a miss). */}
|
|
341
341
|
<div
|
|
342
342
|
style={{
|
|
343
343
|
padding: '12px 32px 12px 12px',
|
|
@@ -363,20 +363,21 @@ export function SymbolInspectionCard({
|
|
|
363
363
|
)}
|
|
364
364
|
|
|
365
365
|
{!error && !hasBody && (
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
</span>
|
|
370
|
-
<span style={{ color: muted, fontFamily: theme.fonts.body, fontSize: body }}>
|
|
371
|
-
{statusText}
|
|
372
|
-
</span>
|
|
373
|
-
</div>
|
|
366
|
+
<span style={{ color: theme.colors.accent ?? theme.colors.secondary, fontWeight: 600 }}>
|
|
367
|
+
{symbolRef.name}
|
|
368
|
+
</span>
|
|
374
369
|
)}
|
|
375
370
|
</div>
|
|
376
371
|
|
|
377
372
|
{/* Meta: where it lives, ambiguity, and the action. */}
|
|
378
373
|
{!error && (
|
|
379
374
|
<div style={{ padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
375
|
+
{!hasBody && (
|
|
376
|
+
<span style={{ color: muted, fontFamily: theme.fonts.body, fontSize: body }}>
|
|
377
|
+
{statusText}
|
|
378
|
+
</span>
|
|
379
|
+
)}
|
|
380
|
+
|
|
380
381
|
{node?.sourceFile && (
|
|
381
382
|
<StackedRow label="Source">
|
|
382
383
|
{onOpenFile ? (
|
package/src/subsystem/model.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type Node,
|
|
19
19
|
} from '@xyflow/react';
|
|
20
20
|
import { computeElkLayout, calculatePathLength } from '../utils/elkLayout';
|
|
21
|
+
import { EDGE_LABEL_SIDE_PADDING, EDGE_ARROW_INSET } from '../utils/edgeLabel';
|
|
21
22
|
import type { GraphifyComponentDetail } from '../graphify';
|
|
22
23
|
import type { SubsystemDeclarationRef } from './declarationRef';
|
|
23
24
|
import { purlOwnerName, purlRepoKey } from './paths';
|
|
@@ -1642,7 +1643,12 @@ export async function buildSubsystemGraph(
|
|
|
1642
1643
|
nodeSpacing: 60,
|
|
1643
1644
|
edgeSpacing: 30,
|
|
1644
1645
|
edgeNodeSpacing: 60,
|
|
1645
|
-
|
|
1646
|
+
// With labels on, ELK's CENTER_LAYER strategy applies this spacing on
|
|
1647
|
+
// both sides of the label layer, so it IS the per-side clearance — the
|
|
1648
|
+
// label box itself is reserved separately in elkLayout. A large value
|
|
1649
|
+
// here would be counted twice. Labels off → ordinary layer gap.
|
|
1650
|
+
interLayerSpacing: showEdgeLabels === false ? 120 : EDGE_LABEL_SIDE_PADDING,
|
|
1651
|
+
endpointInset: EDGE_ARROW_INSET,
|
|
1646
1652
|
preserveNodePositions: false,
|
|
1647
1653
|
edgeLabels: showEdgeLabels === false ? { enabled: false } : { enabled: true, placement: 'CENTER' },
|
|
1648
1654
|
groups: layoutGroups.map((g) => ({
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the shared edge-label geometry.
|
|
3
|
+
*
|
|
4
|
+
* The overlay and the ELK reservation both import these values, so the
|
|
5
|
+
* invariants here (fixed box, min edge run covers the box) are what keep
|
|
6
|
+
* rendered labels consistent and non-overlapping.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from 'bun:test';
|
|
10
|
+
import {
|
|
11
|
+
EDGE_LABEL_WIDTH,
|
|
12
|
+
EDGE_LABEL_SIDE_PADDING,
|
|
13
|
+
EDGE_LABEL_EDGE_GAP,
|
|
14
|
+
} from './edgeLabel';
|
|
15
|
+
|
|
16
|
+
describe('edge-label geometry', () => {
|
|
17
|
+
test('the labelled-edge gap is the box plus clearance on both sides', () => {
|
|
18
|
+
expect(EDGE_LABEL_EDGE_GAP).toBe(
|
|
19
|
+
EDGE_LABEL_WIDTH + EDGE_LABEL_SIDE_PADDING * 2,
|
|
20
|
+
);
|
|
21
|
+
expect(EDGE_LABEL_EDGE_GAP).toBeGreaterThan(EDGE_LABEL_WIDTH);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared geometry for the HTML edge-label overlay and the ELK layout that
|
|
3
|
+
* reserves room for it.
|
|
4
|
+
*
|
|
5
|
+
* The overlay (SubsystemComponentGraph) and the layout (elkLayout) must agree
|
|
6
|
+
* on one fixed box size, or the space ELK leaves for a label drifts from what
|
|
7
|
+
* actually renders. Keeping the geometry here lets both import the same values
|
|
8
|
+
* without a component → util → component import cycle.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Fixed width of every edge label, in flow/screen px. Wide enough for the
|
|
12
|
+
* longest mechanism (`registers-into`) plus horizontal padding at the label
|
|
13
|
+
* font size. */
|
|
14
|
+
export const EDGE_LABEL_WIDTH = 140;
|
|
15
|
+
|
|
16
|
+
/** Fixed height of every edge label. Sized to the label font plus padding. */
|
|
17
|
+
export const EDGE_LABEL_HEIGHT = 40;
|
|
18
|
+
|
|
19
|
+
/** Label font size. Fixed so every label renders at the same text size. */
|
|
20
|
+
export const EDGE_LABEL_FONT_SIZE = 14;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Clear space we want on each side of a label box.
|
|
24
|
+
*
|
|
25
|
+
* Passed as ELK's `interLayerSpacing` (`nodeNodeBetweenLayers`). ELK's
|
|
26
|
+
* `CENTER_LAYER` label strategy inserts a dedicated label layer and applies
|
|
27
|
+
* that spacing on *both* sides of it, so the resulting gap between the two
|
|
28
|
+
* nodes an edge connects is:
|
|
29
|
+
*
|
|
30
|
+
* EDGE_LABEL_WIDTH + 2 * EDGE_LABEL_SIDE_PADDING
|
|
31
|
+
*
|
|
32
|
+
* i.e. the reserved label box plus this clearance at each end. Setting a big
|
|
33
|
+
* "min edge length" here double-counts (it is applied twice), which is how the
|
|
34
|
+
* gap silently ballooned before.
|
|
35
|
+
*/
|
|
36
|
+
export const EDGE_LABEL_SIDE_PADDING = 32;
|
|
37
|
+
|
|
38
|
+
/** Actual end-to-end gap ELK leaves for a labelled edge (box + both clearances). */
|
|
39
|
+
export const EDGE_LABEL_EDGE_GAP =
|
|
40
|
+
EDGE_LABEL_WIDTH + EDGE_LABEL_SIDE_PADDING * 2;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How far (in flow px) the target end of an edge stops short of the node
|
|
44
|
+
* border. ELK ends the path exactly on the border, so the arrowhead tip lands
|
|
45
|
+
* on the border line; pulling it back this much lets the arrow visually touch
|
|
46
|
+
* the node without overlapping its border. 0 = flush (old behaviour).
|
|
47
|
+
*/
|
|
48
|
+
export const EDGE_ARROW_INSET = 2;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a "cloud" outline that fills a `w × h` box: a flat-ish bottom with
|
|
52
|
+
* rounded corners and three top lobes (large left, medium middle, small right).
|
|
53
|
+
* Used as the background silhouette for ambiguous (non-verifiable) edge labels —
|
|
54
|
+
* a real cloud shape, which `border-radius` cannot express. Control points are
|
|
55
|
+
* normalized (`0…1`) so the shape scales with the label box.
|
|
56
|
+
*/
|
|
57
|
+
function cloudPath(w: number, h: number): string {
|
|
58
|
+
const p = (nx: number, ny: number) =>
|
|
59
|
+
`${(nx * w).toFixed(2)} ${(ny * h).toFixed(2)}`;
|
|
60
|
+
return [
|
|
61
|
+
`M ${p(0.043, 0.65)}`,
|
|
62
|
+
// Bottom-left rounded corner + flat bottom.
|
|
63
|
+
`C ${p(0.014, 0.8)} ${p(0.05, 0.925)} ${p(0.114, 0.925)}`,
|
|
64
|
+
`L ${p(0.829, 0.925)}`,
|
|
65
|
+
// Bottom-right rounded corner up into the right (small) lobe.
|
|
66
|
+
`C ${p(0.914, 0.925)} ${p(0.986, 0.825)} ${p(0.986, 0.675)}`,
|
|
67
|
+
`C ${p(0.986, 0.475)} ${p(0.943, 0.325)} ${p(0.879, 0.325)}`,
|
|
68
|
+
`C ${p(0.829, 0.325)} ${p(0.793, 0.425)} ${p(0.771, 0.525)}`,
|
|
69
|
+
// Middle lobe.
|
|
70
|
+
`C ${p(0.736, 0.275)} ${p(0.664, 0.15)} ${p(0.6, 0.2)}`,
|
|
71
|
+
`C ${p(0.564, 0.225)} ${p(0.536, 0.3)} ${p(0.521, 0.375)}`,
|
|
72
|
+
// Big left lobe.
|
|
73
|
+
`C ${p(0.486, 0.1)} ${p(0.4, 0.025)} ${p(0.336, 0.075)}`,
|
|
74
|
+
`C ${p(0.279, 0.1)} ${p(0.243, 0.225)} ${p(0.221, 0.375)}`,
|
|
75
|
+
// Down the left side, back to the corner.
|
|
76
|
+
`C ${p(0.2, 0.5)} ${p(0.143, 0.475)} ${p(0.1, 0.525)}`,
|
|
77
|
+
`C ${p(0.064, 0.55)} ${p(0.057, 0.6)} ${p(0.043, 0.65)}`,
|
|
78
|
+
'Z',
|
|
79
|
+
].join(' ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extra height (flow px) the cloud rises above the label box. The cloud and
|
|
84
|
+
* its text share the label box, but ambiguous labels draw a taller silhouette
|
|
85
|
+
* that overhangs upward — so the cloud gets more presence without moving the
|
|
86
|
+
* text or changing the verifiable labels' box.
|
|
87
|
+
*/
|
|
88
|
+
export const EDGE_LABEL_CLOUD_EXTRA_TOP = 14;
|
|
89
|
+
|
|
90
|
+
/** Cloud silhouette for ambiguous edge labels, matched to the label box. */
|
|
91
|
+
export const EDGE_LABEL_CLOUD_PATH = cloudPath(
|
|
92
|
+
EDGE_LABEL_WIDTH,
|
|
93
|
+
EDGE_LABEL_HEIGHT + EDGE_LABEL_CLOUD_EXTRA_TOP,
|
|
94
|
+
);
|
|
95
|
+
|
package/src/utils/elkLayout.ts
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import ELK, { type ElkNode, type ElkExtendedEdge, type LayoutOptions } from 'elkjs/lib/elk.bundled.js';
|
|
9
9
|
import type { Node, Edge } from '@xyflow/react';
|
|
10
|
+
import {
|
|
11
|
+
EDGE_LABEL_WIDTH,
|
|
12
|
+
EDGE_LABEL_HEIGHT,
|
|
13
|
+
EDGE_LABEL_SIDE_PADDING,
|
|
14
|
+
} from './edgeLabel';
|
|
10
15
|
|
|
11
16
|
/** ELK layout options for different routing styles */
|
|
12
17
|
export type ElkRoutingStyle = 'orthogonal' | 'splines' | 'polyline';
|
|
@@ -54,6 +59,13 @@ export interface ElkLayoutOptions {
|
|
|
54
59
|
*/
|
|
55
60
|
interLayerSpacing?: number;
|
|
56
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Pull the target end of each edge back from the node border by this many
|
|
64
|
+
* flow px, so the arrowhead tip touches the node without sitting on its
|
|
65
|
+
* border. @default 0
|
|
66
|
+
*/
|
|
67
|
+
endpointInset?: number;
|
|
68
|
+
|
|
57
69
|
/**
|
|
58
70
|
* Reserve space along edges for inline labels so they don't overlap nodes
|
|
59
71
|
* or other edges. When enabled ELK places labels inline on the edge with the
|
|
@@ -486,6 +498,7 @@ export async function computeElkLayout(
|
|
|
486
498
|
): Promise<ElkLayoutResult> {
|
|
487
499
|
const { preserveNodePositions = true, keepSingletonGroups = false } = options;
|
|
488
500
|
const edgeLabels = options.edgeLabels;
|
|
501
|
+
const endpointInset = options.endpointInset ?? 0;
|
|
489
502
|
const direction = options.direction ?? 'RIGHT';
|
|
490
503
|
|
|
491
504
|
// Build a map of original node positions BEFORE passing to ELK
|
|
@@ -620,13 +633,12 @@ export async function computeElkLayout(
|
|
|
620
633
|
sources: [sourcePort],
|
|
621
634
|
targets: [targetPort],
|
|
622
635
|
};
|
|
623
|
-
//
|
|
624
|
-
//
|
|
636
|
+
// Reserve the fixed label box so ELK leaves the same room for every label,
|
|
637
|
+
// regardless of text length. The overlay renders into this exact box.
|
|
625
638
|
if (edgeLabels?.enabled !== false && typeof edge.label === 'string') {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
elkEdge.labels = [{ text, width: labelWidth, height: labelHeight }];
|
|
639
|
+
elkEdge.labels = [
|
|
640
|
+
{ text: edge.label, width: EDGE_LABEL_WIDTH, height: EDGE_LABEL_HEIGHT },
|
|
641
|
+
];
|
|
630
642
|
}
|
|
631
643
|
return elkEdge;
|
|
632
644
|
});
|
|
@@ -647,6 +659,12 @@ export async function computeElkLayout(
|
|
|
647
659
|
// edge by ~13px) plus a gap before the first child.
|
|
648
660
|
'elk.padding': '[top=64,left=24,bottom=24,right=24]',
|
|
649
661
|
'elk.spacing.nodeNode': '40',
|
|
662
|
+
// Edges inside a frame host labels too: ELK applies this on both sides of
|
|
663
|
+
// the reserved label layer, so it is the per-side clearance (same as root).
|
|
664
|
+
// Labels off falls back to a plain between-layer gap.
|
|
665
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': String(
|
|
666
|
+
edgeLabels?.enabled === false ? 40 : EDGE_LABEL_SIDE_PADDING,
|
|
667
|
+
),
|
|
650
668
|
};
|
|
651
669
|
|
|
652
670
|
const plan = planCompoundGroups(groupDefs, elkById.keys(), keepSingletonGroups);
|
|
@@ -900,6 +918,23 @@ export async function computeElkLayout(
|
|
|
900
918
|
allPoints.push(end);
|
|
901
919
|
}
|
|
902
920
|
|
|
921
|
+
// Pull the target end back from the node border so the arrowhead tip
|
|
922
|
+
// touches the node without overlapping its border.
|
|
923
|
+
if (endpointInset > 0 && allPoints.length >= 2) {
|
|
924
|
+
const end = allPoints[allPoints.length - 1];
|
|
925
|
+
const prev = allPoints[allPoints.length - 2];
|
|
926
|
+
const dx = end.x - prev.x;
|
|
927
|
+
const dy = end.y - prev.y;
|
|
928
|
+
const segLen = Math.hypot(dx, dy);
|
|
929
|
+
if (segLen > endpointInset) {
|
|
930
|
+
const t = (segLen - endpointInset) / segLen;
|
|
931
|
+
allPoints[allPoints.length - 1] = {
|
|
932
|
+
x: prev.x + dx * t,
|
|
933
|
+
y: prev.y + dy * t,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
903
938
|
// Convert to path
|
|
904
939
|
const path =
|
|
905
940
|
options.routingStyle === 'orthogonal'
|