@weasel-js/diagram 1.4.3 → 1.4.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _weasel_js_core from '@weasel-js/core';
2
- import { Path, Vec2, PoseProjection } from '@weasel-js/core';
2
+ import { Path, Vec2, PoseProjection, DerivedDep, SceneRegistry, ChromeState, FillStyle, Stroke, CursorSpec, Affordance, InvocationCtx, GestureBinding, Action, Contribution, RenderLayer, SimulationNode, createSimulation, EasingFn, NodeId, Scene, NodeShapeEntry } from '@weasel-js/core';
3
3
 
4
4
  /**
5
5
  * The shapes a built body can wear, as paths in the node's bounds.
@@ -23,6 +23,32 @@ type Outline = 'rect' | 'diamond' | 'stadium' | 'parallelogram' | Path;
23
23
  /** The path `outline` describes inside `bounds`. A zero-area box gives a
24
24
  * zero-area rect rather than a degenerate polygon. */
25
25
  declare function outlinePath(outline: Outline, bounds: Bounds): Path;
26
+ /**
27
+ * The largest axis-aligned box a shape's content can occupy without leaving
28
+ * the shape. Rows lay out in here, not in the bounds — a diamond's corners and
29
+ * a parallelogram's lean are outside its own box, and a label placed against
30
+ * the bounding box lands there and gets clipped away by the silhouette.
31
+ *
32
+ * Conservative rather than exact: the inscribed rect of a rhombus is exact,
33
+ * and a stadium reports the flat span between its rounded ends, which gives up
34
+ * a little height near them.
35
+ */
36
+ declare function contentBox(outline: Outline, bounds: Bounds): Bounds;
37
+ /**
38
+ * The box whose {@link contentBox} is at least `content` — the inverse, for
39
+ * sizing a node to what its rows measured.
40
+ *
41
+ * The stadium case is the only inexact one: its inset depends on the height it
42
+ * is solving for, so it assumes the wide orientation and adds one height's
43
+ * worth of end caps.
44
+ */
45
+ declare function boxForContent(outline: Outline, content: {
46
+ width: number;
47
+ height: number;
48
+ }): {
49
+ width: number;
50
+ height: number;
51
+ };
26
52
 
27
53
  /**
28
54
  * Where a port sits on its node, in **normalized bounds coordinates**:
@@ -62,6 +88,10 @@ interface DiagramNode {
62
88
  ports?: readonly PortSpec[];
63
89
  /** Layout must not move this node. */
64
90
  pinned?: boolean;
91
+ /** What the node is *drawn* as. Present on a built body; absent on a
92
+ * participant that already had a look of its own — a text block, an image,
93
+ * a path — which keeps whatever painter it already matched. */
94
+ outline?: Outline;
65
95
  }
66
96
  /** A port resolved against a node's current pose: where it is in world
67
97
  * coordinates, and which way an edge leaves it. */
@@ -132,6 +162,20 @@ interface BodySpec {
132
162
  /** Space between adjacent rows. Default 4. */
133
163
  gap?: number;
134
164
  }
165
+ /** What {@link buildBody} emits: the `AddNodeSpec` fields it fills in. Named
166
+ * structurally rather than imported so a consumer whose scene is typed
167
+ * differently can still spread one. */
168
+ interface BodyNodeSpec<TData, TLayer extends string, TPose> {
169
+ kind: 'container' | 'leaf';
170
+ layer: TLayer;
171
+ pose: TPose;
172
+ data: TData;
173
+ id?: string;
174
+ parent?: string;
175
+ /** Set `false` on every leaf the builder emits: the innermost hit wins, so a
176
+ * row that answers a press is a body that cannot be dragged. */
177
+ pickable?: boolean;
178
+ }
135
179
  /** The floor a body's content puts under its node's size. */
136
180
  interface BodyFloor {
137
181
  minWidth: number;
@@ -143,6 +187,15 @@ interface RowBox extends Bounds {
143
187
  row: Row;
144
188
  index: number;
145
189
  }
190
+ /** Where one row port's label sits, in the same frame as the row box it was
191
+ * laid out in. Its vertical center is also where the port itself attaches, so
192
+ * a label and the port it names cannot drift apart. */
193
+ interface RowPortBox extends Bounds {
194
+ port: RowPort;
195
+ side: 'left' | 'right';
196
+ /** Position within its own side, top to bottom. */
197
+ index: number;
198
+ }
146
199
  /** Adapts the kit's `measureText` for a caller holding a 2D context. Pass the
147
200
  * same context the renderer measures with, or the floor will not match what
148
201
  * is painted. */
@@ -167,6 +220,18 @@ declare function sizeToBody<TPose extends Bounds>(pose: TPose, floor: BodyFloor)
167
220
  * measured heights; the leftover goes unclaimed at the bottom rather than
168
221
  * being distributed, so a row does not move when a sibling grows. */
169
222
  declare function layoutBody(spec: BodySpec, bounds: Bounds, measure?: MeasureRowText): RowBox[];
223
+ /**
224
+ * Where each of a `ports` row's ports goes inside `box`, on the same terms
225
+ * `layoutBody` gives one box per row.
226
+ *
227
+ * Both sides are cut into the same number of slots — the longer side's count —
228
+ * so the nth input faces the nth output whichever side has more. A label box
229
+ * is as wide as its own text and pinned to its own edge of the row; an
230
+ * unlabeled port gets a zero-width box there, which is still where it attaches.
231
+ */
232
+ declare function layoutRowPorts(row: Extract<Row, {
233
+ kind: 'ports';
234
+ }>, box: Bounds, measure?: MeasureRowText): RowPortBox[];
170
235
  /**
171
236
  * The `DiagramNode` trait a built body implies: perimeter ports for the
172
237
  * outline, plus one port per `ports`-row entry, anchored to that row's own
@@ -174,11 +239,57 @@ declare function layoutBody(spec: BodySpec, bounds: Bounds, measure?: MeasureRow
174
239
  *
175
240
  * Anchors are normalized against `bounds`, so they survive the node being
176
241
  * resized — which is the whole reason `PortAnchor` is normalized.
242
+ *
243
+ * **A row port on a side takes that side's compass default with it.** Both sit
244
+ * on the same edge, and a row near the vertical middle puts one exactly on top
245
+ * of `w` or `e` — where the later region wins the hit and the other is
246
+ * grabbable nowhere. A body that declares where its inputs attach has said what
247
+ * that side is for.
177
248
  */
178
249
  declare function bodyTrait(spec: BodySpec, bounds: Bounds, measure?: MeasureRowText): DiagramNode;
179
250
  /** The outline path for a built body at `bounds` — what paints it, and what a
180
251
  * perimeter-hugging port will eventually be placed on. */
181
252
  declare function bodyOutline(spec: BodySpec, bounds: Bounds): _weasel_js_core.Path;
253
+ /** What a built body's rows are handed to, one call per row.
254
+ *
255
+ * `text` is the row's content already formatted — a `label`'s own text, or a
256
+ * `field`'s `"label: value"` — so the common case needs no discrimination.
257
+ * Return `null` for a row the consumer paints itself; `ports` and `slot` rows
258
+ * arrive with `text` empty and are the usual ones to decline. */
259
+ type RowNodeData<TData> = (text: string, box: RowBox) => TData | null;
260
+ interface BuildBodyOptions<TData, TLayer extends string, TPose extends Bounds> {
261
+ layer: TLayer;
262
+ /** The container's id. Edges name it, so pass one for anything an edge or a
263
+ * layout `pin` will refer to. */
264
+ id?: string;
265
+ measure?: MeasureRowText;
266
+ /** The container's own data, given the trait the builder computed and the
267
+ * pose it was computed against. */
268
+ body: (trait: DiagramNode, pose: TPose) => TData;
269
+ /** One row's data, or `null` to leave that row undrawn. */
270
+ row: RowNodeData<TData>;
271
+ /** One row port's label, or `null` to leave it undrawn. Ports with no
272
+ * `label` are never offered. Omit to draw none — a body whose ports are
273
+ * named only for the width they claim needs nothing here. */
274
+ portLabel?: (text: string, box: RowPortBox) => TData | null;
275
+ }
276
+ /**
277
+ * The scene nodes a built body is: one container carrying the trait, and one
278
+ * leaf per row that wants drawing.
279
+ *
280
+ * Rows are ordinary scene nodes rather than something this package paints, so
281
+ * the kit's own text painter draws them and text editing, styling and
282
+ * selection work on them unchanged. The walk that places them is
283
+ * `layoutBody` — the same one `bodyTrait` anchors its row ports against, which
284
+ * is why a label and its ports cannot drift apart.
285
+ *
286
+ * The pose is `sizeToBody(at, measureBody(spec))`: the body measures a floor
287
+ * and the authored size is grown to clear it, never shrunk.
288
+ */
289
+ declare function buildBody<TData, TLayer extends string, TPose extends Bounds>(spec: BodySpec, at: TPose, opts: BuildBodyOptions<TData, TLayer, TPose>): {
290
+ pose: TPose;
291
+ specs: BodyNodeSpec<TData, TLayer, TPose>[];
292
+ };
182
293
 
183
294
  /**
184
295
  * Reading the `DiagramNode` trait off a scene node.
@@ -220,7 +331,12 @@ interface DiagramNodeEntry {
220
331
  * node's own data — a pipeline stage whose ports come from its rows. */
221
332
  trait: DiagramNode | ((data: unknown) => DiagramNode);
222
333
  }
