@weasel-js/diagram 1.9.2 → 1.9.3
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/README.md +16 -0
- package/dist/{chunk-UBM2J3HJ.js → chunk-ERT3XWBS.js} +189 -20
- package/dist/chunk-ERT3XWBS.js.map +1 -0
- package/dist/index.d.ts +94 -6
- package/dist/index.js +185 -9
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +1 -1
- package/dist/{layout.entry-W-8PYfGM.d.ts → layout.entry-KHbx5uGC.d.ts} +331 -273
- package/dist/layout.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-UBM2J3HJ.js.map +0 -1
package/dist/layout.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { j as BodyFloor, k as BodyNodeSpec, B as BodySpec, l as Bounds, m as BuildBodyOptions, C as COMPASS, p as DEFAULT_NODE_GAP, q as DEFAULT_PORTS, r as DEFAULT_RANK_GAP, f as DiagramNode, u as ForceBody, F as ForceOptions, v as ForceRelaxation, i as Graph, w as GraphEdge, y as GraphNode, E as LayoutAxes, H as LayoutDirection, L as LayoutFn, g as LayoutOptions, h as LayoutResult, M as MeasureRowText, O as Outline, P as Port, c as PortAnchor, e as PortSpec, a as PortsOptions, R as Row, I as RowBox, J as RowNodeData, K as RowPort, N as RowPortBox, Q as RowTextStyle, S as Slot, T as axesFor, U as backEdges, V as bodyOutline, X as boxForContent, Y as buildBody, _ as canvasMeasure, $ as contentBox, au as extent, a4 as force, a5 as forceRelaxation, a6 as forestOf, a7 as graphOrder, aa as layered, ab as layoutBody, ac as layoutRowPorts, ad as measureBody, ae as outlinePath, af as outlinePolyline, ag as packAcross, ai as pinnedSet, aj as portOf, ak as portsOf, al as portsOnOutline, an as ranksOf, ao as rayHit, ap as seededOrder, aq as settle, ar as sizeToBody, as as translated, at as tree } from './layout.entry-KHbx5uGC.js';
|
|
2
2
|
import '@weasel-js/geom';
|
|
3
3
|
import '@weasel-js/core/math';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _weasel_js_geom from '@weasel-js/geom';
|
|
2
|
-
import { Path,
|
|
2
|
+
import { Path, PoseDescriptor, Vec2, SimulationNode, createSimulation } from '@weasel-js/core/math';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The shapes a built body can wear, as paths in the node's bounds.
|
|
@@ -53,6 +53,324 @@ declare function boxForContent(outline: Outline, content: {
|
|
|
53
53
|
height: number;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Reading the `DiagramNode` trait off a scene node.
|
|
58
|
+
*
|
|
59
|
+
* Two ways to attach it, both of which this module's default reader answers:
|
|
60
|
+
*
|
|
61
|
+
* - **On the node.** `data.diagram` holds the trait. One node, one answer;
|
|
62
|
+
* what a connect gesture writes when the author drags a new edge.
|
|
63
|
+
* - **By kind.** A consumer declares once which data shapes take part, and
|
|
64
|
+
* every matching node gets the trait without being stamped. This mirrors
|
|
65
|
+
* `createNodeRouting` in the kit — a predicate over `data`, first match
|
|
66
|
+
* wins — and is the shape to reach for when the participants are a class
|
|
67
|
+
* of node rather than a hand-picked set.
|
|
68
|
+
*
|
|
69
|
+
* A reader is a plain function, so a consumer whose data lives somewhere else
|
|
70
|
+
* entirely passes their own and never touches either form.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/** The key on a node's `data` the default reader looks under. */
|
|
74
|
+
declare const DIAGRAM_TRAIT_KEY = "diagram";
|
|
75
|
+
/** All a reader looks at. Deliberately not `SceneNode`: naming `TPose` there
|
|
76
|
+
* would put it in a contravariant position for no gain, since nothing here
|
|
77
|
+
* reads a pose. */
|
|
78
|
+
interface DiagramNodeLike {
|
|
79
|
+
id: string;
|
|
80
|
+
kind: 'leaf' | 'container';
|
|
81
|
+
data: unknown;
|
|
82
|
+
}
|
|
83
|
+
/** Answers "is this node a diagram participant, and on what terms" — `null`
|
|
84
|
+
* for a node that takes no part. */
|
|
85
|
+
type DiagramNodeReader = (node: DiagramNodeLike) => DiagramNode | null;
|
|
86
|
+
/** One class of participant, declared once for every node whose data matches.
|
|
87
|
+
* Mirrors `NodeRoutingEntry`. */
|
|
88
|
+
interface DiagramNodeEntry {
|
|
89
|
+
/** Unique within a reader; the same vocabulary as `NodeRoutingEntry.name`. */
|
|
90
|
+
name: string;
|
|
91
|
+
matches: (data: unknown) => boolean;
|
|
92
|
+
/** The trait every matching node carries. A function when it varies with the
|
|
93
|
+
* node's own data — a pipeline stage whose ports come from its rows. */
|
|
94
|
+
trait: DiagramNode | ((data: unknown) => DiagramNode);
|
|
95
|
+
}
|
|
96
|
+
/** The trait on the node's own `data.diagram`, or `null`.
|
|
97
|
+
*
|
|
98
|
+
* An edge's trait lives under the same key, so `from` and `to` are what tells
|
|
99
|
+
* the two apart. Without that test an edge reads as a participant declaring
|
|
100
|
+
* no ports, and collects the four defaults on the degenerate pose an edge
|
|
101
|
+
* carries — four grabbable ports in the middle of nowhere. An edge label's
|
|
102
|
+
* `label` trait and an anchored node's `anchor` are the same story, and would
|
|
103
|
+
* also take a slot in every layout. */
|
|
104
|
+
declare const dataKeyReader: DiagramNodeReader;
|
|
105
|
+
/**
|
|
106
|
+
* A reader over declared kinds. A trait on the node's own data still wins, so
|
|
107
|
+
* a single node can opt out of, or override, whatever its kind says.
|
|
108
|
+
*
|
|
109
|
+
* Throws on a duplicate `name`, for the reason `mergeContributions` does: a
|
|
110
|
+
* class of node silently losing its ports is what this registry exists to make
|
|
111
|
+
* loud.
|
|
112
|
+
*/
|
|
113
|
+
declare function createDiagramNodes(entries: readonly DiagramNodeEntry[]): DiagramNodeReader;
|
|
114
|
+
/** The trait `node` carries, through `read` (default: its own `data.diagram`). */
|
|
115
|
+
declare function diagramNodeOf(node: DiagramNodeLike, read?: DiagramNodeReader): DiagramNode | null;
|
|
116
|
+
/** True when layout must leave this node where it is. */
|
|
117
|
+
declare function isPinned(node: DiagramNodeLike, read?: DiagramNodeReader): boolean;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `Graph` — the adjacency index a layout runs over.
|
|
121
|
+
*
|
|
122
|
+
* The scene is the document, so there is no second graph kept in sync with it:
|
|
123
|
+
* this one is built from the scene on demand, used, and thrown away. Rebuilding
|
|
124
|
+
* per layout invocation is deliberate — a maintained index is a cache to
|
|
125
|
+
* invalidate on every add, delete, reparent and `dependsOn` edit, and layout is
|
|
126
|
+
* not on the hot path. Measure before changing that.
|
|
127
|
+
*
|
|
128
|
+
* A node's *identity* is its id and its *size* is its bounds; where it currently
|
|
129
|
+
* sits is here too, because a re-layout that ignores where things already are
|
|
130
|
+
* scrambles a diagram the author has arranged.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/** A scene node as the graph reads it. `dependsOn` is what an edge's two
|
|
134
|
+
* endpoints are: the edge trait names *ports*, and the nodes those ports are
|
|
135
|
+
* on are the dependencies the derivation already runs on. */
|
|
136
|
+
interface GraphNodeLike extends DiagramNodeLike {
|
|
137
|
+
dependsOn?: readonly string[] | 'children';
|
|
138
|
+
}
|
|
139
|
+
/** Where the graph gets its nodes. The same thunk shape the port affordance
|
|
140
|
+
* takes, so one source answers both. */
|
|
141
|
+
type GraphSource<TPose> = () => Iterable<{
|
|
142
|
+
node: GraphNodeLike;
|
|
143
|
+
pose: TPose;
|
|
144
|
+
}>;
|
|
145
|
+
/** One participant. */
|
|
146
|
+
interface GraphNode {
|
|
147
|
+
id: string;
|
|
148
|
+
/** Where it is now, in world coordinates. */
|
|
149
|
+
bounds: Bounds;
|
|
150
|
+
/** Layout must not move it. */
|
|
151
|
+
pinned: boolean;
|
|
152
|
+
/** The group it belongs to, when it belongs to one. */
|
|
153
|
+
group?: string;
|
|
154
|
+
}
|
|
155
|
+
/** A box drawn around some participants, which a layout keeps together and
|
|
156
|
+
* leaves room for. */
|
|
157
|
+
interface GraphGroup {
|
|
158
|
+
id: string;
|
|
159
|
+
/** In source order. */
|
|
160
|
+
members: readonly string[];
|
|
161
|
+
/** Room the box keeps around its members, in world units. */
|
|
162
|
+
inset: GroupInset;
|
|
163
|
+
}
|
|
164
|
+
/** One connection. `id` is the edge node's own id, so a caller can go back to
|
|
165
|
+
* the scene for its trait. */
|
|
166
|
+
interface GraphEdge {
|
|
167
|
+
id: string;
|
|
168
|
+
from: string;
|
|
169
|
+
to: string;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Nodes, edges, and the two adjacency reads a layout needs.
|
|
173
|
+
*
|
|
174
|
+
* Every list is in source order — render order, when the source is a scene —
|
|
175
|
+
* which is what a layout's tiebreaks resolve against. There is no RNG anywhere
|
|
176
|
+
* downstream of this, and this is where that starts.
|
|
177
|
+
*/
|
|
178
|
+
interface Graph {
|
|
179
|
+
readonly nodes: readonly GraphNode[];
|
|
180
|
+
readonly edges: readonly GraphEdge[];
|
|
181
|
+
/** Absent or empty for a graph with no groups. */
|
|
182
|
+
readonly groups?: readonly GraphGroup[];
|
|
183
|
+
node(id: string): GraphNode | undefined;
|
|
184
|
+
/** Edges leaving `id`, in source order. */
|
|
185
|
+
outgoing(id: string): readonly GraphEdge[];
|
|
186
|
+
/** Edges arriving at `id`, in source order. */
|
|
187
|
+
incoming(id: string): readonly GraphEdge[];
|
|
188
|
+
}
|
|
189
|
+
/** Options for {@link buildGraph}: how the trait is read, and how a pose
|
|
190
|
+
* yields bounds. Both default to the kit's own. */
|
|
191
|
+
interface BuildGraphOptions<TPose> {
|
|
192
|
+
read?: DiagramNodeReader;
|
|
193
|
+
geometry?: PoseDescriptor<TPose>;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Read a graph out of a source of scene nodes.
|
|
197
|
+
*
|
|
198
|
+
* A node is an edge if it carries the edge trait and names exactly two
|
|
199
|
+
* dependencies; a group if its trait carries `group`, with its dependencies as
|
|
200
|
+
* its members; a participant if the reader hands back any other
|
|
201
|
+
* `DiagramNode`; and otherwise not in the diagram at all. An edge whose
|
|
202
|
+
* endpoints are not both participants or groups is dropped — a dangling edge
|
|
203
|
+
* should not be ranking anything — and an edge to a group ranks against its
|
|
204
|
+
* members (see `expandGroupEdges`).
|
|
205
|
+
*/
|
|
206
|
+
declare function buildGraph<TPose>(source: GraphSource<TPose>, opts?: BuildGraphOptions<TPose>): Graph;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Layout: where the participants go.
|
|
210
|
+
*
|
|
211
|
+
* A layout is a pure function of the graph — no scene, no ops, no history. It
|
|
212
|
+
* hands back the new top-left for every node that **moves**, and a node already
|
|
213
|
+
* standing where the layout wants it is simply absent from the answer. That is
|
|
214
|
+
* what makes re-running a layout free rather than destructive, and it is the
|
|
215
|
+
* property the idempotence test asserts.
|
|
216
|
+
*
|
|
217
|
+
* Three rules keep re-layout non-destructive, and all three live here rather
|
|
218
|
+
* than in each algorithm:
|
|
219
|
+
*
|
|
220
|
+
* - **No RNG anywhere.** Every tiebreak falls back to the graph's own node
|
|
221
|
+
* order, which is the source's order — render order, for a scene.
|
|
222
|
+
* - **Within-rank order is seeded from where the nodes already are**, read
|
|
223
|
+
* off the cross axis, rather than from crossing-minimization. An author who
|
|
224
|
+
* dragged two boxes into an order gets that order back.
|
|
225
|
+
* - **A pin set nothing moves.** Pinned nodes are never in the result, and
|
|
226
|
+
* the rest of the layout is translated to sit around them.
|
|
227
|
+
*
|
|
228
|
+
* `layered` and `tree` are idempotent by construction: the second run reads the
|
|
229
|
+
* order the first run produced and computes the same slots. `force` is an
|
|
230
|
+
* iterative relaxation and makes no such claim — re-running it keeps relaxing.
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
/** Which way the ranks stack. `force` ignores it. */
|
|
234
|
+
type LayoutDirection = 'down' | 'up' | 'right' | 'left';
|
|
235
|
+
/** Options every layout takes. */
|
|
236
|
+
interface LayoutOptions {
|
|
237
|
+
/** Default `'down'`. */
|
|
238
|
+
direction?: LayoutDirection;
|
|
239
|
+
/** Between neighbors within a rank, in world units. Default 48. */
|
|
240
|
+
nodeGap?: number;
|
|
241
|
+
/** Between one rank and the next. Default 96. */
|
|
242
|
+
rankGap?: number;
|
|
243
|
+
/** How `layered` orders nodes within a rank. `'seeded'` (default) keeps
|
|
244
|
+
* the order they already sit in; `'barycenter'` reorders to reduce edge
|
|
245
|
+
* crossings, for a generated diagram with no arranged order to keep.
|
|
246
|
+
* Other layouts ignore it. */
|
|
247
|
+
order?: 'seeded' | 'barycenter';
|
|
248
|
+
/** Ids layout must not move, on top of whatever carries `pinned: true`. */
|
|
249
|
+
pin?: Iterable<string>;
|
|
250
|
+
/** How far a node has to move to be worth moving, in world units. Default
|
|
251
|
+
* `1e-6` — a float-noise floor, not a snapping quantum. Raise it to let a
|
|
252
|
+
* `force` pass that has stopped doing anything useful commit nothing. */
|
|
253
|
+
tolerance?: number;
|
|
254
|
+
}
|
|
255
|
+
/** The new top-left for each node that moves. A node that is already there is
|
|
256
|
+
* absent, so an empty result means "nothing to do". */
|
|
257
|
+
type LayoutResult = ReadonlyMap<string, Vec2>;
|
|
258
|
+
/** Every layout in this package has this shape, and so does a consumer's. */
|
|
259
|
+
type LayoutFn = (graph: Graph, opts?: LayoutOptions) => LayoutResult;
|
|
260
|
+
/** `LayoutOptions.nodeGap`'s default, in world units. */
|
|
261
|
+
declare const DEFAULT_NODE_GAP = 48;
|
|
262
|
+
/** `LayoutOptions.rankGap`'s default, in world units. */
|
|
263
|
+
declare const DEFAULT_RANK_GAP = 96;
|
|
264
|
+
/** Which world axis carries ranks, which carries within-rank order, and
|
|
265
|
+
* whether ranks grow toward positive world coordinates. */
|
|
266
|
+
interface LayoutAxes {
|
|
267
|
+
rank: 'x' | 'y';
|
|
268
|
+
cross: 'x' | 'y';
|
|
269
|
+
/** `-1` for `'up'` and `'left'`, where the canonical frame is mirrored. */
|
|
270
|
+
sign: 1 | -1;
|
|
271
|
+
}
|
|
272
|
+
/** The axes a direction lays ranks along. Default `'down'`. */
|
|
273
|
+
declare function axesFor(direction?: LayoutDirection): LayoutAxes;
|
|
274
|
+
/** A node's extent along one axis. */
|
|
275
|
+
declare function extent(bounds: Bounds, axis: 'x' | 'y'): number;
|
|
276
|
+
/** Every id layout must leave alone. */
|
|
277
|
+
declare function pinnedSet(graph: Graph, opts?: LayoutOptions): Set<string>;
|
|
278
|
+
/**
|
|
279
|
+
* A placement in the canonical `(cross, rank)` frame, before it is turned into
|
|
280
|
+
* world coordinates. `cross` and `rank` are the node's low corner along each.
|
|
281
|
+
*/
|
|
282
|
+
interface Slot {
|
|
283
|
+
cross: number;
|
|
284
|
+
rank: number;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Turn canonical slots into the positions to write.
|
|
288
|
+
*
|
|
289
|
+
* Three steps, in order: map the canonical frame onto the world axes the
|
|
290
|
+
* direction names; translate the whole layout so it lands where the graph
|
|
291
|
+
* already is; drop everything that is pinned or that would not move.
|
|
292
|
+
*
|
|
293
|
+
* The translation is what makes the layout non-destructive, and it is chosen so
|
|
294
|
+
* that a second run lands on the same answer. With a pin, it is whatever keeps
|
|
295
|
+
* the first pinned node exactly where it is. Without one, it is whatever puts
|
|
296
|
+
* the layout's own bounding box where the graph's bounding box already starts —
|
|
297
|
+
* which the first run then makes true, so the second run computes the same
|
|
298
|
+
* translation and writes nothing.
|
|
299
|
+
*/
|
|
300
|
+
declare function settle(graph: Graph, slots: ReadonlyMap<string, Slot>, opts?: LayoutOptions): LayoutResult;
|
|
301
|
+
/**
|
|
302
|
+
* {@link settle}'s second half, for a layout that already works in world
|
|
303
|
+
* coordinates — `force` does, since the integrator moves points around rather
|
|
304
|
+
* than filling slots.
|
|
305
|
+
*/
|
|
306
|
+
declare function translated(graph: Graph, placed: ReadonlyMap<string, Vec2>, pinned: ReadonlySet<string>, tolerance?: number): LayoutResult;
|
|
307
|
+
/**
|
|
308
|
+
* Lay a list of nodes out along the cross axis, in the order given, and hand
|
|
309
|
+
* back each one's low corner plus the total span.
|
|
310
|
+
*
|
|
311
|
+
* Shared by `layered`'s ranks and `tree`'s sibling rows, which pack a row the
|
|
312
|
+
* same way and differ only in what decides the order.
|
|
313
|
+
*/
|
|
314
|
+
declare function packAcross(nodes: readonly GraphNode[], axis: 'x' | 'y', gap: number): {
|
|
315
|
+
at: Map<string, number>;
|
|
316
|
+
span: number;
|
|
317
|
+
};
|
|
318
|
+
/** Order within a rank: where the nodes already sit on the cross axis, with
|
|
319
|
+
* graph order breaking a tie. Never a comparison that could reverse between
|
|
320
|
+
* two runs on the same input. */
|
|
321
|
+
declare function seededOrder(nodes: readonly GraphNode[], axis: 'x' | 'y', indexOf: (id: string) => number): GraphNode[];
|
|
322
|
+
/** Each node's position in `graph.nodes`, for tiebreaks. */
|
|
323
|
+
declare function graphOrder(graph: Graph): (id: string) => number;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Clusters: keeping a group's members together through a ranked layout.
|
|
327
|
+
*
|
|
328
|
+
* A group's box is the union of its members plus an inset, so the layout's job
|
|
329
|
+
* is to leave a rectangle for it that nothing else enters. Each group gets a
|
|
330
|
+
* **column** on the cross axis, reserved in every rank from its first member's
|
|
331
|
+
* to its last — including a rank in between that holds none of its members, or
|
|
332
|
+
* an outsider would land inside the box there. Groups keep one cross position
|
|
333
|
+
* across every rank, so two groups' columns never cross and their boxes never
|
|
334
|
+
* overlap. Ungrouped nodes pack around the columns rank by rank, as before.
|
|
335
|
+
*
|
|
336
|
+
* Groups are flat: a group is not a member of another group.
|
|
337
|
+
*/
|
|
338
|
+
|
|
339
|
+
/** Room a group's box keeps around its members, in world units. */
|
|
340
|
+
interface GroupInset {
|
|
341
|
+
top: number;
|
|
342
|
+
right: number;
|
|
343
|
+
bottom: number;
|
|
344
|
+
left: number;
|
|
345
|
+
}
|
|
346
|
+
/** A `DiagramGroup.inset` as all four sides. Default 12 on every side. */
|
|
347
|
+
declare function insetOf(inset: number | Partial<GroupInset> | undefined): GroupInset;
|
|
348
|
+
/** `DiagramGroup.inset`'s default on each side, in world units. */
|
|
349
|
+
declare const DEFAULT_GROUP_INSET = 12;
|
|
350
|
+
/**
|
|
351
|
+
* The gap between each rank and the next, widened so a group's box fits: the
|
|
352
|
+
* widest inset of a group ending at rank `i` and of one starting at `i + 1`
|
|
353
|
+
* both come on top of `rankGap`, since those two boxes can face each other
|
|
354
|
+
* across it. `rows` is the nodes of each rank.
|
|
355
|
+
*/
|
|
356
|
+
declare function rankGaps(graph: Graph, rows: readonly (readonly GraphNode[])[], axes: LayoutAxes, rankGap: number): number[];
|
|
357
|
+
/**
|
|
358
|
+
* Cross positions for every node, with each group's members kept in a column
|
|
359
|
+
* of their own. `preferred` is where the layout would put each node's low
|
|
360
|
+
* corner with no groups at all; nodes and columns keep that order and are only
|
|
361
|
+
* ever pushed toward the high end, by as much as it takes to stop overlapping.
|
|
362
|
+
*/
|
|
363
|
+
declare function packClusters(graph: Graph, rows: readonly (readonly GraphNode[])[], preferred: ReadonlyMap<string, number>, axes: LayoutAxes, nodeGap: number): Map<string, number>;
|
|
364
|
+
/**
|
|
365
|
+
* Edges as a layout should rank by: one into a group arrives at its entry
|
|
366
|
+
* members — those no other member points at — and one out of it leaves from its
|
|
367
|
+
* exit members. A group with no such member, a pure cycle, is all entries and
|
|
368
|
+
* all exits. An edge between a group and one of its own members, or two
|
|
369
|
+
* members of one group, is a group's internal business and ranks nothing
|
|
370
|
+
* beyond what its member-to-member edges already do.
|
|
371
|
+
*/
|
|
372
|
+
declare function expandGroupEdges(edges: readonly GraphEdge[], groups: readonly GraphGroup[]): GraphEdge[];
|
|
373
|
+
|
|
56
374
|
/**
|
|
57
375
|
* Where a port sits on its node, in **normalized bounds coordinates**:
|
|
58
376
|
* `(0, 0)` is the top-left corner of the node's AABB and `(1, 1)` the
|
|
@@ -95,6 +413,17 @@ interface DiagramNode {
|
|
|
95
413
|
* participant that already had a look of its own — a text block, an image,
|
|
96
414
|
* a path — which keeps whatever painter it already matched. */
|
|
97
415
|
outline?: Outline;
|
|
416
|
+
/** Present on a group's box: a node drawn around the participants it
|
|
417
|
+
* depends on, which layout keeps together and leaves room for. The box
|
|
418
|
+
* itself is never laid out — it derives its pose from its members — but an
|
|
419
|
+
* edge can attach to it like any participant. */
|
|
420
|
+
group?: DiagramGroup;
|
|
421
|
+
}
|
|
422
|
+
/** The group half of a {@link DiagramNode}. */
|
|
423
|
+
interface DiagramGroup {
|
|
424
|
+
/** Room the box keeps around its members, in world units: one number for
|
|
425
|
+
* every side, or per side. Default 12 on each. */
|
|
426
|
+
inset?: number | Partial<GroupInset>;
|
|
98
427
|
}
|
|
99
428
|
/** A port resolved against a node's current pose: where it is in world
|
|
100
429
|
* coordinates, and which way an edge leaves it. */
|
|
@@ -303,67 +632,6 @@ declare function buildBody<TData, TLayer extends string, TPose extends Bounds>(s
|
|
|
303
632
|
specs: BodyNodeSpec<TData, TLayer, TPose>[];
|
|
304
633
|
};
|
|
305
634
|
|
|
306
|
-
/**
|
|
307
|
-
* Reading the `DiagramNode` trait off a scene node.
|
|
308
|
-
*
|
|
309
|
-
* Two ways to attach it, both of which this module's default reader answers:
|
|
310
|
-
*
|
|
311
|
-
* - **On the node.** `data.diagram` holds the trait. One node, one answer;
|
|
312
|
-
* what a connect gesture writes when the author drags a new edge.
|
|
313
|
-
* - **By kind.** A consumer declares once which data shapes take part, and
|
|
314
|
-
* every matching node gets the trait without being stamped. This mirrors
|
|
315
|
-
* `createNodeRouting` in the kit — a predicate over `data`, first match
|
|
316
|
-
* wins — and is the shape to reach for when the participants are a class
|
|
317
|
-
* of node rather than a hand-picked set.
|
|
318
|
-
*
|
|
319
|
-
* A reader is a plain function, so a consumer whose data lives somewhere else
|
|
320
|
-
* entirely passes their own and never touches either form.
|
|
321
|
-
*/
|
|
322
|
-
|
|
323
|
-
/** The key on a node's `data` the default reader looks under. */
|
|
324
|
-
declare const DIAGRAM_TRAIT_KEY = "diagram";
|
|
325
|
-
/** All a reader looks at. Deliberately not `SceneNode`: naming `TPose` there
|
|
326
|
-
* would put it in a contravariant position for no gain, since nothing here
|
|
327
|
-
* reads a pose. */
|
|
328
|
-
interface DiagramNodeLike {
|
|
329
|
-
id: string;
|
|
330
|
-
kind: 'leaf' | 'container';
|
|
331
|
-
data: unknown;
|
|
332
|
-
}
|
|
333
|
-
/** Answers "is this node a diagram participant, and on what terms" — `null`
|
|
334
|
-
* for a node that takes no part. */
|
|
335
|
-
type DiagramNodeReader = (node: DiagramNodeLike) => DiagramNode | null;
|
|
336
|
-
/** One class of participant, declared once for every node whose data matches.
|
|
337
|
-
* Mirrors `NodeRoutingEntry`. */
|
|
338
|
-
interface DiagramNodeEntry {
|
|
339
|
-
/** Unique within a reader; the same vocabulary as `NodeRoutingEntry.name`. */
|
|
340
|
-
name: string;
|
|
341
|
-
matches: (data: unknown) => boolean;
|
|
342
|
-
/** The trait every matching node carries. A function when it varies with the
|
|
343
|
-
* node's own data — a pipeline stage whose ports come from its rows. */
|
|
344
|
-
trait: DiagramNode | ((data: unknown) => DiagramNode);
|
|
345
|
-
}
|
|
346
|
-
/** The trait on the node's own `data.diagram`, or `null`.
|
|
347
|
-
*
|
|
348
|
-
* An edge's trait lives under the same key, so `from` and `to` are what tells
|
|
349
|
-
* the two apart. Without that test an edge reads as a participant declaring
|
|
350
|
-
* no ports, and collects the four defaults on the degenerate pose an edge
|
|
351
|
-
* carries — four grabbable ports in the middle of nowhere. */
|
|
352
|
-
declare const dataKeyReader: DiagramNodeReader;
|
|
353
|
-
/**
|
|
354
|
-
* A reader over declared kinds. A trait on the node's own data still wins, so
|
|
355
|
-
* a single node can opt out of, or override, whatever its kind says.
|
|
356
|
-
*
|
|
357
|
-
* Throws on a duplicate `name`, for the reason `mergeContributions` does: a
|
|
358
|
-
* class of node silently losing its ports is what this registry exists to make
|
|
359
|
-
* loud.
|
|
360
|
-
*/
|
|
361
|
-
declare function createDiagramNodes(entries: readonly DiagramNodeEntry[]): DiagramNodeReader;
|
|
362
|
-
/** The trait `node` carries, through `read` (default: its own `data.diagram`). */
|
|
363
|
-
declare function diagramNodeOf(node: DiagramNodeLike, read?: DiagramNodeReader): DiagramNode | null;
|
|
364
|
-
/** True when layout must leave this node where it is. */
|
|
365
|
-
declare function isPinned(node: DiagramNodeLike, read?: DiagramNodeReader): boolean;
|
|
366
|
-
|
|
367
635
|
/**
|
|
368
636
|
* Where a node's ports are, in world coordinates.
|
|
369
637
|
*
|
|
@@ -434,197 +702,6 @@ declare function portsOf<TPose>(node: DiagramNodeLike, pose: TPose, opts?: Ports
|
|
|
434
702
|
/** One named port, or `undefined` when the node has no such port. */
|
|
435
703
|
declare function portOf<TPose>(node: DiagramNodeLike, pose: TPose, portId: string, opts?: PortsOptions<TPose>): Port | undefined;
|
|
436
704
|
|
|
437
|
-
/**
|
|
438
|
-
* `Graph` — the adjacency index a layout runs over.
|
|
439
|
-
*
|
|
440
|
-
* The scene is the document, so there is no second graph kept in sync with it:
|
|
441
|
-
* this one is built from the scene on demand, used, and thrown away. Rebuilding
|
|
442
|
-
* per layout invocation is deliberate — a maintained index is a cache to
|
|
443
|
-
* invalidate on every add, delete, reparent and `dependsOn` edit, and layout is
|
|
444
|
-
* not on the hot path. Measure before changing that.
|
|
445
|
-
*
|
|
446
|
-
* A node's *identity* is its id and its *size* is its bounds; where it currently
|
|
447
|
-
* sits is here too, because a re-layout that ignores where things already are
|
|
448
|
-
* scrambles a diagram the author has arranged.
|
|
449
|
-
*/
|
|
450
|
-
|
|
451
|
-
/** A scene node as the graph reads it. `dependsOn` is what an edge's two
|
|
452
|
-
* endpoints are: the edge trait names *ports*, and the nodes those ports are
|
|
453
|
-
* on are the dependencies the derivation already runs on. */
|
|
454
|
-
interface GraphNodeLike extends DiagramNodeLike {
|
|
455
|
-
dependsOn?: readonly string[] | 'children';
|
|
456
|
-
}
|
|
457
|
-
/** Where the graph gets its nodes. The same thunk shape the port affordance
|
|
458
|
-
* takes, so one source answers both. */
|
|
459
|
-
type GraphSource<TPose> = () => Iterable<{
|
|
460
|
-
node: GraphNodeLike;
|
|
461
|
-
pose: TPose;
|
|
462
|
-
}>;
|
|
463
|
-
/** One participant. */
|
|
464
|
-
interface GraphNode {
|
|
465
|
-
id: string;
|
|
466
|
-
/** Where it is now, in world coordinates. */
|
|
467
|
-
bounds: Bounds;
|
|
468
|
-
/** Layout must not move it. */
|
|
469
|
-
pinned: boolean;
|
|
470
|
-
}
|
|
471
|
-
/** One connection. `id` is the edge node's own id, so a caller can go back to
|
|
472
|
-
* the scene for its trait. */
|
|
473
|
-
interface GraphEdge {
|
|
474
|
-
id: string;
|
|
475
|
-
from: string;
|
|
476
|
-
to: string;
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Nodes, edges, and the two adjacency reads a layout needs.
|
|
480
|
-
*
|
|
481
|
-
* Every list is in source order — render order, when the source is a scene —
|
|
482
|
-
* which is what a layout's tiebreaks resolve against. There is no RNG anywhere
|
|
483
|
-
* downstream of this, and this is where that starts.
|
|
484
|
-
*/
|
|
485
|
-
interface Graph {
|
|
486
|
-
readonly nodes: readonly GraphNode[];
|
|
487
|
-
readonly edges: readonly GraphEdge[];
|
|
488
|
-
node(id: string): GraphNode | undefined;
|
|
489
|
-
/** Edges leaving `id`, in source order. */
|
|
490
|
-
outgoing(id: string): readonly GraphEdge[];
|
|
491
|
-
/** Edges arriving at `id`, in source order. */
|
|
492
|
-
incoming(id: string): readonly GraphEdge[];
|
|
493
|
-
}
|
|
494
|
-
/** Options for {@link buildGraph}: how the trait is read, and how a pose
|
|
495
|
-
* yields bounds. Both default to the kit's own. */
|
|
496
|
-
interface BuildGraphOptions<TPose> {
|
|
497
|
-
read?: DiagramNodeReader;
|
|
498
|
-
geometry?: PoseDescriptor<TPose>;
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Read a graph out of a source of scene nodes.
|
|
502
|
-
*
|
|
503
|
-
* A node is an edge if it carries the edge trait and names exactly two
|
|
504
|
-
* dependencies; a participant if the reader hands back a `DiagramNode`;
|
|
505
|
-
* neither, and it is not in the diagram at all. An edge whose endpoints are not
|
|
506
|
-
* both participants is dropped — a dangling edge should not be ranking
|
|
507
|
-
* anything.
|
|
508
|
-
*/
|
|
509
|
-
declare function buildGraph<TPose>(source: GraphSource<TPose>, opts?: BuildGraphOptions<TPose>): Graph;
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Layout: where the participants go.
|
|
513
|
-
*
|
|
514
|
-
* A layout is a pure function of the graph — no scene, no ops, no history. It
|
|
515
|
-
* hands back the new top-left for every node that **moves**, and a node already
|
|
516
|
-
* standing where the layout wants it is simply absent from the answer. That is
|
|
517
|
-
* what makes re-running a layout free rather than destructive, and it is the
|
|
518
|
-
* property the idempotence test asserts.
|
|
519
|
-
*
|
|
520
|
-
* Three rules keep re-layout non-destructive, and all three live here rather
|
|
521
|
-
* than in each algorithm:
|
|
522
|
-
*
|
|
523
|
-
* - **No RNG anywhere.** Every tiebreak falls back to the graph's own node
|
|
524
|
-
* order, which is the source's order — render order, for a scene.
|
|
525
|
-
* - **Within-rank order is seeded from where the nodes already are**, read
|
|
526
|
-
* off the cross axis, rather than from crossing-minimization. An author who
|
|
527
|
-
* dragged two boxes into an order gets that order back.
|
|
528
|
-
* - **A pin set nothing moves.** Pinned nodes are never in the result, and
|
|
529
|
-
* the rest of the layout is translated to sit around them.
|
|
530
|
-
*
|
|
531
|
-
* `layered` and `tree` are idempotent by construction: the second run reads the
|
|
532
|
-
* order the first run produced and computes the same slots. `force` is an
|
|
533
|
-
* iterative relaxation and makes no such claim — re-running it keeps relaxing.
|
|
534
|
-
*/
|
|
535
|
-
|
|
536
|
-
/** Which way the ranks stack. `force` ignores it. */
|
|
537
|
-
type LayoutDirection = 'down' | 'up' | 'right' | 'left';
|
|
538
|
-
/** Options every layout takes. */
|
|
539
|
-
interface LayoutOptions {
|
|
540
|
-
/** Default `'down'`. */
|
|
541
|
-
direction?: LayoutDirection;
|
|
542
|
-
/** Between neighbors within a rank, in world units. Default 48. */
|
|
543
|
-
nodeGap?: number;
|
|
544
|
-
/** Between one rank and the next. Default 96. */
|
|
545
|
-
rankGap?: number;
|
|
546
|
-
/** How `layered` orders nodes within a rank. `'seeded'` (default) keeps
|
|
547
|
-
* the order they already sit in; `'barycenter'` reorders to reduce edge
|
|
548
|
-
* crossings, for a generated diagram with no arranged order to keep.
|
|
549
|
-
* Other layouts ignore it. */
|
|
550
|
-
order?: 'seeded' | 'barycenter';
|
|
551
|
-
/** Ids layout must not move, on top of whatever carries `pinned: true`. */
|
|
552
|
-
pin?: Iterable<string>;
|
|
553
|
-
/** How far a node has to move to be worth moving, in world units. Default
|
|
554
|
-
* `1e-6` — a float-noise floor, not a snapping quantum. Raise it to let a
|
|
555
|
-
* `force` pass that has stopped doing anything useful commit nothing. */
|
|
556
|
-
tolerance?: number;
|
|
557
|
-
}
|
|
558
|
-
/** The new top-left for each node that moves. A node that is already there is
|
|
559
|
-
* absent, so an empty result means "nothing to do". */
|
|
560
|
-
type LayoutResult = ReadonlyMap<string, Vec2>;
|
|
561
|
-
/** Every layout in this package has this shape, and so does a consumer's. */
|
|
562
|
-
type LayoutFn = (graph: Graph, opts?: LayoutOptions) => LayoutResult;
|
|
563
|
-
/** `LayoutOptions.nodeGap`'s default, in world units. */
|
|
564
|
-
declare const DEFAULT_NODE_GAP = 48;
|
|
565
|
-
/** `LayoutOptions.rankGap`'s default, in world units. */
|
|
566
|
-
declare const DEFAULT_RANK_GAP = 96;
|
|
567
|
-
/** Which world axis carries ranks, which carries within-rank order, and
|
|
568
|
-
* whether ranks grow toward positive world coordinates. */
|
|
569
|
-
interface LayoutAxes {
|
|
570
|
-
rank: 'x' | 'y';
|
|
571
|
-
cross: 'x' | 'y';
|
|
572
|
-
/** `-1` for `'up'` and `'left'`, where the canonical frame is mirrored. */
|
|
573
|
-
sign: 1 | -1;
|
|
574
|
-
}
|
|
575
|
-
/** The axes a direction lays ranks along. Default `'down'`. */
|
|
576
|
-
declare function axesFor(direction?: LayoutDirection): LayoutAxes;
|
|
577
|
-
/** A node's extent along one axis. */
|
|
578
|
-
declare function extent(bounds: Bounds, axis: 'x' | 'y'): number;
|
|
579
|
-
/** Every id layout must leave alone. */
|
|
580
|
-
declare function pinnedSet(graph: Graph, opts?: LayoutOptions): Set<string>;
|
|
581
|
-
/**
|
|
582
|
-
* A placement in the canonical `(cross, rank)` frame, before it is turned into
|
|
583
|
-
* world coordinates. `cross` and `rank` are the node's low corner along each.
|
|
584
|
-
*/
|
|
585
|
-
interface Slot {
|
|
586
|
-
cross: number;
|
|
587
|
-
rank: number;
|
|
588
|
-
}
|
|
589
|
-
/**
|
|
590
|
-
* Turn canonical slots into the positions to write.
|
|
591
|
-
*
|
|
592
|
-
* Three steps, in order: map the canonical frame onto the world axes the
|
|
593
|
-
* direction names; translate the whole layout so it lands where the graph
|
|
594
|
-
* already is; drop everything that is pinned or that would not move.
|
|
595
|
-
*
|
|
596
|
-
* The translation is what makes the layout non-destructive, and it is chosen so
|
|
597
|
-
* that a second run lands on the same answer. With a pin, it is whatever keeps
|
|
598
|
-
* the first pinned node exactly where it is. Without one, it is whatever puts
|
|
599
|
-
* the layout's own bounding box where the graph's bounding box already starts —
|
|
600
|
-
* which the first run then makes true, so the second run computes the same
|
|
601
|
-
* translation and writes nothing.
|
|
602
|
-
*/
|
|
603
|
-
declare function settle(graph: Graph, slots: ReadonlyMap<string, Slot>, opts?: LayoutOptions): LayoutResult;
|
|
604
|
-
/**
|
|
605
|
-
* {@link settle}'s second half, for a layout that already works in world
|
|
606
|
-
* coordinates — `force` does, since the integrator moves points around rather
|
|
607
|
-
* than filling slots.
|
|
608
|
-
*/
|
|
609
|
-
declare function translated(graph: Graph, placed: ReadonlyMap<string, Vec2>, pinned: ReadonlySet<string>, tolerance?: number): LayoutResult;
|
|
610
|
-
/**
|
|
611
|
-
* Lay a list of nodes out along the cross axis, in the order given, and hand
|
|
612
|
-
* back each one's low corner plus the total span.
|
|
613
|
-
*
|
|
614
|
-
* Shared by `layered`'s ranks and `tree`'s sibling rows, which pack a row the
|
|
615
|
-
* same way and differ only in what decides the order.
|
|
616
|
-
*/
|
|
617
|
-
declare function packAcross(nodes: readonly GraphNode[], axis: 'x' | 'y', gap: number): {
|
|
618
|
-
at: Map<string, number>;
|
|
619
|
-
span: number;
|
|
620
|
-
};
|
|
621
|
-
/** Order within a rank: where the nodes already sit on the cross axis, with
|
|
622
|
-
* graph order breaking a tie. Never a comparison that could reverse between
|
|
623
|
-
* two runs on the same input. */
|
|
624
|
-
declare function seededOrder(nodes: readonly GraphNode[], axis: 'x' | 'y', indexOf: (id: string) => number): GraphNode[];
|
|
625
|
-
/** Each node's position in `graph.nodes`, for tiebreaks. */
|
|
626
|
-
declare function graphOrder(graph: Graph): (id: string) => number;
|
|
627
|
-
|
|
628
705
|
/**
|
|
629
706
|
* `force` — relaxation, for a graph with no direction to read it in.
|
|
630
707
|
*
|
|
@@ -709,25 +786,6 @@ declare function ranksOf(graph: Graph, back: ReadonlySet<string>): Map<string, n
|
|
|
709
786
|
* moves nothing — except under `order: 'barycenter'`, which reorders them. */
|
|
710
787
|
declare const layered: LayoutFn;
|
|
711
788
|
|
|
712
|
-
/**
|
|
713
|
-
* `tree` — a parent centered over the block its children occupy.
|
|
714
|
-
*
|
|
715
|
-
* The hierarchy layout: org charts, file trees, decision trees. Depth is the
|
|
716
|
-
* rank axis, so every child sits one band past its parent, and a subtree is
|
|
717
|
-
* packed as a solid block so two siblings' descendants never interleave.
|
|
718
|
-
*
|
|
719
|
-
* A graph that is not a tree still lays out. Roots are the nodes nothing points
|
|
720
|
-
* at, in graph order; a node reachable from two parents belongs to whichever
|
|
721
|
-
* the depth-first walk reaches first; and anything the walk never reaches — a
|
|
722
|
-
* disconnected part, or a pure cycle — becomes a root of its own. So the answer
|
|
723
|
-
* is always a forest, and which forest depends only on the node order.
|
|
724
|
-
*
|
|
725
|
-
* The block packing is deliberately not Reingold–Tilford: it never slides a
|
|
726
|
-
* deep subtree under a shallow neighbor's overhang. That costs horizontal
|
|
727
|
-
* space and buys an invariant an author can rely on — a subtree's extent is a
|
|
728
|
-
* rectangle, and dragging one never lands it inside another.
|
|
729
|
-
*/
|
|
730
|
-
|
|
731
789
|
interface Forest {
|
|
732
790
|
roots: GraphNode[];
|
|
733
791
|
children: Map<string, GraphNode[]>;
|
|
@@ -767,4 +825,4 @@ declare function rayHit(poly: readonly number[], center: {
|
|
|
767
825
|
*/
|
|
768
826
|
declare function portsOnOutline(ports: readonly Port[], outline: Outline, bounds: Bounds): Port[];
|
|
769
827
|
|
|
770
|
-
export {
|
|
828
|
+
export { contentBox as $, type GroupInset as A, type BodySpec as B, COMPASS as C, type DiagramNodeLike as D, type LayoutAxes as E, type ForceOptions as F, type GraphSource as G, type LayoutDirection as H, type RowBox as I, type RowNodeData as J, type RowPort as K, type LayoutFn as L, type MeasureRowText as M, type RowPortBox as N, type Outline as O, type Port as P, type RowTextStyle as Q, type Row as R, type Slot as S, axesFor as T, backEdges as U, bodyOutline as V, bodyTrait as W, boxForContent as X, buildBody as Y, buildGraph as Z, canvasMeasure as _, type PortsOptions as a, createDiagramNodes as a0, dataKeyReader as a1, diagramNodeOf as a2, expandGroupEdges as a3, force as a4, forceRelaxation as a5, forestOf as a6, graphOrder as a7, insetOf as a8, isPinned as a9, layered as aa, layoutBody as ab, layoutRowPorts as ac, measureBody as ad, outlinePath as ae, outlinePolyline as af, packAcross as ag, packClusters as ah, pinnedSet as ai, portOf as aj, portsOf as ak, portsOnOutline as al, rankGaps as am, ranksOf as an, rayHit as ao, seededOrder as ap, settle as aq, sizeToBody as ar, translated as as, tree as at, extent as au, type DiagramNodeReader as b, type PortAnchor as c, type DiagramGroup as d, type PortSpec as e, type DiagramNode as f, type LayoutOptions as g, type LayoutResult as h, type Graph as i, type BodyFloor as j, type BodyNodeSpec as k, type Bounds as l, type BuildBodyOptions as m, type BuildGraphOptions as n, DEFAULT_GROUP_INSET as o, DEFAULT_NODE_GAP as p, DEFAULT_PORTS as q, DEFAULT_RANK_GAP as r, DIAGRAM_TRAIT_KEY as s, type DiagramNodeEntry as t, type ForceBody as u, type ForceRelaxation as v, type GraphEdge as w, type GraphGroup as x, type GraphNode as y, type GraphNodeLike as z };
|
package/dist/layout.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { COMPASS, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, axesFor, backEdges, bodyOutline, boxForContent, buildBody, canvasMeasure, contentBox, extent, force, forceRelaxation, forestOf, graphOrder, layered, layoutBody, layoutRowPorts, measureBody, outlinePath, outlinePolyline, packAcross, pinnedSet, portOf, portsOf, portsOnOutline, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree } from './chunk-
|
|
1
|
+
export { COMPASS, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, axesFor, backEdges, bodyOutline, boxForContent, buildBody, canvasMeasure, contentBox, extent, force, forceRelaxation, forestOf, graphOrder, layered, layoutBody, layoutRowPorts, measureBody, outlinePath, outlinePolyline, packAcross, pinnedSet, portOf, portsOf, portsOnOutline, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree } from './chunk-ERT3XWBS.js';
|
|
2
2
|
//# sourceMappingURL=layout.js.map
|
|
3
3
|
//# sourceMappingURL=layout.js.map
|