@kekonic/diagrams-layout 1.0.0-rc.4
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/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/index.d.mts +272 -0
- package/dist/index.mjs +4068 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kekonic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @kekonic/diagrams-layout
|
|
2
|
+
|
|
3
|
+
Measurement and ELK layered layout for KDiagram diagrams.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
- Shared `TextMeasurer` (bundled Inter metrics for CLI/browser parity)
|
|
8
|
+
- Node/group measurement from `GraphModel`
|
|
9
|
+
- ELK layered layout + orthogonal edge paths (`elk-layered-v1` / `elk-orthogonal-v1`)
|
|
10
|
+
- Region arrange (`stack` / `row` / `grid`) with obstacle-aware stubs
|
|
11
|
+
- Topology helpers and silhouette endpoint snap
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @kekonic/diagrams-layout
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Most apps should use `@kekonic/diagrams` instead and call `KDiagram.layout` / `renderToSvg`.
|
|
20
|
+
|
|
21
|
+
## Related
|
|
22
|
+
|
|
23
|
+
- Routing post-process: `@kekonic/diagrams-routing`
|
|
24
|
+
- Geometry / ports: `@kekonic/diagrams-geometry`
|
|
25
|
+
- Pipeline boundaries: [`docs/architecture/pipeline.md`](../../docs/architecture/pipeline.md)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { BoxPadding, Direction, GraphModel, GraphNode, LayoutOptions, Point, Rect, SequenceFragment, SequenceIR, TableColumn } from "@kekonic/diagrams-core";
|
|
2
|
+
|
|
3
|
+
//#region src/measure/text-measurer.d.ts
|
|
4
|
+
/** Shared font metrics — browser canvas and approximate server fallback. */
|
|
5
|
+
type TextStyle = {
|
|
6
|
+
fontSize: number;
|
|
7
|
+
fontFamily: string;
|
|
8
|
+
fontWeight?: string;
|
|
9
|
+
};
|
|
10
|
+
type TextMetrics = {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
ascent: number;
|
|
14
|
+
descent: number;
|
|
15
|
+
};
|
|
16
|
+
type WrapOptions = {
|
|
17
|
+
maxWidth: number;
|
|
18
|
+
style: TextStyle;
|
|
19
|
+
};
|
|
20
|
+
type WrappedText = {
|
|
21
|
+
lines: string[];
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
};
|
|
25
|
+
type TextMeasurer = {
|
|
26
|
+
measureText(text: string, style: TextStyle): TextMetrics;
|
|
27
|
+
wrapText(text: string, options: WrapOptions): WrappedText;
|
|
28
|
+
};
|
|
29
|
+
declare const DEFAULT_FONT_FAMILY = "\"Inter\", \"Segoe UI\", system-ui, sans-serif";
|
|
30
|
+
declare function measurerUsedApproximationFallback(): boolean;
|
|
31
|
+
declare function createCanvasMeasurer(canvas?: HTMLCanvasElement): TextMeasurer;
|
|
32
|
+
declare function resetDefaultMeasurer(): void;
|
|
33
|
+
declare const defaultMeasurer: TextMeasurer;
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/measure/measure.d.ts
|
|
36
|
+
type MeasuredNode = {
|
|
37
|
+
nodeId: string;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
contentBox: Rect;
|
|
41
|
+
labelLines: string[]; /** C4 technology tag lines (usually one). */
|
|
42
|
+
technologyLines?: string[]; /** Wrapped C4 / architecture description body. */
|
|
43
|
+
descriptionLines?: string[];
|
|
44
|
+
};
|
|
45
|
+
type MeasureOptions = {
|
|
46
|
+
/** Reserve vertical space for kind subtitles when presentation opts into them. */reserveKindSubtitles?: boolean;
|
|
47
|
+
};
|
|
48
|
+
type MeasureResult = {
|
|
49
|
+
nodes: MeasuredNode[];
|
|
50
|
+
measureMs: number;
|
|
51
|
+
};
|
|
52
|
+
/** Left icon column width for card nodes (matches SVG placement). */
|
|
53
|
+
declare function cardIconColumnWidth(iconId: string, scale?: number): number;
|
|
54
|
+
declare function measureGraph(graph: GraphModel, measurer?: TextMeasurer, options?: MeasureOptions): MeasureResult;
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/layout/sequence/layout-sequence.d.ts
|
|
57
|
+
type SequenceLayoutRouteResult = {
|
|
58
|
+
layout: LayoutResult;
|
|
59
|
+
edges: LayoutEdgePath[];
|
|
60
|
+
routerAlgorithm: string;
|
|
61
|
+
};
|
|
62
|
+
declare const SEQUENCE_LAYOUT_ALGORITHM = "sequence-v1";
|
|
63
|
+
declare const SEQUENCE_ROUTER_ALGORITHM = "sequence-direct-v1";
|
|
64
|
+
type SequenceLifelineLayout = {
|
|
65
|
+
participantId: string; /** Vertical line from below header to diagram bottom. */
|
|
66
|
+
x: number;
|
|
67
|
+
y0: number;
|
|
68
|
+
y1: number;
|
|
69
|
+
};
|
|
70
|
+
type SequenceActivationLayout = {
|
|
71
|
+
id: string;
|
|
72
|
+
participantId: string;
|
|
73
|
+
startOrder: number;
|
|
74
|
+
endOrder: number;
|
|
75
|
+
bounds: Rect;
|
|
76
|
+
};
|
|
77
|
+
type SequenceFragmentLayout = {
|
|
78
|
+
id: string;
|
|
79
|
+
operator: SequenceFragment["operator"];
|
|
80
|
+
label?: string;
|
|
81
|
+
styleRefs: string[];
|
|
82
|
+
unresolvedVars: Record<string, string>;
|
|
83
|
+
bounds: Rect;
|
|
84
|
+
startOrder: number;
|
|
85
|
+
endOrder: number; /** Horizontal separators between operands (alternate/else, parallel/and). */
|
|
86
|
+
separators: number[];
|
|
87
|
+
operandLabels: Array<{
|
|
88
|
+
text: string;
|
|
89
|
+
x: number;
|
|
90
|
+
y: number;
|
|
91
|
+
}>; /** Per-operand tint bands (success vs danger, etc.). */
|
|
92
|
+
operandBands: Array<{
|
|
93
|
+
startOrder: number;
|
|
94
|
+
endOrder: number;
|
|
95
|
+
styleRefs: string[];
|
|
96
|
+
bounds: Rect;
|
|
97
|
+
}>;
|
|
98
|
+
};
|
|
99
|
+
type SequenceNoteLayout = {
|
|
100
|
+
id: string;
|
|
101
|
+
bounds: Rect;
|
|
102
|
+
text: string;
|
|
103
|
+
};
|
|
104
|
+
type SequenceDividerLayout = {
|
|
105
|
+
id: string;
|
|
106
|
+
y: number;
|
|
107
|
+
x0: number;
|
|
108
|
+
x1: number;
|
|
109
|
+
label?: string;
|
|
110
|
+
};
|
|
111
|
+
type SequenceMessageLayout = {
|
|
112
|
+
messageId: string;
|
|
113
|
+
kind: NonNullable<SequenceIR["messages"][number]["kind"]>;
|
|
114
|
+
points: Point[];
|
|
115
|
+
label?: string;
|
|
116
|
+
labelCenter?: Point;
|
|
117
|
+
number?: number;
|
|
118
|
+
};
|
|
119
|
+
type SequenceLayoutArtifacts = {
|
|
120
|
+
lifelines: SequenceLifelineLayout[];
|
|
121
|
+
activations: SequenceActivationLayout[];
|
|
122
|
+
fragments: SequenceFragmentLayout[];
|
|
123
|
+
notes: SequenceNoteLayout[];
|
|
124
|
+
dividers: SequenceDividerLayout[];
|
|
125
|
+
messages: SequenceMessageLayout[];
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Time-axis sequence layout: participants on X, messages ordered on Y.
|
|
129
|
+
*/
|
|
130
|
+
declare function layoutSequence(graph: GraphModel, measured: MeasuredNode[], options?: LayoutOptions): SequenceLayoutRouteResult;
|
|
131
|
+
declare function isSequenceGraph(graph: GraphModel): boolean;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/layout/types.d.ts
|
|
134
|
+
type LaidOutNode = {
|
|
135
|
+
nodeId: string;
|
|
136
|
+
bounds: Rect;
|
|
137
|
+
rank: number;
|
|
138
|
+
order: number;
|
|
139
|
+
};
|
|
140
|
+
type LaidOutGroup = {
|
|
141
|
+
groupId: string;
|
|
142
|
+
bounds: Rect;
|
|
143
|
+
labelBox: Rect;
|
|
144
|
+
padding: BoxPadding;
|
|
145
|
+
};
|
|
146
|
+
/** Orthogonal polyline produced by ELK for one edge. */
|
|
147
|
+
type LayoutEdgePath = {
|
|
148
|
+
edgeId: string;
|
|
149
|
+
points: Point[];
|
|
150
|
+
};
|
|
151
|
+
/** Edge label position from ELK (absolute). */
|
|
152
|
+
type LayoutEdgeLabel = {
|
|
153
|
+
edgeId: string;
|
|
154
|
+
text: string;
|
|
155
|
+
bounds: Rect;
|
|
156
|
+
anchor: Point;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* ELK owns node placement and edge geometry together.
|
|
160
|
+
* `edgePaths` / `edgeLabels` come from the same layout call as `nodes`/`groups`.
|
|
161
|
+
* Sequence diagrams attach optional `sequence` artifacts (lifelines, activations, …).
|
|
162
|
+
*/
|
|
163
|
+
type LayoutResult = {
|
|
164
|
+
nodes: LaidOutNode[];
|
|
165
|
+
groups: LaidOutGroup[];
|
|
166
|
+
edgePaths: LayoutEdgePath[];
|
|
167
|
+
edgeLabels: LayoutEdgeLabel[];
|
|
168
|
+
direction: Direction;
|
|
169
|
+
algorithmVersion: string;
|
|
170
|
+
layoutMs: number;
|
|
171
|
+
width: number;
|
|
172
|
+
height: number; /** Present when layout ran the sequence time-axis engine. */
|
|
173
|
+
sequence?: SequenceLayoutArtifacts;
|
|
174
|
+
};
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/layout-from-graph.d.ts
|
|
177
|
+
declare function layoutFromGraph(graph: GraphModel, layoutOpts?: LayoutOptions): Promise<LayoutResult>;
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/measure/browser-font.d.ts
|
|
180
|
+
/** Load bundled Inter via FontFace so canvas metrics match CLI opentype measurer. */
|
|
181
|
+
declare function browserFontsReady(): boolean;
|
|
182
|
+
declare function ensureBrowserFonts(fontUrl: string): Promise<void>;
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/measure/table-measure.d.ts
|
|
185
|
+
/** Dense ERD chrome — flatter and tighter than architecture cards. */
|
|
186
|
+
declare const TABLE_PAD_X = 10;
|
|
187
|
+
declare const TABLE_HEADER_H = 28;
|
|
188
|
+
declare const TABLE_ROW_H = 20;
|
|
189
|
+
/** Badge chip width — must match SVG render. */
|
|
190
|
+
declare const TABLE_BADGE_W = 18;
|
|
191
|
+
declare const TABLE_BADGE_GAP = 3;
|
|
192
|
+
/**
|
|
193
|
+
* Fixed key-badge gutter (fits PK+FK, or empty spacer).
|
|
194
|
+
* Names always start at padX + KEY_COL regardless of how many key chips are present.
|
|
195
|
+
*/
|
|
196
|
+
declare const TABLE_KEY_COL: number;
|
|
197
|
+
declare const TABLE_RX = 4;
|
|
198
|
+
/** Gap between type / NN / note in the right-side attrs cluster. */
|
|
199
|
+
declare const TABLE_ATTR_GAP = 6;
|
|
200
|
+
declare function isErdTableNode(node: GraphNode): boolean;
|
|
201
|
+
declare function columnAnchorY(tableTop: number, rowIndex: number, scale?: number): number;
|
|
202
|
+
/** SQL-ish type only — muted mono on the right (flags/notes are separate). */
|
|
203
|
+
declare function columnTypeLabel(col: TableColumn): string;
|
|
204
|
+
/** Enum / comment secondary text — never joined into the type with bullets. */
|
|
205
|
+
declare function columnNoteLabel(col: TableColumn): string;
|
|
206
|
+
/** Key chips only (PK/FK/UK) — fixed left gutter; NN is not a key. */
|
|
207
|
+
declare function tableKeyBadges(col: TableColumn): string[];
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/layout/elk/layout-with-elk.d.ts
|
|
210
|
+
type ElkLayoutAndRouteResult = {
|
|
211
|
+
layout: LayoutResult; /** Same as `layout.edgePaths`. */
|
|
212
|
+
edges: LayoutEdgePath[];
|
|
213
|
+
routerAlgorithm: string;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Layout + orthogonal edge routes via ELK (elkjs API; elk-rs drop-in when published).
|
|
217
|
+
*/
|
|
218
|
+
declare function layoutAndRouteWithElk(graph: GraphModel, measured: MeasuredNode[], options?: LayoutOptions): Promise<ElkLayoutAndRouteResult>;
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/layout/elk/elk-engine.d.ts
|
|
221
|
+
declare const ELK_LAYOUT_ALGORITHM = "elk-layered-v1";
|
|
222
|
+
declare const ELK_ROUTER_ALGORITHM = "elk-orthogonal-v1";
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/layout/group-bounds.d.ts
|
|
225
|
+
declare const GROUP_ICON_SIZE = 14;
|
|
226
|
+
declare const GROUP_ICON_GAP = 6;
|
|
227
|
+
declare function computeGroupBounds(graph: GraphModel, laidOut: LaidOutNode[]): LaidOutGroup[];
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/layout/erd-snap.d.ts
|
|
230
|
+
/**
|
|
231
|
+
* Snap ERD edge endpoints to column midlines while preserving orthogonal routes.
|
|
232
|
+
* Runs after ELK; before crossing treatment / trim.
|
|
233
|
+
*/
|
|
234
|
+
declare function snapErdEdgeEndpoints(graph: GraphModel, layout: LayoutResult, edgePaths: LayoutEdgePath[]): LayoutEdgePath[];
|
|
235
|
+
declare function erdRelationshipLabel(edge: {
|
|
236
|
+
fromColumn?: string;
|
|
237
|
+
toColumn?: string;
|
|
238
|
+
label?: string;
|
|
239
|
+
}): string | undefined;
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/layout/attach-endpoints.d.ts
|
|
242
|
+
/**
|
|
243
|
+
* Replace path termini with shape-perimeter hits.
|
|
244
|
+
* Attach rays are forced orthogonal; a corner is inserted when needed so the
|
|
245
|
+
* final stub never becomes a diagonal shortcut.
|
|
246
|
+
*/
|
|
247
|
+
declare function snapEdgeEndpointsToGeometry(graph: GraphModel, nodes: LaidOutNode[], edgePaths: LayoutEdgePath[]): LayoutEdgePath[];
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/topology/analyze.d.ts
|
|
250
|
+
type DiagramTopology = {
|
|
251
|
+
direction: Direction;
|
|
252
|
+
isWorkflowTD: boolean;
|
|
253
|
+
choiceNodes: string[];
|
|
254
|
+
mergeNodes: string[];
|
|
255
|
+
fanOutNodes: string[];
|
|
256
|
+
choiceBranches: Set<string>;
|
|
257
|
+
incoming: Map<string, number>;
|
|
258
|
+
outgoing: Map<string, number>;
|
|
259
|
+
};
|
|
260
|
+
declare function analyzeDiagramTopology(graph: GraphModel, direction: Direction): DiagramTopology;
|
|
261
|
+
declare function incomingCount(topology: DiagramTopology, nodeId: string): number;
|
|
262
|
+
declare function outgoingCount(topology: DiagramTopology, nodeId: string): number;
|
|
263
|
+
declare function isChoiceBranch(topology: DiagramTopology, nodeId: string): boolean;
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/direction/index.d.ts
|
|
266
|
+
/** True for left↔right flow (`LR` / `RL`). */
|
|
267
|
+
declare function isHorizontal(direction: Direction): boolean;
|
|
268
|
+
/** True for top↔bottom flow (`TD` / `BT`). */
|
|
269
|
+
declare function isVertical(direction: Direction): boolean;
|
|
270
|
+
//#endregion
|
|
271
|
+
export { DEFAULT_FONT_FAMILY, type DiagramTopology, ELK_LAYOUT_ALGORITHM, ELK_ROUTER_ALGORITHM, type ElkLayoutAndRouteResult, GROUP_ICON_GAP, GROUP_ICON_SIZE, type LaidOutGroup, type LaidOutNode, type LayoutEdgeLabel, type LayoutEdgePath, type LayoutResult, type MeasureOptions, type MeasureResult, type MeasuredNode, SEQUENCE_LAYOUT_ALGORITHM, SEQUENCE_ROUTER_ALGORITHM, type SequenceLayoutArtifacts, TABLE_ATTR_GAP, TABLE_BADGE_GAP, TABLE_BADGE_W, TABLE_HEADER_H, TABLE_KEY_COL, TABLE_PAD_X, TABLE_ROW_H, TABLE_RX, type TextMeasurer, type TextStyle, type WrapOptions, analyzeDiagramTopology, browserFontsReady, cardIconColumnWidth, columnAnchorY, columnNoteLabel, columnTypeLabel, computeGroupBounds, createCanvasMeasurer, defaultMeasurer, ensureBrowserFonts, erdRelationshipLabel, incomingCount, isChoiceBranch, isErdTableNode, isHorizontal, isSequenceGraph, isVertical, layoutAndRouteWithElk, layoutFromGraph, layoutSequence, measureGraph, measurerUsedApproximationFallback, outgoingCount, resetDefaultMeasurer, snapEdgeEndpointsToGeometry, snapErdEdgeEndpoints, tableKeyBadges };
|
|
272
|
+
//# sourceMappingURL=index.d.mts.map
|