223
- /** The trait on the node's own `data.diagram`, or `null`. */
334
+ /** The trait on the node's own `data.diagram`, or `null`.
335
+ *
336
+ * An edge's trait lives under the same key, so `from` and `to` are what tells
337
+ * the two apart. Without that test an edge reads as a participant declaring
338
+ * no ports, and collects the four defaults on the degenerate pose an edge
339
+ * carries — four grabbable ports in the middle of nowhere. */
224
340
  declare const dataKeyReader: DiagramNodeReader;
225
341
  /**
226
342
  * A reader over declared kinds. A trait on the node's own data still wins, so
@@ -305,4 +421,985 @@ declare function portsOf<TPose>(node: DiagramNodeLike, pose: TPose, opts?: Ports
305
421
  /** One named port, or `undefined` when the node has no such port. */
306
422
  declare function portOf<TPose>(node: DiagramNodeLike, pose: TPose, portId: string, opts?: PortsOptions<TPose>): Port | undefined;
307
423
 
308
- export { type BodyFloor, type BodySpec, type Bounds, COMPASS, DEFAULT_PORTS, DIAGRAM_TRAIT_KEY, type DiagramNode, type DiagramNodeEntry, type DiagramNodeLike, type DiagramNodeReader, type MeasureRowText, type Outline, type Port, type PortAnchor, type PortSpec, type PortsOptions, type Row, type RowBox, type RowPort, type RowTextStyle, bodyOutline, bodyTrait, canvasMeasure, createDiagramNodes, dataKeyReader, diagramNodeOf, isPinned, layoutBody, measureBody, outlinePath, portOf, portsOf, sizeToBody };
424
+ /**
425
+ * `DiagramEdge` — a connection between two participants.
426
+ *
427
+ * An edge is an ordinary leaf scene node with `dependsOn: [from, to]` and a
428
+ * `derivePath` that runs a **router**. Making it a scene node rather than a
429
+ * plugin-owned render layer is the load-bearing choice: it buys selection,
430
+ * hit-testing, hover, styling, z-order, SVG export, undo and copy/paste
431
+ * without implementing any of them. The cost is that an edge costs what a node
432
+ * costs — right for diagrams of tens to hundreds of edges, wrong for a
433
+ * 10k-edge force graph, which keeps its render layer.
434
+ *
435
+ * **Author-dragged waypoints are data on the edge and the router routes
436
+ * through them.** Manual control never authors the path; it authors
437
+ * constraints on the path. That is what keeps derived geometry and hand-tuning
438
+ * compatible — a moved endpoint still re-routes, through the waypoints the
439
+ * author put there.
440
+ *
441
+ * Arrowheads are not here. They are stroke markers, the same
442
+ * `markerStart` / `markerEnd` any other stroke carries.
443
+ */
444
+
445
+ /** Where an edge attaches at one end. */
446
+ interface EdgeEnd {
447
+ /** The port to leave from, when the endpoint declares one. Falls back to
448
+ * the nearest port, so an edge drawn before ports were thought about still
449
+ * meets the shape. */
450
+ port?: string;
451
+ }
452
+ /** The trait on an edge's `data`, under the same key participants use. */
453
+ interface DiagramEdge {
454
+ from: EdgeEnd;
455
+ to: EdgeEnd;
456
+ /** Registry key for the router. Resolved by whoever builds the edge's
457
+ * `derivePath`; unknown keys fall back to `straight`. */
458
+ router?: string;
459
+ /** Points in world coordinates the route must pass through, in order. The
460
+ * author owns these; the router owns everything between them. */
461
+ waypoints?: readonly Vec2[];
462
+ }
463
+ /** What a router is given: the two resolved ends, and the author's waypoints
464
+ * between them. */
465
+ interface RouteRequest {
466
+ from: Port;
467
+ to: Port;
468
+ waypoints: readonly Vec2[];
469
+ }
470
+ /** A router returns the points the edge runs through, `from.point` first and
471
+ * `to.point` last. Turning those into a `Path` is `edgePath`'s job, so a
472
+ * router never has to know about path encodings. */
473
+ type Router = (req: RouteRequest) => readonly Vec2[];
474
+ /** The straight run between the two ends, through any waypoints. */
475
+ declare const straight: Router;
476
+ /**
477
+ * An axis-aligned route: leave along the departing end's normal and arrive
478
+ * against the receiving one's.
479
+ *
480
+ * A leg is one elbow where the two ends face different axes and two where they
481
+ * face the same one, turning at the midpoint of the axis they share. Arriving
482
+ * along the port's own facing is what makes the arrowhead point *into* the
483
+ * shape — a route that lands on a west-facing port from above puts its marker
484
+ * across the corner, which reads as aimed at nothing.
485
+ */
486
+ declare const orthogonal: Router;
487
+ /**
488
+ * A smooth route, sampled into points.
489
+ *
490
+ * Each leg is a cubic that leaves along the departing end's normal and arrives
491
+ * against the receiving one's, which is what makes a bezier edge read as
492
+ * plugged into its port rather than aimed at it. Sampled rather than emitted
493
+ * as curve commands so every router returns the same thing; `edgePath` is free
494
+ * to emit curves later without changing the contract.
495
+ */
496
+ declare const bezier: Router;
497
+ /** The routers this package ships. A consumer's own go in the same shape. */
498
+ declare const ROUTERS: Readonly<Record<string, Router>>;
499
+ /** The edge trait on a node's data, or `null`. */
500
+ declare function diagramEdgeOf(node: {
501
+ data: unknown;
502
+ }): DiagramEdge | null;
503
+ /**
504
+ * The port an end resolves to: the one it named, else the port facing the
505
+ * other end most directly.
506
+ *
507
+ * "Facing" rather than "nearest": a port on the far side of a wide node can be
508
+ * closer in a straight line than the one pointing at the target, and an edge
509
+ * that leaves through its own node reads as a bug however short it is.
510
+ */
511
+ declare function resolveEnd<TPose>(end: EdgeEnd, dep: DerivedDep<TPose>, toward: Vec2, opts: PortsOptions<TPose>, allPorts: (d: DerivedDep<TPose>) => Port[]): Port | undefined;
512
+ interface EdgeRouteOptions<TPose> extends PortsOptions<TPose> {
513
+ /** Routers by key. Defaults to {@link ROUTERS}. */
514
+ routers?: Readonly<Record<string, Router>>;
515
+ }
516
+ /**
517
+ * A `derivePath` that routes an edge between its two dependencies.
518
+ *
519
+ * Returns `null` — nothing to draw — when the node is not an edge, when either
520
+ * dependency is gone, or when an endpoint offers no ports at all. A dangling
521
+ * edge painting a line to the origin is worse than a dangling edge painting
522
+ * nothing.
523
+ */
524
+ declare function edgeDerivePath<TPose>(opts?: EdgeRouteOptions<TPose>): (node: {
525
+ data: unknown;
526
+ }, deps: readonly (DerivedDep<TPose> | undefined)[]) => Path | null;
527
+ /** Registry key for {@link EDGE_DERIVE_PATH}. */
528
+ declare const DIAGRAM_EDGE = "diagram:edge";
529
+ /** The default edge router, as one stable function reference so `toJSON` can
530
+ * find its key. A consumer wanting different options registers their own
531
+ * `edgeDerivePath(...)` under a key of their choosing. */
532
+ declare const EDGE_DERIVE_PATH: (node: {
533
+ data: unknown;
534
+ }, deps: readonly (DerivedDep<unknown> | undefined)[]) => Path | null;
535
+ /** Merge this package's registry entries under a consumer's, so an edge
536
+ * round-trips through `toJSON` without the consumer wiring the key. */
537
+ declare function withDiagramRegistry<TPose>(registry?: SceneRegistry<TPose>): SceneRegistry<TPose>;
538
+
539
+ /**
540
+ * Ports as affordances — what makes a painted port a grabbable one.
541
+ *
542
+ * A port is declared as an `AffordanceRegion`, so the kit's own region walk
543
+ * supplies the hit-test, the screen-pixel hit radius, the cursor and the
544
+ * exclusive claim that keeps a port drag out of the select tool's hands. That
545
+ * is the "visible chrome is always hittable" rule doing the work: the walk runs
546
+ * on every pointerdown before any tool sees the event, so a port answers
547
+ * whatever tool happens to be active.
548
+ *
549
+ * Two things about this are easy to get wrong, and both fail silently.
550
+ *
551
+ * **`targetId` is `null`.** The framework composes a target's rotation about
552
+ * its bounds center for any region naming one, and `portsOf` has already
553
+ * rotated the port. Naming the participant here would rotate it twice.
554
+ *
555
+ * **`hitKind` is not set.** On the registered-layer route the discriminator is
556
+ * the layer's own id — `<SceneCanvas>` stamps the hit `layer:<RenderLayer.id>`
557
+ * and carries `initialScratch` through as `payload`. A `hitKind` declared here
558
+ * would be dropped, and a binding written against it would never match.
559
+ */
560
+
561
+ /** The layer id this package composes its port regions into. */
562
+ declare const PORT_LAYER_ID = "diagram-ports";
563
+ /** The affordance kind a press on a port reports — what a binding's
564
+ * `target: 'affordance:...'` has to name. `<SceneCanvas>` builds it from
565
+ * {@link PORT_LAYER_ID}, so the two cannot drift. */
566
+ declare const PORT_AFFORDANCE_KIND: "layer:diagram-ports";
567
+ /** A participant and the pose it is **painted** at. `effectivePose`, not
568
+ * `node.pose` — a port that answers from the document pose while its node is
569
+ * mid-drag sits somewhere the user can see the node is not. */
570
+ interface ParticipantPose<TPose> {
571
+ node: DiagramNodeLike;
572
+ pose: TPose;
573
+ }
574
+ /** Where the affordance gets its participants. A thunk rather than a scene so
575
+ * the geometry is testable without one, and so a consumer whose participants
576
+ * come from somewhere else needs no adapter. */
577
+ type ParticipantSource<TPose> = () => Iterable<ParticipantPose<TPose>>;
578
+ /** What a press on a port hands the action that picks up the drag. Arrives as
579
+ * `InvocationCtx.drag.affordance.payload`. */
580
+ interface PortScratch {
581
+ /** `CommonAffordanceScratch.targetId` — becomes `AffordanceHit.targetIds`. */
582
+ targetId: string;
583
+ nodeId: string;
584
+ portId: string;
585
+ /** The port resolved at press time: where it was and which way it faces. */
586
+ port: Port;
587
+ }
588
+ interface PortAffordanceOptions<TPose> {
589
+ /** Also the chrome-caps visibility id. Default {@link PORT_LAYER_ID}. */
590
+ id?: string;
591
+ read?: DiagramNodeReader;
592
+ geometry?: PoseProjection<TPose>;
593
+ /** Which participants show their ports. Default: all of them. Gate on
594
+ * selection or hover by reading `state`. */
595
+ shows?: (node: DiagramNodeLike, state: ChromeState) => boolean;
596
+ /** Screen-pixel hit radius. Default 8, the kit's `HANDLE_BASE_PX`. */
597
+ hitRadiusPx?: number;
598
+ /** Screen-pixel side of the painted square. Default 7. Omit `paint` entirely
599
+ * by passing 0 — a port the consumer draws itself is still hittable. */
600
+ sizePx?: number;
601
+ fill?: FillStyle;
602
+ stroke?: Stroke;
603
+ cursor?: CursorSpec;
604
+ /** Default `'exclusive'` on pointer gestures: a drag from a port is a
605
+ * connect, never a move of the node underneath it. */
606
+ strength?: 'exclusive' | 'shared';
607
+ }
608
+ /** Read a port press's scratch back off the hit an action was handed. */
609
+ declare function portScratchOf(hit: {
610
+ payload?: unknown;
611
+ } | null | undefined): PortScratch | null;
612
+ /**
613
+ * The affordance. Compose it into a layer with `composeAffordanceLayer` and
614
+ * attach that with `CanvasExtensionApi.registerLayer` — the only attach route
615
+ * that is hit-tested. A `Contribution.overlay` is painted and never hit.
616
+ */
617
+ declare function createPortAffordance<TPose>(participants: ParticipantSource<TPose>, opts?: PortAffordanceOptions<TPose>): Affordance;
618
+
619
+ /**
620
+ * The connect gesture — dragging one port onto another to author an edge.
621
+ *
622
+ * Connect is an ordinary drag binding, not a mode and not a tool of its own:
623
+ * the binding is gated on a press landing on a port affordance, so a connect
624
+ * starts from whatever tool happens to be active. The gate lives in the
625
+ * binding's `target`, which is what lets a press that is *not* on a port fall
626
+ * through to the tool that wants it instead of being swallowed here.
627
+ *
628
+ * **Where validity is decided.** The two halves are decided in different
629
+ * places, and conflating them is the mistake this package is written to avoid:
630
+ *
631
+ * - *Which presses start a connect* is routing, and belongs to the binding
632
+ * spec. `connectBinding` names the port layer's affordance kind; nothing in
633
+ * the action body re-checks what was pressed.
634
+ * - *Which ports a live connect may land on* cannot be routing — the
635
+ * dispatcher never re-reads the affordance under a moving pointer, so a
636
+ * spec has no vocabulary for it. It is decided by `canConnect` filtering
637
+ * the **candidate set**: an illegal port is never a candidate, so the
638
+ * preview will not snap to it and a release over it commits nothing. That
639
+ * is a narrower thing than an action body that inspects a hit and bails.
640
+ *
641
+ * One resolver answers both the live preview and the commit, for the reason
642
+ * `insertAction` does the same: a preview computed separately from the commit
643
+ * is a preview that can lie about what releasing will do.
644
+ */
645
+
646
+ /** The default action id, and what {@link connectBinding} points at. */
647
+ declare const CONNECT_ACTION_ID = "diagram.connect";
648
+ /** Whether an edge may run from `from` to `to`. */
649
+ type CanConnect = (from: Port, to: Port) => boolean;
650
+ /** The edge a finished connect describes. Both ends are resolved ports, so a
651
+ * consumer minting the node has nothing left to look up. */
652
+ interface PendingEdge {
653
+ from: Port;
654
+ to: Port;
655
+ /** Registry key for the router the new edge should name. */
656
+ router: string;
657
+ }
658
+ /**
659
+ * The shipped policy: a port may not join itself, and two ports that both
660
+ * declare a `type` must declare the same one.
661
+ *
662
+ * A port with no type joins anything — `PortSpec.type` is uninterpreted by this
663
+ * package, so an untyped diagram stays fully connectable without opting out.
664
+ * Two ports on the *same node* may be joined: a self-edge between two different
665
+ * ports is meaningful in a state machine, and refusing it here would be policy
666
+ * masquerading as a rule.
667
+ */
668
+ declare const defaultCanConnect: CanConnect;
669
+ interface ConnectActionOptions<TPose> {
670
+ /** Where the droppable ports come from. The same source the port affordance
671
+ * reads, so what is grabbable and what is landable cannot disagree. */
672
+ participants: ParticipantSource<TPose>;
673
+ /** Default {@link CONNECT_ACTION_ID}. */
674
+ id?: string;
675
+ read?: DiagramNodeReader;
676
+ geometry?: PoseProjection<TPose>;
677
+ /** Default {@link defaultCanConnect}. */
678
+ canConnect?: CanConnect;
679
+ /** How near the pointer must come to a port, in world units, before the
680
+ * edge snaps to it. Default 16. */
681
+ snapRadius?: number;
682
+ /** Registry key the new edge names, and the router the preview draws with.
683
+ * Default `'straight'`. */
684
+ router?: string;
685
+ routers?: Readonly<Record<string, Router>>;
686
+ /** How the in-flight edge is painted. */
687
+ stroke?: Stroke;
688
+ /**
689
+ * Mints and commits the edge. Called once, on release over a valid port.
690
+ *
691
+ * Defaults to {@link commitEdgeToScene} against `deps.scene`, which is what
692
+ * makes the gesture work with nothing wired. Override it the way a consumer
693
+ * overrides the kit's `insert` dep — to choose the edge node's data, its
694
+ * layer, or its stroke.
695
+ */
696
+ commit?: (edge: PendingEdge, ctx: InvocationCtx) => void;
697
+ }
698
+ /** What a connect-authored edge is drawn with, when the consumer overrides
699
+ * nothing. */
700
+ declare const DEFAULT_EDGE_STROKE: Stroke;
701
+ /**
702
+ * The binding that starts a connect: a drag whose press landed on the port
703
+ * layer's own chrome.
704
+ *
705
+ * The string form of `target` rather than a predicate, because it ranks higher
706
+ * in specificity — a bare `{ kind: 'drag' }` on the select tool cannot outrank
707
+ * it, so no tool needs a predicate of its own to keep out of the way.
708
+ */
709
+ declare function connectBinding(actionId?: string): GestureBinding;
710
+ /** The action id that absorbs a press on a port. */
711
+ declare const GRAB_PORT_ACTION_ID = "diagram.grabPort";
712
+ /**
713
+ * Absorbs a press on a port, and does nothing else.
714
+ *
715
+ * A port claims the press **protocol**, not one gesture in it: `pointerDown`,
716
+ * `click` and `drag` are a single claimable `'pointer'` token, and an exclusive
717
+ * claim bars every binding whose target does not consult the affordance. So a
718
+ * bundle that binds only `drag` leaves the pointerdown with nowhere to go, and
719
+ * the dispatcher drops the press — the drag it would have grown into never
720
+ * happens. Absorbing the press is also the behavior you want on its own: a
721
+ * press on a port should not pick or deselect the node underneath it.
722
+ */
723
+ declare function grabPortAction(id?: string): Action;
724
+ /** The two bindings that keep a port's claim from dropping the press. */
725
+ declare function grabPortBindings(actionId?: string): GestureBinding[];
726
+ /**
727
+ * The default commit: one leaf node carrying the edge trait, added to
728
+ * `deps.scene`. One `add` is one history entry, so the edge undoes in one.
729
+ *
730
+ * The edge lands on the **source node's** layer rather than the bottom one: an
731
+ * edge between two nodes on a `wires` layer belongs on `wires`, and picking the
732
+ * first layer in the stack would quietly move it somewhere the author did not
733
+ * put its endpoints.
734
+ */
735
+ declare function commitEdgeToScene(edge: PendingEdge, ctx: InvocationCtx): void;
736
+ /**
737
+ * The `diagram.connect` action: an ongoing drag whose preview is an ephemeral
738
+ * edge and whose commit is one op batch.
739
+ *
740
+ * A factory rather than a singleton because every input it needs — which nodes
741
+ * are droppable, what counts as a legal pair, how an edge node is shaped — is
742
+ * the consumer's, and this package has no scene at module scope. Same reason
743
+ * `edgeDerivePath` and `diagramShape` are factories.
744
+ */
745
+ declare function createConnectAction<TPose>(opts: ConnectActionOptions<TPose>): Action;
746
+
747
+ /**
748
+ * The plugin as a `Contribution` bundle — one object a consumer hands to
749
+ * `<SceneCanvas ambient={…}>`.
750
+ *
751
+ * **Ambient, not a tool.** Connect is not a mode the author enters: a port is
752
+ * grabbable whatever tool is active, which is the same reason `@weasel-js/hud`
753
+ * ships `eligibility: { claimed: true }`. `claimed` is what makes the bundle
754
+ * live for a press the port layer's affordance claimed, and dormant otherwise.
755
+ *
756
+ * **The layer is not in here, on purpose.** `Contribution.overlay` is painted
757
+ * and never hit-tested — only `CanvasExtensionApi.registerLayer` gets a
758
+ * `hitTest` call. Shipping the port layer as an overlay would paint every port
759
+ * and make none of them grabbable, which is exactly the state this arc started
760
+ * from. {@link diagramPorts} hands back the bundle and the layer together so
761
+ * the two cannot be wired half-way.
762
+ */
763
+
764
+ interface DiagramContributionOptions<TPose> extends ConnectActionOptions<TPose>, PortAffordanceOptions<TPose> {
765
+ }
766
+ /** The bundle: the connect action and the one binding that reaches it. */
767
+ declare function createDiagramContribution<TPose>(opts: DiagramContributionOptions<TPose>): Contribution;
768
+ /**
769
+ * Both halves of the port affordance: the layer to `registerLayer`, and the
770
+ * contribution to pass as `ambient`.
771
+ *
772
+ * They are returned together because attaching one without the other fails
773
+ * quietly — the layer alone paints ports that start no gesture, and the
774
+ * contribution alone binds an affordance kind nothing ever reports.
775
+ */
776
+ declare function diagramPorts<TPose>(opts: DiagramContributionOptions<TPose>): {
777
+ layer: RenderLayer<unknown>;
778
+ contribution: Contribution;
779
+ };
780
+
781
+ /**
782
+ * `Graph` — the adjacency index a layout runs over.
783
+ *
784
+ * The scene is the document, so there is no second graph kept in sync with it:
785
+ * this one is built from the scene on demand, used, and thrown away. Rebuilding
786
+ * per layout invocation is deliberate — a maintained index is a cache to
787
+ * invalidate on every add, delete, reparent and `dependsOn` edit, and layout is
788
+ * not on the hot path. Measure before changing that.
789
+ *
790
+ * A node's *identity* is its id and its *size* is its bounds; where it currently
791
+ * sits is here too, because a re-layout that ignores where things already are
792
+ * scrambles a diagram the author has arranged.
793
+ */
794
+
795
+ /** A scene node as the graph reads it. `dependsOn` is what an edge's two
796
+ * endpoints are: the edge trait names *ports*, and the nodes those ports are
797
+ * on are the dependencies the derivation already runs on. */
798
+ interface GraphNodeLike extends DiagramNodeLike {
799
+ dependsOn?: readonly string[] | 'children';
800
+ }
801
+ /** Where the graph gets its nodes. The same thunk shape the port affordance
802
+ * takes, so one source answers both. */
803
+ type GraphSource<TPose> = () => Iterable<{
804
+ node: GraphNodeLike;
805
+ pose: TPose;
806
+ }>;
807
+ /** One participant. */
808
+ interface GraphNode {
809
+ id: string;
810
+ /** Where it is now, in world coordinates. */
811
+ bounds: Bounds;
812
+ /** Layout must not move it. */
813
+ pinned: boolean;
814
+ }
815
+ /** One connection. `id` is the edge node's own id, so a caller can go back to
816
+ * the scene for its trait. */
817
+ interface GraphEdge {
818
+ id: string;
819
+ from: string;
820
+ to: string;
821
+ }
822
+ /**
823
+ * Nodes, edges, and the two adjacency reads a layout needs.
824
+ *
825
+ * Every list is in source order — render order, when the source is a scene —
826
+ * which is what a layout's tiebreaks resolve against. There is no RNG anywhere
827
+ * downstream of this, and this is where that starts.
828
+ */
829
+ interface Graph {
830
+ readonly nodes: readonly GraphNode[];
831
+ readonly edges: readonly GraphEdge[];
832
+ node(id: string): GraphNode | undefined;
833
+ /** Edges leaving `id`, in source order. */
834
+ outgoing(id: string): readonly GraphEdge[];
835
+ /** Edges arriving at `id`, in source order. */
836
+ incoming(id: string): readonly GraphEdge[];
837
+ }
838
+ interface BuildGraphOptions<TPose> {
839
+ read?: DiagramNodeReader;
840
+ geometry?: PoseProjection<TPose>;
841
+ }
842
+ /**
843
+ * Read a graph out of a source of scene nodes.
844
+ *
845
+ * A node is an edge if it carries the edge trait and names exactly two
846
+ * dependencies; a participant if the reader hands back a `DiagramNode`;
847
+ * neither, and it is not in the diagram at all. An edge whose endpoints are not
848
+ * both participants is dropped — a dangling edge should not be ranking
849
+ * anything.
850
+ */
851
+ declare function buildGraph<TPose>(source: GraphSource<TPose>, opts?: BuildGraphOptions<TPose>): Graph;
852
+
853
+ /**
854
+ * A label on an edge: an ordinary leaf node that derives its *pose* from the
855
+ * route it sits on.
856
+ *
857
+ * It reads the edge's resolved path off its dependency rather than routing
858
+ * again, so the label and the arrowhead can never disagree about where the
859
+ * edge went. Everything else about it is a normal scene node — its own size,
860
+ * data, styling, z-order and selection.
861
+ */
862
+
863
+ /** Where along an edge a label sits. */
864
+ interface DiagramLabel {
865
+ /** `'start'`, `'mid'` and `'end'` are 0, 0.5 and 1; a number is that
866
+ * fraction of the route's length. */
867
+ at: 'start' | 'mid' | 'end' | number;
868
+ /** World units perpendicular to the route, to the **left of the direction of
869
+ * travel** — above a left-to-right edge. Default 0, which centers the label
870
+ * on the line. */
871
+ offset?: number;
872
+ }
873
+ /** The label trait on a node's data, or `null`. */
874
+ declare function diagramLabelOf(node: {
875
+ data: unknown;
876
+ }): DiagramLabel | null;
877
+ interface LabelPoseOptions<TPose> {
878
+ geometry?: PoseProjection<TPose>;
879
+ }
880
+ /**
881
+ * A `derivePose` that stations a label along its dependency's path.
882
+ *
883
+ * Returns `null` — nothing derived, so the node keeps its authored pose —
884
+ * when the node carries no label trait, when the dependency is gone, or when
885
+ * the dependency derives no path. A label that snapped to the origin because
886
+ * its edge was deleted is worse than one that stayed where the author left it.
887
+ */
888
+ declare function labelDerivePose<TPose>(opts?: LabelPoseOptions<TPose>): (node: {
889
+ pose: TPose;
890
+ data: unknown;
891
+ }, deps: readonly (DerivedDep<TPose> | undefined)[]) => TPose | null;
892
+ /** Registry key for {@link LABEL_DERIVE_POSE}. */
893
+ declare const DIAGRAM_LABEL = "diagram:label";
894
+ /** The default label derivation, as one stable function reference so `toJSON`
895
+ * can find its key. */
896
+ declare const LABEL_DERIVE_POSE: (node: {
897
+ pose: unknown;
898
+ data: unknown;
899
+ }, deps: readonly (DerivedDep<unknown> | undefined)[]) => unknown;
900
+
901
+ /**
902
+ * Layout: where the participants go.
903
+ *
904
+ * A layout is a pure function of the graph — no scene, no ops, no history. It
905
+ * hands back the new top-left for every node that **moves**, and a node already
906
+ * standing where the layout wants it is simply absent from the answer. That is
907
+ * what makes re-running a layout free rather than destructive, and it is the
908
+ * property the idempotence test asserts.
909
+ *
910
+ * Three rules keep re-layout non-destructive, and all three live here rather
911
+ * than in each algorithm:
912
+ *
913
+ * - **No RNG anywhere.** Every tiebreak falls back to the graph's own node
914
+ * order, which is the source's order — render order, for a scene.
915
+ * - **Within-rank order is seeded from where the nodes already are**, read
916
+ * off the cross axis, rather than from crossing-minimization. An author who
917
+ * dragged two boxes into an order gets that order back.
918
+ * - **A pin set nothing moves.** Pinned nodes are never in the result, and
919
+ * the rest of the layout is translated to sit around them.
920
+ *
921
+ * `layered` and `tree` are idempotent by construction: the second run reads the
922
+ * order the first run produced and computes the same slots. `force` is an
923
+ * iterative relaxation and makes no such claim — re-running it keeps relaxing.
924
+ */
925
+
926
+ /** Which way the ranks stack. `force` ignores it. */
927
+ type LayoutDirection = 'down' | 'up' | 'right' | 'left';
928
+ interface LayoutOptions {
929
+ /** Default `'down'`. */
930
+ direction?: LayoutDirection;
931
+ /** Between neighbors within a rank, in world units. Default 48. */
932
+ nodeGap?: number;
933
+ /** Between one rank and the next. Default 96. */
934
+ rankGap?: number;
935
+ /** Ids layout must not move, on top of whatever carries `pinned: true`. */
936
+ pin?: Iterable<string>;
937
+ /** How far a node has to move to be worth moving, in world units. Default
938
+ * `1e-6` — a float-noise floor, not a snapping quantum. Raise it to let a
939
+ * `force` pass that has stopped doing anything useful commit nothing. */
940
+ tolerance?: number;
941
+ }
942
+ /** The new top-left for each node that moves. A node that is already there is
943
+ * absent, so an empty result means "nothing to do". */
944
+ type LayoutResult = ReadonlyMap<string, Vec2>;
945
+ /** Every layout in this package has this shape, and so does a consumer's. */
946
+ type LayoutFn = (graph: Graph, opts?: LayoutOptions) => LayoutResult;
947
+ declare const DEFAULT_NODE_GAP = 48;
948
+ declare const DEFAULT_RANK_GAP = 96;
949
+ /** Which world axis carries ranks, which carries within-rank order, and
950
+ * whether ranks grow toward positive world coordinates. */
951
+ interface LayoutAxes {
952
+ rank: 'x' | 'y';
953
+ cross: 'x' | 'y';
954
+ /** `-1` for `'up'` and `'left'`, where the canonical frame is mirrored. */
955
+ sign: 1 | -1;
956
+ }
957
+ declare function axesFor(direction?: LayoutDirection): LayoutAxes;
958
+ /** Every id layout must leave alone. */
959
+ declare function pinnedSet(graph: Graph, opts?: LayoutOptions): Set<string>;
960
+ /**
961
+ * A placement in the canonical `(cross, rank)` frame, before it is turned into
962
+ * world coordinates. `cross` and `rank` are the node's low corner along each.
963
+ */
964
+ interface Slot {
965
+ cross: number;
966
+ rank: number;
967
+ }
968
+ /**
969
+ * Turn canonical slots into the positions to write.
970
+ *
971
+ * Three steps, in order: map the canonical frame onto the world axes the
972
+ * direction names; translate the whole layout so it lands where the graph
973
+ * already is; drop everything that is pinned or that would not move.
974
+ *
975
+ * The translation is what makes the layout non-destructive, and it is chosen so
976
+ * that a second run lands on the same answer. With a pin, it is whatever keeps
977
+ * the first pinned node exactly where it is. Without one, it is whatever puts
978
+ * the layout's own bounding box where the graph's bounding box already starts —
979
+ * which the first run then makes true, so the second run computes the same
980
+ * translation and writes nothing.
981
+ */
982
+ declare function settle(graph: Graph, slots: ReadonlyMap<string, Slot>, opts?: LayoutOptions): LayoutResult;
983
+ /**
984
+ * {@link settle}'s second half, for a layout that already works in world
985
+ * coordinates — `force` does, since the integrator moves points around rather
986
+ * than filling slots.
987
+ */
988
+ declare function translated(graph: Graph, placed: ReadonlyMap<string, Vec2>, pinned: ReadonlySet<string>, tolerance?: number): LayoutResult;
989
+ /**
990
+ * Lay a list of nodes out along the cross axis, in the order given, and hand
991
+ * back each one's low corner plus the total span.
992
+ *
993
+ * Shared by `layered`'s ranks and `tree`'s sibling rows, which pack a row the
994
+ * same way and differ only in what decides the order.
995
+ */
996
+ declare function packAcross(nodes: readonly GraphNode[], axis: 'x' | 'y', gap: number): {
997
+ at: Map<string, number>;
998
+ span: number;
999
+ };
1000
+ /** Order within a rank: where the nodes already sit on the cross axis, with
1001
+ * graph order breaking a tie. Never a comparison that could reverse between
1002
+ * two runs on the same input. */
1003
+ declare function seededOrder(nodes: readonly GraphNode[], axis: 'x' | 'y', indexOf: (id: string) => number): GraphNode[];
1004
+ /** Each node's position in `graph.nodes`, for tiebreaks. */
1005
+ declare function graphOrder(graph: Graph): (id: string) => number;
1006
+
1007
+ /**
1008
+ * `force` — relaxation, for a graph with no direction to read it in.
1009
+ *
1010
+ * Seeded from where the nodes already are rather than from a fresh random
1011
+ * scatter: an author who has arranged half a diagram gets that arrangement
1012
+ * relaxed, not replaced. There is no RNG in it at all — two coincident nodes
1013
+ * are separated along a golden-angle spiral keyed to their index, which is what
1014
+ * a random jiggle is for elsewhere.
1015
+ *
1016
+ * The integrator is the kit's own `createSimulation`, the same one
1017
+ * `useSimulation` runs a frame at a time. Only the forces are local, and they
1018
+ * are the naive O(n²) kind — right for the tens-to-hundreds of nodes an
1019
+ * edge-per-scene-node diagram is already sized for, wrong for a 10k-node graph,
1020
+ * which wants d3-force's Barnes–Hut through `useSimulation` and its own render
1021
+ * layer.
1022
+ *
1023
+ * **This is the one layout that is not idempotent.** A relaxation re-run from
1024
+ * its own output keeps relaxing; `direction` means nothing to it. Pins and the
1025
+ * `tolerance` option are what keep a second pass from scrambling a settled
1026
+ * diagram.
1027
+ */
1028
+
1029
+ interface ForceOptions extends LayoutOptions {
1030
+ /** Ticks to run. Default 300 — one full cooling schedule. */
1031
+ iterations?: number;
1032
+ /** Rest length of an edge, in world units. Default 160. */
1033
+ linkDistance?: number;
1034
+ /** How hard an edge pulls, 0..1. Default 0.35. */
1035
+ linkStrength?: number;
1036
+ /** Pairwise repulsion. Negative repels; default -3000. */
1037
+ charge?: number;
1038
+ /** How hard the graph is held around its own centroid. Default 0.02. */
1039
+ gravity?: number;
1040
+ /** Clear space kept between two boxes, in world units. Default 24. */
1041
+ padding?: number;
1042
+ }
1043
+ interface Body extends SimulationNode {
1044
+ id: string;
1045
+ /** Half the node's box, so separation can work on the shape the author sees
1046
+ * rather than on a point. */
1047
+ hw: number;
1048
+ hh: number;
1049
+ }
1050
+ /**
1051
+ * The bodies and the force list, wound up and ready to tick.
1052
+ *
1053
+ * Shared by the one-shot `force` below and the live relaxation in `live.ts`,
1054
+ * which ticks it a frame at a time. Two copies of a force list drift, and the
1055
+ * only thing that would say so is the arrangement they produce.
1056
+ */
1057
+ interface ForceRelaxation {
1058
+ bodies: Body[];
1059
+ byId: Map<string, Body>;
1060
+ /** Ids the caller must not move: declared pins, held as `fx`/`fy`. */
1061
+ pinned: ReadonlySet<string>;
1062
+ sim: ReturnType<typeof createSimulation<Body>>;
1063
+ /** Where a body's node's top-left sits, given the body's center. */
1064
+ placed(): Map<string, {
1065
+ x: number;
1066
+ y: number;
1067
+ }>;
1068
+ }
1069
+ declare function forceRelaxation(graph: Graph, opts?: ForceOptions): ForceRelaxation;
1070
+ declare const force: (graph: Graph, opts?: ForceOptions) => LayoutResult;
1071
+
1072
+ /** What a live producer is told, and what it answers with. */
1073
+ interface LiveLayoutCtx<TPose> {
1074
+ /** Nodes another gesture is moving, and where each is drawn. */
1075
+ pinned: ReadonlyMap<NodeId, TPose>;
1076
+ /** Frames since this run heated, starting at 0. */
1077
+ frame: number;
1078
+ }
1079
+ /** A frame of a live layout: where the participants are, and whether it is
1080
+ * finished. Positions are top-lefts, the same as a `LayoutResult`. */
1081
+ interface LiveLayoutFrame {
1082
+ result: LayoutResult;
1083
+ done: boolean;
1084
+ }
1085
+ type LiveLayoutProducer<TPose> = (ctx: LiveLayoutCtx<TPose>) => LiveLayoutFrame;
1086
+ interface ForceProducerOptions<TPose> extends ForceOptions {
1087
+ geometry?: PoseProjection<TPose>;
1088
+ /** Alpha held while something is pinned, so the graph keeps answering a drag
1089
+ * instead of freezing under it. Default 0.3. */
1090
+ dragAlpha?: number;
1091
+ }
1092
+ /**
1093
+ * A relaxation, one tick a frame, done when the simulation settles.
1094
+ *
1095
+ * The forces are `forceRelaxation`'s — the same ones the one-shot `force`
1096
+ * runs, wound up once here and ticked rather than run to completion.
1097
+ *
1098
+ * It does **not** re-anchor the way the one-shot does. That translation exists
1099
+ * so a single 300-tick jump does not move the diagram off where the author left
1100
+ * it; a live run cannot jump, every frame being one step from the last, and
1101
+ * gravity holds the graph around the centroid it was seeded from. Re-anchoring
1102
+ * per frame would also fight a drag: the anchor is measured from where the
1103
+ * nodes were, and a pinned node is deliberately somewhere else.
1104
+ */
1105
+ declare function forceProducer<TPose>(graph: Graph, opts?: ForceProducerOptions<TPose>): LiveLayoutProducer<TPose>;
1106
+ interface EasedProducerOptions {
1107
+ layout?: LayoutOptions;
1108
+ /** Frames to arrive in. Default 24. */
1109
+ frames?: number;
1110
+ easing?: EasingFn;
1111
+ }
1112
+ /**
1113
+ * A layout that knows its destination, walked into place.
1114
+ *
1115
+ * The target is computed once, from the graph as it stood when the run heated;
1116
+ * each frame is that target scaled by the easing curve, measured from where the
1117
+ * nodes started. A pinned node is dropped from the target rather than fought
1118
+ * over.
1119
+ */
1120
+ declare function easedProducer<TPose>(graph: Graph, algorithm: LayoutFn, opts?: EasedProducerOptions): LiveLayoutProducer<TPose>;
1121
+ interface UseLiveLayoutOptions<TPose> {
1122
+ scene: Scene<unknown, string, TPose>;
1123
+ /** Where the graph is read from. The same thunk the port affordance takes. */
1124
+ source: GraphSource<TPose>;
1125
+ /** A key in `LAYOUTS`, or a layout of the consumer's own. `'force'` relaxes
1126
+ * continuously; anything else eases to its answer. Default `'force'`. */
1127
+ algorithm?: string | LayoutFn;
1128
+ layout?: LayoutOptions;
1129
+ force?: ForceOptions;
1130
+ /** Frames an eased layout takes to arrive. Default 24. */
1131
+ frames?: number;
1132
+ easing?: EasingFn;
1133
+ read?: DiagramNodeReader;
1134
+ geometry?: PoseProjection<TPose>;
1135
+ /** The undo entry's name. Default `'Layout'`. */
1136
+ label?: string;
1137
+ /** Fires once the run has settled or been stopped and its poses are in the
1138
+ * document, with how many participants moved. */
1139
+ onSettle?: (moved: number) => void;
1140
+ /** Whether a node or edge appearing or disappearing re-heats the run.
1141
+ * Default true — a diagram that ignores a new edge looks broken. */
1142
+ reheatOnGraphChange?: boolean;
1143
+ /** Clock injection, for tests. Defaults to `requestAnimationFrame`. */
1144
+ requestFrame?: (cb: (t: number) => void) => number;
1145
+ cancelFrame?: (handle: number) => void;
1146
+ }
1147
+ interface LiveLayout {
1148
+ /** Heat and run. Idempotent while running. */
1149
+ start(): void;
1150
+ /** Finish now: commit where the nodes stand. */
1151
+ stop(): void;
1152
+ /** Abandon: the document keeps the poses it had. */
1153
+ cancel(): void;
1154
+ /** Rebuild the graph and run again from where the nodes are now. */
1155
+ restart(): void;
1156
+ isRunning(): boolean;
1157
+ }
1158
+ /**
1159
+ * Run a layout live against a scene.
1160
+ *
1161
+ * The returned handle is stable. Dragging a participant while it runs needs no
1162
+ * wiring: the consumer's move tool publishes an override, and the run treats
1163
+ * that as a pin.
1164
+ */
1165
+ declare function useLiveLayout<TPose>(opts: UseLiveLayoutOptions<TPose>): LiveLayout;
1166
+ /** The layouts that ease rather than relax, for a consumer choosing one. */
1167
+ declare const EASED_LAYOUTS: Readonly<Record<string, LayoutFn>>;
1168
+
1169
+ /**
1170
+ * `layered` — ranks running one way, nodes running across.
1171
+ *
1172
+ * The flowchart / pipeline layout: an edge points from one rank to the next, so
1173
+ * reading down the page is reading the order things happen in. Ranks come from
1174
+ * longest-path layering, which puts every node as far along as its deepest
1175
+ * predecessor allows.
1176
+ *
1177
+ * Cycles do not disqualify a graph. A depth-first walk in graph order names the
1178
+ * edges that close a cycle, layering ignores those, and they draw as edges
1179
+ * running back up the page — which is what a reader expects a loop to look
1180
+ * like. Which edges get named depends only on the node order, so it is the same
1181
+ * every run.
1182
+ *
1183
+ * This is not crossing-minimization: within a rank, nodes keep the order they
1184
+ * already have on the cross axis. An author who dragged two branches into an
1185
+ * order gets that order back, and re-running the layout is free.
1186
+ */
1187
+
1188
+ /** Edge ids that close a cycle, found by a depth-first walk in graph order. */
1189
+ declare function backEdges(graph: Graph): Set<string>;
1190
+ /** Longest-path rank per node: one past its deepest predecessor. */
1191
+ declare function ranksOf(graph: Graph, back: ReadonlySet<string>): Map<string, number>;
1192
+ declare const layered: LayoutFn;
1193
+
1194
+ /**
1195
+ * Running a layout against a scene: `LAYOUTS`, one batch of pose writes, and
1196
+ * the action that reaches both.
1197
+ *
1198
+ * The split is the point. A layout is a pure function of the graph and is
1199
+ * tested as one; `applyLayout` is the only thing that touches a scene, and it
1200
+ * writes every move inside a single `scene.batch`, so a re-layout is one undo
1201
+ * entry rather than one per node.
1202
+ */
1203
+
1204
+ /** The layouts this package ships. A consumer's own go in the same shape. */
1205
+ declare const LAYOUTS: Readonly<Record<string, LayoutFn>>;
1206
+ interface ApplyLayoutOptions<TPose> {
1207
+ geometry?: PoseProjection<TPose>;
1208
+ /** The undo entry's name. Default `'Layout'`. */
1209
+ label?: string;
1210
+ }
1211
+ /**
1212
+ * Write a layout's answer to the scene, as one undoable batch.
1213
+ *
1214
+ * Returns how many nodes moved, which is zero for a layout that had nothing to
1215
+ * do — nothing is written and no history entry is pushed.
1216
+ *
1217
+ * Nodes are **translated**, never re-posed from the layout's numbers: a node's
1218
+ * pose can be a path or anything else a consumer wired, and only its own
1219
+ * descriptor knows how to move one.
1220
+ *
1221
+ * A participant that is a container takes its whole subtree along. `setPose`
1222
+ * does not cascade — poses are absolute — so a built body would otherwise walk
1223
+ * out from under its own label rows with every test green.
1224
+ */
1225
+ declare function applyLayout<TPose>(scene: Scene<unknown, string, TPose>, result: LayoutResult, opts?: ApplyLayoutOptions<TPose>): number;
1226
+ /**
1227
+ * The poses a layout result implies, without writing any of them: each moved
1228
+ * node translated to its new top-left, and its subtree translated with it.
1229
+ *
1230
+ * Split out because a *live* layout needs the same answer every frame and must
1231
+ * not write it — it publishes to the override channel and commits once. The
1232
+ * translation is measured from the node's document pose, which a run in flight
1233
+ * never changes, so a frame's answer does not depend on the frame before it.
1234
+ */
1235
+ declare function layoutPoses<TPose>(scene: Scene<unknown, string, TPose>, result: LayoutResult, geometry?: PoseProjection<TPose>): Map<string, TPose>;
1236
+ /** The default action id. */
1237
+ declare const LAYOUT_ACTION_ID = "diagram.layout";
1238
+ interface LayoutActionOptions<TPose> {
1239
+ /** Where the graph is read from. The same thunk the port affordance takes. */
1240
+ source: GraphSource<TPose>;
1241
+ /** Default {@link LAYOUT_ACTION_ID}. */
1242
+ id?: string;
1243
+ label?: string;
1244
+ /** Which `<ActionBar group>` it lands in. Default `'diagram'`, alongside
1245
+ * connect — give a bar of its own a group of its own. */
1246
+ group?: string;
1247
+ /** A key in {@link LAYOUTS}, or a layout of the consumer's own. Default
1248
+ * `'layered'`. */
1249
+ algorithm?: string | LayoutFn;
1250
+ layout?: LayoutOptions;
1251
+ read?: DiagramNodeReader;
1252
+ geometry?: PoseProjection<TPose>;
1253
+ }
1254
+ /**
1255
+ * `diagram.layout` — rebuild the graph, run a layout, write the result.
1256
+ *
1257
+ * A factory rather than a singleton for the reason every other entry point in
1258
+ * this package is one: which nodes are in the diagram is the consumer's, and
1259
+ * this package has no scene at module scope.
1260
+ */
1261
+ declare function createLayoutAction<TPose>(opts: LayoutActionOptions<TPose>): Action;
1262
+
1263
+ /**
1264
+ * `tree` — a parent centered over the block its children occupy.
1265
+ *
1266
+ * The hierarchy layout: org charts, file trees, decision trees. Depth is the
1267
+ * rank axis, so every child sits one band past its parent, and a subtree is
1268
+ * packed as a solid block so two siblings' descendants never interleave.
1269
+ *
1270
+ * A graph that is not a tree still lays out. Roots are the nodes nothing points
1271
+ * at, in graph order; a node reachable from two parents belongs to whichever
1272
+ * the depth-first walk reaches first; and anything the walk never reaches — a
1273
+ * disconnected part, or a pure cycle — becomes a root of its own. So the answer
1274
+ * is always a forest, and which forest depends only on the node order.
1275
+ *
1276
+ * The block packing is deliberately not Reingold–Tilford: it never slides a
1277
+ * deep subtree under a shallow neighbor's overhang. That costs horizontal
1278
+ * space and buys an invariant an author can rely on — a subtree's extent is a
1279
+ * rectangle, and dragging one never lands it inside another.
1280
+ */
1281
+
1282
+ interface Forest {
1283
+ roots: GraphNode[];
1284
+ children: Map<string, GraphNode[]>;
1285
+ depth: Map<string, number>;
1286
+ }
1287
+ /** The forest a depth-first walk in graph order finds. Exported because "which
1288
+ * node ended up whose child" is the question anyone debugging a tree layout
1289
+ * asks first. */
1290
+ declare function forestOf(graph: Graph, crossAxis: 'x' | 'y'): Forest;
1291
+ declare const tree: LayoutFn;
1292
+
1293
+ /** The outline as a closed polyline: `[x0, y0, x1, y1, …]`. */
1294
+ declare function outlinePolyline(outline: Outline, bounds: Bounds): number[];
1295
+ /**
1296
+ * Where the ray from `center` through `point` last crosses `poly`, or `null`
1297
+ * when it never does.
1298
+ *
1299
+ * The *last* crossing rather than the first: a concave outline can be crossed
1300
+ * more than once, and the far side is the boundary an edge should attach to.
1301
+ */
1302
+ declare function rayHit(poly: readonly number[], center: {
1303
+ x: number;
1304
+ y: number;
1305
+ }, point: {
1306
+ x: number;
1307
+ y: number;
1308
+ }): {
1309
+ x: number;
1310
+ y: number;
1311
+ } | null;
1312
+ /**
1313
+ * Every port moved onto `outline`. A port the ray misses — one at the node's
1314
+ * dead center, with no direction to cast along — is left where it was.
1315
+ */
1316
+ declare function portsOnOutline(ports: readonly Port[], outline: Outline, bounds: Bounds): Port[];
1317
+
1318
+ /**
1319
+ * The port affordance, composed into a layer and pointed at a scene.
1320
+ *
1321
+ * Two conveniences over {@link createPortAffordance}, both of which encode a
1322
+ * rule that is easy to get wrong rather than merely saving a line.
1323
+ */
1324
+
1325
+ /** The minimum a participant source needs from a scene. Narrower than `Scene`
1326
+ * so a consumer can pass anything that answers these two. */
1327
+ interface ParticipantScene<TPose> {
1328
+ renderOrderNodes(): readonly {
1329
+ id: string;
1330
+ kind: 'leaf' | 'container';
1331
+ data: unknown;
1332
+ pose: TPose;
1333
+ dependsOn?: readonly string[] | 'children';
1334
+ }[];
1335
+ readonly overrides: {
1336
+ get(id: string): {
1337
+ pose?: TPose;
1338
+ } | undefined;
1339
+ };
1340
+ get(id: string): unknown;
1341
+ childrenOf(id: string): readonly string[];
1342
+ }
1343
+ /**
1344
+ * Every node in `scene`, at the pose it is **painted** at.
1345
+ *
1346
+ * `effectivePose`, not `node.pose`: an override is what a gesture is currently
1347
+ * showing, and a port that answers from the document pose while its node is
1348
+ * mid-drag sits somewhere the user can see the node is not. Nodes that carry no
1349
+ * participant trait cost one reader call each and contribute no regions.
1350
+ *
1351
+ * One source answers both the ports and the graph — `GraphSource` is a
1352
+ * `ParticipantSource` that also carries `dependsOn`, which is where an edge's
1353
+ * two endpoints live.
1354
+ */
1355
+ declare function sceneParticipants<TPose>(scene: ParticipantScene<TPose>): GraphSource<TPose>;
1356
+ /**
1357
+ * The composed layer, ready for `CanvasExtensionApi.registerLayer` — the only
1358
+ * attach route that is hit-tested. Handing this to `SceneCanvas`'s `layers`
1359
+ * prop, or to a `Contribution.overlay`, paints the ports and makes none of them
1360
+ * grabbable.
1361
+ *
1362
+ * The cast is `composeAffordanceLayer` typing its data slot as `ChromeState`
1363
+ * while the layer registry takes `RenderLayer<unknown>`; both are handed the
1364
+ * same live `CanvasHelpers` envelope at runtime.
1365
+ */
1366
+ declare function portLayer<TPose>(participants: ParticipantSource<TPose>, opts?: PortAffordanceOptions<TPose>): RenderLayer<unknown> & {
1367
+ hitTest: NonNullable<RenderLayer<unknown>['hitTest']>;
1368
+ };
1369
+
1370
+ /**
1371
+ * The painter for a node whose trait names an `outline`.
1372
+ *
1373
+ * Registered the way every other trait's renderer is — a `NodeShapeEntry` with
1374
+ * a `matches` over the node's data — so a diagram body is picked, clipped and
1375
+ * area-selected by the same walk as everything else, and a participant that
1376
+ * already had a look of its own (a text block, an image, a path) keeps it by
1377
+ * naming no outline at all.
1378
+ *
1379
+ * Rows are **not** painted here. A built body's rows are ordinary scene nodes
1380
+ * under the container, so the kit's own text painter draws them and text
1381
+ * editing, selection and styling work on them without a special case.
1382
+ */
1383
+
1384
+ interface DiagramShapeOptions<TPose> {
1385
+ /** Distinguishes one registration from another, and is what the disposer
1386
+ * removes. Default `'diagram:outline'`. */
1387
+ id?: string;
1388
+ /** How the trait is read. Default: the node's own `data.diagram`. */
1389
+ read?: DiagramNodeReader;
1390
+ /** Reads the box the outline is built in. Default `RECT_POSE_DESCRIPTOR`. */
1391
+ geometry?: PoseProjection<TPose>;
1392
+ /** Painted when the node's data declares neither. */
1393
+ defaultFill?: FillStyle | null;
1394
+ defaultStroke?: Stroke | null;
1395
+ }
1396
+ /** The entry, for a consumer composing their own painter list. */
1397
+ declare function diagramShape<TPose>(opts?: DiagramShapeOptions<TPose>): NodeShapeEntry<unknown, TPose>;
1398
+ /**
1399
+ * Register {@link diagramShape}. Returns the disposer, and takes `'high'`
1400
+ * priority so a body outline wins over a `data.shape` or `data.path` the same
1401
+ * node may also carry.
1402
+ */
1403
+ declare function registerDiagramShape<TPose>(opts?: DiagramShapeOptions<TPose>): () => void;
1404
+
1405
+ export { type ApplyLayoutOptions, type BodyFloor, type BodyNodeSpec, type BodySpec, type Bounds, type BuildBodyOptions, type BuildGraphOptions, COMPASS, CONNECT_ACTION_ID, type CanConnect, type ConnectActionOptions, DEFAULT_EDGE_STROKE, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_EDGE, DIAGRAM_LABEL, DIAGRAM_TRAIT_KEY, type DiagramContributionOptions, type DiagramEdge, type DiagramLabel, type DiagramNode, type DiagramNodeEntry, type DiagramNodeLike, type DiagramNodeReader, type DiagramShapeOptions, EASED_LAYOUTS, EDGE_DERIVE_PATH, type EasedProducerOptions, type EdgeEnd, type EdgeRouteOptions, type ForceOptions, type ForceProducerOptions, type ForceRelaxation, GRAB_PORT_ACTION_ID, type Graph, type GraphEdge, type GraphNode, type GraphNodeLike, type GraphSource, LABEL_DERIVE_POSE, LAYOUTS, LAYOUT_ACTION_ID, type LabelPoseOptions, type LayoutActionOptions, type LayoutAxes, type LayoutDirection, type LayoutFn, type LayoutOptions, type LayoutResult, type LiveLayout, type LiveLayoutCtx, type LiveLayoutFrame, type LiveLayoutProducer, type MeasureRowText, type Outline, PORT_AFFORDANCE_KIND, PORT_LAYER_ID, type ParticipantPose, type ParticipantScene, type ParticipantSource, type PendingEdge, type Port, type PortAffordanceOptions, type PortAnchor, type PortScratch, type PortSpec, type PortsOptions, ROUTERS, type RouteRequest, type Router, type Row, type RowBox, type RowNodeData, type RowPort, type RowPortBox, type RowTextStyle, type Slot, type UseLiveLayoutOptions, applyLayout, axesFor, backEdges, bezier, bodyOutline, bodyTrait, boxForContent, buildBody, buildGraph, canvasMeasure, commitEdgeToScene, connectBinding, contentBox, createConnectAction, createDiagramContribution, createDiagramNodes, createLayoutAction, createPortAffordance, dataKeyReader, defaultCanConnect, diagramEdgeOf, diagramLabelOf, diagramNodeOf, diagramPorts, diagramShape, easedProducer, edgeDerivePath, force, forceProducer, forceRelaxation, forestOf, grabPortAction, grabPortBindings, graphOrder, isPinned, labelDerivePose, layered, layoutBody, layoutPoses, layoutRowPorts, measureBody, orthogonal, outlinePath, outlinePolyline, packAcross, pinnedSet, portLayer, portOf, portScratchOf, portsOf, portsOnOutline, ranksOf, rayHit, registerDiagramShape, resolveEnd, sceneParticipants, seededOrder, settle, sizeToBody, straight, translated, tree, useLiveLayout, withDiagramRegistry };