@invana/graph 0.0.1

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.
@@ -0,0 +1,4872 @@
1
+ import { SourceEmitter, CanvasEventBus, ConnectorLabelPlacement, ConnectorLabelStyle, InsetAnchor, ShapeFill, ShapeLabelPlacement, ShapeLabelStyle, WorldLayer, WorldLayerHit, PrimitivesRenderer, LayerOptions, CanvasContext, ScreenLayer, ScreenLayerHit, EventEmitter, Behaviour, BehaviourOptions, ElementSizeLODBehaviour, ElementSizeLODBehaviourOptions, NumberOrGetter } from '@invana/canvas';
2
+ import { RingConnectorDecorationStyle, GlowConnectorDecorationStyle, MarchingAntsConnectorDecorationStyle, RippleConnectorDecorationStyle, FlyMarkerConnectorDecorationStyle, FlowParticlesConnectorDecorationStyle, RevealConnectorDecorationStyle, Point, RingDecorationStyle, GlowDecorationStyle, PulseRingDecorationStyle, MarchingAntsDecorationStyle, LiquidFillDecorationStyle, ToggleDecorationStyle, ResizeHandleDecorationStyle, SelectionFrameDecorationStyle, TogglePlacement, Rect } from '@invana/canvas/primitives';
3
+
4
+ /**
5
+ * `@invana/graph` — `GraphStore` type definitions.
6
+ *
7
+ * See `apps/docs/graph/data-model.md` for the user-facing description and
8
+ * `apps/docs/graph/store-plan.md` for the implementation rationale.
9
+ *
10
+ * **v3 G6-aligned shape**: per-instance descriptor is flat with
11
+ * `{ id, type, data, style, state, states, combo, children, ... }`.
12
+ * `state` (singular) is the per-instance overlay catalogue;
13
+ * `states` (plural) is the active-state list.
14
+ */
15
+ /** A node in the graph. `id` is unique within a `GraphStore`. */
16
+ interface GraphNode<D = unknown> {
17
+ /** Stable identity. Must be unique within the store. */
18
+ id: string;
19
+ /** Type tag — matches a `NodeOption.type` template if any. Free-form. */
20
+ type?: string;
21
+ /** Arbitrary user payload — opaque to the store. */
22
+ data?: D;
23
+ /** Logical parent. Cycles are rejected at write time. */
24
+ parentId?: string;
25
+ /** Canonical position. Owned by the store; mutated by layouts and drags. */
26
+ position?: {
27
+ x: number;
28
+ y: number;
29
+ };
30
+ /** True iff layouts must not move this node. */
31
+ pinned?: boolean;
32
+ /**
33
+ * Currently-active state names (plural). Each name should match a key in
34
+ * `style.state` (per-instance overlay catalogue) or in
35
+ * `GraphLayerOptions.node.state` (layer-level catalogue).
36
+ *
37
+ * The store treats this field as opaque metadata. The layer reads it on
38
+ * insert and update to toggle visual states via `setNodeState`. On
39
+ * update with `states` present in the patch the layer REPLACES the
40
+ * visible state set with the new array — runtime states applied via
41
+ * `setNodeState` (e.g. hover) are wiped. Pass an empty array (or
42
+ * `null`) to clear.
43
+ */
44
+ states?: readonly string[] | null;
45
+ /**
46
+ * Visual + structural style for this node. Typed via
47
+ * `import('../layer/types').NodeStyle` in consumer code; left as `unknown`
48
+ * here to avoid a store → layer dependency cycle.
49
+ */
50
+ style?: unknown;
51
+ /**
52
+ * Per-instance overlay catalogue keyed by state name (singular `state`).
53
+ * Each value is a `NodeStyle` patch applied when that name appears in
54
+ * {@link states}. Typed by the consumer as
55
+ * `Readonly<Record<string, NodeStyle>>`.
56
+ */
57
+ state?: unknown;
58
+ }
59
+ /** A directed edge. Multi-edges between the same pair are allowed. */
60
+ interface GraphEdge<D = unknown> {
61
+ /** Stable identity. Must be unique within the store. */
62
+ id: string;
63
+ /** Source node id. */
64
+ source: string;
65
+ /** Target node id. */
66
+ target: string;
67
+ /** Predicate / FK label / "calls" / "depends-on" — free-form. */
68
+ type?: string;
69
+ /** Arbitrary user payload — opaque to the store. */
70
+ data?: D;
71
+ /** Sibling of {@link GraphNode.states} — currently-active state names. */
72
+ states?: readonly string[] | null;
73
+ /** Per-instance style. Typed by consumer as `EdgeStyle`. */
74
+ style?: unknown;
75
+ /** Per-instance overlay catalogue. Typed by consumer as `Record<string, EdgeStyle>`. */
76
+ state?: unknown;
77
+ }
78
+ /**
79
+ * Constructor options for `GraphStore`.
80
+ *
81
+ * Defaults are tuned for sync, single-process, batch-driven use. Streaming
82
+ * feeds should set `flushMode: 'frame'` and `unknownEndpoint: 'buffer'`.
83
+ */
84
+ interface GraphStoreOptions {
85
+ /**
86
+ * `'sync'` — events fire synchronously at each mutation / on `batch` exit.
87
+ * `'frame'` — events coalesce into a single flush per animation frame.
88
+ * Default `'sync'`.
89
+ */
90
+ flushMode?: 'sync' | 'frame';
91
+ /**
92
+ * What to do when `addEdge` is called with an unknown source or target id.
93
+ * - `'throw'` (default) — reject and throw.
94
+ * - `'buffer'` — park in the pending-edge buffer; admit when the endpoint arrives.
95
+ * - `'drop'` — silently discard.
96
+ */
97
+ unknownEndpoint?: 'throw' | 'buffer' | 'drop';
98
+ /**
99
+ * Drop a buffered edge (and emit `edge:orphaned`) if it has been pending
100
+ * for more than this many frames. Default `Infinity` (never expire).
101
+ * Only meaningful with `unknownEndpoint: 'buffer'`.
102
+ */
103
+ pendingEdgeTTL?: number;
104
+ /**
105
+ * Initial slot capacity for the underlying `ColumnStore`s. Larger up-front
106
+ * capacity avoids early geometric growth on bulk inserts. Default 256.
107
+ */
108
+ initialCapacity?: number;
109
+ /**
110
+ * Identity for the store's event-source envelopes on the canvas tap channel
111
+ * (telemetry). Becomes `source.id` on every `{ kind: 'store' }` event the bus
112
+ * publishes. Default `'graph-store'`; pass the owning layer's id to
113
+ * disambiguate multiple graphs. See `store-owns-state-plan.md` § 6.
114
+ */
115
+ id?: string;
116
+ }
117
+ /**
118
+ * Event-map shape for `GraphStore.events` (used by `EventEmitter<E>`).
119
+ *
120
+ * Subscribe to fine-grained `node:*` / `edge:*` events for per-entity updates,
121
+ * or to `flush` for aggregated per-batch counts.
122
+ */
123
+ type GraphStoreEventMap = {
124
+ 'node:add': {
125
+ nodeId: string;
126
+ };
127
+ 'node:update': {
128
+ nodeId: string;
129
+ patch: Partial<GraphNode>;
130
+ };
131
+ 'node:remove': {
132
+ nodeId: string;
133
+ };
134
+ 'edge:add': {
135
+ edgeId: string;
136
+ };
137
+ 'edge:update': {
138
+ edgeId: string;
139
+ patch: Partial<GraphEdge>;
140
+ };
141
+ 'edge:remove': {
142
+ edgeId: string;
143
+ };
144
+ /** Emitted when a buffered edge is dropped after exceeding `pendingEdgeTTL`. */
145
+ 'edge:orphaned': {
146
+ edgeId: string;
147
+ };
148
+ /**
149
+ * A runtime (presence) state was toggled on a node — `on` reflects the
150
+ * post-change membership of the runtime set. Fired per-toggle on flush,
151
+ * deduped per `(id, name)` within the flush window. `actor` is reserved for
152
+ * collaboration (the originating user); `undefined` in single-user mode.
153
+ * Document `states[]` changes ride `node:update`, not this event.
154
+ */
155
+ 'node:state': {
156
+ nodeId: string;
157
+ name: string;
158
+ on: boolean;
159
+ actor?: string;
160
+ };
161
+ /** Edge sibling of `node:state`. */
162
+ 'edge:state': {
163
+ edgeId: string;
164
+ name: string;
165
+ on: boolean;
166
+ actor?: string;
167
+ };
168
+ /** Aggregate counts per flush. Fires once per batch / RAF flush. */
169
+ flush: {
170
+ addedNodes: number;
171
+ updatedNodes: number;
172
+ removedNodes: number;
173
+ addedEdges: number;
174
+ updatedEdges: number;
175
+ removedEdges: number;
176
+ };
177
+ };
178
+ /** Direction of an adjacency / neighbor query. */
179
+ type EdgeDirection = 'in' | 'out' | 'both';
180
+ /** Position record (used by `getPosition` / `setPosition`). */
181
+ interface Vec2 {
182
+ x: number;
183
+ y: number;
184
+ }
185
+
186
+ /**
187
+ * `GraphStore` — the domain primitive for `@invana/graph`.
188
+ *
189
+ * Composes two `ColumnStore`s (from `@invana/canvas`) for hot fields plus
190
+ * `Map<id, payload>` for cold fields. Adjacency uses per-slot `Int32Array`
191
+ * indices via {@link AdjacencyIndex}. Streaming feeds get RAF-coalesced
192
+ * flushing via {@link FrameFlushScheduler} and out-of-order edge handling
193
+ * via {@link PendingEdges}.
194
+ *
195
+ * See `apps/docs/graph/data-model.md` for the user-facing description and
196
+ * `apps/docs/graph/store-plan.md` for the implementation rationale.
197
+ */
198
+
199
+ declare class GraphStore {
200
+ private readonly flushMode;
201
+ private readonly unknownEndpoint;
202
+ private readonly pendingEdgeTTL;
203
+ private readonly nodeCols;
204
+ private readonly edgeCols;
205
+ private readonly nodeMap;
206
+ private readonly edgeMap;
207
+ private readonly childrenIndex;
208
+ private readonly nodeRuntimeStates;
209
+ private readonly edgeRuntimeStates;
210
+ private readonly outAdj;
211
+ private readonly inAdj;
212
+ private readonly pending;
213
+ /**
214
+ * Public event bus. Subscribe via `store.events.on('node:add', ...)`.
215
+ *
216
+ * A `SourceEmitter` (`{ kind: 'store' }`): once the owning layer calls
217
+ * {@link bindBus} on mount, every emit also publishes a `CanvasEvent` envelope
218
+ * to the canvas tap channel, so telemetry sees all store mutations
219
+ * (`store-owns-state-plan.md` § 6).
220
+ */
221
+ readonly events: SourceEmitter<GraphStoreEventMap>;
222
+ /** Per-flush counters. Reset on `flush()`. */
223
+ private counters;
224
+ /** Pending dedup-ed event payloads to fire on the next flush. */
225
+ private pendingNodeAdds;
226
+ private pendingNodeUpdates;
227
+ private pendingNodeRemoves;
228
+ private pendingEdgeAdds;
229
+ private pendingEdgeUpdates;
230
+ private pendingEdgeRemoves;
231
+ private pendingEdgeOrphans;
232
+ /**
233
+ * Pending runtime-state toggles, keyed by `"<id>\u0000<name>"` so repeated
234
+ * toggles of the same (id, name) within a flush window collapse to one event.
235
+ * The emitted `on` is read from live set membership at flush time, so an
236
+ * add+remove in the same frame nets out correctly.
237
+ */
238
+ private pendingNodeStates;
239
+ private pendingEdgeStates;
240
+ /** Depth of nested `batch()` calls. Flushes only on outermost exit. */
241
+ private batchDepth;
242
+ private flushScheduler;
243
+ /** Monotonic version counter. Bumps on every mutation including silent. */
244
+ private _version;
245
+ /**
246
+ * Monotonic flush counter. Bumps on every `doFlush` regardless of which
247
+ * code path triggered the flush. Used by `PendingEdges` TTL accounting.
248
+ */
249
+ private _frame;
250
+ constructor(opts?: GraphStoreOptions);
251
+ /**
252
+ * Attach (or detach with `undefined`) the canvas event bus this store's
253
+ * events forward to. Called by the owning `GraphLayer` on mount/unmount so
254
+ * store mutations reach the telemetry tap channel (§ 6). Local
255
+ * `store.events.on(...)` subscribers work with or without a bus.
256
+ */
257
+ bindBus(bus: CanvasEventBus | undefined): void;
258
+ /** Monotonic counter. Bumps on every mutation including silent position writes. */
259
+ get version(): number;
260
+ /** Number of live (non-tombstoned) nodes. */
261
+ nodeCount(): number;
262
+ /** Number of live (non-tombstoned) edges. */
263
+ edgeCount(): number;
264
+ hasNode(id: string): boolean;
265
+ hasEdge(id: string): boolean;
266
+ getNode<D = unknown>(id: string): GraphNode<D> | undefined;
267
+ getEdge<D = unknown>(id: string): GraphEdge<D> | undefined;
268
+ nodes(): IterableIterator<GraphNode>;
269
+ edges(): IterableIterator<GraphEdge>;
270
+ outDegree(nodeId: string): number;
271
+ inDegree(nodeId: string): number;
272
+ /**
273
+ * Yield edges incident to `nodeId` in the requested direction.
274
+ * `'out'` — edges where `nodeId` is the source.
275
+ * `'in'` — edges where `nodeId` is the target.
276
+ * `'both'` — out then in.
277
+ */
278
+ edgesOf(nodeId: string, dir?: EdgeDirection): IterableIterator<GraphEdge>;
279
+ /** Yield neighbor node ids in the requested direction. */
280
+ neighborsOf(nodeId: string, dir?: EdgeDirection): IterableIterator<string>;
281
+ parentOf(id: string): string | undefined;
282
+ childrenOf(parentId: string): IterableIterator<string>;
283
+ descendantsOf(id: string): IterableIterator<string>;
284
+ ancestorsOf(id: string): IterableIterator<string>;
285
+ getPosition(id: string): Vec2 | undefined;
286
+ /**
287
+ * Set a single node's position.
288
+ *
289
+ * Default fires `node:update`. `opts.silent: true` skips the event and just
290
+ * bumps `version` — use for layout sim ticks at 60fps.
291
+ */
292
+ setPosition(id: string, pos: Vec2, opts?: {
293
+ silent?: boolean;
294
+ }): void;
295
+ /**
296
+ * Set many positions in a single tight loop.
297
+ *
298
+ * `xy` is packed `[x0, y0, x1, y1, ...]` (length must equal `ids.length * 2`).
299
+ * `opts.silent: true` skips events — sim-tick fastpath. Otherwise emits one
300
+ * deduped `node:update` per id.
301
+ */
302
+ setPositionsBulk(ids: readonly string[], xy: Float32Array, opts?: {
303
+ silent?: boolean;
304
+ }): void;
305
+ setPinned(id: string, pinned: boolean): void;
306
+ isPinned(id: string): boolean;
307
+ pinnedIds(): IterableIterator<string>;
308
+ /** Strict add — throws on duplicate. */
309
+ addNode<D>(node: GraphNode<D>): void;
310
+ /** Add-or-merge. Streaming-friendly path. */
311
+ upsertNode<D>(node: GraphNode<D>): void;
312
+ updateNode<D>(id: string, patch: Partial<GraphNode<D>>): void;
313
+ /**
314
+ * Remove a node. Cascades by default — removes all incident edges first.
315
+ * `cascade: false` throws if any incident edges still exist.
316
+ */
317
+ removeNode(id: string, opts?: {
318
+ cascade?: boolean;
319
+ }): void;
320
+ addEdge<D>(edge: GraphEdge<D>): void;
321
+ upsertEdge<D>(edge: GraphEdge<D>): void;
322
+ updateEdge<D>(id: string, patch: Partial<GraphEdge<D>>): void;
323
+ /**
324
+ * Reverse an edge's direction — swap its `source` and `target`. No-op if the
325
+ * edge doesn't exist. Routes through {@link updateEdge}, so adjacency indexes
326
+ * are rewired and an `edge:update` is enqueued like any other re-pointing.
327
+ */
328
+ reverseEdge(id: string): void;
329
+ removeEdge(id: string): void;
330
+ /**
331
+ * Add a runtime (presence) state to a node. Idempotent — re-adding an already
332
+ * active state is a no-op (no event). No-op if the node id is unknown.
333
+ *
334
+ * @param id Node id.
335
+ * @param name State name (e.g. `'selected'`, `'highlighted'`, `'lineage'`).
336
+ * @param _opts Reserved for collaboration — `actor` will tag the change with
337
+ * its originating user once presence replication lands (§ 5). Unused today.
338
+ */
339
+ addNodeState(id: string, name: string, _opts?: {
340
+ actor?: string;
341
+ }): void;
342
+ /**
343
+ * Remove a runtime (presence) state from a node. No-op if the state isn't
344
+ * currently active (no event) or the node is unknown.
345
+ */
346
+ removeNodeState(id: string, name: string, _opts?: {
347
+ actor?: string;
348
+ }): void;
349
+ /**
350
+ * Toggle a runtime (presence) state on a node — `on ? addNodeState :
351
+ * removeNodeState`. Convenience for callers (e.g. hover) that compute the
352
+ * desired membership as a boolean. Default `on = true`.
353
+ */
354
+ setNodeState(id: string, name: string, on?: boolean, opts?: {
355
+ actor?: string;
356
+ }): void;
357
+ /** Toggle a runtime (presence) state on an edge. See {@link setNodeState}. */
358
+ setEdgeState(id: string, name: string, on?: boolean, opts?: {
359
+ actor?: string;
360
+ }): void;
361
+ /**
362
+ * Strip a runtime (presence) state from every node that carries it, in one
363
+ * pass — e.g. clearing a transient `'selected'` / `'lineage'` set. Touches the
364
+ * presence compartment only; a document state of the same name in `states[]`
365
+ * is unaffected (change those via {@link updateNode}).
366
+ */
367
+ clearNodeState(name: string): void;
368
+ /** Add a runtime (presence) state to an edge. See {@link addNodeState}. */
369
+ addEdgeState(id: string, name: string, _opts?: {
370
+ actor?: string;
371
+ }): void;
372
+ /** Remove a runtime (presence) state from an edge. See {@link removeNodeState}. */
373
+ removeEdgeState(id: string, name: string, _opts?: {
374
+ actor?: string;
375
+ }): void;
376
+ /** Strip a runtime (presence) state from every edge. See {@link clearNodeState}. */
377
+ clearEdgeState(name: string): void;
378
+ /**
379
+ * Effective active states of a node — the **union** of its document `states[]`
380
+ * (feed-owned) and its runtime presence set. This is what the renderer iterates
381
+ * to apply state overlays. Returns a fresh array; empty if the node is unknown
382
+ * or carries no states.
383
+ */
384
+ nodeStatesOf(id: string): readonly string[];
385
+ /** Effective active states of an edge — union of document + presence. */
386
+ edgeStatesOf(id: string): readonly string[];
387
+ /** True iff `name` is in a node's effective (document ∪ presence) state set. */
388
+ hasNodeState(id: string, name: string): boolean;
389
+ /** True iff `name` is in an edge's effective (document ∪ presence) state set. */
390
+ hasEdgeState(id: string, name: string): boolean;
391
+ /**
392
+ * Ids of every node whose effective (document ∪ presence) state set contains
393
+ * `name`. Scans live nodes; useful for snapshots / iteration.
394
+ */
395
+ nodesWithState(name: string): IterableIterator<string>;
396
+ /** Edge sibling of {@link nodesWithState}. */
397
+ edgesWithState(name: string): IterableIterator<string>;
398
+ addNodesBulk(nodes: readonly GraphNode[]): void;
399
+ addEdgesBulk(edges: readonly GraphEdge[]): void;
400
+ /**
401
+ * Append nodes + edges in one batch — non-destructive (does NOT clear).
402
+ * Convenience for streaming feeds that push a fresh chunk of items as
403
+ * they arrive. Subscribers see a single `flush`.
404
+ *
405
+ * Differs from `GraphLayer.setData`, which clears the store first.
406
+ */
407
+ addData(data: {
408
+ nodes?: readonly GraphNode[];
409
+ edges?: readonly GraphEdge[];
410
+ }): void;
411
+ /**
412
+ * Apply a streaming delta in a single batch. Order within the batch:
413
+ * 1. `removed.edgeIds` — removed first so node removals can't cascade
414
+ * them again (no-op double removal is harmless, but explicit is cleaner).
415
+ * 2. `removed.nodeIds` — cascade-removes incident edges per `removeNode`'s
416
+ * default `cascade: true`.
417
+ * 3. `added.nodes` — `upsertNode` (idempotent; safe to re-send).
418
+ * 4. `added.edges` — `upsertEdge`.
419
+ * 5. `updated.nodes` — partial patches via `updateNode`.
420
+ * 6. `updated.edges` — partial patches via `updateEdge`.
421
+ *
422
+ * Use `upsertNode` / `upsertEdge` for the `added` lists so a feed that
423
+ * re-sends an existing id (common in pub-sub) merges rather than throwing.
424
+ * If you have hard-add semantics, use `addData` instead.
425
+ *
426
+ * Subscribers see one `flush` regardless of how many items were touched.
427
+ */
428
+ applyDelta(delta: {
429
+ added?: {
430
+ nodes?: readonly GraphNode[];
431
+ edges?: readonly GraphEdge[];
432
+ };
433
+ updated?: {
434
+ nodes?: ReadonlyArray<{
435
+ id: string;
436
+ patch: Partial<GraphNode>;
437
+ }>;
438
+ edges?: ReadonlyArray<{
439
+ id: string;
440
+ patch: Partial<GraphEdge>;
441
+ }>;
442
+ };
443
+ removed?: {
444
+ nodeIds?: readonly string[];
445
+ edgeIds?: readonly string[];
446
+ };
447
+ }): void;
448
+ /**
449
+ * Coalesce all mutations inside `fn` into a single flush. Nested `batch`
450
+ * calls flush only on the outermost exit.
451
+ */
452
+ batch<T>(fn: () => T): T;
453
+ /**
454
+ * Drain any pending events. Cancels the RAF-scheduled flush (frame mode).
455
+ * In sync mode this still works — handy for forcing the TTL eviction sweep
456
+ * even if no mutation has happened since the last flush.
457
+ */
458
+ flush(): void;
459
+ /** Wipe all data. Cancels any pending flush. */
460
+ clear(): void;
461
+ /**
462
+ * Reclaim tombstoned slots. Invalidates any external code that cached
463
+ * slot indices. Renderer batch buffers etc. must invalidate first.
464
+ */
465
+ compact(): void;
466
+ private installNode;
467
+ private installEdge;
468
+ private handleUnknownEndpoint;
469
+ private tryAdmitPending;
470
+ /** True iff `candidateAncestor` is already a descendant of `id`. */
471
+ private wouldCreateCycle;
472
+ private readPosition;
473
+ private enqueueNodeAdd;
474
+ private enqueueNodeUpdate;
475
+ private enqueueNodeRemove;
476
+ private enqueueEdgeAdd;
477
+ private enqueueEdgeUpdate;
478
+ private enqueueEdgeRemove;
479
+ private enqueueEdgeOrphan;
480
+ /** Queue a node runtime-state change, deduped per `(id, name)` per flush. */
481
+ private enqueueNodeState;
482
+ /** Queue an edge runtime-state change, deduped per `(id, name)` per flush. */
483
+ private enqueueEdgeState;
484
+ private scheduleFlushIfNeeded;
485
+ private doFlush;
486
+ }
487
+
488
+ /**
489
+ * A field value that's either a static value or a function that derives the
490
+ * value from the host item (node / edge / raw data).
491
+ *
492
+ * Used on every field of `NodeStyle` / `EdgeStyle` (via `ResolvableNodeStyle`
493
+ * / `ResolvableEdgeStyle`) so callers can supply per-item-derived styling
494
+ * on the layer template (`options.node.style`) or per-instance input
495
+ * (`NodeInput.style`) without spreading hints into every node's `data`.
496
+ *
497
+ * Resolved per render (layer-level) or once at insert (per-input). Keep
498
+ * resolvers cheap and pure — they may run per frame. Recursive returns
499
+ * (a function returning another function) are not unwrapped — return the
500
+ * final value.
501
+ *
502
+ * @example
503
+ * ```ts
504
+ * node: {
505
+ * style: {
506
+ * bgFill: (n) => groupColors[(n.data as Group).group % groupColors.length],
507
+ * shape: (n) => ({ kind: 'circle', radius: 12 + Math.sqrt((n.data as N).degree ?? 1) * 4 }),
508
+ * labelText: (n) => (n.data as N).name,
509
+ * },
510
+ * }
511
+ * ```
512
+ */
513
+ type Resolvable<T, I> = T | ((input: I) => T);
514
+ /** Convenience alias for id-resolvers; `D` is the raw data type on input. */
515
+ type ResolvableId<D> = string | ((data: D) => string);
516
+ /**
517
+ * Unwrap a {@link Resolvable} field for `input`. Static values pass through
518
+ * untouched; function values are invoked once with `input` and their return
519
+ * is used. Functions returning further functions are NOT unwrapped — return
520
+ * the final value.
521
+ */
522
+ declare function resolveField<T, I>(v: Resolvable<T, I> | undefined, input: I): T | undefined;
523
+ /** Path-style shortcut for an edge. Maps to the canvas router + pathStyle pair. */
524
+ type EdgePathType = 'straight' | 'bezier' | 'quadratic' | 'bump-radial' | 'bump-horizontal' | 'step-radial' | 'orth' | 'manhattan' | 'rounded' | 'smooth' | 'bundle' | 'loop-curve' | 'loop-polyline';
525
+ /**
526
+ * Endpoint anchor.
527
+ *
528
+ * - `'boundary'` (default) — trim the endpoint at the node's outline along
529
+ * the line from the other endpoint. Visually the edge stops at the node
530
+ * boundary; works with arrows and connector decorations cleanly.
531
+ * - `'center'` — leave the endpoint at the node's centre. The edge passes
532
+ * through the node visually; rely on z-order (nodes drawn on top) to make
533
+ * it look like the edge terminates at the boundary. Pick this for radial
534
+ * layouts so polar pathStyles (e.g. `bump-radial`) compute their tangent
535
+ * from the true node-centre angle rather than the trimmed cut point.
536
+ * - `'perpendicular'` — exit / enter perpendicular to the host edge of a
537
+ * rect-like node. Reserved for box-shaped nodes.
538
+ * - `'edge-port'` — attach to a specific point on one face of the node's
539
+ * bounding box, picked by `{ side, offset }` on the per-endpoint
540
+ * `sourceAnchorOpts` / `targetAnchorOpts`. Used by the Sankey layout to
541
+ * stack ribbons along the right face of source and left face of target.
542
+ *
543
+ * Widened to `string` so anchors registered at runtime (e.g. domain-specific
544
+ * port anchors) can be referenced by name.
545
+ */
546
+ type EdgeAnchor = 'boundary' | 'center' | 'perpendicular' | 'edge-port' | (string & {});
547
+ /** Initial-load shape passed to `graphLayer.setData(data)`. */
548
+ interface GraphData {
549
+ nodes: GraphNode[];
550
+ edges: GraphEdge[];
551
+ }
552
+ /**
553
+ /**
554
+ * Canonical interaction-state names with sensible defaults baked into the
555
+ * GraphLayer's resolver. State styling lives on the layer-level
556
+ * {@link NodeOption.state} / per-node {@link NodeData.state} catalogue —
557
+ * `default` is intentionally absent (it's the absence of any active state,
558
+ * not a state itself).
559
+ *
560
+ * The state-config map is open-keyed: consumers can declare additional
561
+ * named states (e.g. `'pinned'`, `'flagged'`, `'error'`, `'focused'`)
562
+ * directly on `options.node.state` / `node.state` with the same shape —
563
+ * they compose via the same merge rules. The canonical set below is
564
+ * deliberately small; reach for it only when the named driver applies.
565
+ *
566
+ * ### Driver → state map
567
+ *
568
+ * Each canonical state has a distinct *driver* (what causes the state to
569
+ * be written) and *lifetime* (when it clears). The visual treatments
570
+ * overlap (most are stroke rings of various colours), but the semantics
571
+ * do not — a single node can carry several states simultaneously (e.g.
572
+ * `selected + hover`) and a behaviour should only write the states it
573
+ * owns.
574
+ *
575
+ * | State | Driver | Lifetime | Cardinality |
576
+ * | ------------- | ----------------------------------------- | --------------------------------- | ----------------- |
577
+ * | `hovered` | Mouse / touch pointer-over | Transient — clears on pointer-out | ≤ 1 per layer |
578
+ * | `selected` | Click / lasso / brush — user's chosen set | Sticky until explicitly cleared | 0–N per layer |
579
+ * | `highlighted` | 1-hop neighbours of the hovered / selected | Transient — clears with the driver | 0–N per layer |
580
+ * | `dimmed` | Complement of the focal-emphasis set | Transient — clears with the driver | 0–N per layer |
581
+ * | `disabled` | Data flag — "not interactive" | Sticky — owned by the data feed | 0–N per layer |
582
+ *
583
+ * ### Sticky chosen set — `selected`
584
+ *
585
+ * `selected` is the click / lasso / brush state — what the user *chose*.
586
+ * Persists until explicitly deselected. **Multi-select doesn't need its
587
+ * own state**: it's just the same `selected` state applied to every
588
+ * member of `selectedIds: Set<string>`. One node selected → one ring;
589
+ * ten nodes selected → ten rings.
590
+ *
591
+ * `selected` can co-exist with `hovered` — clicking a node doesn't stop
592
+ * it from being hovered.
593
+ *
594
+ * ### Focal-emphasis flow — `highlighted` + `dimmed`
595
+ *
596
+ * Written together by a focal-emphasis behaviour (typically driven by
597
+ * hover or selection). When the user hovers / selects a node:
598
+ * - its 1-hop neighbours go `highlighted` — *supporting cast*,
599
+ * - everyone else goes `dimmed` — *pushed back so the focal set pops*.
600
+ *
601
+ * Both clear together when emphasis ends. Drop the pair if the product
602
+ * never needs the "fade everyone except the focal subgraph" interaction.
603
+ *
604
+ * ### Data-driven — `disabled`
605
+ *
606
+ * Sticky and owned by the data feed (not by an interaction behaviour).
607
+ * `disabled` is "this node isn't interactive — don't let the user pick
608
+ * it". Visually overlaps `dimmed` but they're semantically distinct:
609
+ * - `dimmed` says *"you're focusing elsewhere"* (transient, behaviour).
610
+ * - `disabled` says *"you can't interact with me"* (sticky, data).
611
+ * Conflating them would couple interaction code to data code — keep them
612
+ * separate even if the visual treatment is similar.
613
+ */
614
+ type CanonicalStateName =
615
+ /** Mouse / touch pointer is over the node. Transient; one node at a time. */
616
+ 'hovered'
617
+ /** User's chosen set (click / lasso / brush). Sticky; many at a time. Multi-select is just this state applied to each member of the selection. */
618
+ | 'selected'
619
+ /** 1-hop neighbour of the hovered / selected focal — "supporting cast". Transient; cleared with the focal. */
620
+ | 'highlighted'
621
+ /** Complement of the focal-emphasis set — pushed back so `selected` + `highlighted` pop. Transient. NOT `disabled` — that's a data flag. */
622
+ | 'dimmed'
623
+ /** Data flag: "not interactive". Sticky; owned by the data feed. Visually similar to `dimmed` but semantically distinct (data, not interaction). */
624
+ | 'disabled';
625
+ /** Rect-shape option. `cornerRadius` is optional; everything else required. */
626
+ interface RectShapeOption {
627
+ readonly kind: 'rect';
628
+ readonly width: number;
629
+ readonly height: number;
630
+ readonly cornerRadius?: number;
631
+ }
632
+ /** Circle-shape option. */
633
+ interface CircleShapeOption {
634
+ readonly kind: 'circle';
635
+ readonly radius: number;
636
+ }
637
+ /** Arc (annular sector) shape option. All four geometry params required. */
638
+ interface ArcShapeOption {
639
+ readonly kind: 'arc';
640
+ readonly innerR: number;
641
+ readonly outerR: number;
642
+ readonly startAngle: number;
643
+ readonly endAngle: number;
644
+ }
645
+ /**
646
+ * Regular n-gon. With `rotation = 0` the first vertex points straight up, so
647
+ * a triangle / pentagon / hexagon points up by default. Pass
648
+ * `rotation: Math.PI / sides` for flat-top.
649
+ */
650
+ interface RegularPolygonShapeOption {
651
+ readonly kind: 'regular-polygon';
652
+ readonly sides: number;
653
+ readonly radius: number;
654
+ readonly rotation?: number;
655
+ }
656
+ /**
657
+ * N-pointed star. Classic 5-point star uses
658
+ * `{ points: 5, outerRadius: r, innerRadius: r * 0.4 }`.
659
+ */
660
+ interface StarShapeOption {
661
+ readonly kind: 'star';
662
+ readonly points: number;
663
+ readonly innerRadius: number;
664
+ readonly outerRadius: number;
665
+ readonly rotation?: number;
666
+ }
667
+ /**
668
+ * Free-form polygon. `vertices` are centre-relative — closed implicitly
669
+ * (last vertex connects to first).
670
+ */
671
+ interface PolygonShapeOption {
672
+ readonly kind: 'polygon';
673
+ readonly vertices: ReadonlyArray<Point>;
674
+ }
675
+ /**
676
+ * Escape-hatch variant for shape kinds registered at runtime via
677
+ * `canvas.primitives.registerShape(name, ctor)`. The widened `kind` accepts
678
+ * any string the type-checker can't match against a built-in variant;
679
+ * additional spec params are erased at the type level but pass through to
680
+ * the renderer untouched at runtime (the adapter spreads the whole shape
681
+ * record into the spec).
682
+ *
683
+ * Authors of custom shapes typically declare a local interface
684
+ * (`interface ChevronShapeOption { kind: 'chevron'; size: number }`) and
685
+ * cast at the boundary (`style: { shape: chevron as NodeShapeOptions }`).
686
+ * The index signature was deliberately omitted here so that discriminant
687
+ * narrowing on the typed built-in variants (`shape.kind === 'rect'` →
688
+ * `RectShapeOption`) keeps working everywhere else in the codebase.
689
+ *
690
+ * Built-in kinds (`'rect'`, `'circle'`, `'arc'`, `'regular-polygon'`,
691
+ * `'star'`, `'polygon'`) are matched by the typed variants above before
692
+ * this fallback applies.
693
+ */
694
+ interface CustomShapeOption {
695
+ readonly kind: string & {};
696
+ }
697
+ /**
698
+ * Closed union of the six shape kinds that `@invana/canvas` registers out
699
+ * of the box. Exported so internal switch-narrowing sites can target it
700
+ * directly via the {@link isBuiltInNodeShape} type guard.
701
+ */
702
+ type BuiltInNodeShapeOptions = RectShapeOption | CircleShapeOption | ArcShapeOption | RegularPolygonShapeOption | StarShapeOption | PolygonShapeOption;
703
+ /**
704
+ * Discriminated union of node shape options. The `kind` field enforces
705
+ * per-variant required fields at compile time for the six built-in kinds
706
+ * registered by `@invana/canvas`. {@link CustomShapeOption} provides an
707
+ * open-keyed fallback for shapes registered at runtime by the consumer.
708
+ *
709
+ * Internal call sites that need to read variant-specific fields should
710
+ * narrow via the {@link isBuiltInNodeShape} type guard first — the
711
+ * open-keyed `CustomShapeOption.kind` prevents `switch (shape.kind)` over
712
+ * literals from excluding the custom variant on its own.
713
+ */
714
+ type NodeShapeOptions = BuiltInNodeShapeOptions | CustomShapeOption;
715
+ /**
716
+ * Type guard separating the typed built-in variants from
717
+ * {@link CustomShapeOption}. Use this before reading variant-specific
718
+ * fields so TypeScript narrows cleanly inside each `case`.
719
+ */
720
+ declare function isBuiltInNodeShape(shape: NodeShapeOptions): shape is BuiltInNodeShapeOptions;
721
+ /**
722
+ * Vector inset rendered inside a node's body — glyph (font codepoint), SVG
723
+ * path, or SVG by URL. Kept structured (discriminated union) because each
724
+ * kind carries different required params.
725
+ */
726
+ type NodeIcon = {
727
+ readonly kind: 'glyph';
728
+ readonly char: string;
729
+ readonly fontFamily?: string;
730
+ readonly fontWeight?: number | string;
731
+ readonly fontStyle?: 'normal' | 'italic';
732
+ readonly color?: number;
733
+ readonly alpha?: number;
734
+ readonly sizeRatio?: number;
735
+ readonly anchor?: InsetAnchor;
736
+ } | {
737
+ readonly kind: 'svg';
738
+ readonly pathD: string;
739
+ readonly viewBox?: {
740
+ readonly width: number;
741
+ readonly height: number;
742
+ };
743
+ readonly strokeWidth?: number;
744
+ readonly color?: number;
745
+ readonly alpha?: number;
746
+ readonly sizeRatio?: number;
747
+ readonly anchor?: InsetAnchor;
748
+ } | {
749
+ readonly kind: 'svg-url';
750
+ readonly url: string;
751
+ readonly viewBox?: {
752
+ readonly width: number;
753
+ readonly height: number;
754
+ };
755
+ readonly strokeWidth?: number;
756
+ readonly color?: number;
757
+ readonly alpha?: number;
758
+ readonly sizeRatio?: number;
759
+ readonly anchor?: InsetAnchor;
760
+ };
761
+ /**
762
+ * Raster image attached to a node. Mirrors the canvas-level `kind: 'image'`
763
+ * `ShapeFillLayer` field-for-field. Two orthogonal sizing knobs:
764
+ *
765
+ * - `fit` (default `'cover'`) — `'cover'` scales by `max(...)` and fully
766
+ * covers the silhouette's AABB (may crop on the cross-axis);
767
+ * `'contain'` scales by `min(...)` and fully fits, leaving the
768
+ * cross-axis margin transparent (the underlying `bgFill` reads
769
+ * through; the texture sampler is pinned to `clamp-to-edge` so the
770
+ * margin doesn't tile).
771
+ * - `padding` (default `0`) — pixel inset on the silhouette before fit
772
+ * math runs. The silhouette is re-traced at that inset for the image
773
+ * layer only, so the gap between full and inset silhouette paints
774
+ * from layers underneath (typically a `solid` `bgFill`). Useful when
775
+ * the host silhouette is more restrictive than its AABB (circle,
776
+ * polygon, star, arc) and texture corners would otherwise clip
777
+ * against the curve.
778
+ */
779
+ interface NodeImage {
780
+ readonly url: string;
781
+ readonly alpha?: number;
782
+ readonly fit?: 'cover' | 'contain';
783
+ readonly padding?: number;
784
+ }
785
+ /**
786
+ * Anchor point on the host node where a badge attaches. The eight cardinal
787
+ * names address the midpoints / corners of the host's axis-aligned bounding
788
+ * box; the `{ x, y }` variant pins to an explicit world point and is rarely
789
+ * needed (use {@link NodeBadge.offsetX} / `offsetY` to nudge an enum anchor
790
+ * before reaching for raw coordinates).
791
+ */
792
+ type BadgePlacement = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top' | 'bottom' | 'left' | 'right' | {
793
+ readonly x: number;
794
+ readonly y: number;
795
+ };
796
+ /**
797
+ * Point on the badge's own AABB that lands at the host anchor.
798
+ *
799
+ * - The eight cardinal names mirror {@link BadgePlacement} (without the
800
+ * custom `{x, y}` variant — origin is always a named point on the badge).
801
+ * - `'center'` centres the badge on the host anchor — yields the classic
802
+ * "half-overhanging" notification-bubble look.
803
+ *
804
+ * When omitted, the projection defaults to the **mirror** of `placement`
805
+ * (e.g. `placement: 'top-right'` → origin `'bottom-left'`) so the badge
806
+ * sits fully outside the host edge.
807
+ */
808
+ type BadgeOrigin = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top' | 'bottom' | 'left' | 'right' | 'center';
809
+ /**
810
+ * Host-modulation effects on a badge. Re-exports the {@link NodeEffects}
811
+ * surface because a badge is rendered as a shape under the hood — the same
812
+ * `shake` / `breathing` / future shape-effect kinds apply field-for-field.
813
+ */
814
+ type BadgeEffects = NodeEffects;
815
+ /**
816
+ * Small overlay attached to a node — e.g. notification dot, count chip,
817
+ * status indicator. A badge is rendered as a real shape, so it inherits the
818
+ * full shape surface: any registered {@link NodeShapeOptions} kind as the
819
+ * plate, optional {@link NodeIcon} as content, optional label text, plus
820
+ * nested {@link decorations} / {@link effects} that compose exactly the way
821
+ * they do on a node body.
822
+ *
823
+ * Position resolves from the host's AABB + the `placement` anchor + an
824
+ * `origin` (which point of the badge sits at the anchor — defaults to the
825
+ * mirror of `placement` so the badge nests fully outside the host edge).
826
+ * Use `'center'` for the half-overhanging notification-bubble look.
827
+ */
828
+ interface NodeBadge {
829
+ /**
830
+ * Stable id within the node, for keyed updates / state-overlay diffing.
831
+ * When omitted, identity falls back to the badge's position in the
832
+ * containing `badges[]` array.
833
+ */
834
+ readonly id?: string;
835
+ /** Anchor point on the host node's AABB, or an explicit world point. */
836
+ readonly placement: BadgePlacement;
837
+ /**
838
+ * Which point of the badge's own AABB lands at the host anchor.
839
+ * Default: mirror of `placement` (badge sits fully outside the host edge).
840
+ * Use `'center'` for the half-overhanging look.
841
+ */
842
+ readonly origin?: BadgeOrigin;
843
+ /**
844
+ * Pure geometry — any registered {@link NodeShapeOptions} kind. Fill /
845
+ * stroke / alpha come from the flat sugar fields below, mirroring the
846
+ * `NodeStyle.shape` + `bgFill` split used for node bodies.
847
+ */
848
+ readonly shape: NodeShapeOptions;
849
+ /** Solid plate colour — projects to the badge shape's first fill layer. */
850
+ readonly fill?: number;
851
+ readonly alpha?: number;
852
+ readonly strokeColor?: number;
853
+ readonly strokeWidth?: number;
854
+ /**
855
+ * Vector inset rendered inside the badge plate (glyph / svg / svg-url).
856
+ * Projects to an extra fill layer stacked on top of the solid plate.
857
+ */
858
+ readonly icon?: NodeIcon;
859
+ /**
860
+ * Optional short text rendered centred on the badge (count "3", "!").
861
+ * Projects to a `'label'` decoration on the badge.
862
+ */
863
+ readonly labelText?: string;
864
+ readonly labelColor?: number;
865
+ readonly labelFontSize?: number;
866
+ /** Pixel offset applied after placement resolution. */
867
+ readonly offsetX?: number;
868
+ readonly offsetY?: number;
869
+ readonly zIndex?: number;
870
+ /**
871
+ * Decorations attached to the badge plate. Each entry is a regular
872
+ * {@link NodeDecorationSpec} — glow, ring, marching-ants, pulse-ring, etc.
873
+ * Identity / merge rules match {@link NodeStyle.decorations} (id-keyed,
874
+ * `remove: true` drops earlier same-id entries from base under a state
875
+ * overlay).
876
+ */
877
+ readonly decorations?: readonly NodeDecorationSpec[];
878
+ /**
879
+ * Effects modulating the badge plate's transform / style each frame
880
+ * (`shake`, `breathing`, …). Same surface as {@link NodeStyle.effects}.
881
+ */
882
+ readonly effects?: BadgeEffects;
883
+ }
884
+ /**
885
+ * Anchor point along an edge's routed path.
886
+ *
887
+ * - `'start'` / `'end'` — anchored *near* the source / target endpoint with
888
+ * automatic clearance: the badge is shifted tangentially by its own
889
+ * half-extent so it kisses the endpoint node's silhouette from outside
890
+ * rather than half-overlapping it. The natural choice for endpoint
891
+ * chips, status icons, etc.
892
+ * - `'middle'` — exact arc-length midpoint (`t = 0.5`).
893
+ * - A `number` in `[0, 1]` — raw arc-length `t`, no clearance applied.
894
+ * Use `placement: 1` when you explicitly want a badge centred on the
895
+ * silhouette point. Values outside `[0, 1]` are clamped.
896
+ *
897
+ * `'middle'` (not `'center'`) avoids term-clashing with `BadgeOrigin`
898
+ * where `'center'` means "centre the badge on its own AABB".
899
+ */
900
+ type EdgeBadgePlacement = 'start' | 'middle' | 'end' | number;
901
+ /**
902
+ * Small overlay attached to an edge — e.g. flow-rate chip on the midpoint,
903
+ * count badge at the source endpoint, arrow-tag at the target. A badge is
904
+ * rendered as a real shape (any registered {@link NodeShapeOptions} kind);
905
+ * placement is parametric along the routed path.
906
+ *
907
+ * Position resolves via `samplePathAt(path, t)` so the badge re-anchors
908
+ * automatically when the path changes (source / target shape moves, anchor
909
+ * / router / waypoints change). For loop edges, `'middle'` naturally lands
910
+ * on the loop apex because the path passes through it at `t ≈ 0.5`.
911
+ *
912
+ * Decorations and effects compose exactly the way they do on
913
+ * {@link NodeBadge}; the badge being shape-rendered means shape decorations
914
+ * (`glow`, `ring`, `marching-ants`, `pulse-ring`, …) apply uniformly.
915
+ */
916
+ interface EdgeBadge {
917
+ /**
918
+ * Stable id within the edge, for keyed updates / state-overlay diffing.
919
+ * When omitted, identity falls back to the badge's position in the
920
+ * containing `badges[]` array.
921
+ */
922
+ readonly id?: string;
923
+ /** Where along the routed path the badge attaches. */
924
+ readonly placement: EdgeBadgePlacement;
925
+ /**
926
+ * Which point of the badge's own AABB lands at the path anchor.
927
+ * Default for edge badges is `'center'` — the badge centres on the path
928
+ * point. Use other origins to lift the badge off the line (e.g.
929
+ * `origin: 'bottom'` puts the badge above the line with its bottom edge
930
+ * touching the path).
931
+ */
932
+ readonly origin?: BadgeOrigin;
933
+ /**
934
+ * Pure geometry — any registered {@link NodeShapeOptions} kind. Fill /
935
+ * stroke / alpha come from the flat sugar fields below, mirroring the
936
+ * `NodeStyle.shape` + `bgFill` split used for node bodies.
937
+ */
938
+ readonly shape: NodeShapeOptions;
939
+ /** Solid plate colour — projects to the badge shape's first fill layer. */
940
+ readonly fill?: number;
941
+ readonly alpha?: number;
942
+ readonly strokeColor?: number;
943
+ readonly strokeWidth?: number;
944
+ /**
945
+ * Vector inset rendered inside the badge plate (glyph / svg / svg-url).
946
+ * Projects to an extra fill layer stacked on top of the solid plate.
947
+ */
948
+ readonly icon?: NodeIcon;
949
+ /**
950
+ * Optional short text rendered centred on the badge (count "3", "!").
951
+ * Projects to a `'label'` decoration on the badge.
952
+ */
953
+ readonly labelText?: string;
954
+ readonly labelColor?: number;
955
+ readonly labelFontSize?: number;
956
+ /**
957
+ * Shift the path-anchor along the local tangent (positive = forward
958
+ * toward `'end'`, negative = backward toward `'start'`). Useful for
959
+ * nudging a `'middle'`-anchored badge sideways without changing `t`.
960
+ */
961
+ readonly pathOffset?: number;
962
+ /** Pixel offset applied after placement resolution. */
963
+ readonly offsetX?: number;
964
+ readonly offsetY?: number;
965
+ /**
966
+ * When `true`, the badge rotates to follow the path tangent at the
967
+ * anchor point. Default `false` (badges stay axis-aligned). Useful for
968
+ * arrow-shaped or directional badges on curved edges.
969
+ */
970
+ readonly autoRotate?: boolean;
971
+ /**
972
+ * When {@link autoRotate} is `true`, flip the badge by 180° on the
973
+ * "downward" half of the path so text decorations stay readable on
974
+ * every edge orientation. Default `true`. Ignored when `autoRotate` is
975
+ * `false`.
976
+ */
977
+ readonly keepUpright?: boolean;
978
+ readonly zIndex?: number;
979
+ /**
980
+ * Decorations attached to the badge plate. Same surface as
981
+ * {@link NodeBadge.decorations} — shape decorations (`glow`, `ring`,
982
+ * `marching-ants`, `pulse-ring`) apply because the badge is itself a
983
+ * shape, regardless of being hosted on a connector.
984
+ */
985
+ readonly decorations?: readonly NodeDecorationSpec[];
986
+ /**
987
+ * Effects modulating the badge plate's transform / style each frame
988
+ * (`shake`, `breathing`, …). Same surface as
989
+ * {@link NodeBadge.effects}.
990
+ */
991
+ readonly effects?: BadgeEffects;
992
+ }
993
+ /**
994
+ * Common fields on every entry in a `decorations[]` array. The `id` gives
995
+ * stable diff identity (state overlays can re-declare the same id to
996
+ * override, or set `remove: true` to drop a base-level decoration while a
997
+ * state is active). When `id` is absent, identity falls back to `kind + array index`.
998
+ */
999
+ interface DecorationSpecCommon {
1000
+ /** Stable id for diffing. Optional — falls back to `kind#<index>` when absent. */
1001
+ readonly id?: string;
1002
+ /**
1003
+ * When `true`, this entry instructs the resolver to drop any earlier-
1004
+ * precedence decoration with the same `id`. Use it in a state overlay to
1005
+ * temporarily remove a base-level decoration while the state is active.
1006
+ */
1007
+ readonly remove?: boolean;
1008
+ }
1009
+ /**
1010
+ * Discriminated union of decoration specs attachable to a node via
1011
+ * {@link NodeStyle.decorations}. Each variant pairs `kind` (the registered
1012
+ * canvas decoration name) with the matching style payload from
1013
+ * `@invana/canvas/primitives`.
1014
+ *
1015
+ * Multiples are allowed — the same kind can appear several times (e.g. an
1016
+ * inner + outer ring on a single node), as long as their `id`s differ.
1017
+ * `label` is intentionally absent — labels are managed by the flat
1018
+ * `labelText` / `label*` fields on `NodeStyle`, not by the decorations
1019
+ * array.
1020
+ */
1021
+ type NodeDecorationSpec = (DecorationSpecCommon & {
1022
+ readonly kind: 'ring';
1023
+ } & RingDecorationStyle) | (DecorationSpecCommon & {
1024
+ readonly kind: 'glow';
1025
+ } & GlowDecorationStyle) | (DecorationSpecCommon & {
1026
+ readonly kind: 'pulse-ring';
1027
+ } & PulseRingDecorationStyle) | (DecorationSpecCommon & {
1028
+ readonly kind: 'marching-ants';
1029
+ } & MarchingAntsDecorationStyle) | (DecorationSpecCommon & {
1030
+ readonly kind: 'liquid-fill';
1031
+ } & LiquidFillDecorationStyle) | (DecorationSpecCommon & {
1032
+ readonly kind: 'toggle';
1033
+ } & ToggleDecorationStyle) | (DecorationSpecCommon & {
1034
+ readonly kind: 'resize-handle';
1035
+ } & ResizeHandleDecorationStyle) | (DecorationSpecCommon & {
1036
+ readonly kind: 'selection-frame';
1037
+ } & SelectionFrameDecorationStyle);
1038
+ /**
1039
+ * Discriminated union of decoration specs attachable to an edge via
1040
+ * {@link EdgeStyle.decorations}. Mirrors {@link NodeDecorationSpec} for
1041
+ * the connector-target decoration registry. `label-connector` is excluded
1042
+ * for the same reason `label` is — labels live on the flat label fields.
1043
+ */
1044
+ type EdgeDecorationSpec = (DecorationSpecCommon & {
1045
+ readonly kind: 'ring-connector';
1046
+ } & RingConnectorDecorationStyle) | (DecorationSpecCommon & {
1047
+ readonly kind: 'glow-connector';
1048
+ } & GlowConnectorDecorationStyle) | (DecorationSpecCommon & {
1049
+ readonly kind: 'marching-ants-connector';
1050
+ } & MarchingAntsConnectorDecorationStyle) | (DecorationSpecCommon & {
1051
+ readonly kind: 'ripple-connector';
1052
+ } & RippleConnectorDecorationStyle) | (DecorationSpecCommon & {
1053
+ readonly kind: 'fly-marker-connector';
1054
+ } & FlyMarkerConnectorDecorationStyle) | (DecorationSpecCommon & {
1055
+ readonly kind: 'flow-particles-connector';
1056
+ } & FlowParticlesConnectorDecorationStyle) | (DecorationSpecCommon & {
1057
+ readonly kind: 'reveal-connector';
1058
+ } & RevealConnectorDecorationStyle);
1059
+ /**
1060
+ * Host-modulation effects (sibling of decorations). Effects don't add
1061
+ * geometry — they modulate the host's transform (`shake`, `breathing`) or
1062
+ * style channels (tint/alpha). One spec per kind.
1063
+ */
1064
+ interface NodeEffects {
1065
+ readonly shake?: unknown | null;
1066
+ readonly breathing?: unknown | null;
1067
+ readonly [kind: string]: unknown | null | undefined;
1068
+ }
1069
+ /**
1070
+ * Marks a node as a **compound group** — a visual frame drawn behind its
1071
+ * descendants (children point to it via `parentId`). The presence of this
1072
+ * field on a node's resolved {@link NodeStyle} is the only signal the layer
1073
+ * uses to decide whether to apply group semantics; the structural shape
1074
+ * (`style.shape`) stays a regular `rect` / `circle` / etc.
1075
+ *
1076
+ * Group semantics, in summary:
1077
+ *
1078
+ * - **Expanded state** (`collapsed !== true`):
1079
+ * - The node renders behind its children (z-index pushed underneath when
1080
+ * `behindChildren !== false`) and is **non-hittable** — pointer events
1081
+ * pass through the frame to the canvas background. The frame is a pure
1082
+ * drawing, not an interactive node.
1083
+ * - With `autoFit: true`, the layer recomputes `width` / `height` (rect)
1084
+ * or `radius` (circle) every flush from the children's bounding box,
1085
+ * plus `padding` and optional `headerHeight`. The declared `width` /
1086
+ * `height` / `radius` fields act as a **lower bound** in this mode.
1087
+ * - With `autoFit: false`, the layer uses the declared `width` / `height`
1088
+ * / `radius` literally; children may visually leak outside.
1089
+ *
1090
+ * - **Collapsed state** (`collapsed === true`):
1091
+ * - The node renders as a normal interactive node (`hittable: true`,
1092
+ * default z-order). All descendants are hidden from the renderer; edges
1093
+ * pointing at a hidden descendant are re-routed to the nearest visible
1094
+ * collapsed-group ancestor at render time (no mutation to the edge data).
1095
+ * - The layer synthesises a count badge showing the number of hidden
1096
+ * descendants. The `+`/`−` toggle is rendered via the
1097
+ * {@link ToggleDecorationStyle} decoration on the group — wire up
1098
+ * `CollapseExpandBehaviour` to make the toggle clickable.
1099
+ *
1100
+ * Nested groups fall out of the `parentId` chain for free: a group node
1101
+ * whose own `parentId` points at another group becomes a sub-group; the
1102
+ * recompute walks deepest-first.
1103
+ *
1104
+ * Membership uses the existing `GraphNode.parentId` (single hierarchy field
1105
+ * shared with tree structures) — no separate group-membership concept.
1106
+ */
1107
+ interface GroupOptions {
1108
+ /**
1109
+ * When `true`, the frame's size tracks the bounding box of its direct
1110
+ * children (computed every flush). When `false`, the declared `width` /
1111
+ * `height` / `radius` are used verbatim. Default `false`.
1112
+ */
1113
+ readonly autoFit?: boolean;
1114
+ /**
1115
+ * When `true`, `GroupResizeBehaviour` mounts corner / radial handle
1116
+ * decorations on this group and lets the user drag to resize. Composes
1117
+ * with `autoFit` per the floor rule on `width` / `height` / `radius`.
1118
+ * Default `false`.
1119
+ */
1120
+ readonly userResizable?: boolean;
1121
+ /** Inset around the children bbox before the frame outline. Default `16`. */
1122
+ readonly padding?: number;
1123
+ /**
1124
+ * True = render the group as a collapsed super-node (children hidden,
1125
+ * +/- toggle shows `+`, count badge shows the hidden descendant count).
1126
+ * Toggle through `CollapseExpandBehaviour` or by updating this field
1127
+ * directly via `store.updateNode`. Default `false`.
1128
+ */
1129
+ readonly collapsed?: boolean;
1130
+ /**
1131
+ * Frame renders at `style.zIndex − 1` so descendants paint on top. Set to
1132
+ * `false` to keep the frame at its declared z-index (and let descendants
1133
+ * paint underneath when their z-index is lower). Default `true`.
1134
+ */
1135
+ readonly behindChildren?: boolean;
1136
+ /**
1137
+ * Optional header band height (px) added above the children bbox. The
1138
+ * frame still draws as a single rect / circle — `headerHeight` only
1139
+ * shifts the auto-fit recompute so the label area at the top stays clear
1140
+ * of children. Default `0`.
1141
+ */
1142
+ readonly headerHeight?: number;
1143
+ /**
1144
+ * Floor (with `autoFit`) or fixed (without) width. Rect frames only.
1145
+ * Ignored for circle frames.
1146
+ */
1147
+ readonly width?: number;
1148
+ /** Sibling of {@link width} for `kind: 'rect'`. */
1149
+ readonly height?: number;
1150
+ /**
1151
+ * Floor (with `autoFit`) or fixed (without) radius. Circle frames only.
1152
+ * Ignored for rect frames.
1153
+ */
1154
+ readonly radius?: number;
1155
+ /**
1156
+ * Where the auto-attached `+` / `−` toggle sits relative to the group's
1157
+ * frame. Two forms:
1158
+ *
1159
+ * - **Keyword** — one of the {@link TogglePlacement} aliases
1160
+ * (`'bottom'`, `'inside-bottom'`, `'top-right'`, `'bottom-left'`, …).
1161
+ * Resolved against the host's AABB by the toggle decoration.
1162
+ * - **Shape-local coords** — `{ x, y }`, an absolute point inside the
1163
+ * host shape's local frame (centre-relative for `circle`, top-left-
1164
+ * relative for `rect`). Use this when none of the keywords place the
1165
+ * toggle where you want it (diagonal offsets, mock-specific spots).
1166
+ *
1167
+ * Default `'bottom'` — centred just below the silhouette, matching the
1168
+ * "small bubble attached to the rim" pattern in the reference UI.
1169
+ * Clicks are dispatched at the canvas level by `CollapseExpandBehaviour`,
1170
+ * so the toggle remains clickable regardless of whether the resolved
1171
+ * position falls inside or outside the host's hit area.
1172
+ */
1173
+ readonly togglePlacement?: TogglePlacement | {
1174
+ readonly x: number;
1175
+ readonly y: number;
1176
+ };
1177
+ }
1178
+ /**
1179
+ * Visual + structural style for a node. Flat-prefixed scalars for orthogonal
1180
+ * properties (`bgFill`, `bgStrokeWidth`, `labelColor`); polymorphic values
1181
+ * kept structured (`shape`, `icon`, `image`, `decorations`, `effects`,
1182
+ * `badges`).
1183
+ *
1184
+ * Per-instance state overlays for a node live at {@link NodeData.state}
1185
+ * (a sibling of `style`), NOT inside `NodeStyle`.
1186
+ */
1187
+ interface NodeStyle {
1188
+ readonly shape?: NodeShapeOptions;
1189
+ /**
1190
+ * Unified normalized size. When set, overrides the resolved `shape`'s
1191
+ * intrinsic size fields at style-resolution time (before the spec reaches
1192
+ * the renderer, `boundsOfNode`, or any layout's bounds query). Per-kind
1193
+ * mapping:
1194
+ *
1195
+ * - `circle` / `regular-polygon` — `shape.radius = size`
1196
+ * - `rect` — `shape.width = shape.height = 2 * size`
1197
+ * - `arc` — `shape.outerR = size` (and `shape.innerR` scaled so its ratio
1198
+ * to `outerR` is preserved)
1199
+ * - `star` — `shape.outerRadius = size` (and `shape.innerRadius` scaled to
1200
+ * preserve its ratio)
1201
+ * - `polygon` / custom — no canonical size axis; `size` is ignored
1202
+ *
1203
+ * Honoured uniformly by `boundsOfNode`, `D3ForceLayout` (collide.radius
1204
+ * receives the `GraphNode` and reads the normalized `shape.radius` via
1205
+ * `resolveNodeStyle`), and `ElkLayout` (reads bounds via `boundsOfNode`).
1206
+ * Use this when a single number should drive a node's footprint regardless
1207
+ * of which shape kind it renders as — e.g. degree-based sizing,
1208
+ * data-driven scaling.
1209
+ */
1210
+ readonly size?: number;
1211
+ /**
1212
+ * Marks this node as a compound group (visual frame drawn behind its
1213
+ * descendants). See {@link GroupOptions} for the full contract — autoFit
1214
+ * vs userResizable, expanded vs collapsed semantics, header band, edge
1215
+ * re-routing.
1216
+ *
1217
+ * Presence of this field is the only discriminator. The structural shape
1218
+ * (`shape: { kind: 'rect' | 'circle' }`) is unchanged; groups reuse the
1219
+ * same primitives as regular nodes.
1220
+ */
1221
+ readonly group?: GroupOptions;
1222
+ /**
1223
+ * When `true`, `NodeResizeBehaviour` mounts corner-handle decorations on
1224
+ * this node (rect / circle only) and lets the user drag to resize. The
1225
+ * drag writes back to `style.shape.width` / `height` / `radius` directly
1226
+ * (and `position` for non-corner-anchored rect drags). Independent from
1227
+ * `style.group?.userResizable`, which targets group frames specifically
1228
+ * — but both are honoured by the same behaviour, so a single registered
1229
+ * `NodeResizeBehaviour` handles every resizable node in the layer.
1230
+ */
1231
+ readonly resizable?: boolean;
1232
+ /**
1233
+ * Accepts every `ShapeFillLayer` kind — `solid` / `image` / `glyph` /
1234
+ * `svg` / `svg-url` — and arrays for stacked layers. The `image` kind
1235
+ * doubles as silhouette filler and inset content via its `fit` field
1236
+ * (`'inset'` vs the silhouette modes).
1237
+ */
1238
+ readonly bgFill?: ShapeFill;
1239
+ readonly bgAlpha?: number;
1240
+ readonly bgStrokeColor?: number;
1241
+ readonly bgStrokeAlpha?: number;
1242
+ readonly bgStrokeWidth?: number;
1243
+ readonly bgStrokeAlignment?: 'inside' | 'center' | 'outside';
1244
+ readonly bgStrokeDashArray?: readonly [number, number];
1245
+ readonly bgStrokeDashOffset?: number;
1246
+ readonly bgStrokeCap?: 'butt' | 'round' | 'square';
1247
+ readonly bgStrokeJoin?: 'miter' | 'round' | 'bevel';
1248
+ readonly icon?: NodeIcon;
1249
+ readonly image?: NodeImage;
1250
+ readonly labelText?: string;
1251
+ readonly labelColor?: number;
1252
+ readonly labelFontSize?: number;
1253
+ readonly labelFontFamily?: string;
1254
+ readonly labelFontWeight?: number | string;
1255
+ readonly labelFontStyle?: 'normal' | 'italic';
1256
+ readonly labelAlign?: 'left' | 'center' | 'right';
1257
+ readonly labelLineHeight?: number;
1258
+ readonly labelLetterSpacing?: number;
1259
+ readonly labelPlacement?: ShapeLabelPlacement;
1260
+ readonly labelOffsetX?: number;
1261
+ readonly labelOffsetY?: number;
1262
+ readonly labelAlpha?: number;
1263
+ readonly labelMinFontSize?: number;
1264
+ /** Radians. */
1265
+ readonly labelRotation?: number;
1266
+ /** Hide the label below this camera zoom level. */
1267
+ readonly labelMinZoom?: number;
1268
+ /** Hide the label above this camera zoom level. */
1269
+ readonly labelMaxZoom?: number;
1270
+ /** Collision priority — higher wins when two labels overlap. */
1271
+ readonly labelPriority?: number;
1272
+ /** Collision partition — labels in different groups never compete. */
1273
+ readonly labelCollisionGroup?: string;
1274
+ /** Bypass collision entirely — label always renders. */
1275
+ readonly labelForceShow?: boolean;
1276
+ readonly labelBackgroundFill?: number;
1277
+ readonly labelBackgroundAlpha?: number;
1278
+ readonly labelBackgroundStrokeColor?: number;
1279
+ readonly labelBackgroundStrokeWidth?: number;
1280
+ readonly labelBackgroundPadding?: number;
1281
+ readonly labelBackgroundCornerRadius?: number;
1282
+ /**
1283
+ * Escape hatch — full `ShapeLabelStyle` payload from `@invana/canvas`.
1284
+ * Use this when the flat `label*` fields don't cover the case (wrap,
1285
+ * html-text content, custom collision settings, etc.). When set, the
1286
+ * adapter uses this payload verbatim instead of building one from the
1287
+ * flat fields. Flat label fields are ignored on the same node.
1288
+ */
1289
+ readonly labelStyle?: ShapeLabelStyle;
1290
+ readonly badges?: readonly NodeBadge[];
1291
+ /**
1292
+ * Ordered list of decorations attached to the node. Each entry's `kind`
1293
+ * names a registered canvas decoration; the rest of the entry is that
1294
+ * decoration's style payload. See {@link NodeDecorationSpec}.
1295
+ *
1296
+ * The resolver concatenates this array across base style + every active
1297
+ * state's overlay, then dedupes by `id` (later precedence wins). Use
1298
+ * `remove: true` in a higher-precedence overlay to drop an earlier entry
1299
+ * with the same id while a state is active.
1300
+ */
1301
+ readonly decorations?: readonly NodeDecorationSpec[];
1302
+ readonly effects?: NodeEffects;
1303
+ }
1304
+ /**
1305
+ * Resolver-aware mirror of {@link NodeStyle}. Each field is either a static
1306
+ * value or `(D) => T`. Two scopes use this generic at different `D`:
1307
+ *
1308
+ * - `NodeInput<D>.style` — resolvers fire at insert (`D` = raw node data).
1309
+ * - `NodeOption.style` — resolvers fire at render (`D` = stored `GraphNode`).
1310
+ */
1311
+ type ResolvableNodeStyle<D = unknown> = {
1312
+ readonly [K in keyof NodeStyle]?: Resolvable<NonNullable<NodeStyle[K]>, D>;
1313
+ };
1314
+ /**
1315
+ * Per-instance node descriptor as stored by `GraphStore`. All values
1316
+ * concrete (no functions). Flat field layout matching G6's `NodeData`
1317
+ * convention.
1318
+ *
1319
+ * - `state` (singular) = per-instance overlay catalogue.
1320
+ * - `states` (plural) = currently-active state names.
1321
+ */
1322
+ interface NodeData<D = unknown> {
1323
+ readonly id: string;
1324
+ /** Type tag (free-form). Matches a `NodeOption` template if any. */
1325
+ readonly type?: string;
1326
+ readonly data?: D;
1327
+ readonly style?: NodeStyle;
1328
+ /** Per-instance overlay catalogue (singular `state`). */
1329
+ readonly state?: Readonly<Record<string, NodeStyle>>;
1330
+ /** Currently-active state names (plural `states`). */
1331
+ readonly states?: readonly string[] | null;
1332
+ readonly position?: {
1333
+ readonly x: number;
1334
+ readonly y: number;
1335
+ };
1336
+ readonly pinned?: boolean;
1337
+ /**
1338
+ * Logical parent id — the only hierarchy field. Use this for both tree
1339
+ * structures AND group/combo membership (the parent is just a regular
1340
+ * node that visually represents the group). The store auto-maintains an
1341
+ * inverse index, queryable via `store.childrenOf(id)` /
1342
+ * `store.descendantsOf(id)`.
1343
+ */
1344
+ readonly parentId?: string;
1345
+ }
1346
+ /**
1347
+ * What the consumer passes to `GraphLayer.setData`. Same shape as
1348
+ * {@link NodeData} but with Resolvable fields — `id` and per-field styles
1349
+ * may be functions over `data`. Resolvers fire once at insert; the store
1350
+ * holds `NodeData`.
1351
+ */
1352
+ interface NodeInput<D = unknown> {
1353
+ readonly id?: ResolvableId<D>;
1354
+ readonly type?: string;
1355
+ readonly data?: D;
1356
+ readonly style?: ResolvableNodeStyle<D>;
1357
+ readonly state?: Readonly<Record<string, ResolvableNodeStyle<D>>>;
1358
+ readonly states?: readonly string[];
1359
+ readonly position?: {
1360
+ readonly x: number;
1361
+ readonly y: number;
1362
+ };
1363
+ readonly pinned?: boolean;
1364
+ readonly parentId?: string;
1365
+ }
1366
+ /**
1367
+ * Layer-level node template — G6's `node` field on GraphOptions. Resolvers
1368
+ * fire every frame against the stored `GraphNode`.
1369
+ *
1370
+ * No `animation` field — per [[feedback_decoration_vs_animation]], animation
1371
+ * is the per-frame engine, not a node-level config. Decoration / effect
1372
+ * attachments live on `NodeStyle.decorations` / `NodeStyle.effects`.
1373
+ */
1374
+ interface NodeOption {
1375
+ /** Type tag this template defines (e.g. 'person', 'doc'). Optional. */
1376
+ readonly type?: string;
1377
+ readonly style?: ResolvableNodeStyle<GraphNode>;
1378
+ readonly state?: Readonly<Record<string, ResolvableNodeStyle<GraphNode>>>;
1379
+ /** Reserved for palette-driven theming. Deferred wiring. */
1380
+ readonly palette?: unknown;
1381
+ }
1382
+ /** Arrowhead shape catalogue. */
1383
+ type ArrowShape = 'triangle' | 'diamond' | 'circle' | 'none';
1384
+ /**
1385
+ * Structural variant of an edge — the three-stage connector pipeline
1386
+ * (anchor → router → pathStyle). Variant-specific params live inside
1387
+ * `pathStyleOpts`, so this stays non-discriminated.
1388
+ */
1389
+ interface EdgeShapeOptions {
1390
+ readonly pathType?: EdgePathType;
1391
+ readonly sourceAnchor?: EdgeAnchor;
1392
+ readonly targetAnchor?: EdgeAnchor;
1393
+ readonly sourceAnchorOpts?: Readonly<Record<string, unknown>>;
1394
+ readonly targetAnchorOpts?: Readonly<Record<string, unknown>>;
1395
+ readonly pathStyleOpts?: Readonly<Record<string, unknown>>;
1396
+ readonly waypoints?: ReadonlyArray<{
1397
+ readonly x: number;
1398
+ readonly y: number;
1399
+ }>;
1400
+ }
1401
+ /**
1402
+ * Flat-prefixed style bag for an edge. Edges have one stroke (the path), so
1403
+ * stroke fields are unprefixed. Arrow ends and label keep their distinct
1404
+ * prefixes.
1405
+ */
1406
+ interface EdgeStyle {
1407
+ readonly shape?: EdgeShapeOptions;
1408
+ readonly strokeColor?: number;
1409
+ readonly strokeAlpha?: number;
1410
+ readonly strokeWidth?: number;
1411
+ readonly strokeAlignment?: 'inside' | 'center' | 'outside';
1412
+ readonly strokeDashArray?: readonly [number, number];
1413
+ readonly strokeDashOffset?: number;
1414
+ readonly strokeCap?: 'butt' | 'round' | 'square';
1415
+ readonly strokeJoin?: 'miter' | 'round' | 'bevel';
1416
+ readonly arrowSourceShape?: ArrowShape;
1417
+ readonly arrowSourceSize?: number;
1418
+ readonly arrowSourceColor?: number;
1419
+ readonly arrowSourceAlpha?: number;
1420
+ readonly arrowTargetShape?: ArrowShape;
1421
+ readonly arrowTargetSize?: number;
1422
+ readonly arrowTargetColor?: number;
1423
+ readonly arrowTargetAlpha?: number;
1424
+ readonly labelText?: string;
1425
+ readonly labelColor?: number;
1426
+ readonly labelFontSize?: number;
1427
+ readonly labelFontFamily?: string;
1428
+ readonly labelFontWeight?: number | string;
1429
+ readonly labelFontStyle?: 'normal' | 'italic';
1430
+ readonly labelAlign?: 'left' | 'center' | 'right';
1431
+ readonly labelLineHeight?: number;
1432
+ readonly labelLetterSpacing?: number;
1433
+ readonly labelPlacement?: ConnectorLabelPlacement;
1434
+ readonly labelPathOffset?: number;
1435
+ readonly labelAutoRotate?: boolean;
1436
+ readonly labelKeepUpright?: boolean;
1437
+ readonly labelOffsetX?: number;
1438
+ readonly labelOffsetY?: number;
1439
+ readonly labelAlpha?: number;
1440
+ readonly labelMinFontSize?: number;
1441
+ /** Hide the label below this camera zoom level. */
1442
+ readonly labelMinZoom?: number;
1443
+ /** Hide the label above this camera zoom level. */
1444
+ readonly labelMaxZoom?: number;
1445
+ /** Collision priority — higher wins when two labels overlap. */
1446
+ readonly labelPriority?: number;
1447
+ /** Collision partition — labels in different groups never compete. */
1448
+ readonly labelCollisionGroup?: string;
1449
+ /** Bypass collision entirely — label always renders. */
1450
+ readonly labelForceShow?: boolean;
1451
+ readonly labelBackgroundFill?: number;
1452
+ readonly labelBackgroundAlpha?: number;
1453
+ readonly labelBackgroundStrokeColor?: number;
1454
+ readonly labelBackgroundStrokeWidth?: number;
1455
+ readonly labelBackgroundPadding?: number;
1456
+ readonly labelBackgroundCornerRadius?: number;
1457
+ /**
1458
+ * Escape hatch — full `ConnectorLabelStyle` payload from `@invana/canvas`.
1459
+ * Use this when the flat `label*` fields don't cover the case (wrap,
1460
+ * html-text content, etc.). When set, the adapter uses this payload
1461
+ * verbatim instead of building one from the flat fields.
1462
+ */
1463
+ readonly labelStyle?: ConnectorLabelStyle;
1464
+ /**
1465
+ * Ordered list of decorations attached to the edge. Each entry's `kind`
1466
+ * names a registered canvas connector-decoration; the rest is that
1467
+ * decoration's style payload. See {@link EdgeDecorationSpec}.
1468
+ *
1469
+ * Resolver semantics match {@link NodeStyle.decorations}: concatenate
1470
+ * across base + active state overlays, dedupe by `id`, later precedence
1471
+ * wins.
1472
+ */
1473
+ readonly decorations?: readonly EdgeDecorationSpec[];
1474
+ /**
1475
+ * Ordered list of badges attached to the edge. Each entry is a real
1476
+ * {@link EdgeBadge} — any registered shape kind as the plate, optional
1477
+ * icon / labelText sugar, optional nested decorations and effects.
1478
+ * Placement is parametric along the routed path (`'start' | 'middle' |
1479
+ * 'end' | number`) and re-anchors automatically when the path changes
1480
+ * (source / target shape moves, anchor / router / waypoints change).
1481
+ *
1482
+ * Resolver semantics match {@link decorations}: concatenate across base
1483
+ * + active state overlays, dedupe by `id`, later precedence wins.
1484
+ */
1485
+ readonly badges?: readonly EdgeBadge[];
1486
+ }
1487
+ /** Resolver-aware mirror of {@link EdgeStyle}; generic over the resolver argument. */
1488
+ type ResolvableEdgeStyle<D = unknown> = {
1489
+ readonly [K in keyof EdgeStyle]?: Resolvable<NonNullable<EdgeStyle[K]>, D>;
1490
+ };
1491
+ /** Per-instance edge descriptor — stored by GraphStore, concrete values. */
1492
+ interface EdgeData<D = unknown> {
1493
+ readonly id: string;
1494
+ readonly source: string;
1495
+ readonly target: string;
1496
+ /** Predicate / FK label. Free-form. G6 calls this `type`. */
1497
+ readonly type?: string;
1498
+ readonly data?: D;
1499
+ readonly style?: EdgeStyle;
1500
+ readonly state?: Readonly<Record<string, EdgeStyle>>;
1501
+ readonly states?: readonly string[] | null;
1502
+ }
1503
+ /** Resolver-aware input shape for an edge. */
1504
+ interface EdgeInput<D = unknown> {
1505
+ readonly id?: ResolvableId<D>;
1506
+ readonly source: string;
1507
+ readonly target: string;
1508
+ readonly type?: string;
1509
+ readonly data?: D;
1510
+ readonly style?: ResolvableEdgeStyle<D>;
1511
+ readonly state?: Readonly<Record<string, ResolvableEdgeStyle<D>>>;
1512
+ readonly states?: readonly string[];
1513
+ }
1514
+ /** Layer-level edge template — G6's `edge` field. */
1515
+ interface EdgeOption {
1516
+ readonly type?: string;
1517
+ readonly style?: ResolvableEdgeStyle<GraphEdge>;
1518
+ readonly state?: Readonly<Record<string, ResolvableEdgeStyle<GraphEdge>>>;
1519
+ readonly palette?: unknown;
1520
+ }
1521
+ /**
1522
+ * Canonical node-state overlays auto-merged into every `GraphLayer`'s
1523
+ * `options.node.state` catalogue on construction (unless
1524
+ * `GraphLayerOptions.useDefaultStates: false`). Consumer-supplied
1525
+ * `options.node.state[name]` entries override individual fields per the
1526
+ * normal merge precedence; this map provides the resting visual identity
1527
+ * of each canonical state so a layer that touches no state code still
1528
+ * gets a sensible hover / select / error ring out of the box.
1529
+ *
1530
+ * All values are flat NodeStyle fields — extending or overriding is the
1531
+ * same shape as any other layer-template state overlay. Decorations are
1532
+ * intentionally not declared here so consumers compose them additively
1533
+ * (e.g. a ring decoration on hover) without colliding with the canonical
1534
+ * stroke treatment below.
1535
+ */
1536
+ declare const DEFAULT_NODE_STATES: Readonly<Record<CanonicalStateName, NodeStyle>>;
1537
+ /**
1538
+ * Canonical edge-state overlays — sibling of {@link DEFAULT_NODE_STATES}.
1539
+ * Auto-merged into every `GraphLayer`'s `options.edge.state` catalogue
1540
+ * unless `GraphLayerOptions.useDefaultStates: false`.
1541
+ */
1542
+ declare const DEFAULT_EDGE_STATES: Readonly<Record<CanonicalStateName, EdgeStyle>>;
1543
+ /**
1544
+ * Top-level data input shape for `GraphLayer.setData(opts)`. Carries node /
1545
+ * edge inputs plus optional layer-wide id resolvers.
1546
+ */
1547
+ interface GraphDataOptions<DN = unknown, DE = unknown> {
1548
+ readonly nodes: readonly NodeInput<DN>[];
1549
+ readonly edges: readonly EdgeInput<DE>[];
1550
+ /** Optional layer-wide id resolver applied to nodes that lack an explicit `id`. */
1551
+ readonly nodeIdResolver?: (data: DN) => string;
1552
+ readonly edgeIdResolver?: (data: DE) => string;
1553
+ }
1554
+ /** Constructor options for `GraphLayer`. */
1555
+ interface GraphLayerOptions {
1556
+ /**
1557
+ * Optional pre-built store. If omitted, the layer creates its own with
1558
+ * default options (`flushMode: 'sync'`, `unknownEndpoint: 'throw'`). Pass
1559
+ * a store you own to share data with other layers / sync code.
1560
+ */
1561
+ store?: GraphStore;
1562
+ /**
1563
+ * Layer-level node template (G6's `node` field). Carries `style` (base
1564
+ * appearance) and `state` (catalogue of named overlays applied while a
1565
+ * state in `node.states[]` is active). Resolver-aware: every field on
1566
+ * `style` / each `state[name]` may be a static value or a function
1567
+ * `(node: GraphNode) => value` that fires every render.
1568
+ */
1569
+ node?: NodeOption;
1570
+ /** Sibling of {@link node} for edges. */
1571
+ edge?: EdgeOption;
1572
+ /**
1573
+ * Auto-merge {@link DEFAULT_NODE_STATES} / {@link DEFAULT_EDGE_STATES}
1574
+ * into `options.node.state` / `options.edge.state` on construction so
1575
+ * every canonical state has a sensible default appearance even when the
1576
+ * consumer supplied no state overlays. Consumer entries win on a
1577
+ * per-name basis (no per-field deep merge here — declare a full
1578
+ * `NodeStyle` if you want to replace a default entry). Default `true`.
1579
+ */
1580
+ useDefaultStates?: boolean;
1581
+ /**
1582
+ * Minimum hover/click target in screen pixels, forwarded to the
1583
+ * internal `PrimitivesRenderer`. Default `6`.
1584
+ *
1585
+ * Behaves as a *fallback*: exact geometric hits always win; only
1586
+ * when no shape contains the cursor does the dispatcher pick the
1587
+ * closest candidate within `hitFloorPx` screen pixels. See
1588
+ * `PrimitivesRendererOptions.hitFloorPx` for details.
1589
+ */
1590
+ hitFloorPx?: number;
1591
+ }
1592
+ /**
1593
+ * Layer-level event payloads (separate from store events). Pointer/drag/etc.
1594
+ * arrive in later phases; today this is just the aggregated lifecycle.
1595
+ */
1596
+ interface GraphLayerEvents {
1597
+ 'data:changed': {
1598
+ addedNodes: number;
1599
+ removedNodes: number;
1600
+ updatedNodes: number;
1601
+ addedEdges: number;
1602
+ removedEdges: number;
1603
+ updatedEdges: number;
1604
+ };
1605
+ 'positions:updated': {
1606
+ count: number;
1607
+ };
1608
+ /**
1609
+ * A user-driven node drag began. Behaviours emitting this signal the
1610
+ * intent to hold a node's position against any physics / layout that
1611
+ * would otherwise move it. Layouts (e.g. `D3ForceLayout`) subscribe and
1612
+ * apply a *transient* lock — they MUST NOT mutate the store's
1613
+ * `GraphNode.pinned` flag in response, since that is reserved for
1614
+ * user-data semantics (permanent pin). The matching `node:drag-end`
1615
+ * releases the transient lock.
1616
+ *
1617
+ * `nodeId` is the *grabbed* node (the gesture's primary). `nodeIds` is the
1618
+ * full set of primary nodes being dragged together — `[nodeId]` for a plain
1619
+ * single-node drag, or every selected node for a multi-selection drag. Group
1620
+ * descendants are NOT listed here; consumers that care about them expand via
1621
+ * `store.descendantsOf(id)`.
1622
+ */
1623
+ 'node:drag-start': {
1624
+ nodeId: string;
1625
+ nodeIds: readonly string[];
1626
+ };
1627
+ 'node:drag-end': {
1628
+ nodeId: string;
1629
+ nodeIds: readonly string[];
1630
+ };
1631
+ [event: string]: unknown;
1632
+ }
1633
+
1634
+ /**
1635
+ * `GraphLayer` — `WorldLayer` subclass that renders a `GraphStore` via a
1636
+ * `PrimitivesRenderer`. Subscribes to store events and projects them into
1637
+ * `addShape` / `addConnector` / `updateShape` / `updateConnector` /
1638
+ * `removeShape` / `removeConnector` calls.
1639
+ *
1640
+ * The store is the source of truth. The layer is a thin projection — no
1641
+ * domain data lives here. Re-emits aggregated `data:changed` and
1642
+ * `positions:updated` events at the layer level for application code that
1643
+ * only cares about visible-graph changes.
1644
+ *
1645
+ * See `apps/docs/graph/data-model.md` for the data model and
1646
+ * `apps/docs/graph/events.md` for the event model.
1647
+ */
1648
+
1649
+ interface GraphLayerState {
1650
+ /** Reserved for hover / selection / decoration state in later phases. */
1651
+ readonly _placeholder?: never;
1652
+ }
1653
+ declare class GraphLayer extends WorldLayer<GraphLayerOptions, GraphLayerState, GraphLayerEvents, never, WorldLayerHit> {
1654
+ /**
1655
+ * Pixi-backed primitives renderer; created in `onMount`.
1656
+ *
1657
+ * Public so behaviours can subscribe to `shape:*` / `connector:*` pointer
1658
+ * events on `graph.getRenderer().events`. Returns `undefined` before mount.
1659
+ */
1660
+ private _renderer?;
1661
+ /** Renderer accessor for behaviours. Undefined before `onMount`. */
1662
+ getRenderer(): PrimitivesRenderer | undefined;
1663
+ /**
1664
+ * Per-frame tick — delegated to `PrimitivesRenderer.tickAnimations` so
1665
+ * animated decorations (`pulse-ring`, `marching-ants`, …) and the
1666
+ * viewport-clipped label-resolution sweep advance every frame.
1667
+ *
1668
+ * `Canvas.tickOnce` duck-types this hook on each layer; without it the
1669
+ * renderer would never tick for graph layers because the field that
1670
+ * holds it (`_renderer`) is private and the alternative fallback path
1671
+ * looks for a public `renderer` property.
1672
+ */
1673
+ tickAnimations(deltaMs: number): void;
1674
+ /** Data source. Either supplied by the caller or self-created. */
1675
+ readonly store: GraphStore;
1676
+ /** Subscription disposers, called in `onUnmount`. */
1677
+ private subs;
1678
+ /**
1679
+ * Edge ids whose endpoint moved since last flush. The connector path is
1680
+ * pinned to shape positions via the `boundary` anchor, but PixiJS doesn't
1681
+ * auto-reroute connectors when an anchored shape moves — we drain this set
1682
+ * on each store flush and call `updateConnector(eid, {})` to force re-route.
1683
+ */
1684
+ private dirtyConnectors;
1685
+ /**
1686
+ * Last-projected collapsed flag per group node id. Read by the
1687
+ * `node:update` handler so it can detect a collapse → expand (or
1688
+ * expand → collapse) flip — the patch itself doesn't carry the
1689
+ * previous style, and the store has already overwritten it by the
1690
+ * time the event fires. Updated by {@link syncGroupSyntheticDecorations}
1691
+ * on every group render.
1692
+ */
1693
+ private readonly lastCollapsedByGroup;
1694
+ /**
1695
+ * Group node ids whose visible frame may need re-projection (auto-fit
1696
+ * recompute, descendant visibility change, collapse / expand toggle).
1697
+ * Drained per flush in deepest-first order — see {@link drainDirtyGroups}.
1698
+ *
1699
+ * Populated by store subscriptions when:
1700
+ * - a group's own spec changes (add / update with `style.group` patch),
1701
+ * - a child's position changes and its parent is a group with `autoFit`,
1702
+ * - a child is added / removed from a group.
1703
+ *
1704
+ * Holding ids (not full snapshots) keeps the bucket idempotent — multiple
1705
+ * mutations within a single batch coalesce to one rerender per group.
1706
+ */
1707
+ private dirtyGroups;
1708
+ /**
1709
+ * Nodes / edges whose active-state set changed since the last flush — drained
1710
+ * once per flush into `rerenderNode` / `rerenderEdge`. Populated from the
1711
+ * store's `node:state` / `edge:state` events; the dedup + once-per-flush drain
1712
+ * keeps an N-item highlight to ≤1 rebuild per item (mirrors
1713
+ * {@link dirtyConnectors}). State itself is owned by the `GraphStore` (presence
1714
+ * compartment) — the layer holds none, just reads `store.nodeStatesOf` at
1715
+ * render. See `store-owns-state-plan.md` § 0 / § 2.5.
1716
+ */
1717
+ private readonly dirtyStateNodes;
1718
+ private readonly dirtyStateEdges;
1719
+ /**
1720
+ * Currently-mounted decoration slot ids per node / edge, so the resolver
1721
+ * can diff (mount new / dispose removed / replace changed) against the
1722
+ * previous render's set. Slot ids are synthesized from `spec.id` or
1723
+ * `${kind}#<index>`. The `'label'` slot is managed separately by
1724
+ * `syncNodeLabel` / `syncEdgeLabel` and never appears in these maps.
1725
+ */
1726
+ private readonly nodeDecorationSlots;
1727
+ private readonly edgeDecorationSlots;
1728
+ /**
1729
+ * Currently-mounted badge slot ids per node, mirroring
1730
+ * {@link nodeDecorationSlots} for badge diffing. Slot id falls back to
1731
+ * `${badge.placement-name}#<index>` when `NodeBadge.id` is absent so
1732
+ * id-less badges stack rather than collapse.
1733
+ */
1734
+ private readonly nodeBadgeSlots;
1735
+ /** Edge-side counterpart of {@link nodeBadgeSlots}. */
1736
+ private readonly edgeBadgeSlots;
1737
+ private nodeOption;
1738
+ private edgeOption;
1739
+ constructor(opts: LayerOptions<GraphLayerOptions>);
1740
+ protected createState(): GraphLayerState;
1741
+ protected onMount(ctx: CanvasContext): void;
1742
+ protected onUnmount(): void;
1743
+ /**
1744
+ * Bulk-load nodes + edges, **replacing** any prior data. Wraps the
1745
+ * underlying store inserts in a single `batch()` so subscribers see one
1746
+ * flush.
1747
+ *
1748
+ * For streaming consumers (constantly arriving data), use the store
1749
+ * directly: `graph.store.addData({ nodes, edges })` appends without
1750
+ * clearing, and `graph.store.applyDelta({ added, updated, removed })`
1751
+ * applies an incremental change in one batch. All other per-id CRUD
1752
+ * (`upsertNode`, `updateNode`, `removeNode`, edge equivalents, `batch`,
1753
+ * `flush`, `clear`) lives on `graph.store` — the store is the single
1754
+ * source of truth and the layer just orchestrates store → renderer.
1755
+ */
1756
+ setData(data: GraphData): void;
1757
+ /**
1758
+ * Remove every node and edge — tearing down their rendered shapes /
1759
+ * connectors and notifying full-repaint consumers (e.g. `MiniMapLayer`). The
1760
+ * canonical way to empty the graph; prefer it over
1761
+ * `setData({ nodes: [], edges: [] })`.
1762
+ *
1763
+ * Note the difference from the low-level `graph.store.clear()`: that is a
1764
+ * silent fast-wipe (no events, drops the pending queues), so on its own it
1765
+ * would leave the canvas painted and dependent layers stale. This method
1766
+ * keeps the renderer and store in sync and fires a single `data:changed`
1767
+ * (which `store.clear()` alone never produces, since `doFlush` skips an empty
1768
+ * flush) so consumers update immediately rather than on some later event.
1769
+ */
1770
+ clear(): void;
1771
+ /**
1772
+ * Force a full re-render of every node and edge from current store state +
1773
+ * active states. Does **not** mutate data and is **not** undoable — it is a
1774
+ * pure render pass. Use it after an external style/theme change that bypassed
1775
+ * the store (e.g. swapping the renderer's palette) or to recover from a
1776
+ * suspected render desync. For data edits prefer the store mutators, which
1777
+ * re-render the affected items automatically.
1778
+ */
1779
+ redraw(): void;
1780
+ /**
1781
+ * Drop every shape / connector this layer has mounted on the renderer and
1782
+ * reset transient routing state. Shared by `clear` / `setData`: `store.clear()`
1783
+ * is silent, so it never drives the renderer's event-based removal path —
1784
+ * the layer must detach explicitly.
1785
+ */
1786
+ private detachAllFromRenderer;
1787
+ /**
1788
+ * Patch the layer-level node template (`options.node.style`) and re-render
1789
+ * every node so the change takes effect immediately. Use this for global
1790
+ * "apply to all nodes" changes (e.g. a toolbar default-fill picker) instead
1791
+ * of looping `store.updateNode` per node.
1792
+ *
1793
+ * Merge is shallow (top-level): structured fields (`shape`, `decorations`,
1794
+ * `badges`, `effects`) are replaced wholesale — spread the prior value if you
1795
+ * mean to patch a single sub-field. Per-node `style`, active states, and
1796
+ * resolver functions still win over the template at resolve time (see
1797
+ * {@link resolveNodeStyle}). No-op visually if the layer isn't mounted yet,
1798
+ * but the template is still updated so later mounts pick it up.
1799
+ */
1800
+ setNodeDefaults(patch: Partial<NodeStyle>): void;
1801
+ /**
1802
+ * Sibling of {@link setNodeDefaults} for the edge template
1803
+ * (`options.edge.style`). Patches the shared edge styling and re-renders
1804
+ * every edge. Same shallow-merge contract — e.g. changing edge "type" means
1805
+ * `setEdgeDefaults({ shape: { ...prevShape, pathType: 'bezier' } })`.
1806
+ */
1807
+ setEdgeDefaults(patch: Partial<EdgeStyle>): void;
1808
+ /** Read-only snapshot of the current node template style (resolved per node at render). */
1809
+ get nodeDefaults(): ResolvableNodeStyle<GraphNode> | undefined;
1810
+ /** Read-only snapshot of the current edge template style. */
1811
+ get edgeDefaults(): ResolvableEdgeStyle<GraphEdge> | undefined;
1812
+ /**
1813
+ * Highlight a node together with its neighbours (in `dir`) and incident edges
1814
+ * — adds the runtime state `state` to all of them in a single
1815
+ * {@link GraphStore.batch}, so the whole neighbourhood repaints in one flush.
1816
+ * No-op if the seed id is unknown. Clear with `store.clearNodeState(state)` +
1817
+ * `store.clearEdgeState(state)`.
1818
+ *
1819
+ * @param id Seed node id.
1820
+ * @param dir Adjacency direction for neighbours + incident edges. Default `'both'`.
1821
+ * @param state Runtime state name to apply. Default `'highlighted'`.
1822
+ */
1823
+ highlightNeighbourhood(id: string, dir?: EdgeDirection, state?: string): void;
1824
+ /**
1825
+ * Placeholder hit test — returns `null` until proper hit testing wires up
1826
+ * in a later phase (likely via the canvas hit-test pipeline reading the
1827
+ * renderer's shape registry).
1828
+ */
1829
+ hitTest(_worldX: number, _worldY: number): WorldLayerHit | null;
1830
+ /**
1831
+ * Resolve the active node hints — merges base `node.data` hints (legacy)
1832
+ * and v3 `node.style` with each active state's overlay (legacy + v3),
1833
+ * resolving layer-side resolver functions against the current node.
1834
+ *
1835
+ * Precedence (lowest → highest):
1836
+ * 1. layer `node.style` (resolved against GraphNode, adapted)
1837
+ * 2. `node.data` (legacy hints)
1838
+ * 3. per-node `node.style` (concrete NodeStyle, adapted)
1839
+ * 4. For each active state name in `node.states[]`:
1840
+ * a. layer legacy `nodeStateConfigs[name]` (resolved)
1841
+ * b. layer v3 `node.state[name]` (resolved against GraphNode, adapted)
1842
+ * c. per-node `node.state[name]` (concrete NodeStyle, adapted)
1843
+ */
1844
+ /**
1845
+ * Resolve the final flat NodeStyle for a node by merging contributions from
1846
+ * the layer-level template (`options.node.style`), the per-node `style`,
1847
+ * and every active state's layer + per-node overlay. Object.assign order
1848
+ * encodes precedence (later wins).
1849
+ *
1850
+ * Exposed publicly so behaviours (NodeSizeLODBehaviour, label collision,
1851
+ * minimap, etc.) can read the same effective style the renderer sees,
1852
+ * without duplicating the merge logic.
1853
+ */
1854
+ resolveNodeStyle(node: GraphNode): Partial<NodeStyle>;
1855
+ /** Sibling of {@link resolveNodeStyle} for edges. Public for the same reason. */
1856
+ resolveEdgeStyle(edge: GraphEdge): Partial<EdgeStyle>;
1857
+ /**
1858
+ * Build the renderer-facing shape spec from the resolved {@link NodeStyle}.
1859
+ * Geometry is driven by the discriminated `style.shape` union; paint comes
1860
+ * from the flat `bg*` fields.
1861
+ *
1862
+ * The `kind` discriminator and any spec params on `style.shape` pass
1863
+ * straight through to the renderer, so any shape registered via
1864
+ * `canvas.primitives.registerShape(name, ctor)` is usable by name — built-
1865
+ * ins (`rect` / `circle` / `arc` / `regular-polygon` / `star` / `polygon`)
1866
+ * and custom shapes alike. An unknown `kind` errors loudly in the
1867
+ * renderer's `addShape` rather than silently falling back to a circle.
1868
+ */
1869
+ /**
1870
+ * Local AABB for `node`'s resolved shape. Delegates to the registered
1871
+ * shape's `static boundsOf` via `PrimitivesRenderer.boundsOfSpec`, so
1872
+ * built-in and custom shape kinds flow through the same hook.
1873
+ *
1874
+ * Returns `undefined` when:
1875
+ * - the renderer isn't mounted yet,
1876
+ * - the resolved `style.shape.kind` isn't registered, or
1877
+ * - the registered ctor doesn't implement `boundsOf`.
1878
+ *
1879
+ * The returned rect is in the shape's local (centre-relative) frame —
1880
+ * `node.position` is *not* baked in. Consumers that only need a size
1881
+ * read `width` / `height`; consumers that need world-space corners
1882
+ * offset by `node.position` themselves.
1883
+ *
1884
+ * Used by `MiniMapLayer` to estimate node footprint before the source
1885
+ * renderer mounts and by `ElkLayout` (and other layouts) to read node
1886
+ * sizes for layout-time placement — both without switching over a
1887
+ * closed shape-kind enum.
1888
+ */
1889
+ boundsOfNode(node: GraphNode): Rect | undefined;
1890
+ /**
1891
+ * Frame the camera on a set of nodes — fit the viewport to the world-space
1892
+ * box spanning their positions, plus `padding` screen px. Unknown ids are
1893
+ * skipped; a no-op when none resolve or the layer isn't mounted.
1894
+ *
1895
+ * Graph-domain sugar over the geometry-only {@link Camera.fitContent}: it
1896
+ * resolves ids → positions so callers (e.g. a "zoom to node" context-menu
1897
+ * action) don't have to.
1898
+ *
1899
+ * @param ids Node ids to frame.
1900
+ * @param padding Screen-pixel padding around the fitted box. Default `160`.
1901
+ */
1902
+ focusNodes(ids: Iterable<string>, padding?: number): void;
1903
+ /**
1904
+ * Frame the camera on a set of edges — fit the viewport to the box spanning
1905
+ * both endpoints of each edge, plus `padding` screen px. Unknown ids (or
1906
+ * edges with an unplaced endpoint) are skipped; a no-op when none resolve or
1907
+ * the layer isn't mounted.
1908
+ *
1909
+ * @param ids Edge ids to frame.
1910
+ * @param padding Screen-pixel padding around the fitted box. Default `160`.
1911
+ */
1912
+ focusEdges(ids: Iterable<string>, padding?: number): void;
1913
+ /** Fit the camera to the AABB spanning `pts` (+ padding). No-op if empty / unmounted. */
1914
+ private fitPoints;
1915
+ private nodeSpec;
1916
+ /**
1917
+ * Build the renderer-facing connector spec from the resolved
1918
+ * {@link EdgeStyle}. The three-stage pipeline (anchor → router →
1919
+ * pathStyle) is driven by `style.shape.pathType` + the anchor / router
1920
+ * options on the same struct; paint comes from the flat `stroke*` fields.
1921
+ */
1922
+ private edgeSpec;
1923
+ /**
1924
+ * Re-render a single node from its current data + active state stack.
1925
+ *
1926
+ * Prefers `renderer.updateShape` (instance-preserving) over the
1927
+ * `removeShape + addShape` fallback so the renderer's per-instance
1928
+ * state — `gfxScale` (written by `NodeSizeLODBehaviour`), attached
1929
+ * decorations, badges, effects — survives a state toggle. Falls back
1930
+ * to remove+add only when the rebuilt spec has a different `kind`,
1931
+ * which `updateShape` can't safely handle (the `IShape` class is
1932
+ * fixed at construction time).
1933
+ */
1934
+ private rerenderNode;
1935
+ /**
1936
+ * Re-render a single edge from its current data + active state stack.
1937
+ *
1938
+ * Always uses `renderer.updateConnector` so `inst.strokeWidthScale`
1939
+ * (written by `EdgeSizeLODBehaviour` as `1/cameraScale`) survives the
1940
+ * state-driven full-spec replacement. The fresh spec carries the new
1941
+ * "base" stroke width; the multiplier applies on top at draw time.
1942
+ */
1943
+ private rerenderEdge;
1944
+ private drainDirtyConnectors;
1945
+ private installNodeShape;
1946
+ private installEdgeConnector;
1947
+ /**
1948
+ * Project the resolved `label` hint onto the canvas `'label'` decoration
1949
+ * slot for the given node. A `null` / `undefined` hint clears the slot.
1950
+ * Called after every `addShape` and every `rerenderNode` since decorations
1951
+ * are dropped when the shape is destroyed.
1952
+ */
1953
+ private syncNodeLabel;
1954
+ private syncEdgeLabel;
1955
+ /**
1956
+ * Resolve the final list of decorations for a node by concatenating every
1957
+ * contributing layer (layer template + per-node base + each active state
1958
+ * overlay) and deduping by `id`. Later precedence wins; `remove: true`
1959
+ * drops an earlier same-id entry. Entries without an explicit `id` fall
1960
+ * back to `${kind}#<combined-index>` — unique per source position, so
1961
+ * id-less decorations stack rather than collapsing.
1962
+ *
1963
+ * Returns a `Map<slotId, NodeDecorationSpec>` keyed by the resolved
1964
+ * identity. Caller emits one `setDecoration` call per slot to the
1965
+ * renderer; the slot id is reused on subsequent renders to enable diffing.
1966
+ */
1967
+ private resolveNodeDecorations;
1968
+ /** Sibling of {@link resolveNodeDecorations} for edges. */
1969
+ private resolveEdgeDecorations;
1970
+ /**
1971
+ * Project the resolved decoration array onto the canvas renderer for the
1972
+ * given node. Diffs against the previous render's slot set tracked in
1973
+ * {@link nodeDecorationSlots}: mounts new ids, removes vanished ones,
1974
+ * replaces specs whose slot id appears in both.
1975
+ */
1976
+ private syncNodeDecorations;
1977
+ /** Sibling of {@link syncNodeDecorations} for edges. */
1978
+ private syncEdgeDecorations;
1979
+ /**
1980
+ * Resolve the final list of badges for a node by concatenating every
1981
+ * contributing layer (layer template + per-node base + each active state
1982
+ * overlay) and deduping by `id`. Later precedence wins. Entries without an
1983
+ * explicit `id` fall back to `badge#<combined-index>` so id-less badges
1984
+ * stack rather than collapsing.
1985
+ */
1986
+ private resolveNodeBadges;
1987
+ /**
1988
+ * Project the resolved badge map onto the canvas renderer for the given
1989
+ * node. Diffs against the previous render's slot set tracked in
1990
+ * {@link nodeBadgeSlots}: mounts new ids, removes vanished ones, replaces
1991
+ * specs whose slot id appears in both.
1992
+ */
1993
+ private syncNodeBadges;
1994
+ /** Sibling of {@link resolveNodeBadges} for edges. */
1995
+ private resolveEdgeBadges;
1996
+ /** Sibling of {@link syncNodeBadges} for edges. */
1997
+ private syncEdgeBadges;
1998
+ private updateNodeShape;
1999
+ private queueIncidentConnectors;
2000
+ /**
2001
+ * True iff `node`'s resolved style carries a `group` field — the only
2002
+ * signal that promotes the node from a regular renderable into a
2003
+ * compound-group frame.
2004
+ *
2005
+ * Cheap to call: reads {@link resolveNodeStyle} which is already
2006
+ * memoised per render cycle through `Object.assign` of the merged
2007
+ * contributions.
2008
+ */
2009
+ isGroupNode(node: GraphNode): boolean;
2010
+ /** True when this group node's resolved style carries `group.collapsed === true`. */
2011
+ isCollapsedGroup(node: GraphNode): boolean;
2012
+ /**
2013
+ * Public predicate behaviours can use to filter group nodes out of their
2014
+ * own hit pipeline. Hover / select / drag should typically skip groups
2015
+ * when the group is *expanded* (the frame is interaction-less) but treat
2016
+ * a collapsed group as a regular node. Returns one of:
2017
+ *
2018
+ * - `'none'` — the id is not a group (treat as a regular node).
2019
+ * - `'expanded'` — group, currently expanded. Behaviours wanting to honour
2020
+ * the "interaction-less frame" intent should early-return.
2021
+ * - `'collapsed'` — group, currently collapsed. Behaviours that act on
2022
+ * regular nodes should treat this as a normal target.
2023
+ * - `undefined` — no such node.
2024
+ *
2025
+ * The string form is preferred over a boolean pair so a future
2026
+ * `'collapsed-locked'` (or similar) can be added without breaking callers.
2027
+ */
2028
+ getGroupRole(nodeId: string): 'none' | 'expanded' | 'collapsed' | undefined;
2029
+ /**
2030
+ * Climb the `parentId` chain from `nodeId` (exclusive) and return the
2031
+ * first ancestor whose resolved style has `group.collapsed === true`, or
2032
+ * `undefined` if no such ancestor exists. Used to decide whether a node
2033
+ * is currently hidden (any collapsed ancestor → hidden) and where to
2034
+ * re-route an incident edge (to that collapsed ancestor).
2035
+ */
2036
+ collapsedAncestor(nodeId: string): string | undefined;
2037
+ /**
2038
+ * Resolve which renderer-side shape id an edge endpoint should attach to
2039
+ * for `nodeId`. Returns the nearest collapsed-group ancestor when the
2040
+ * node is hidden, or `nodeId` unchanged when the node is visible. Pure
2041
+ * read — the store's `edge.source` / `edge.target` are never mutated.
2042
+ */
2043
+ effectiveEndpoint(nodeId: string): string;
2044
+ /**
2045
+ * Walk the `parentId` chain from `nodeId` and `add` every group ancestor
2046
+ * to {@link dirtyGroups}. Called whenever a descendant moves, is added,
2047
+ * or otherwise triggers an auto-fit recompute.
2048
+ */
2049
+ private markGroupAncestorsDirty;
2050
+ /**
2051
+ * After a group flips `collapsed`, every descendant changes visibility
2052
+ * and every incident edge of every descendant needs re-routing (the
2053
+ * endpoint now resolves to either the original node or to the collapsed
2054
+ * group ancestor). Walk the subtree once, re-render each descendant
2055
+ * (which re-projects `visible`), and queue incident edges for re-route.
2056
+ */
2057
+ private refreshDescendantsAndIncidentEdges;
2058
+ /**
2059
+ * Compute the world-space AABB of the direct (one-level) children of
2060
+ * `groupId`. Returns `undefined` when the group has no children — the
2061
+ * caller falls back to whatever floor size the group's declared
2062
+ * width/height/radius provides.
2063
+ *
2064
+ * Recurses into child groups via {@link boundsOfNode} so a child whose
2065
+ * own frame has already been auto-fit contributes its current size, not
2066
+ * its stored declared size.
2067
+ */
2068
+ private directChildrenWorldBounds;
2069
+ /**
2070
+ * Project an expanded group's spec — apply auto-fit (when enabled),
2071
+ * compose the children-derived width/height/radius with the group's
2072
+ * declared floor, and shift `pos` so the frame wraps the bbox correctly.
2073
+ *
2074
+ * For `kind: 'rect'`: `pos` becomes top-left of the framed area; size is
2075
+ * `max(declared, childrenAABB) + 2 · padding (+ headerHeight on y)`.
2076
+ *
2077
+ * For `kind: 'circle'`: `pos` becomes the AABB centroid; `radius` is
2078
+ * `max(declared, AABB half-diagonal) + padding`. The half-diagonal is
2079
+ * the smallest enclosing-circle approximation that's still cheap
2080
+ * (`Math.hypot` over AABB half-extents); true minimum-enclosing-circle
2081
+ * (Welzl) is out of scope.
2082
+ *
2083
+ * Non-rect / non-circle group shapes pass through untouched — autoFit is
2084
+ * a no-op outside those two kinds. Domain shapes that want their own
2085
+ * fit math can extend the layer's projection later.
2086
+ */
2087
+ private projectGroupShape;
2088
+ /**
2089
+ * Force a group's frame to re-project right now (outside the normal
2090
+ * flush cycle). Public escape hatch for feeds that remove children
2091
+ * individually without triggering a position change on a sibling — the
2092
+ * `node:remove` event doesn't carry the parentId, so the layer can't
2093
+ * mark the parent dirty on its own. Domain code can call this after
2094
+ * `store.removeNode` to make the auto-fit frame catch up.
2095
+ */
2096
+ recomputeGroup(groupId: string): void;
2097
+ /**
2098
+ * Drain {@link dirtyGroups} in deepest-first order. Each pop re-renders
2099
+ * the group (re-running `nodeSpec` with the latest auto-fit math) and
2100
+ * marks the group's own parent chain dirty so a multi-level nested
2101
+ * group cascade settles in one flush.
2102
+ *
2103
+ * Bounded by `MAX_PASSES` to defend against a pathological cycle (which
2104
+ * the cycle-rejecting store should already prevent on insert, but the
2105
+ * extra guard is cheap).
2106
+ */
2107
+ private drainDirtyGroups;
2108
+ /**
2109
+ * Count of ancestors between `nodeId` and the root (`parentId === undefined`).
2110
+ * Used by {@link drainDirtyGroups} to order deepest first so a child
2111
+ * group recomputes before any group that depends on its bounds.
2112
+ */
2113
+ private depthOf;
2114
+ /**
2115
+ * Project the synthetic group-only decorations onto the renderer:
2116
+ * - the `+` / `−` toggle button at the group's bottom anchor; and
2117
+ * - a centred count badge (label decoration) when the group is
2118
+ * collapsed, showing the number of hidden descendants.
2119
+ *
2120
+ * Called on every node lifecycle event for group nodes. Cleared
2121
+ * automatically when `style.group` goes away (the slots get `null` so
2122
+ * any previous mount disposes).
2123
+ */
2124
+ private syncGroupSyntheticDecorations;
2125
+ private updateEdgeConnector;
2126
+ }
2127
+
2128
+ /**
2129
+ * `MiniMapLayer` — bird's-eye overview of a `GraphLayer` with a draggable
2130
+ * viewport indicator.
2131
+ *
2132
+ * Implemented as a `ScreenLayer` rather than a self-hosted pixi `Application`
2133
+ * — much cheaper, no extra GPU surface, and pans/zooms/visibility integrate
2134
+ * with the main canvas naturally. The minimap occupies a fixed
2135
+ * `width × height` rectangle anchored to a corner of the viewport.
2136
+ *
2137
+ * Cross-layer dependency declared via `graphLayerId` per the canvas
2138
+ * architecture rule: no inference of "the only graph layer". You must point
2139
+ * the minimap at a specific id.
2140
+ *
2141
+ * @example
2142
+ * ```ts
2143
+ * const graph = new GraphLayer({ id: 'graph', options: {} });
2144
+ * canvas.layers.add(graph);
2145
+ *
2146
+ * const minimap = new MiniMapLayer({
2147
+ * id: 'minimap',
2148
+ * options: { graphLayerId: 'graph', position: 'bottom-right' },
2149
+ * });
2150
+ * canvas.layers.add(minimap);
2151
+ * ```
2152
+ */
2153
+
2154
+ /** Anchor corner inside the canvas viewport. */
2155
+ type MiniMapPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
2156
+ /** Constructor options for `MiniMapLayer`. */
2157
+ interface MiniMapLayerOptions {
2158
+ /** Required — the `GraphLayer` id this minimap mirrors. */
2159
+ graphLayerId: string;
2160
+ /** Minimap width in screen pixels. Default `200`. */
2161
+ width?: number;
2162
+ /** Minimap height in screen pixels. Default `150`. */
2163
+ height?: number;
2164
+ /** Background fill `0xRRGGBB`. Default `0x1a1a2e`. */
2165
+ backgroundColor?: number;
2166
+ /** Border colour `0xRRGGBB`. Default `0x444444`. */
2167
+ borderColor?: number;
2168
+ /** Border stroke width. Default `1`. */
2169
+ borderWidth?: number;
2170
+ /** Viewport indicator fill. Default `0x4a90d9`. */
2171
+ viewportFill?: number;
2172
+ /** Viewport indicator stroke. Default `0x2a70b9`. */
2173
+ viewportStroke?: number;
2174
+ /** Viewport indicator fill alpha 0–1. Default `0.3`. */
2175
+ viewportFillAlpha?: number;
2176
+ /** Viewport indicator stroke width. Default `2`. */
2177
+ viewportStrokeWidth?: number;
2178
+ /** World-space padding around node bounds. Default `20`. */
2179
+ padding?: number;
2180
+ /** Whether dragging the minimap pans the main camera. Default `true`. */
2181
+ enableDrag?: boolean;
2182
+ /** Anchor corner. Default `'bottom-right'`. */
2183
+ position?: MiniMapPosition;
2184
+ /**
2185
+ * Inset from the chosen corner, in screen pixels. Pass a single number for a
2186
+ * symmetric inset, or `{ x, y }` for independent horizontal / vertical insets
2187
+ * (e.g. to bottom-align the minimap with a control rail while clearing its
2188
+ * width). A missing axis on the object form falls back to `10`. Default `10`.
2189
+ */
2190
+ margin?: number | {
2191
+ x?: number;
2192
+ y?: number;
2193
+ };
2194
+ }
2195
+ interface MiniMapState {
2196
+ readonly _placeholder?: never;
2197
+ }
2198
+ declare class MiniMapLayer extends ScreenLayer<MiniMapLayerOptions, MiniMapState, Record<string, never>, never, ScreenLayerHit> {
2199
+ private opts;
2200
+ private readonly graphLayerId;
2201
+ private graph;
2202
+ private ctxRef;
2203
+ /** Inner content container (the minimap's drawable area). */
2204
+ private inner;
2205
+ private bgGfx;
2206
+ private worldGfx;
2207
+ private viewportGfx;
2208
+ /** Per-frame projection scale + offset (world → minimap-local). */
2209
+ private scale;
2210
+ private offsetX;
2211
+ private offsetY;
2212
+ /** Drag state. */
2213
+ private isDragging;
2214
+ private dragOffsetX;
2215
+ private dragOffsetY;
2216
+ /** ResizeObserver disposer. */
2217
+ private offResize;
2218
+ private offCameraPan;
2219
+ private offCameraZoom;
2220
+ constructor(opts: LayerOptions<MiniMapLayerOptions>);
2221
+ protected createState(): MiniMapState;
2222
+ protected onMount(ctx: CanvasContext): void;
2223
+ protected onUnmount(): void;
2224
+ hitTest(): ScreenLayerHit | null;
2225
+ /** Force a re-paint. Cheap — call after mutating colours / sizes externally. */
2226
+ refresh(): void;
2227
+ setOptions(patch: Partial<Omit<MiniMapLayerOptions, 'graphLayerId'>>): void;
2228
+ private viewportSize;
2229
+ private layoutPosition;
2230
+ private repaint;
2231
+ private paintBackground;
2232
+ private paintWorld;
2233
+ /**
2234
+ * Pre-mount / pre-install fallback for shape bounds. Used when the renderer
2235
+ * hasn't yet built an instance for a node (very brief window — the layer's
2236
+ * `data:changed` event repaints the minimap as soon as the renderer catches
2237
+ * up).
2238
+ *
2239
+ * Delegates to {@link GraphLayer.boundsOfNode}, which routes through the
2240
+ * shape registry's `static boundsOf` hook — built-in and custom shape
2241
+ * kinds flow through the same code path. Falls back to a 32px square
2242
+ * AABB centred on the node's stored position when the resolved shape
2243
+ * isn't registered or its ctor doesn't expose `boundsOf`.
2244
+ */
2245
+ private fallbackNodeBounds;
2246
+ private paintViewportIndicator;
2247
+ /** Union node bounds with the current camera-visible bounds. */
2248
+ private effectiveBounds;
2249
+ /**
2250
+ * AABB of all drawn node *footprints* + padding, queried directly from the
2251
+ * renderer's per-instance world bounds — so arcs / polygons / stars /
2252
+ * custom shapes contribute their true extent, not a hardcoded size-derived
2253
+ * estimate. Pre-mount nodes fall through to a position+default-size box.
2254
+ * Returns a 1000×1000 placeholder when the layer has no nodes yet.
2255
+ */
2256
+ private nodeBounds;
2257
+ private projectBounds;
2258
+ private worldToMinimap;
2259
+ private minimapToWorld;
2260
+ private wireInteractions;
2261
+ private onMiniPointerDown;
2262
+ private onMiniPointerMove;
2263
+ private onMiniPointerUp;
2264
+ /** Position the main camera so the world point `(wx, wy)` lands at screen centre. */
2265
+ private panMainCameraTo;
2266
+ }
2267
+
2268
+ /**
2269
+ * `@invana/graph` — undo/redo history types.
2270
+ *
2271
+ * History is a **command/transaction journal**: each undoable change is one
2272
+ * {@link HistoryEntry} holding the ordered {@link HistoryOp}s applied to the
2273
+ * {@link GraphStore}, each carrying enough state to be inverted. Inverses are
2274
+ * captured *before* the mutation runs (read-before-write), which is the only way
2275
+ * to reconstruct a deleted node/edge — the store's `node:remove` / `edge:remove`
2276
+ * events fire *after* the data is already gone.
2277
+ *
2278
+ * Only mutations routed through {@link GraphHistory.transaction} (or pushed via
2279
+ * {@link GraphHistory.push}) are recorded. Silent layout-sim position writes and
2280
+ * streaming feed deltas bypass the journal by design, so they never flood the
2281
+ * undo stack.
2282
+ */
2283
+
2284
+ /**
2285
+ * A single invertible store mutation. `removeNode` carries the cascade-removed
2286
+ * incident `edges` so undo can restore them alongside the node. `update*` ops
2287
+ * carry both `before` (for undo) and `after` (for redo) partial states.
2288
+ */
2289
+ type HistoryOp = {
2290
+ kind: 'addNode';
2291
+ node: GraphNode;
2292
+ } | {
2293
+ kind: 'removeNode';
2294
+ node: GraphNode;
2295
+ edges: GraphEdge[];
2296
+ } | {
2297
+ kind: 'updateNode';
2298
+ id: string;
2299
+ before: Partial<GraphNode>;
2300
+ after: Partial<GraphNode>;
2301
+ } | {
2302
+ kind: 'moveNode';
2303
+ id: string;
2304
+ before: Vec2;
2305
+ after: Vec2;
2306
+ } | {
2307
+ kind: 'addEdge';
2308
+ edge: GraphEdge;
2309
+ } | {
2310
+ kind: 'removeEdge';
2311
+ edge: GraphEdge;
2312
+ } | {
2313
+ kind: 'updateEdge';
2314
+ id: string;
2315
+ before: Partial<GraphEdge>;
2316
+ after: Partial<GraphEdge>;
2317
+ };
2318
+ /** One undoable unit of work — a labelled, ordered list of {@link HistoryOp}s. */
2319
+ interface HistoryEntry {
2320
+ /** Ops in application order. Undo replays inverses in reverse; redo replays forward. */
2321
+ ops: HistoryOp[];
2322
+ /** Human label for the change (e.g. `'delete selection'`, `'paste'`). */
2323
+ label?: string;
2324
+ }
2325
+ /**
2326
+ * The mutation surface handed to {@link GraphHistory.transaction}'s callback.
2327
+ * Each method applies the change to the store **and** journals its inverse.
2328
+ * Use these instead of calling `store.*` directly so the change is undoable.
2329
+ */
2330
+ interface HistoryRecorder {
2331
+ /** Add a node (inverse: remove it). */
2332
+ addNode(node: GraphNode): void;
2333
+ /** Remove a node + its incident edges, cascading (inverse: re-add node + edges). */
2334
+ removeNode(id: string): void;
2335
+ /** Patch a node (inverse: restore the patched fields' prior values). */
2336
+ updateNode(id: string, patch: Partial<GraphNode>): void;
2337
+ /** Move a node (inverse: restore the prior position). */
2338
+ moveNode(id: string, position: Vec2): void;
2339
+ /** Add an edge (inverse: remove it). */
2340
+ addEdge(edge: GraphEdge): void;
2341
+ /** Remove an edge (inverse: re-add it). */
2342
+ removeEdge(id: string): void;
2343
+ /** Patch an edge (inverse: restore the patched fields' prior values). */
2344
+ updateEdge(id: string, patch: Partial<GraphEdge>): void;
2345
+ }
2346
+ /** Event-map for {@link GraphHistory.events}. */
2347
+ type GraphHistoryEventMap = {
2348
+ /** Fired after every undo / redo / record / clear so observers can re-read state. */
2349
+ change: {
2350
+ canUndo: boolean;
2351
+ canRedo: boolean;
2352
+ undoDepth: number;
2353
+ redoDepth: number;
2354
+ };
2355
+ };
2356
+ /** Constructor options for {@link GraphHistory}. */
2357
+ interface GraphHistoryOptions {
2358
+ /** Maximum undo depth; oldest entries are dropped past this. Default `100`. */
2359
+ limit?: number;
2360
+ }
2361
+
2362
+ /**
2363
+ * `GraphHistory` — undo/redo for a {@link GraphStore}.
2364
+ *
2365
+ * A **command/transaction journal**, not a snapshot store and not a passive
2366
+ * event listener. Mutations are recorded only when routed through
2367
+ * {@link GraphHistory.transaction} (or {@link GraphHistory.push}); everything
2368
+ * else — streaming feed deltas, silent layout-sim position writes — bypasses the
2369
+ * journal, so the undo stack stays meaningful and small.
2370
+ *
2371
+ * @example
2372
+ * ```ts
2373
+ * const history = new GraphHistory(layer.store);
2374
+ * history.transaction('delete selection', (rec) => {
2375
+ * for (const id of selectedIds) rec.removeNode(id);
2376
+ * });
2377
+ * history.undo(); // restores the nodes + their incident edges
2378
+ * ```
2379
+ */
2380
+
2381
+ declare class GraphHistory {
2382
+ /** Fires `change` after every mutation so observers can re-read undo/redo state. */
2383
+ readonly events: EventEmitter<GraphHistoryEventMap>;
2384
+ private readonly store;
2385
+ private readonly limit;
2386
+ private readonly undoStack;
2387
+ private readonly redoStack;
2388
+ /** Ops buffer for the in-flight transaction. Non-null only while recording. */
2389
+ private recording;
2390
+ /** Nesting depth — only the outermost `transaction` commits an entry. */
2391
+ private depth;
2392
+ constructor(store: GraphStore, opts?: GraphHistoryOptions);
2393
+ /** True iff there is at least one entry that can be undone. */
2394
+ get canUndo(): boolean;
2395
+ /** True iff there is at least one undone entry that can be redone. */
2396
+ get canRedo(): boolean;
2397
+ /**
2398
+ * Run `fn`'s mutations as one undoable entry. Mutations MUST go through the
2399
+ * {@link HistoryRecorder} passed to `fn` to be journaled. The whole body runs
2400
+ * inside {@link GraphStore.batch}, so the canvas sees a single flush. Nested
2401
+ * `transaction` calls merge into the outermost entry. Returns `fn`'s result.
2402
+ */
2403
+ transaction<T>(label: string, fn: (rec: HistoryRecorder) => T): T;
2404
+ /**
2405
+ * Record an already-applied entry. Escape hatch for mutations that happen
2406
+ * outside {@link transaction} — e.g. a drag behaviour that writes positions
2407
+ * during the gesture and, on release, pushes a single `moveNode` op with the
2408
+ * captured start/end positions. The ops are assumed to be applied already;
2409
+ * this only journals them.
2410
+ */
2411
+ push(entry: HistoryEntry): void;
2412
+ /** Revert the most recent entry and move it onto the redo stack. No-op if empty. */
2413
+ undo(): void;
2414
+ /** Re-apply the most recently undone entry and move it back onto the undo stack. */
2415
+ redo(): void;
2416
+ /** Wipe both stacks. Use when loading a fresh dataset. */
2417
+ clear(): void;
2418
+ private commit;
2419
+ private emitChange;
2420
+ /**
2421
+ * The recorder handed to `transaction` callbacks. Each method applies the
2422
+ * change to the store and appends its op (carrying the pre-mutation state) to
2423
+ * the in-flight ops buffer.
2424
+ */
2425
+ private readonly recorder;
2426
+ private record;
2427
+ /** Snapshot the patched fields' prior values for a node. `null` if unknown id. */
2428
+ private captureNodeBefore;
2429
+ private captureEdgeBefore;
2430
+ /** Cloned incident edges (both directions), deduped — self-loops appear once. */
2431
+ private incidentEdges;
2432
+ private applyForward;
2433
+ private applyInverse;
2434
+ /** Re-add edges whose both endpoints exist; skip duplicates and danglers. */
2435
+ private readdEdges;
2436
+ }
2437
+
2438
+ /**
2439
+ * `GraphClipboard` — copy / cut / paste / delete for a {@link GraphStore}.
2440
+ *
2441
+ * Holds an in-memory buffer of cloned node/edge specs. It does **not** read the
2442
+ * current selection itself — the caller passes the ids in (the canvas-react
2443
+ * `useClipboard` hook reads them off a `ClickSelectBehaviour`). This keeps the
2444
+ * clipboard decoupled from the selection mechanism and trivially testable.
2445
+ *
2446
+ * cut / paste / delete each run as a single undoable {@link GraphHistory}
2447
+ * transaction when a history instance is supplied; otherwise they fall back to a
2448
+ * plain {@link GraphStore.batch} (one flush, not undoable).
2449
+ *
2450
+ * @example
2451
+ * ```ts
2452
+ * const clipboard = new GraphClipboard(layer.store);
2453
+ * clipboard.copy(selectedNodeIds, selectedEdgeIds);
2454
+ * const { nodeIds } = clipboard.paste(history); // offset + re-id'd
2455
+ * clickSelect.selectMultiple(nodeIds.map((id) => ({ id })));
2456
+ * ```
2457
+ */
2458
+
2459
+ /** Event-map for {@link GraphClipboard.events}. */
2460
+ type GraphClipboardEventMap = {
2461
+ /** Fired whenever the buffer's contents change (copy / clear). Drives "can paste". */
2462
+ change: {
2463
+ hasContent: boolean;
2464
+ };
2465
+ };
2466
+ /** Constructor options for {@link GraphClipboard}. */
2467
+ interface GraphClipboardOptions {
2468
+ /** Offset applied to pasted node positions to avoid exact overlap. Default `{x:24,y:24}`. */
2469
+ pasteOffset?: Vec2;
2470
+ /**
2471
+ * Candidate id generator for pasted nodes/edges. Called with increasing
2472
+ * `attempt` until the returned id is free. Default `${oldId}-copy[-N]`.
2473
+ */
2474
+ remapId?: (oldId: string, attempt: number) => string;
2475
+ }
2476
+ /** Ids produced by a {@link GraphClipboard.paste}. */
2477
+ interface PasteResult {
2478
+ nodeIds: string[];
2479
+ edgeIds: string[];
2480
+ }
2481
+ declare class GraphClipboard {
2482
+ /** Fires `change` whenever the buffer's contents change (copy / clear). */
2483
+ readonly events: EventEmitter<GraphClipboardEventMap>;
2484
+ private readonly store;
2485
+ private readonly pasteOffset;
2486
+ private readonly remapId;
2487
+ private bufferedNodes;
2488
+ private bufferedEdges;
2489
+ constructor(store: GraphStore, opts?: GraphClipboardOptions);
2490
+ /** True iff the buffer holds at least one node or edge (drives "can paste"). */
2491
+ get hasContent(): boolean;
2492
+ /** Empty the buffer. */
2493
+ clearBuffer(): void;
2494
+ /**
2495
+ * Snapshot the given ids into the buffer (clones, so later store mutations
2496
+ * don't mutate the buffer). Unknown ids are skipped. Replaces prior contents.
2497
+ */
2498
+ copy(nodeIds: readonly string[], edgeIds?: readonly string[]): void;
2499
+ /** Copy the ids into the buffer, then delete them as one undoable transaction. */
2500
+ cut(nodeIds: readonly string[], edgeIds: readonly string[], history?: GraphHistory): void;
2501
+ /** Delete the given ids as one undoable transaction. Buffer is left untouched. */
2502
+ delete(nodeIds: readonly string[], edgeIds: readonly string[], history?: GraphHistory): void;
2503
+ /**
2504
+ * Insert the buffer with fresh ids (collision-free) and a position offset, as
2505
+ * one undoable transaction. Only buffered edges whose **both** endpoints were
2506
+ * also buffered are pasted, with endpoints remapped to the new node ids.
2507
+ * `parentId` is remapped when the parent was pasted too, else dropped.
2508
+ *
2509
+ * Returns the new ids so the caller can re-select the pasted items.
2510
+ */
2511
+ paste(history?: GraphHistory): PasteResult;
2512
+ /** Remove explicit edges first, then nodes (cascade), mirroring `applyDelta`. */
2513
+ private removeAsTransaction;
2514
+ /** Pick the first remapped id that is neither already in the store nor reserved. */
2515
+ private freshId;
2516
+ /**
2517
+ * A recorder that mutates the store directly without journaling — used when no
2518
+ * {@link GraphHistory} is supplied so paste/cut/delete still run as one batch.
2519
+ */
2520
+ private plainRecorder;
2521
+ }
2522
+
2523
+ /**
2524
+ * `HoverActivateBehaviour` — toggles a named visual state on hovered nodes /
2525
+ * edges (and optionally their N-hop neighbours), with optional dimming of
2526
+ * everything else.
2527
+ *
2528
+ * Layer-scoped: constructed with a target `layerId` referencing a
2529
+ * {@link GraphLayer}. Subscribes to that layer's renderer pointer events
2530
+ * (`shape:pointerover` / `connector:pointerover`) and drives layer state via
2531
+ * `layer.store.setNodeState` / `layer.store.setEdgeState`.
2532
+ *
2533
+ * Default `enabled: false` — register, then explicitly enable. Matches the
2534
+ * project rule that no behaviour auto-activates.
2535
+ *
2536
+ * Defaults align with the canonical state catalogue auto-merged into
2537
+ * every `GraphLayer`: `state: 'hovered'` for the focal (and N-hop
2538
+ * neighbours when `degree > 0`), and optional `inactiveState: 'dimmed'`
2539
+ * for everything else. Override these when the project's state
2540
+ * vocabulary diverges.
2541
+ *
2542
+ * @example
2543
+ * ```ts
2544
+ * // Layer defaults already include `hovered`, `highlighted`, `dimmed` —
2545
+ * // no setup needed beyond registering the behaviour.
2546
+ *
2547
+ * canvas.behaviours.register(
2548
+ * new HoverActivateBehaviour({
2549
+ * id: 'hover',
2550
+ * layerId: 'graph',
2551
+ * enabled: true,
2552
+ * // state defaults to 'hovered'
2553
+ * inactiveState: 'dimmed',
2554
+ * degree: 1,
2555
+ * }),
2556
+ * );
2557
+ * ```
2558
+ */
2559
+
2560
+ /** Element kind for hover targets. */
2561
+ type HoverableElementType = 'shape' | 'connector';
2562
+ /** Edge-traversal direction filter for neighbour expansion. */
2563
+ type HoverDirection = 'in' | 'out' | 'both';
2564
+ /** Element handed to hover callbacks. */
2565
+ interface HoverableElement {
2566
+ readonly id: string;
2567
+ readonly type: HoverableElementType;
2568
+ /** Arbitrary user payload from `node.data` or `edge.data`. */
2569
+ readonly data: unknown;
2570
+ }
2571
+ /** Constructor options for `HoverActivateBehaviour`. */
2572
+ interface HoverActivateBehaviourOptions extends BehaviourOptions {
2573
+ /** Required — the `GraphLayer` id this behaviour drives. */
2574
+ layerId: string;
2575
+ /**
2576
+ * Per-target enable predicate. `boolean` is a global on/off; a function
2577
+ * runs per pointer-over and may veto activation. Default `true`.
2578
+ */
2579
+ enable?: boolean | ((element: HoverableElement) => boolean);
2580
+ /**
2581
+ * State name applied to the hovered focal element (and its N-hop
2582
+ * neighbours when `degree > 0`). Default `'hovered'` — matches the
2583
+ * canonical state catalogue auto-merged into every `GraphLayer`. Pass
2584
+ * a custom name when the behaviour should write a project-specific
2585
+ * state instead (e.g. `'focal'`).
2586
+ */
2587
+ state?: string;
2588
+ /**
2589
+ * State name applied to every element *not* in the active set. Leave
2590
+ * `undefined` to skip inactive dimming. Default `undefined`.
2591
+ */
2592
+ inactiveState?: string;
2593
+ /**
2594
+ * Lift the active set (the hovered focal element + its N-hop neighbours)
2595
+ * above the rest within its render layer for the duration of the hover, so
2596
+ * unrelated nodes / edges don't paint over the highlighted data. Edges raise
2597
+ * above other edges (still below all nodes); neighbour nodes raise above
2598
+ * other nodes. Reset when the hover clears. Visual-only — restacking doesn't
2599
+ * affect hit-testing. Default `true`.
2600
+ */
2601
+ raiseActive?: boolean;
2602
+ /**
2603
+ * N-hop neighbour radius. `0` = hovered element only; `1` = direct
2604
+ * neighbours + connecting edges; `N` = N-hop. Default `0`.
2605
+ */
2606
+ degree?: number;
2607
+ /** Direction for neighbour traversal. Default `'both'`. */
2608
+ direction?: HoverDirection;
2609
+ /**
2610
+ * Camera scale at or below which the behaviour swaps `state` for
2611
+ * `zoomedOutState` (and `zoomedOutEdgeState` for edges). The hovered set
2612
+ * gets re-painted through the swapped state names whenever the camera
2613
+ * crosses this threshold mid-hover. Omit (or leave both zoomed-out names
2614
+ * undefined) and the behaviour is identical to today.
2615
+ *
2616
+ * Typical use: at world-level zoom every node collapses to ~1 anti-aliased
2617
+ * pixel, so the normal `active` state is invisible against background
2618
+ * dots. A bigger `active-far` config (size + strokeWidth bumped) makes
2619
+ * the hovered node pop.
2620
+ */
2621
+ zoomThreshold?: number;
2622
+ /**
2623
+ * State name applied to the hovered node + N-hop neighbour nodes when
2624
+ * `camera.scale <= zoomThreshold`. Falls back to `state` when undefined
2625
+ * (no node-side zoom swap, but edges may still swap via
2626
+ * `zoomedOutEdgeState`).
2627
+ */
2628
+ zoomedOutState?: string;
2629
+ /**
2630
+ * State name applied to connecting edges when
2631
+ * `camera.scale <= zoomThreshold` AND `degree > 0`. Falls back to `state`
2632
+ * when undefined.
2633
+ */
2634
+ zoomedOutEdgeState?: string;
2635
+ /**
2636
+ * Gfx-transform scale multiplier applied to each hovered node (and the
2637
+ * N-hop neighbour nodes) when `camera.scale <= zoomThreshold`. Pure
2638
+ * transform write via {@link PrimitivesRenderer.scaleShape} — no geometry
2639
+ * rebuild, no styling change. Use this when you want the hovered node to
2640
+ * just *grow visually* at low zoom (so it stands out against ~1 px
2641
+ * background dots) while keeping its original colour, stroke, and label.
2642
+ *
2643
+ * Multiplies the existing `gfx.scale`, so if `NodeSizeLODBehaviour` is
2644
+ * also active it will overwrite the multiplier on the next zoom frame —
2645
+ * prefer `zoomedOutState` with a bigger `size` in that case. For stories
2646
+ * without an LOD behaviour, this is the cleanest "scale on hover" knob.
2647
+ *
2648
+ * Only nodes are scaled — connectors don't compose cleanly with
2649
+ * `gfx.scale` (the polyline would shift, not just thicken). The hovered
2650
+ * node's outgoing edges still anchor to its geometric position, which
2651
+ * sits inside the now-bigger silhouette — visually acceptable.
2652
+ *
2653
+ * `undefined` (default) and `1` both disable the multiplier.
2654
+ */
2655
+ zoomedOutScale?: number;
2656
+ /** Fired when an element first becomes hovered. */
2657
+ onHover?: (element: HoverableElement) => void;
2658
+ /** Fired when hover ends on a previously hovered element. */
2659
+ onHoverEnd?: (element: HoverableElement) => void;
2660
+ }
2661
+ interface ResolvedOptions$6 {
2662
+ enable: boolean | ((element: HoverableElement) => boolean);
2663
+ state: string;
2664
+ inactiveState: string | undefined;
2665
+ raiseActive: boolean;
2666
+ degree: number;
2667
+ direction: HoverDirection;
2668
+ zoomThreshold: number | undefined;
2669
+ zoomedOutState: string | undefined;
2670
+ zoomedOutEdgeState: string | undefined;
2671
+ zoomedOutScale: number | undefined;
2672
+ onHover: ((element: HoverableElement) => void) | undefined;
2673
+ onHoverEnd: ((element: HoverableElement) => void) | undefined;
2674
+ }
2675
+ declare class HoverActivateBehaviour extends Behaviour {
2676
+ /** Bound target layer — resolved in `onRegister`. */
2677
+ private layer;
2678
+ private opts;
2679
+ /** Subscription disposers, called in `onDestroy`. */
2680
+ private subs;
2681
+ /** Currently hovered element, or `null`. */
2682
+ private current;
2683
+ /** Neighbour ids that received the active state (excluding `current`). */
2684
+ private activeIds;
2685
+ /** Element ids that received the inactive state. */
2686
+ private inactiveIds;
2687
+ /**
2688
+ * State name actually applied to nodes for the current hover — equals
2689
+ * `opts.state` normally, `opts.zoomedOutState` when the camera was below
2690
+ * `opts.zoomThreshold` at activation (or after a mid-hover swap). Tracked
2691
+ * so `clearHover` / `swapStates` remove whatever was actually applied,
2692
+ * not just whatever the current `opts.state` is now.
2693
+ */
2694
+ private appliedNodeState;
2695
+ /** Sibling of {@link appliedNodeState} for edges. */
2696
+ private appliedEdgeState;
2697
+ /**
2698
+ * Gfx-transform multiplier currently applied to the hovered node set.
2699
+ * `1` (or `null`) means no multiplier is active. Tracked so a threshold
2700
+ * cross or clear can reset only the ids we actually scaled.
2701
+ */
2702
+ private appliedScale;
2703
+ /** Node ids currently scaled via `renderer.scaleShape` — reset on clear. */
2704
+ private readonly scaledNodeIds;
2705
+ /**
2706
+ * `gfx.zIndex` written to the active set when `raiseActive` is on. Any value
2707
+ * above the default `0` lifts the element over its untouched peers; `1` is
2708
+ * enough and keeps hover- and selection-raises on the same tier.
2709
+ */
2710
+ private static readonly RAISED_Z_INDEX;
2711
+ /** Ids currently raised via `renderer.raiseShape` / `raiseConnector`. */
2712
+ private readonly raisedIds;
2713
+ constructor(opts: HoverActivateBehaviourOptions);
2714
+ protected onRegister(ctx: CanvasContext): void;
2715
+ protected onDestroy(): void;
2716
+ protected onDisable(): void;
2717
+ /** The element currently driving the hover effect, or `null`. */
2718
+ get hoveredElement(): HoverableElement | null;
2719
+ /** Read-only snapshot of resolved options. */
2720
+ get options(): Readonly<ResolvedOptions$6>;
2721
+ /**
2722
+ * Runtime option update. State-affecting changes clear any in-flight hover
2723
+ * so the next hover applies the new visuals cleanly.
2724
+ */
2725
+ setOptions(patch: Partial<HoverActivateBehaviourOptions>): void;
2726
+ /** Clear all states applied by the current hover. */
2727
+ clearHover(): void;
2728
+ private handlePointerOver;
2729
+ private handlePointerOut;
2730
+ private activate;
2731
+ /**
2732
+ * Choose which node + edge state names AND gfx scale to apply right now,
2733
+ * based on `camera.scale` vs. `opts.zoomThreshold`.
2734
+ *
2735
+ * - `node` / `edge`: `opts.state` (or `opts.zoomedOutState` /
2736
+ * `opts.zoomedOutEdgeState` at far zoom). Each role falls back to
2737
+ * `opts.state` independently.
2738
+ * - `scale`: `1` (or `opts.zoomedOutScale` at far zoom). The scale
2739
+ * multiplier is independent of the state-name swap — a story can
2740
+ * configure either, both, or neither.
2741
+ */
2742
+ private pickTier;
2743
+ /**
2744
+ * Handle a `camera:zoom` event while a hover is active. Two independent
2745
+ * dimensions may change as the camera crosses the threshold:
2746
+ *
2747
+ * - State names — swap via {@link swapStates} (state-config-driven
2748
+ * restyle, composes with `NodeSizeLODBehaviour`).
2749
+ * - Scale multiplier — re-apply via {@link applyScale} (`gfx.scale`
2750
+ * write, does NOT compose with LOD).
2751
+ *
2752
+ * Idempotent: a zoom that doesn't cross the threshold leaves both
2753
+ * dimensions unchanged and exits cheaply.
2754
+ */
2755
+ private handleCameraZoom;
2756
+ /**
2757
+ * Set / reset `gfx.scale` on the hovered node set. Pure transform write
2758
+ * via {@link PrimitivesRenderer.scaleShape} — no geometry rebuild,
2759
+ * preserves the node's spec-driven colour, stroke, label, etc.
2760
+ *
2761
+ * Resets any previously-scaled ids first so `applyScale(1)` is a clean
2762
+ * teardown. Only ids that resolve to shapes (not connectors) are
2763
+ * touched — `activeIds` is a flat set of mixed kinds; `renderer.hasShape`
2764
+ * filters out edge ids cheaply.
2765
+ */
2766
+ private applyScale;
2767
+ /**
2768
+ * Raise the current hovered set (`current` + `activeIds`) above their peers
2769
+ * via `renderer.raiseShape` / `raiseConnector`. `activeIds` is a flat set of
2770
+ * mixed kinds, so each id is dispatched by `hasShape` / `hasConnector`.
2771
+ * Tracked in {@link raisedIds} so {@link resetRaise} restores exactly the
2772
+ * ids we touched.
2773
+ */
2774
+ private applyRaise;
2775
+ /** Reset every id raised by {@link applyRaise} back to the default z (0). */
2776
+ private resetRaise;
2777
+ /**
2778
+ * Walk the current hovered set (`current` + `activeIds`) and replace the
2779
+ * previously-applied state names with `picked.node` / `picked.edge`.
2780
+ * Skips work per-role when the state name didn't change for that role
2781
+ * (e.g. only the edge state swapped while the node state stayed put).
2782
+ *
2783
+ * `activeIds` is a flat set containing both node and edge ids — we don't
2784
+ * track type per id, so we call `setNodeState` / `setEdgeState` for both;
2785
+ * mismatched calls (an id that doesn't exist in that store) no-op
2786
+ * gracefully. Matches the existing pattern in {@link clearHover}.
2787
+ */
2788
+ private swapStates;
2789
+ /** BFS neighbourhood expansion using the store's adjacency index. */
2790
+ private collectNeighbours;
2791
+ private applyInactive;
2792
+ private resolveElement;
2793
+ }
2794
+
2795
+ /**
2796
+ * `ModifierTracker` — global helper that tracks which keyboard modifier keys
2797
+ * are currently held. Several behaviours (Click/Brush/Lasso) need to know
2798
+ * the modifier state at the moment of a pointer event, but the renderer's
2799
+ * shape/connector pointer events don't carry that info — they're synthesised
2800
+ * from PixiJS events. Tracking modifiers via window-level key events is the
2801
+ * pragmatic substitute.
2802
+ *
2803
+ * Reference-counted: the first behaviour to call `attach()` installs the
2804
+ * window listeners; subsequent `attach()` calls just bump the counter.
2805
+ * `detach()` removes the listeners when the count returns to zero.
2806
+ *
2807
+ * @internal — not exported from `@invana/graph`. Used by behaviours.
2808
+ */
2809
+ type ModifierKey = 'shift' | 'control' | 'alt' | 'meta';
2810
+
2811
+ /**
2812
+ * `ClickSelectBehaviour` — toggles a named visual state on clicked nodes /
2813
+ * edges with optional N-degree neighbour expansion, modifier-driven
2814
+ * multi-select, and optional dimming of unselected elements.
2815
+ *
2816
+ * Layer-scoped: constructed with a target `layerId`. Subscribes to that
2817
+ * layer's renderer click events and to the canvas-level `background:click`
2818
+ * for clear-on-background behaviour.
2819
+ *
2820
+ * Default `enabled: false` — register, then explicitly enable.
2821
+ *
2822
+ * The canonical `selected` state is auto-merged into every
2823
+ * `GraphLayer`'s state catalogue — no setup needed. Override the layer's
2824
+ * `options.node.state.selected` to customise the visual.
2825
+ *
2826
+ * @example
2827
+ * ```ts
2828
+ * canvas.behaviours.register(
2829
+ * new ClickSelectBehaviour({
2830
+ * id: 'select',
2831
+ * layerId: 'graph',
2832
+ * enabled: true,
2833
+ * multiple: true,
2834
+ * degree: 1,
2835
+ * }),
2836
+ * );
2837
+ * ```
2838
+ */
2839
+
2840
+ /** Element kind for selection targets. */
2841
+ type SelectableElementType = HoverableElementType;
2842
+ /** Edge-traversal direction filter. */
2843
+ type SelectDirection = HoverDirection;
2844
+ /** Modifier-key names accepted by `trigger`. */
2845
+ type SelectModifierKey = ModifierKey;
2846
+ /** Element handed to selection callbacks. */
2847
+ interface SelectableElement {
2848
+ readonly id: string;
2849
+ readonly type: SelectableElementType;
2850
+ /** Arbitrary user payload from `node.data` or `edge.data`. */
2851
+ readonly data: unknown;
2852
+ }
2853
+ /** Per-flush snapshot fired to `onSelectionChange`. */
2854
+ interface SelectionSnapshot {
2855
+ shapeIds: string[];
2856
+ connectorIds: string[];
2857
+ }
2858
+ /** Event-map for {@link ClickSelectBehaviour.events}. */
2859
+ type ClickSelectEventMap = {
2860
+ /**
2861
+ * Fired once whenever the selection set is replaced (click, `select*`,
2862
+ * `clearSelection`, or brush/lasso delegation). The non-clobbering complement
2863
+ * to the `onSelectionChange` callback — observers (e.g. the canvas-react
2864
+ * `useSelection` hook) subscribe here instead of hijacking the callback.
2865
+ */
2866
+ 'selection:change': SelectionSnapshot;
2867
+ };
2868
+ /** Constructor options for `ClickSelectBehaviour`. */
2869
+ interface ClickSelectBehaviourOptions extends BehaviourOptions {
2870
+ /** Required — the `GraphLayer` id this behaviour drives. */
2871
+ layerId: string;
2872
+ /**
2873
+ * Per-target enable predicate. `boolean` is a global on/off; a function
2874
+ * runs per click and may veto. Default `true`.
2875
+ */
2876
+ enable?: boolean | ((element: SelectableElement) => boolean);
2877
+ /**
2878
+ * Allow more than one element selected at a time. When `true`, a qualifying
2879
+ * click (see `trigger`) toggles the element in/out of the selection; when
2880
+ * `false` it replaces the selection with the clicked element. Default `false`.
2881
+ */
2882
+ multiple?: boolean;
2883
+ /**
2884
+ * Modifier key(s) required for a click to affect the selection **at all**.
2885
+ * When non-empty, a click that holds none of these is ignored — a plain
2886
+ * (unmodified) click selects nothing, and a plain left-drag stays a pure
2887
+ * pan. With a modifier held, the click selects (replacing the selection, or
2888
+ * toggling membership when `multiple` is `true`). Empty array = every click
2889
+ * selects, no modifier needed. Default `[]` (plain click selects). Pass
2890
+ * `['shift']` to gate selection behind the Shift key.
2891
+ */
2892
+ trigger?: SelectModifierKey[];
2893
+ /**
2894
+ * N-hop neighbour radius around each seed. `0` = clicked element only.
2895
+ * Default `0`.
2896
+ */
2897
+ degree?: number;
2898
+ /** Direction for neighbour traversal. Default `'both'`. */
2899
+ direction?: SelectDirection;
2900
+ /** Active-state name. Default `'selected'`. */
2901
+ state?: string;
2902
+ /**
2903
+ * State applied to every element that is *not* selected. `undefined`
2904
+ * disables dimming. Default `undefined`.
2905
+ */
2906
+ unselectedState?: string;
2907
+ /**
2908
+ * Lift the selected set (seeds + degree-expanded neighbours) above the rest
2909
+ * within its render layer, so unrelated nodes / edges don't paint over the
2910
+ * selection. Edges raise above other edges (still below all nodes); nodes
2911
+ * raise above other nodes. Reset when the selection clears. Visual-only —
2912
+ * restacking doesn't affect hit-testing. Default `true`.
2913
+ */
2914
+ raiseActive?: boolean;
2915
+ /** Clear selection when clicking the empty canvas background. Default `true`. */
2916
+ clearOnBackground?: boolean;
2917
+ /** Fired when an element becomes selected. */
2918
+ onSelect?: (element: SelectableElement) => void;
2919
+ /** Fired when an element becomes deselected. */
2920
+ onDeselect?: (element: SelectableElement) => void;
2921
+ /** Fired once per click with the post-settle selection snapshot. */
2922
+ onSelectionChange?: (snapshot: SelectionSnapshot) => void;
2923
+ }
2924
+ interface ResolvedOptions$5 {
2925
+ enable: boolean | ((element: SelectableElement) => boolean);
2926
+ multiple: boolean;
2927
+ trigger: SelectModifierKey[];
2928
+ degree: number;
2929
+ direction: SelectDirection;
2930
+ state: string;
2931
+ unselectedState: string | undefined;
2932
+ raiseActive: boolean;
2933
+ clearOnBackground: boolean;
2934
+ onSelect: ((element: SelectableElement) => void) | undefined;
2935
+ onDeselect: ((element: SelectableElement) => void) | undefined;
2936
+ onSelectionChange: ((snapshot: SelectionSnapshot) => void) | undefined;
2937
+ }
2938
+ declare class ClickSelectBehaviour extends Behaviour {
2939
+ /**
2940
+ * Selection event bus. Subscribe to `'selection:change'` for a reactive
2941
+ * snapshot every time the selection set is replaced. Independent of (and
2942
+ * additive to) the `onSelectionChange` option.
2943
+ */
2944
+ readonly events: EventEmitter<ClickSelectEventMap>;
2945
+ private layer;
2946
+ private opts;
2947
+ /** Subscription disposers. */
2948
+ private subs;
2949
+ /** Seed set — ids the user *directly* clicked / passed to `select*`. */
2950
+ private seeds;
2951
+ /** Expanded set — seeds + degree-expanded neighbours. */
2952
+ private selected;
2953
+ /** Ids currently rendered with the `unselectedState`. */
2954
+ private unselectedIds;
2955
+ /**
2956
+ * `gfx.zIndex` written to the selected set when `raiseActive` is on. Any
2957
+ * value above the default `0` lifts the element over its untouched peers;
2958
+ * `1` matches `HoverActivateBehaviour`'s raise tier.
2959
+ */
2960
+ private static readonly RAISED_Z_INDEX;
2961
+ /** Ids currently raised via `renderer.raiseShape` / `raiseConnector`. */
2962
+ private readonly raisedIds;
2963
+ /** True when the most recent click already consumed an element. */
2964
+ private clickConsumedByElement;
2965
+ /** Pointerdown screen-position — used to distinguish a click from a drag. */
2966
+ private pointerDownScreen;
2967
+ /**
2968
+ * Set once the pointer travels past the click/drag threshold while a button
2969
+ * is held. Used to suppress the synthetic element `click` that fires at the
2970
+ * end of a node drag — without it, dragging a selected node would collapse
2971
+ * the whole selection down to that one node on release.
2972
+ */
2973
+ private pressMoved;
2974
+ constructor(opts: ClickSelectBehaviourOptions);
2975
+ protected onRegister(ctx: CanvasContext): void;
2976
+ protected onDestroy(): void;
2977
+ protected onDisable(): void;
2978
+ /** Resolved current options (read-only snapshot). */
2979
+ get options(): Readonly<ResolvedOptions$5>;
2980
+ /**
2981
+ * Runtime option update. State-affecting changes clear the current
2982
+ * visual selection and re-apply with the new options.
2983
+ */
2984
+ setOptions(patch: Partial<ClickSelectBehaviourOptions>): void;
2985
+ /** Replace the selection with a single element. */
2986
+ select(id: string, type?: SelectableElementType): void;
2987
+ /** Replace the selection with the given (id, type) pairs. */
2988
+ selectMultiple(elements: Array<{
2989
+ id: string;
2990
+ type?: SelectableElementType;
2991
+ }>): void;
2992
+ /** Add a single element to the current selection. */
2993
+ addToSelection(id: string, type?: SelectableElementType): void;
2994
+ /** Remove a single element from the current selection. */
2995
+ deselect(id: string): void;
2996
+ /** Toggle the membership of `id` in the selection. */
2997
+ toggle(id: string, type?: SelectableElementType): void;
2998
+ /** True iff `id` is part of the rendered selection (seed or expanded). */
2999
+ isSelected(id: string): boolean;
3000
+ /** All currently selected ids (seeds + expanded). */
3001
+ getSelectedIds(): string[];
3002
+ /** Currently selected shape (node) ids. */
3003
+ getSelectedShapeIds(): string[];
3004
+ /** Currently selected connector (edge) ids. */
3005
+ getSelectedConnectorIds(): string[];
3006
+ /** Clear the entire selection and any dimming. */
3007
+ clearSelection(): void;
3008
+ /**
3009
+ * Select every node and edge on the target layer. Replaces the current
3010
+ * selection. No-op if the layer isn't mounted.
3011
+ */
3012
+ selectAll(): void;
3013
+ /**
3014
+ * Select a node together with its neighbours (in the given direction) and the
3015
+ * edges incident to it. Replaces the current selection. No-op if the layer
3016
+ * isn't mounted.
3017
+ *
3018
+ * @param id Seed node id.
3019
+ * @param dir Adjacency direction for neighbours + incident edges. Default `'both'`.
3020
+ */
3021
+ selectNeighbourhood(id: string, dir?: 'in' | 'out' | 'both'): void;
3022
+ private handleElementClick;
3023
+ /**
3024
+ * Core selection engine: replace seeds, recompute expansion, swap visuals,
3025
+ * diff-emit callbacks.
3026
+ */
3027
+ private applySelection;
3028
+ /** Expand seeds by `degree` hops (BFS) — same shape as HoverActivate. */
3029
+ private expandSeeds;
3030
+ private clearVisualsOnly;
3031
+ /**
3032
+ * Raise the selected set above its peers via `renderer.raiseShape` /
3033
+ * `raiseConnector`. `selected` carries the element type per id, so each is
3034
+ * dispatched directly. Tracked in {@link raisedIds} so {@link resetRaise}
3035
+ * restores exactly the ids we touched.
3036
+ */
3037
+ private applyRaise;
3038
+ /** Reset every id raised by {@link applyRaise} back to the default z (0). */
3039
+ private resetRaise;
3040
+ private applyUnselected;
3041
+ private resolveElement;
3042
+ private buildSnapshot;
3043
+ }
3044
+
3045
+ /**
3046
+ * `ClickInspectBehaviour` — tracks the **single** node / edge a user clicked for
3047
+ * *inspection / editing*, independent of {@link ClickSelectBehaviour}.
3048
+ *
3049
+ * Selection and inspection are different concerns: selection drives highlighting
3050
+ * and multi-element drag (and may hold many elements at once); inspection feeds a
3051
+ * property editor (`InspectorPanel`), which only ever edits **one** element. This
3052
+ * behaviour is the authority for the latter — it remembers the last element
3053
+ * clicked and clears on a background click — so the editor never has to reach
3054
+ * into the selection set (where a multi-select would leave it with nothing single
3055
+ * to show).
3056
+ *
3057
+ * Layer-scoped: constructed with a target `layerId`. Subscribes to that layer's
3058
+ * renderer click events; uses a native DOM `click` listener for the
3059
+ * clear-on-background path (the engine doesn't emit `background:click` today),
3060
+ * mirroring `ClickSelectBehaviour`.
3061
+ *
3062
+ * Default `enabled: false` — register, then explicitly enable.
3063
+ *
3064
+ * @example
3065
+ * ```ts
3066
+ * canvas.behaviours.register(
3067
+ * new ClickInspectBehaviour({ id: 'click-inspect', layerId: 'graph', enabled: true }),
3068
+ * );
3069
+ * canvas.behaviours.get<ClickInspectBehaviour>('click-inspect')
3070
+ * ?.events.on('inspect:change', (t) => console.log(t)); // { kind, id } | null
3071
+ * ```
3072
+ */
3073
+
3074
+ /** The single element currently targeted for inspection / editing. */
3075
+ interface InspectTarget {
3076
+ kind: 'node' | 'edge';
3077
+ id: string;
3078
+ }
3079
+ /** Event-map for {@link ClickInspectBehaviour.events}. */
3080
+ type ClickInspectEventMap = {
3081
+ /**
3082
+ * Fired whenever the inspected element changes — a node / edge click sets it,
3083
+ * a background click (or `clear`) sets it to `null`.
3084
+ */
3085
+ 'inspect:change': InspectTarget | null;
3086
+ };
3087
+ /** Constructor options for `ClickInspectBehaviour`. */
3088
+ interface ClickInspectBehaviourOptions extends BehaviourOptions {
3089
+ /** Required — the `GraphLayer` id this behaviour reads clicks from. */
3090
+ layerId: string;
3091
+ /** Clear the inspected element when clicking the empty canvas. Default `true`. */
3092
+ clearOnBackground?: boolean;
3093
+ }
3094
+ declare class ClickInspectBehaviour extends Behaviour {
3095
+ /**
3096
+ * Inspection event bus. Subscribe to `'inspect:change'` for the current
3097
+ * single target (or `null`) every time it changes.
3098
+ */
3099
+ readonly events: EventEmitter<ClickInspectEventMap>;
3100
+ private layer;
3101
+ private readonly clearOnBackground;
3102
+ /** Subscription disposers. */
3103
+ private subs;
3104
+ /** Current inspected element, or `null`. */
3105
+ private target;
3106
+ /** True when the most recent click already consumed an element. */
3107
+ private clickConsumedByElement;
3108
+ /** Pointerdown screen-position — used to distinguish a click from a drag. */
3109
+ private pointerDownScreen;
3110
+ /**
3111
+ * Set once the pointer travels past the click/drag threshold while a button
3112
+ * is held — suppresses the synthetic element `click` fired at the end of a
3113
+ * node drag so a drag doesn't open the inspector.
3114
+ */
3115
+ private pressMoved;
3116
+ constructor(opts: ClickInspectBehaviourOptions);
3117
+ protected onRegister(ctx: CanvasContext): void;
3118
+ protected onDestroy(): void;
3119
+ protected onDisable(): void;
3120
+ /** The element currently targeted for inspection, or `null`. */
3121
+ getTarget(): InspectTarget | null;
3122
+ /** Set the inspected element explicitly (e.g. from a context menu). */
3123
+ setTarget(target: InspectTarget | null): void;
3124
+ /** Clear the inspected element. */
3125
+ clear(): void;
3126
+ private handleElementClick;
3127
+ }
3128
+
3129
+ /**
3130
+ * `BrushSelectBehaviour` — click-and-drag rectangular selection in screen
3131
+ * space. Every shape (and optionally connector) enclosed by the rectangle is
3132
+ * unioned with the existing selection.
3133
+ *
3134
+ * When a {@link ClickSelectBehaviour} is registered on the canvas (default
3135
+ * id `'click-select'`), the brush delegates selection mutations through it
3136
+ * so both behaviours stay in sync. Without a click-select target, the brush
3137
+ * falls back to driving `GraphLayer` state directly.
3138
+ *
3139
+ * Default `enabled: false` — register, then explicitly enable.
3140
+ *
3141
+ * Note: this behaviour creates a `Graphics` overlay attached to `ctx.stage`
3142
+ * for the rubber-band rectangle. That's one of two narrow carve-outs where
3143
+ * `@invana/graph` reaches into pixi directly — the alternative would be to
3144
+ * mount a private `ScreenLayer` per behaviour, which leaks an unnamed layer
3145
+ * id into the public registry.
3146
+ *
3147
+ * @example
3148
+ * ```ts
3149
+ * canvas.behaviours.register(
3150
+ * new BrushSelectBehaviour({
3151
+ * id: 'brush',
3152
+ * layerId: 'graph',
3153
+ * enabled: true,
3154
+ * trigger: ['shift'],
3155
+ * enableElements: ['shape', 'connector'],
3156
+ * }),
3157
+ * );
3158
+ * ```
3159
+ */
3160
+
3161
+ /** Element kinds the brush will pick up. */
3162
+ type BrushSelectElementType = SelectableElementType;
3163
+ /** Modifier-key names accepted by `trigger`. */
3164
+ type BrushModifierKey = ModifierKey;
3165
+ /** Visual style for the rubber-band rectangle. */
3166
+ interface BrushSelectStyle {
3167
+ /** Fill color `0xRRGGBB`. Default `0x1677ff`. */
3168
+ fill?: number;
3169
+ /** Fill opacity 0–1. Default `0.1`. */
3170
+ fillAlpha?: number;
3171
+ /** Stroke color `0xRRGGBB`. Default `0x1677ff`. */
3172
+ stroke?: number;
3173
+ /** Stroke opacity 0–1. Default `0.8`. */
3174
+ strokeAlpha?: number;
3175
+ /** Stroke width in pixels. Default `1`. */
3176
+ strokeWidth?: number;
3177
+ /** Dash pattern `[dashLen, gapLen]`. `[]` for solid. Default `[4, 4]`. */
3178
+ strokeDash?: number[];
3179
+ }
3180
+ /** Constructor options for `BrushSelectBehaviour`. */
3181
+ interface BrushSelectBehaviourOptions extends BehaviourOptions {
3182
+ /** Required — the `GraphLayer` id this behaviour brushes over. */
3183
+ layerId: string;
3184
+ /**
3185
+ * Optional `ClickSelectBehaviour` id to delegate to. Default `'click-select'`.
3186
+ * If found, the brush hands the merged selection to the click-select layer
3187
+ * so both stay in sync. Otherwise the brush mutates `GraphLayer` state
3188
+ * directly.
3189
+ */
3190
+ clickSelectId?: string;
3191
+ /**
3192
+ * Per-drag enable predicate. `boolean` global on/off; or a function
3193
+ * called with the pointerdown native event. Default `true`.
3194
+ */
3195
+ enable?: boolean | ((event: PointerEvent) => boolean);
3196
+ /**
3197
+ * Element types eligible for brush selection. Default `['shape', 'connector']`.
3198
+ */
3199
+ enableElements?: BrushSelectElementType[];
3200
+ /**
3201
+ * Modifier key(s) that must be held during pointerdown to activate the
3202
+ * brush. Empty array = any left-drag activates. Default `['shift']`.
3203
+ */
3204
+ trigger?: BrushModifierKey[];
3205
+ /**
3206
+ * Live-update the selection as the rect grows. `false` = apply only on
3207
+ * release. Default `false`.
3208
+ */
3209
+ immediately?: boolean;
3210
+ /**
3211
+ * Visual state name applied to brushed elements when no `ClickSelectBehaviour`
3212
+ * is targeted. Ignored on the delegate path. Default `'selected'`.
3213
+ */
3214
+ state?: string;
3215
+ /** Rectangle style. */
3216
+ style?: BrushSelectStyle;
3217
+ /**
3218
+ * Clear selection when the user clicks on the empty background (no drag).
3219
+ * Default `true`.
3220
+ */
3221
+ clearOnBackground?: boolean;
3222
+ /** Fired once on release if the brush produced a selection change. */
3223
+ onSelect?: (snapshot: SelectionSnapshot) => void;
3224
+ }
3225
+ interface ResolvedOptions$4 {
3226
+ enable: boolean | ((event: PointerEvent) => boolean);
3227
+ enableElements: BrushSelectElementType[];
3228
+ trigger: BrushModifierKey[];
3229
+ immediately: boolean;
3230
+ state: string;
3231
+ style: BrushSelectStyle;
3232
+ clearOnBackground: boolean;
3233
+ onSelect: ((snapshot: SelectionSnapshot) => void) | undefined;
3234
+ }
3235
+ declare class BrushSelectBehaviour extends Behaviour {
3236
+ private readonly clickSelectId;
3237
+ private opts;
3238
+ private layer;
3239
+ private ctxRef;
3240
+ private overlay;
3241
+ private gfx;
3242
+ private dragActive;
3243
+ private dragStart;
3244
+ private dragCurrent;
3245
+ private listenerDisposers;
3246
+ constructor(opts: BrushSelectBehaviourOptions);
3247
+ protected onRegister(ctx: CanvasContext): void;
3248
+ protected onDestroy(): void;
3249
+ protected onDisable(): void;
3250
+ get options(): Readonly<ResolvedOptions$4>;
3251
+ setOptions(patch: Partial<BrushSelectBehaviourOptions>): void;
3252
+ private screenFromEvent;
3253
+ private handlePointerDown;
3254
+ private handlePointerMove;
3255
+ private handlePointerUp;
3256
+ private cancelDrag;
3257
+ private drawRect;
3258
+ private drawDashedRect;
3259
+ private clearRect;
3260
+ private rectHasArea;
3261
+ private triggerActive;
3262
+ private applySelection;
3263
+ private clearSelection;
3264
+ }
3265
+
3266
+ /**
3267
+ * `LassoSelectBehaviour` — click-and-drag freeform polygon selection in
3268
+ * **world** space (the polygon stays anchored to the graph during pan/zoom).
3269
+ *
3270
+ * Mirrors {@link BrushSelectBehaviour} but operates with point-in-polygon
3271
+ * containment instead of an AABB rect. Delegates to a
3272
+ * {@link ClickSelectBehaviour} when one is registered.
3273
+ *
3274
+ * Default `enabled: false` — register, then explicitly enable.
3275
+ *
3276
+ * Same pixi-overlay carve-out as `BrushSelectBehaviour` — the world-space
3277
+ * polygon is rendered into a `Graphics` attached to `ctx.world` (which
3278
+ * pans/zooms with the camera).
3279
+ */
3280
+
3281
+ /** Element kinds the lasso will pick up. */
3282
+ type LassoSelectElementType = SelectableElementType;
3283
+ /** Modifier-key names accepted by `trigger`. */
3284
+ type LassoModifierKey = ModifierKey;
3285
+ /** Visual style for the polygon overlay. Same shape as the brush style. */
3286
+ interface LassoSelectStyle {
3287
+ fill?: number;
3288
+ fillAlpha?: number;
3289
+ stroke?: number;
3290
+ strokeAlpha?: number;
3291
+ /** Stroke width in *screen* pixels (auto-divided by zoom). Default `1`. */
3292
+ strokeWidth?: number;
3293
+ /** Dash pattern in screen pixels `[dashLen, gapLen]`. Default `[4, 4]`. */
3294
+ strokeDash?: number[];
3295
+ }
3296
+ /** Constructor options for `LassoSelectBehaviour`. */
3297
+ interface LassoSelectBehaviourOptions extends BehaviourOptions {
3298
+ layerId: string;
3299
+ clickSelectId?: string;
3300
+ enable?: boolean | ((event: PointerEvent) => boolean);
3301
+ enableElements?: LassoSelectElementType[];
3302
+ trigger?: LassoModifierKey[];
3303
+ immediately?: boolean;
3304
+ state?: string;
3305
+ style?: LassoSelectStyle;
3306
+ clearOnBackground?: boolean;
3307
+ onSelect?: (snapshot: SelectionSnapshot) => void;
3308
+ }
3309
+ interface ResolvedOptions$3 {
3310
+ enable: boolean | ((event: PointerEvent) => boolean);
3311
+ enableElements: LassoSelectElementType[];
3312
+ trigger: LassoModifierKey[];
3313
+ immediately: boolean;
3314
+ state: string;
3315
+ style: LassoSelectStyle;
3316
+ clearOnBackground: boolean;
3317
+ onSelect: ((snapshot: SelectionSnapshot) => void) | undefined;
3318
+ }
3319
+ declare class LassoSelectBehaviour extends Behaviour {
3320
+ private readonly clickSelectId;
3321
+ private opts;
3322
+ private layer;
3323
+ private ctxRef;
3324
+ private overlay;
3325
+ private gfx;
3326
+ private dragActive;
3327
+ /** Polygon points in WORLD space. */
3328
+ private worldPoints;
3329
+ private lastScreen;
3330
+ private startScreen;
3331
+ private listenerDisposers;
3332
+ constructor(opts: LassoSelectBehaviourOptions);
3333
+ protected onRegister(ctx: CanvasContext): void;
3334
+ protected onDestroy(): void;
3335
+ protected onDisable(): void;
3336
+ get options(): Readonly<ResolvedOptions$3>;
3337
+ setOptions(patch: Partial<LassoSelectBehaviourOptions>): void;
3338
+ private screenFromEvent;
3339
+ private handlePointerDown;
3340
+ private handlePointerMove;
3341
+ private handlePointerUp;
3342
+ private cancelDrag;
3343
+ private gestureHasArea;
3344
+ private drawPolygon;
3345
+ private drawDashedClosed;
3346
+ private clearPolygon;
3347
+ private triggerActive;
3348
+ private applySelection;
3349
+ private clearSelection;
3350
+ }
3351
+
3352
+ /**
3353
+ * `DragNodeBehaviour` — pointer-drag a `GraphLayer` node by writing the new
3354
+ * position to its `GraphStore` (not directly to the renderer).
3355
+ *
3356
+ * Differs from `@invana/canvas` `DragShapeBehaviour` in one important way:
3357
+ * the position update flows through the store, so:
3358
+ * - `node:update` events fire — anyone listening (server replication,
3359
+ * analytics, animations) sees the move.
3360
+ * - The layer's connector-reroute pass runs naturally on the store flush.
3361
+ *
3362
+ * **Doesn't pin during the drag.** The transient hold against an active
3363
+ * physics layout is done via the layer's `node:drag-start` / `node:drag-end`
3364
+ * events — layouts (e.g. `D3ForceLayout` clamping `fx/fy`) subscribe and
3365
+ * manage the lock internally. The store's `GraphNode.pinned` flag is *not*
3366
+ * touched mid-gesture, since that flag is user-data semantics (permanent
3367
+ * pin) and a drag shouldn't silently mutate it.
3368
+ *
3369
+ * **Pin on release is opt-in.** Set `pinOnRelease: true` to call
3370
+ * `store.setPinned(id, true)` on drag-end — useful when you want the user's
3371
+ * placement to survive future layout passes. Off by default; when off, a
3372
+ * released node is free again and the next layout tick may move it.
3373
+ *
3374
+ * **Selection-aware.** With `dragSelection` (default on), grabbing a node that
3375
+ * is part of the current selection drags the whole selection together. The
3376
+ * selection is read from the layer's `selectionState` visual state, so it works
3377
+ * with any select behaviour (click / lasso / brush) — this behaviour is not
3378
+ * coupled to a specific one.
3379
+ *
3380
+ * Default `enabled: false` — register, then explicitly enable.
3381
+ *
3382
+ * @example
3383
+ * ```ts
3384
+ * canvas.behaviours.register(
3385
+ * new DragNodeBehaviour({ id: 'drag', layerId: 'graph', enabled: true }),
3386
+ * );
3387
+ * ```
3388
+ */
3389
+
3390
+ /** Constructor options for `DragNodeBehaviour`. */
3391
+ interface DragNodeBehaviourOptions extends BehaviourOptions {
3392
+ /** Required — the `GraphLayer` id whose nodes this behaviour drags. */
3393
+ layerId: string;
3394
+ /**
3395
+ * Predicate to restrict which node ids are draggable. Returning `false`
3396
+ * ignores the pointerdown. Default = every node is draggable.
3397
+ */
3398
+ filter?: (id: string) => boolean;
3399
+ /** Cursor applied to the canvas while dragging. Default `'grabbing'`. */
3400
+ dragCursor?: string;
3401
+ /**
3402
+ * When `true`, set `GraphNode.pinned = true` on the dragged node when
3403
+ * the gesture ends (real drag only — a click that didn't move is a
3404
+ * no-op). The store's pinned flag is read by layouts (e.g.
3405
+ * `D3ForceLayout` writes pinned nodes to d3-force's `fx/fy`) so the
3406
+ * node stays where the user dropped it across future layout passes.
3407
+ * Default `false`. To un-pin a pinned node, call
3408
+ * `graph.store.setPinned(id, false)` explicitly.
3409
+ */
3410
+ pinOnRelease?: boolean;
3411
+ /**
3412
+ * When `true` (the default), dragging a node that is itself a compound
3413
+ * group (resolved `style.group` set) translates every descendant by the
3414
+ * same delta in one `setPositionsBulk` call so the whole subtree moves
3415
+ * together. Set to `false` to drag the group frame on its own — useful
3416
+ * only when descendants are layout-driven and should stay put.
3417
+ *
3418
+ * For auto-fit groups the frame's position is layer-derived from the
3419
+ * children bbox; moving descendants moves the frame naturally on the
3420
+ * next flush. For non-auto-fit groups, the group's stored `position`
3421
+ * is also updated so the declared frame follows the cursor.
3422
+ */
3423
+ groupAware?: boolean;
3424
+ /**
3425
+ * When `true` (the default), grabbing a node that is part of the current
3426
+ * selection drags the **whole selection** together — every selected node
3427
+ * moves by the same delta. Grabbing an unselected node (or a selection of
3428
+ * one) falls back to a plain single-node drag. Set `false` to always drag
3429
+ * just the grabbed node regardless of selection.
3430
+ *
3431
+ * Selection is read from the layer's visual state (see `selectionState`),
3432
+ * so this works uniformly whatever set it — click, lasso, or brush — with
3433
+ * no coupling to a specific select behaviour.
3434
+ */
3435
+ dragSelection?: boolean;
3436
+ /**
3437
+ * Name of the layer visual-state that marks a node as selected. Default
3438
+ * `'selected'`, matching `ClickSelectBehaviour`'s default `state`. Only
3439
+ * consulted when `dragSelection` is on. Override if your select behaviour
3440
+ * writes a different state name.
3441
+ */
3442
+ selectionState?: string;
3443
+ /**
3444
+ * When `true` (the default), a plain (no-modifier) press anywhere inside the
3445
+ * current selection's union bounding box — *including the empty world space
3446
+ * between the selected nodes* — grabs the whole selection and drags it, the
3447
+ * way Figma / PowerPoint let you drag a multi-selection by its body rather
3448
+ * than by a specific item.
3449
+ *
3450
+ * Without this, a selection is only draggable by pressing squarely on one of
3451
+ * the selected nodes; pressing in the gaps does nothing (no shape is hit, so
3452
+ * no drag starts). Off the back of a brush/lasso selection that almost always
3453
+ * reads as "the selection won't move" — hence default on.
3454
+ *
3455
+ * Only meaningful when `dragSelection` is also on. The press must carry no
3456
+ * modifier key (so it never collides with brush / lasso / shift-to-add) and
3457
+ * must not land on a node (those go through the normal per-node path). This
3458
+ * does mean panning the camera by dragging from *inside* the selection box is
3459
+ * no longer possible — drag from outside the box, or set this `false`.
3460
+ */
3461
+ selectionBodyDrag?: boolean;
3462
+ /**
3463
+ * Extra world-space padding added around the selection's union bounding box
3464
+ * when testing a press for {@link selectionBodyDrag}. Widens the grab target
3465
+ * so presses just outside the tightest box still catch. Default `0`.
3466
+ */
3467
+ selectionBodyPadding?: number;
3468
+ }
3469
+ declare class DragNodeBehaviour extends Behaviour {
3470
+ private layer;
3471
+ private ctxRef;
3472
+ private readonly filter?;
3473
+ private readonly dragCursor;
3474
+ private readonly groupAware;
3475
+ private readonly pinOnRelease;
3476
+ private readonly dragSelection;
3477
+ private readonly selectionState;
3478
+ private readonly selectionBodyDrag;
3479
+ private readonly selectionBodyPadding;
3480
+ private state;
3481
+ private offShapeDown;
3482
+ private offCanvasDown;
3483
+ private canvasEl;
3484
+ private prevCursor;
3485
+ /**
3486
+ * Pointer id captured on `pointerdown` so we can hold the capture on the
3487
+ * canvas element for the duration of the drag — otherwise the cursor
3488
+ * crossing the canvas bounds (e.g. over a lil-gui panel) fires
3489
+ * `pointercancel` on the document and the drag ends prematurely.
3490
+ */
3491
+ private capturedPointerId;
3492
+ constructor(opts: DragNodeBehaviourOptions);
3493
+ protected onRegister(ctx: CanvasContext): void;
3494
+ protected onDestroy(): void;
3495
+ protected onDisable(): void;
3496
+ /**
3497
+ * Resolve the set of *primary* nodes a gesture on `grabbedId` should drag.
3498
+ * With `dragSelection` on, a grab on a selected node drags every selected
3499
+ * node (filtered by `filter`); otherwise — or for a selection of one — just
3500
+ * the grabbed node. Group descendants are added later, in the first move.
3501
+ */
3502
+ private resolveDragSet;
3503
+ /** Current selection (nodes carrying `selectionState`), filtered by `filter`. */
3504
+ private selectedNodeIds;
3505
+ /**
3506
+ * World-space union AABB of the given nodes, or `null` if none resolve. Each
3507
+ * node contributes its `boundsOfNode` rect (centre-relative, so offset by the
3508
+ * node's stored position); a node whose shape kind reports no bounds collapses
3509
+ * to a zero-size point at its centre.
3510
+ */
3511
+ private selectionBounds;
3512
+ /**
3513
+ * Selection-body drag entry point (see `selectionBodyDrag`). A plain press on
3514
+ * empty world space that falls inside the selection's union bounds grabs the
3515
+ * whole selection. Presses on a node, with a modifier held, or outside the
3516
+ * bounds are left alone so the per-node / brush / lasso / pan paths win.
3517
+ */
3518
+ private onCanvasPointerDown;
3519
+ private startDrag;
3520
+ /**
3521
+ * Low-level drag start shared by the per-node path ({@link startDrag}) and the
3522
+ * selection-body path ({@link onCanvasPointerDown}). `primaryId` is the gesture's
3523
+ * emitted primary; `ids` is the full primary set to translate together.
3524
+ */
3525
+ private beginDrag;
3526
+ private endDrag;
3527
+ private readonly onWindowPointerMove;
3528
+ private readonly onWindowPointerUp;
3529
+ private clientToScreen;
3530
+ }
3531
+
3532
+ /**
3533
+ * `ContextMenuBehaviour` — surfaces right-click (context-menu) gestures on
3534
+ * nodes, edges, and the empty canvas as a single `onContextMenu` callback.
3535
+ *
3536
+ * Layer-scoped: constructed with a target `layerId`. Subscribes to that
3537
+ * layer's renderer `shape:contextmenu` / `connector:contextmenu` events and to
3538
+ * the engine-level `background:contextmenu` (empty-canvas right-click).
3539
+ *
3540
+ * **Headless.** The behaviour does not render any menu UI — it resolves the
3541
+ * target (node id + `data`, edge id + `data`, or canvas) and hands the caller
3542
+ * everything needed to position and populate their own menu. The browser's
3543
+ * native menu is suppressed by `Canvas` (`suppressBrowserContextMenu`, default
3544
+ * `true`), so no `preventDefault` is needed here.
3545
+ *
3546
+ * Default `enabled: false` — register, then explicitly enable.
3547
+ *
3548
+ * @example
3549
+ * ```ts
3550
+ * canvas.behaviours.register(
3551
+ * new ContextMenuBehaviour({
3552
+ * id: 'context-menu',
3553
+ * layerId: 'graph',
3554
+ * enabled: true,
3555
+ * onContextMenu: ({ targetType, id, screen }) => {
3556
+ * showMenu(targetType, id, screen.x, screen.y);
3557
+ * },
3558
+ * }),
3559
+ * );
3560
+ * ```
3561
+ */
3562
+
3563
+ /** Which kind of target a context-menu gesture landed on. */
3564
+ type ContextMenuTargetType = 'node' | 'edge' | 'canvas';
3565
+ /** Payload handed to {@link ContextMenuBehaviourOptions.onContextMenu}. */
3566
+ interface ContextMenuEvent {
3567
+ /** What was right-clicked. */
3568
+ readonly targetType: ContextMenuTargetType;
3569
+ /** Node/edge id, or `null` for an empty-canvas right-click. */
3570
+ readonly id: string | null;
3571
+ /**
3572
+ * Arbitrary user payload from `node.data` / `edge.data`. `undefined` for a
3573
+ * canvas right-click or when the resolved item carries no `data`.
3574
+ */
3575
+ readonly data: unknown;
3576
+ /** Pointer position in world (scene) coordinates. */
3577
+ readonly world: {
3578
+ readonly x: number;
3579
+ readonly y: number;
3580
+ };
3581
+ /**
3582
+ * Pointer position in screen (canvas-relative) coordinates, via
3583
+ * `camera.toScreen`. Add the canvas element's bounding-rect offset to place
3584
+ * a `position: fixed` menu, or use directly inside a `position: relative`
3585
+ * canvas container.
3586
+ */
3587
+ readonly screen: {
3588
+ readonly x: number;
3589
+ readonly y: number;
3590
+ };
3591
+ }
3592
+ /** Constructor options for `ContextMenuBehaviour`. */
3593
+ interface ContextMenuBehaviourOptions extends BehaviourOptions {
3594
+ /** Required — the `GraphLayer` id this behaviour drives. */
3595
+ layerId: string;
3596
+ /**
3597
+ * Which targets fire `onContextMenu`. A right-click on a target not in this
3598
+ * list is ignored. Default `['node', 'edge', 'canvas']`.
3599
+ */
3600
+ targets?: readonly ContextMenuTargetType[];
3601
+ /**
3602
+ * Optional transient state name applied to the right-clicked node/edge (e.g.
3603
+ * `'context-open'`). The previously marked target is cleared first, so at
3604
+ * most one element carries it at a time. Cleared on disable/destroy.
3605
+ * `null`/`undefined` disables this. Default `null`.
3606
+ */
3607
+ state?: string | null;
3608
+ /** Fired on a qualifying right-click. */
3609
+ onContextMenu?: (event: ContextMenuEvent) => void;
3610
+ }
3611
+ interface ResolvedOptions$2 {
3612
+ targets: readonly ContextMenuTargetType[];
3613
+ state: string | null;
3614
+ onContextMenu: ((event: ContextMenuEvent) => void) | undefined;
3615
+ }
3616
+ declare class ContextMenuBehaviour extends Behaviour {
3617
+ private layer;
3618
+ private opts;
3619
+ /** Subscription disposers. */
3620
+ private subs;
3621
+ /** Element currently carrying `opts.state`, so we can clear it on the next open. */
3622
+ private statedTarget;
3623
+ constructor(opts: ContextMenuBehaviourOptions);
3624
+ protected onRegister(ctx: CanvasContext): void;
3625
+ protected onDestroy(): void;
3626
+ protected onDisable(): void;
3627
+ /** Resolved current options (read-only snapshot). */
3628
+ get options(): Readonly<ResolvedOptions$2>;
3629
+ /** Merge new options. Unspecified fields keep their current value. */
3630
+ setOptions(patch: Partial<ContextMenuBehaviourOptions>): void;
3631
+ private handle;
3632
+ /** Move the transient `opts.state` marker onto the freshly clicked target. */
3633
+ private applyStatedTarget;
3634
+ private clearStatedTarget;
3635
+ }
3636
+
3637
+ /**
3638
+ * `CreateNodeBehaviour` — click empty canvas to add a node to a `GraphLayer`.
3639
+ *
3640
+ * Layer-scoped; mutates the store directly (like `DragNodeBehaviour`) and also
3641
+ * fires an `onNodeCreate` callback. A `createNode` factory lets the consumer
3642
+ * shape the node (id, style, data) or veto by returning `null`.
3643
+ *
3644
+ * Background-click detection mirrors `ClickSelectBehaviour`: the renderer fires
3645
+ * `shape:click` / `connector:click` synchronously during the native DOM click,
3646
+ * so a flag set there tells us the click landed on an element (don't create).
3647
+ * A pointer-move threshold between `pointerdown` and `click` distinguishes a
3648
+ * click from a camera pan.
3649
+ *
3650
+ * Default `enabled: false` — register, then explicitly enable (e.g. an "Add"
3651
+ * tool mode toggles it on).
3652
+ */
3653
+
3654
+ /** Constructor options for `CreateNodeBehaviour`. */
3655
+ interface CreateNodeBehaviourOptions extends BehaviourOptions {
3656
+ /** Required — the `GraphLayer` id this behaviour adds nodes to. */
3657
+ layerId: string;
3658
+ /**
3659
+ * Build the node to insert from the click's world position. Return `null`
3660
+ * to veto creation. Default: `{ id: <generated>, position }`.
3661
+ */
3662
+ createNode?: (world: {
3663
+ x: number;
3664
+ y: number;
3665
+ }) => GraphNode | null;
3666
+ /** Fired after a node is added to the store. */
3667
+ onNodeCreate?: (node: GraphNode) => void;
3668
+ }
3669
+ declare class CreateNodeBehaviour extends Behaviour {
3670
+ private layer;
3671
+ private ctxRef;
3672
+ private canvasEl;
3673
+ private readonly makeNode;
3674
+ private readonly onNodeCreate?;
3675
+ /** Subscription disposers. */
3676
+ private subs;
3677
+ /** True when the in-flight click already landed on a node/edge. */
3678
+ private clickConsumedByElement;
3679
+ /** Pointerdown screen-position — used to distinguish a click from a drag/pan. */
3680
+ private pointerDownScreen;
3681
+ constructor(opts: CreateNodeBehaviourOptions);
3682
+ protected onRegister(ctx: CanvasContext): void;
3683
+ protected onDestroy(): void;
3684
+ private clientToScreen;
3685
+ }
3686
+
3687
+ /**
3688
+ * `DrawEdgeBehaviour` — drag from a source node to a target node to create an
3689
+ * edge, with a dashed rubber-band preview that follows the cursor.
3690
+ *
3691
+ * Layer-scoped; mutates the store directly (like `DragNodeBehaviour`) and fires
3692
+ * an `onEdgeCreate` callback. A `createEdge` factory shapes the edge (id, style,
3693
+ * data) or vetoes by returning `null` (e.g. to reject duplicates).
3694
+ *
3695
+ * The preview is a **transient renderer connector** routed from the source
3696
+ * shape to a free `{ kind: 'point' }` endpoint updated on every pointermove —
3697
+ * no "cursor node", so hit-testing for the drop target is unaffected. The
3698
+ * target node under the cursor is found with `renderer.hitTest(...)`.
3699
+ *
3700
+ * Pointer-capture + `clientToScreen` lifecycle mirrors `DragNodeBehaviour`.
3701
+ *
3702
+ * Default `enabled: false`. Don't run this and `DragNodeBehaviour` enabled at
3703
+ * the same time — both start on `shape:pointerdown` (a tool mode toggle should
3704
+ * pick one).
3705
+ */
3706
+
3707
+ /** Constructor options for `DrawEdgeBehaviour`. */
3708
+ interface DrawEdgeBehaviourOptions extends BehaviourOptions {
3709
+ /** Required — the `GraphLayer` id this behaviour draws edges in. */
3710
+ layerId: string;
3711
+ /**
3712
+ * Allow releasing on the *source* node to create a self-loop. Default
3713
+ * `false` (releasing on the source cancels). When `true`, the default
3714
+ * `createEdge` factory styles a self-loop as `pathType: 'loop-curve'` with
3715
+ * `sourceAnchor`/`targetAnchor` set to `'center'` (a loop needs center
3716
+ * anchors — `'boundary'` collapses it onto a single silhouette point).
3717
+ */
3718
+ allowSelfLoop?: boolean;
3719
+ /**
3720
+ * Build the edge to insert from the endpoints. Return `null` to veto (e.g.
3721
+ * a duplicate or disallowed pair). Default: `{ id: <generated>, source, target }`,
3722
+ * or a loop-styled edge when `source === target` (see {@link allowSelfLoop}).
3723
+ */
3724
+ createEdge?: (source: string, target: string) => GraphEdge | null;
3725
+ /** Fired after an edge is added to the store. */
3726
+ onEdgeCreate?: (edge: GraphEdge) => void;
3727
+ /** Rubber-band preview stroke. Defaults to a dashed light-blue line. */
3728
+ draftStyle?: Partial<{
3729
+ color: number;
3730
+ width: number;
3731
+ alpha: number;
3732
+ dash: [number, number];
3733
+ }>;
3734
+ }
3735
+ declare class DrawEdgeBehaviour extends Behaviour {
3736
+ private layer;
3737
+ private ctxRef;
3738
+ private canvasEl;
3739
+ private readonly makeEdge;
3740
+ private readonly onEdgeCreate?;
3741
+ private readonly allowSelfLoop;
3742
+ private readonly draft;
3743
+ private offShapeDown;
3744
+ private sourceId;
3745
+ private candidateTarget;
3746
+ private capturedPointerId;
3747
+ constructor(opts: DrawEdgeBehaviourOptions);
3748
+ protected onRegister(ctx: CanvasContext): void;
3749
+ protected onDestroy(): void;
3750
+ protected onDisable(): void;
3751
+ private startDraw;
3752
+ private endDraw;
3753
+ private readonly onWindowPointerMove;
3754
+ private readonly onWindowPointerUp;
3755
+ private clientToScreen;
3756
+ }
3757
+
3758
+ /**
3759
+ * `EraseBehaviour` — click a node or edge to delete it from a `GraphLayer`.
3760
+ *
3761
+ * The "eraser" tool of a drawing toolbar: clicking a node removes it **and**
3762
+ * cascades its incident edges; clicking an edge removes just that edge. Like
3763
+ * {@link CreateNodeBehaviour} / `DrawEdgeBehaviour` it mutates the store
3764
+ * directly and fires a callback — but `onErase` reports the *removed* element
3765
+ * with enough captured state (the node + its incident edges, or the edge) to
3766
+ * reconstruct it, so a consumer can journal an undoable delete.
3767
+ *
3768
+ * Hit detection rides the renderer's synchronous `shape:click` /
3769
+ * `connector:click` channels — the same ones `ClickSelectBehaviour` uses — so
3770
+ * the clicked element's id arrives directly; no world-space hit-testing needed.
3771
+ *
3772
+ * Default `enabled: false` — register, then explicitly enable (e.g. a "Delete"
3773
+ * tool mode toggles it on). Don't run it enabled alongside `ClickSelect` /
3774
+ * `DragNode` on the same layer: all three react to a node press, so a tool-mode
3775
+ * switch should leave exactly one on.
3776
+ */
3777
+
3778
+ /** Which element kinds the eraser removes. */
3779
+ type EraseTargetKind = 'node' | 'edge' | 'both';
3780
+ /**
3781
+ * Payload describing what {@link EraseBehaviour} just removed. Carries the full
3782
+ * pre-removal element(s) so a consumer can rebuild them (undo). A removed node
3783
+ * carries its cascade-removed incident `edges`.
3784
+ */
3785
+ type ErasedElement = {
3786
+ kind: 'node';
3787
+ node: GraphNode;
3788
+ edges: GraphEdge[];
3789
+ } | {
3790
+ kind: 'edge';
3791
+ edge: GraphEdge;
3792
+ };
3793
+ /** Constructor options for `EraseBehaviour`. */
3794
+ interface EraseBehaviourOptions extends BehaviourOptions {
3795
+ /** Required — the `GraphLayer` id this behaviour erases from. */
3796
+ layerId: string;
3797
+ /** Which element kinds a click removes. Default `'both'`. */
3798
+ target?: EraseTargetKind;
3799
+ /** Fired after an element is removed, with the captured pre-removal state. */
3800
+ onErase?: (removed: ErasedElement) => void;
3801
+ }
3802
+ declare class EraseBehaviour extends Behaviour {
3803
+ private layer;
3804
+ private readonly target;
3805
+ private readonly onErase?;
3806
+ /** Subscription disposers. */
3807
+ private subs;
3808
+ constructor(opts: EraseBehaviourOptions);
3809
+ protected onRegister(ctx: CanvasContext): void;
3810
+ protected onDestroy(): void;
3811
+ /** Capture node + incident edges, cascade-remove, then report. */
3812
+ private eraseNode;
3813
+ /** Capture the edge, remove it, then report. */
3814
+ private eraseEdge;
3815
+ /** Cloned incident edges (both directions), deduped — self-loops appear once. */
3816
+ private incidentEdges;
3817
+ }
3818
+
3819
+ /**
3820
+ * `CollapseExpandBehaviour` — clicks on a group node's `+` / `−` toggle
3821
+ * decoration flip the group between expanded and collapsed states.
3822
+ *
3823
+ * Listens for native DOM `pointerdown` on the canvas element rather than
3824
+ * the renderer's `shape:pointerdown` channel. The reason: the toggle
3825
+ * decoration is typically anchored to (or *outside*) the host's
3826
+ * silhouette — outside-`'bottom'` for collapsed circles in the reference
3827
+ * UI — and PixiJS's hit-test rejects clicks outside the silhouette so a
3828
+ * shape-level subscription would never fire for those placements. A
3829
+ * canvas-wide listener tests the click against the toggle's cached hit
3830
+ * geometry regardless of where it sits.
3831
+ *
3832
+ * Layer-scoped. Default `enabled: false` per the no-auto-registration rule.
3833
+ *
3834
+ * @example
3835
+ * ```ts
3836
+ * canvas.behaviours.register(
3837
+ * new CollapseExpandBehaviour({
3838
+ * id: 'collapse-expand',
3839
+ * layerId: 'graph',
3840
+ * enabled: true,
3841
+ * }),
3842
+ * );
3843
+ * ```
3844
+ */
3845
+
3846
+ /**
3847
+ * Slot id the `GraphLayer` mounts the group's `+` / `−` toggle decoration on.
3848
+ * Re-exported for advanced consumers that want to read or override the
3849
+ * decoration; most callers shouldn't need it.
3850
+ */
3851
+ declare const GROUP_TOGGLE_SLOT = "group-toggle";
3852
+ interface CollapseExpandBehaviourOptions extends BehaviourOptions {
3853
+ /** Required — the `GraphLayer` id this behaviour drives. */
3854
+ layerId: string;
3855
+ }
3856
+ declare class CollapseExpandBehaviour extends Behaviour {
3857
+ private layer;
3858
+ private ctxRef;
3859
+ private canvasEl;
3860
+ constructor(opts: CollapseExpandBehaviourOptions);
3861
+ protected onRegister(ctx: CanvasContext): void;
3862
+ protected onDestroy(): void;
3863
+ private readonly onPointerDown;
3864
+ /**
3865
+ * Convert a `PointerEvent` into world coordinates and walk every group
3866
+ * node in the layer. Return the first group whose mounted toggle
3867
+ * decoration's hit area contains the click, or `null` if none match.
3868
+ */
3869
+ private findToggleHit;
3870
+ /**
3871
+ * Flip `style.group.collapsed` via `store.updateNode`, preserving the
3872
+ * rest of the prior style. Per `feedback_updatenode_replaces_style` the
3873
+ * store replaces `style` wholesale on update — the spread keeps every
3874
+ * other field intact.
3875
+ */
3876
+ private toggleCollapsed;
3877
+ }
3878
+
3879
+ /**
3880
+ * `NodeResizeBehaviour` — drag-resize any node (group or regular) whose
3881
+ * resolved style opts into resizing.
3882
+ *
3883
+ * The behaviour:
3884
+ * 1. mounts a single `SelectionFrameDecoration` on every eligible node.
3885
+ * The decoration paints a dashed AABB outline plus round handles at
3886
+ * the corners (and edge midpoints for rect hosts). For circle hosts
3887
+ * only the radial `'right'` handle is exposed so the user always
3888
+ * grows / shrinks the radius isotropically. Eligibility =
3889
+ * `style.resizable === true` (regular nodes) OR
3890
+ * `style.group?.userResizable === true` (compound groups);
3891
+ * 2. listens at the **canvas DOM level** for `pointerdown`. The handles
3892
+ * sit at AABB corners, which fall outside the silhouette for circles
3893
+ * and on the edge for rects — PixiJS's shape hit-test rejects those
3894
+ * clicks. Canvas-level listening sidesteps the issue: every click is
3895
+ * tested against the decoration's cached per-handle hit geometry;
3896
+ * 3. on drag (window-level `pointermove`), writes the new size back via
3897
+ * `store.updateNode`. The target field depends on the eligibility
3898
+ * flag: groups go to `style.group.width / height / radius`, regular
3899
+ * nodes go to `style.shape.width / height / radius`. Position is also
3900
+ * rewritten so the opposite anchor stays fixed on rect drags (except
3901
+ * for auto-fit groups, where the layer derives the frame's position
3902
+ * from the children bbox).
3903
+ *
3904
+ * Layer-scoped. Default `enabled: false`.
3905
+ *
3906
+ * @example
3907
+ * ```ts
3908
+ * canvas.behaviours.register(
3909
+ * new NodeResizeBehaviour({ id: 'resize', layerId: 'graph', enabled: true }),
3910
+ * );
3911
+ * ```
3912
+ */
3913
+
3914
+ interface NodeResizeBehaviourOptions extends BehaviourOptions {
3915
+ /** Required — the `GraphLayer` id this behaviour drives. */
3916
+ layerId: string;
3917
+ /** Handle outer radius in px. Default `5`. */
3918
+ handleRadius?: number;
3919
+ /** Handle fill colour. Default `0xffffff`. */
3920
+ handleFill?: number;
3921
+ /** Frame border + handle outline colour. Default `0x6b7fff`. */
3922
+ frameColor?: number;
3923
+ /** Dash pattern `[dashLength, gapLength]` in px. Default `[5, 4]`. */
3924
+ dashArray?: readonly [number, number];
3925
+ /** Gap between host silhouette and the dashed frame. Default `4`. */
3926
+ framePadding?: number;
3927
+ /** Minimum width / height / radius the behaviour allows during drag. Default `20`. */
3928
+ minSize?: number;
3929
+ }
3930
+ declare class NodeResizeBehaviour extends Behaviour {
3931
+ private layer;
3932
+ private ctxRef;
3933
+ private canvasEl;
3934
+ private prevCursor;
3935
+ private state;
3936
+ private subs;
3937
+ private readonly opts;
3938
+ /** Node ids that currently have a selection-frame decoration mounted. */
3939
+ private readonly mountedNodes;
3940
+ constructor(opts: NodeResizeBehaviourOptions);
3941
+ protected onRegister(ctx: CanvasContext): void;
3942
+ protected onDestroy(): void;
3943
+ protected onEnable(): void;
3944
+ protected onDisable(): void;
3945
+ /**
3946
+ * Returns the write-target a drag on this node would use, or `null` when
3947
+ * the node isn't resizable. A group with `userResizable: true` always
3948
+ * writes to `style.group.*`; a non-group with `style.resizable: true`
3949
+ * writes to `style.shape.*`.
3950
+ */
3951
+ private resizeTarget;
3952
+ private refreshAllFrames;
3953
+ private mountFrameFor;
3954
+ private clearFrameFor;
3955
+ private clearAllFrames;
3956
+ private readonly onCanvasPointerDown;
3957
+ private findHandleHit;
3958
+ private startDrag;
3959
+ private endDrag;
3960
+ private readonly onWindowPointerMove;
3961
+ private readonly onWindowPointerUp;
3962
+ /**
3963
+ * Apply the new geometry to the store. Branch on `target`:
3964
+ *
3965
+ * - `target === 'group'` — write to `style.group.width / height / radius`.
3966
+ * Position is updated for rect drags only when `autoFit !== true` (the
3967
+ * layer derives the position from the children bbox when auto-fit is on,
3968
+ * so writing it would either be redundant or fight the recompute).
3969
+ * - `target === 'shape'` — write to `style.shape.width / height / radius`.
3970
+ * Position is updated for rect drags so the opposite anchor stays put.
3971
+ *
3972
+ * Either way the store replaces `style` wholesale on update (see
3973
+ * `feedback_updatenode_replaces_style`), so the spread preserves every
3974
+ * other field.
3975
+ */
3976
+ private commit;
3977
+ }
3978
+
3979
+ /**
3980
+ * `LabelCollisionBehaviour` — hides overlapping node / edge labels via a
3981
+ * greedy priority-sorted sweep so dense graphs stay legible.
3982
+ *
3983
+ * Strategy: each pass collects every label's world-space AABB, sorts the
3984
+ * label set by `priority` (configurable resolver — `priority` field on the
3985
+ * style, node-degree, or a custom callback), and walks high-to-low. A label
3986
+ * is **shown** if its AABB doesn't overlap any label already shown in the
3987
+ * same `collisionGroup`; otherwise it's hidden for this frame. Labels with
3988
+ * `forceShow: true` skip the check entirely (use for hovered / selected
3989
+ * elements).
3990
+ *
3991
+ * Default groups partition node labels and edge labels — a node label never
3992
+ * loses to an edge label of higher priority.
3993
+ *
3994
+ * The behaviour reruns on every store flush (data churn) and on every
3995
+ * `camera:zoom` / `camera:pan` event (viewport churn). A small hysteresis
3996
+ * timer keeps just-flipped labels from immediately flipping back when zoom
3997
+ * leaves them right on the overlap boundary.
3998
+ *
3999
+ * Default `enabled: false` — register, then explicitly enable. Matches the
4000
+ * project rule that no behaviour auto-activates.
4001
+ *
4002
+ * @example
4003
+ * ```ts
4004
+ * canvas.behaviours.register(
4005
+ * new LabelCollisionBehaviour({
4006
+ * id: 'label-collision',
4007
+ * layerId: 'graph',
4008
+ * enabled: true,
4009
+ * prioritise: 'node-degree',
4010
+ * }),
4011
+ * );
4012
+ * ```
4013
+ */
4014
+
4015
+ /** What the behaviour does with an overlap. `'hide'` is the only strategy in v0. */
4016
+ type LabelCollisionStrategy = 'hide';
4017
+ /** How label priority is resolved when sorting. */
4018
+ type LabelPriorityResolver = 'priority-field' | 'node-degree' | ((kind: 'node' | 'edge', id: string) => number);
4019
+ interface LabelCollisionBehaviourOptions extends BehaviourOptions {
4020
+ /** Required — the `GraphLayer` id this behaviour drives. */
4021
+ layerId: string;
4022
+ /** Default `'hide'`. */
4023
+ strategy?: LabelCollisionStrategy;
4024
+ /** Default `'priority-field'`. Falls back to node-degree when undefined. */
4025
+ prioritise?: LabelPriorityResolver;
4026
+ /**
4027
+ * Hysteresis: a just-hidden label stays hidden for at least this many ms
4028
+ * before it can re-appear, and vice versa. Stops flicker when zoom is
4029
+ * right at an overlap boundary. Default `100`.
4030
+ */
4031
+ flickerGuardMs?: number;
4032
+ /**
4033
+ * Default `'nodes'` for node labels, `'edges'` for edge labels. Set to a
4034
+ * custom mapping if you want different partitioning (e.g. all in one
4035
+ * group so edges can win priority against nodes).
4036
+ */
4037
+ groups?: {
4038
+ nodes?: string;
4039
+ edges?: string;
4040
+ };
4041
+ }
4042
+ declare class LabelCollisionBehaviour extends Behaviour {
4043
+ private layer;
4044
+ private opts;
4045
+ /** Last-flip timestamp per label id (perf.now()). */
4046
+ private readonly lastFlip;
4047
+ /** Last visibility decision per label id. */
4048
+ private readonly lastVisible;
4049
+ /** Subscription disposers, called in onDestroy. */
4050
+ private subs;
4051
+ /** Coalesce repeated triggers within a single frame. */
4052
+ private scheduled;
4053
+ constructor(opts: LabelCollisionBehaviourOptions);
4054
+ protected onRegister(ctx: CanvasContext): void;
4055
+ protected onDestroy(): void;
4056
+ protected onEnable(): void;
4057
+ protected onDisable(): void;
4058
+ /** Coalesce repeated triggers within a microtask. Cheap; runs at most once per frame. */
4059
+ private schedule;
4060
+ /**
4061
+ * Walk every label, sort by priority, greedy-hide overlaps within the
4062
+ * same `collisionGroup`. Mutates label `gfx.visible`; doesn't touch
4063
+ * decoration state otherwise.
4064
+ */
4065
+ private runPass;
4066
+ private priorityFor;
4067
+ private degreeOf;
4068
+ }
4069
+
4070
+ /**
4071
+ * `LabelResolutionLODBehaviour` — re-rasterise label glyphs at higher
4072
+ * resolution as the camera zooms in, so text stays crisp instead of
4073
+ * sampling-blurry when the user inspects nodes up close.
4074
+ *
4075
+ * **Why this is tier-based, not step-based.** Pixi rasterises each `Text`
4076
+ * to a glyph texture exactly once (default resolution = renderer DPR).
4077
+ * When the world is scaled 5×, that texture is upsampled 5× — fuzzy.
4078
+ * Re-rasterising fixes the fuzziness but regenerates every label's
4079
+ * texture on the GPU, which is the expensive part. With a few thousand
4080
+ * labels (e.g. the H-1B pack story) a re-raster is a multi-hundred-ms
4081
+ * frame pause — perceptible as a stutter mid-zoom.
4082
+ *
4083
+ * An earlier design snapped the zoom to a step (e.g. `step: 0.5`) and
4084
+ * re-rastered at every snap boundary — so a continuous zoom from 1× to
4085
+ * 6× crossed ~10 boundaries and dropped frames at each one. This design
4086
+ * uses **discrete tiers**: a few widely-spaced minZoom thresholds, with
4087
+ * a multiplier per tier. In a typical zoom-in-and-stay session the user
4088
+ * crosses one boundary, pays one re-raster, and the rest is GPU-cheap.
4089
+ *
4090
+ * **Hysteresis** keeps boundary scrolls (1.49 → 1.51 → 1.49) from
4091
+ * flickering between tiers. After crossing UP into tier N, the behaviour
4092
+ * only reverts to tier N-1 when zoom drops below
4093
+ * `levels[N].minZoom - hysteresis`.
4094
+ *
4095
+ * Default `enabled: false` — register, then explicitly enable. Matches
4096
+ * the project rule that no behaviour auto-activates.
4097
+ *
4098
+ * @example
4099
+ * ```ts
4100
+ * canvas.behaviours.register(
4101
+ * new LabelResolutionLODBehaviour({
4102
+ * id: 'label-resolution',
4103
+ * layerId: 'graph',
4104
+ * enabled: true,
4105
+ * // Defaults: 1× DPR by default, jump to 4× DPR once zoom > 1.5.
4106
+ * // Override for more or fewer tiers.
4107
+ * levels: [
4108
+ * { minZoom: 0, multiplier: 1 },
4109
+ * { minZoom: 1.5, multiplier: 4 },
4110
+ * { minZoom: 5, multiplier: 8 },
4111
+ * ],
4112
+ * }),
4113
+ * );
4114
+ * ```
4115
+ */
4116
+
4117
+ /** One discrete LOD tier. Highest `minZoom` ≤ current zoom wins. */
4118
+ interface LabelResolutionLODTier {
4119
+ /** Camera zoom (`canvas.camera.scale`) at which this tier becomes active. */
4120
+ minZoom: number;
4121
+ /** Multiplier applied to `baseResolution` while this tier is active. */
4122
+ multiplier: number;
4123
+ }
4124
+ interface LabelResolutionLODBehaviourOptions extends BehaviourOptions {
4125
+ /** Required — the `GraphLayer` id this behaviour drives. */
4126
+ layerId: string;
4127
+ /**
4128
+ * Base resolution to multiply by the active tier's multiplier. Default
4129
+ * `window.devicePixelRatio` (≈ 1 on standard displays, 2 on retina). Set
4130
+ * this if your Canvas was initialised with a custom `resolution` option.
4131
+ */
4132
+ baseResolution?: number;
4133
+ /**
4134
+ * Discrete zoom tiers, evaluated as a step function. Each tier names a
4135
+ * `minZoom` at which it activates and a `multiplier` applied to
4136
+ * `baseResolution` while it's active. Order doesn't matter — the
4137
+ * behaviour sorts by `minZoom` internally.
4138
+ *
4139
+ * Pick *few, widely-spaced* tiers: every additional tier means another
4140
+ * GPU re-raster of every label during a typical zoom-in pass. Default:
4141
+ * `[{ minZoom: 0, multiplier: 1 }, { minZoom: 1.5, multiplier: 4 }]` —
4142
+ * one threshold, one re-raster.
4143
+ */
4144
+ levels?: LabelResolutionLODTier[];
4145
+ /**
4146
+ * Hysteresis applied to *downward* tier changes. After crossing UP into
4147
+ * tier N at `levels[N].minZoom`, the behaviour only reverts to tier N-1
4148
+ * once zoom drops below `levels[N].minZoom - hysteresis`. Prevents
4149
+ * flicker when the user dithers on a threshold. Default `0.1`.
4150
+ */
4151
+ hysteresis?: number;
4152
+ }
4153
+ declare class LabelResolutionLODBehaviour extends Behaviour {
4154
+ private layer;
4155
+ private readonly opts;
4156
+ private subs;
4157
+ /**
4158
+ * Index into `opts.levels` of the currently active tier. Re-evaluated
4159
+ * on every camera-zoom event; the renderer is only nudged when this
4160
+ * index actually changes, so a continuous zoom inside one tier costs
4161
+ * nothing past the cheap comparison below.
4162
+ */
4163
+ private currentTierIdx;
4164
+ /** Last resolution actually pushed to the renderer. */
4165
+ private lastPushed;
4166
+ constructor(opts: LabelResolutionLODBehaviourOptions);
4167
+ protected onRegister(ctx: CanvasContext): void;
4168
+ protected onDestroy(): void;
4169
+ protected onEnable(): void;
4170
+ protected onDisable(): void;
4171
+ /**
4172
+ * Re-evaluate the active tier under the current camera zoom and push the
4173
+ * tier's resolution to the renderer only when the tier index changes.
4174
+ * Continuous zoom inside one tier is a constant-time no-op past the
4175
+ * `idx === currentTierIdx` check below.
4176
+ */
4177
+ private apply;
4178
+ }
4179
+
4180
+ /**
4181
+ * `NodeSizeLODBehaviour` — keep `GraphLayer` node bodies (and their
4182
+ * outline strokes) at a fixed screen-pixel size across camera zoom.
4183
+ *
4184
+ * Concrete subclass of `ElementSizeLODBehaviour` — that base owns the
4185
+ * RAF coalescing, `camera:zoom` subscription, and enable/disable
4186
+ * lifecycle. This class only knows how to rescale graph nodes.
4187
+ *
4188
+ * ## How it works (transform-scale fast path)
4189
+ *
4190
+ * On enable (and on `reflow()` after a GUI knob moves), the behaviour
4191
+ * does **one** expensive O(N) pass: it rewrites every node's spec so
4192
+ * the geometric values (`radius`, `width`, `height`, `stroke.width`)
4193
+ * carry the target-pixel sizes as if they were world units. Then per
4194
+ * `camera:zoom` it does the cheap pass: a single
4195
+ * `renderer.scaleShape(id, 1 / cameraScale)` per node, which just writes
4196
+ * the gfx transform — no `Graphics.clear()`, no path retrace, no spec
4197
+ * mutation. That collapses thousands of geometry rebuilds per zoom
4198
+ * frame into thousands of transform writes (~50× cheaper).
4199
+ *
4200
+ * Stroke width travels along the transform (Pixi strokes are in local
4201
+ * units), so the stroke is pixel-constant by construction. There is no
4202
+ * way to opt the stroke out of the transform while keeping the body in
4203
+ * — the two are coupled by the single scale factor.
4204
+ *
4205
+ * Pair with {@link EdgeSizeLODBehaviour} when you also want pixel-constant
4206
+ * edge strokes. They're independent behaviours; their RAF callbacks
4207
+ * batch into the same animation frame, so registering both has the same
4208
+ * per-frame cost as one monolith doing both passes.
4209
+ *
4210
+ * Supports `circle` and `rect` node shapes. `arc`-shape nodes are
4211
+ * skipped — their geometry is in `innerR` / `outerR` / sweep angles and
4212
+ * doesn't map cleanly to a single screen-px input.
4213
+ *
4214
+ * Hosts with `setDecoration` decorations or attached badges are also
4215
+ * supported, but those auxiliary visuals are **not** re-anchored on
4216
+ * each zoom — the underlying `scaleShape` fast path skips the
4217
+ * decoration / badge refresh that `updateShape` performs. Acceptable
4218
+ * for halos/glows (still centred on the host); inappropriate for
4219
+ * placement-sensitive badges. Use `updateShape` directly in that case.
4220
+ *
4221
+ * @example
4222
+ * ```ts
4223
+ * import { NodeSizeLODBehaviour } from '@invana/graph';
4224
+ *
4225
+ * canvas.behaviours.register(
4226
+ * new NodeSizeLODBehaviour({
4227
+ * id: 'node-size-lod',
4228
+ * enabled: true,
4229
+ * layers: [
4230
+ * {
4231
+ * layerId: 'graph',
4232
+ * sizePx: 6, // node diameter in screen px
4233
+ * strokeWidthPx: 1, // outline width in screen px (omit to leave in world units)
4234
+ * },
4235
+ * ],
4236
+ * }),
4237
+ * );
4238
+ * ```
4239
+ */
4240
+
4241
+ /** Per-`GraphLayer` config — one entry per layer this behaviour rescales. */
4242
+ interface NodeSizeLODConfig {
4243
+ /** Required — the `GraphLayer` whose nodes are rescaled. */
4244
+ layerId: string;
4245
+ /**
4246
+ * Target body size in screen px for nodes that don't carry a per-node
4247
+ * `data.size` override. Falls back to the layer's `nodeDefaults.size`
4248
+ * when omitted. Accepts a static number or a getter — getters re-read
4249
+ * on every reflow so GUI sliders update live.
4250
+ */
4251
+ sizePx?: NumberOrGetter;
4252
+ /**
4253
+ * Target outline width in screen px. When omitted, the layer's
4254
+ * `nodeDefaults.strokeWidth` (or each node's `data.strokeWidth`) is
4255
+ * reinterpreted as the implicit pixel target — the transform-scale
4256
+ * fast path always pins both body and stroke together, so the stroke
4257
+ * is pixel-constant even without an explicit value here. Setting an
4258
+ * explicit value just changes what that pixel target is.
4259
+ */
4260
+ strokeWidthPx?: NumberOrGetter;
4261
+ }
4262
+ interface NodeSizeLODBehaviourOptions extends ElementSizeLODBehaviourOptions {
4263
+ /** One config per `GraphLayer` to drive. */
4264
+ layers: NodeSizeLODConfig[];
4265
+ }
4266
+ declare class NodeSizeLODBehaviour extends ElementSizeLODBehaviour {
4267
+ private readonly configs;
4268
+ private resolved;
4269
+ /**
4270
+ * Pending reanchor timer. The per-frame `scaleShape` fast path is cheap
4271
+ * (transform writes only), but `reanchorAllConnectors` rebuilds every
4272
+ * connector's Pixi geometry — at thousands of edges that drops fps to
4273
+ * the floor under a continuous zoom gesture. Coalesce to a single
4274
+ * trailing-edge call.
4275
+ */
4276
+ private reanchorTimer;
4277
+ constructor(opts: NodeSizeLODBehaviourOptions);
4278
+ protected onResolveTargets(ctx: CanvasContext): void;
4279
+ protected onReleaseTargets(): void;
4280
+ /**
4281
+ * Per-frame fast path. Sets `gfx.scale = 1 / cameraScale` on every node
4282
+ * via the renderer's transform fast path — no geometry rebuild. The
4283
+ * spec was pre-set to "target-px values treated as world units" by
4284
+ * {@link writeBaseline} at enable / reflow time, so:
4285
+ *
4286
+ * on-screen = nativeWorldSize × cameraScale × gfxScale
4287
+ * = (sizePx / 1) × cameraScale × (1 / cameraScale)
4288
+ * = sizePx ✓
4289
+ *
4290
+ * Stroke width scales with the body (Pixi's stroke is in local units)
4291
+ * — which is precisely the pixel-constant intent.
4292
+ */
4293
+ protected apply(rawScale: number): void;
4294
+ private scheduleReanchor;
4295
+ private flushReanchor;
4296
+ protected onEnable(): void;
4297
+ protected onDisable(): void;
4298
+ reflow(): void;
4299
+ /**
4300
+ * One-shot O(N) pass that rewrites every node's spec via
4301
+ * `renderer.updateShape`. Two flavours:
4302
+ *
4303
+ * - `'target'` — write the LOD-on baseline: `radius = sizePx / 2`,
4304
+ * `stroke.width = strokeWidthPx`. The per-frame `gfx.scale = 1 / cs`
4305
+ * then collapses the world-unit values back to pixel-constant.
4306
+ * - `'worldUnit'` — restore the LOD-off baseline: `radius = (data.size
4307
+ * ?? defaults.size) / 2`, `stroke.width = data.strokeWidth ??
4308
+ * defaults.strokeWidth`. Matches what `GraphLayer.nodeSpec` would
4309
+ * write for a fresh `addShape`.
4310
+ *
4311
+ * Expensive (each `updateShape` rebuilds the underlying Pixi geometry)
4312
+ * — only call on transitions (enable / disable / slider change), not
4313
+ * per frame.
4314
+ */
4315
+ private writeBaseline;
4316
+ private writeLayerBaseline;
4317
+ }
4318
+
4319
+ /**
4320
+ * `EdgeSizeLODBehaviour` — keep `GraphLayer` connector stroke widths at
4321
+ * a fixed screen-pixel width across camera zoom.
4322
+ *
4323
+ * Concrete subclass of `ElementSizeLODBehaviour`. Pair with
4324
+ * {@link NodeSizeLODBehaviour} when you also want pixel-constant nodes
4325
+ * (typical for map-overlay use cases — at city zoom a `strokeWidth: 0.6`
4326
+ * becomes a 150-px slab without this behaviour).
4327
+ *
4328
+ * Uses `PrimitivesRenderer.setConnectorStroke` (not `updateConnector`)
4329
+ * to patch the stroke spec and redraw on the cached path. Crucially,
4330
+ * this skips `recomputeConnectorPath`, which would iterate every shape
4331
+ * in the renderer to build an obstacle list — `O(edges × shapes)` per
4332
+ * reflow and lethal during continuous zoom. The path doesn't depend on
4333
+ * camera scale, so re-routing on a stroke-only change is wasted work.
4334
+ *
4335
+ * @example
4336
+ * ```ts
4337
+ * import { EdgeSizeLODBehaviour } from '@invana/graph';
4338
+ *
4339
+ * canvas.behaviours.register(
4340
+ * new EdgeSizeLODBehaviour({
4341
+ * id: 'edge-size-lod',
4342
+ * enabled: true,
4343
+ * layers: [{ layerId: 'graph', strokeWidthPx: 0.6 }],
4344
+ * }),
4345
+ * );
4346
+ * ```
4347
+ */
4348
+
4349
+ /** Per-`GraphLayer` config — one entry per layer this behaviour rescales. */
4350
+ interface EdgeSizeLODConfig {
4351
+ /** Required — the `GraphLayer` whose edges are rescaled. */
4352
+ layerId: string;
4353
+ /**
4354
+ * Target stroke width in screen px for edges that don't carry a
4355
+ * per-edge `data.strokeWidth` override. Falls back to the layer's
4356
+ * `edgeDefaults.strokeWidth`. Accepts a static number or a getter
4357
+ * (`() => settings.targetEdgePx`).
4358
+ */
4359
+ strokeWidthPx?: NumberOrGetter;
4360
+ }
4361
+ interface EdgeSizeLODBehaviourOptions extends ElementSizeLODBehaviourOptions {
4362
+ /** One config per `GraphLayer` to drive. */
4363
+ layers: EdgeSizeLODConfig[];
4364
+ }
4365
+ declare class EdgeSizeLODBehaviour extends ElementSizeLODBehaviour {
4366
+ private readonly configs;
4367
+ private resolved;
4368
+ constructor(opts: EdgeSizeLODBehaviourOptions);
4369
+ protected onResolveTargets(ctx: CanvasContext): void;
4370
+ protected onReleaseTargets(): void;
4371
+ /**
4372
+ * Per-zoom-frame apply: write the screen-px / world-px ratio to every
4373
+ * managed edge as a render-time stroke multiplier. The renderer's draw
4374
+ * pipeline reads `inst.strokeWidthScale` and multiplies it into the
4375
+ * spec's `stroke.width` at draw time, so state-config strokes (e.g.
4376
+ * `active: { strokeWidth: 1.5 }`) are interpreted in the same screen-px
4377
+ * unit the layer's "live" strokes are interpreted in — no LOD-loss
4378
+ * across a `GraphLayer.rerenderEdge` rebuild, and no inversion of the
4379
+ * caller's intent.
4380
+ *
4381
+ * The strokeWidthPx config field is unused under this model — every
4382
+ * spec width is treated as the target screen-px. Kept on the type for
4383
+ * back-compat; a future revision may remove it.
4384
+ */
4385
+ protected apply(rawScale: number): void;
4386
+ }
4387
+
4388
+ /**
4389
+ * `ParallelEdgeBehaviour` — fans edges that share the same `(source, target)`
4390
+ * pair so they don't overlap. Cross-edge coordination that belongs above the
4391
+ * per-edge connector pipeline (anchor → router → pathStyle), since each
4392
+ * pipeline stage sees only one edge in isolation.
4393
+ *
4394
+ * Layer-scoped: constructed with a target `layerId` referencing a
4395
+ * {@link GraphLayer}. Watches the store for edge add/remove and node
4396
+ * position changes, groups edges by `groupBy(edge)` (default
4397
+ * `${source}::${target}`), and rewrites each group's `style.shape` so its
4398
+ * members fan out symmetrically. Two dimensions are written, depending on
4399
+ * which anchor each edge uses:
4400
+ *
4401
+ * 1. **`sourceAnchorOpts.offset` / `targetAnchorOpts.offset`** — for edges
4402
+ * using `'edge-port'` or `'silhouette-port'` anchors, the endpoints are
4403
+ * pushed along the host's face by `rank × spacing`. Combined with the
4404
+ * port anchor's `side: 'auto'` mode, this fans the endpoints across the
4405
+ * shape silhouette without the caller having to derive sides manually.
4406
+ * 2. **A single midpoint `waypoint`** — bows the path's middle by
4407
+ * `rank × spacing`. The bow axis is chosen per edge from its
4408
+ * `pathType`: axis-aligned routers (`manhattan` / `orth` / `rounded`)
4409
+ * get an axis-aligned offset along the non-dominant axis;
4410
+ * curve-through-control-point styles (`straight` / `smooth` / `bundle`)
4411
+ * get a perpendicular offset. Override via `basis`.
4412
+ *
4413
+ * Default `enabled: false` — register, then explicitly enable. Matches the
4414
+ * project rule that no behaviour auto-activates.
4415
+ *
4416
+ * `onDisable` does **not** undo prior patches — edges keep whatever waypoint
4417
+ * / anchor opts they had at the moment of disable. Re-enabling resumes
4418
+ * patching on the next store mutation. Callers that need a clean slate must
4419
+ * clear edge styles themselves.
4420
+ *
4421
+ * @example
4422
+ * ```ts
4423
+ * canvas.behaviours.register(
4424
+ * new ParallelEdgeBehaviour({
4425
+ * id: 'parallel-edges',
4426
+ * layerId: 'graph',
4427
+ * enabled: true,
4428
+ * spacing: 12,
4429
+ * }),
4430
+ * );
4431
+ * ```
4432
+ */
4433
+
4434
+ /**
4435
+ * Axis along which a group of parallel edges spreads.
4436
+ *
4437
+ * - `'auto'` — derive from each edge's `pathType`. Axis-aligned routers
4438
+ * (`manhattan`, `orth`, `rounded`) use `'axis-aligned'`; all others use
4439
+ * `'perpendicular'`.
4440
+ * - `'perpendicular'` — offset along the unit vector perpendicular to
4441
+ * `target - source`. Suitable for curve-through-midpoint styles
4442
+ * (`straight`, `smooth`, `bundle`).
4443
+ * - `'axis-aligned'` — offset along the non-dominant axis between source and
4444
+ * target. Suitable for axis-aligned routers (`manhattan`, `orth`,
4445
+ * `rounded`) where the bow control point should sit on a horizontal or
4446
+ * vertical mid-corridor.
4447
+ */
4448
+ type ParallelEdgeBasis = 'auto' | 'perpendicular' | 'axis-aligned';
4449
+ /** A bucket of edges that share endpoints and should be fanned together. */
4450
+ interface ParallelEdgeGroup {
4451
+ /** Source node id shared by every edge in this group. */
4452
+ readonly sourceId: string;
4453
+ /** Target node id shared by every edge in this group. */
4454
+ readonly targetId: string;
4455
+ /** Geometric centre of the source node (renderer ref or store position). */
4456
+ readonly sourceCenter: Vec2;
4457
+ /** Geometric centre of the target node. */
4458
+ readonly targetCenter: Vec2;
4459
+ /**
4460
+ * Edges in this group, in store iteration order. Distribution policies
4461
+ * decide which edge gets which rank — the default centres the group so
4462
+ * `edges[i]` receives rank `k = i - (N-1)/2`.
4463
+ */
4464
+ readonly edges: ReadonlyArray<GraphEdge>;
4465
+ }
4466
+ /** Patch a distribution policy emits for one edge in the group. */
4467
+ interface ParallelEdgePatch {
4468
+ /** Edge id this patch applies to. */
4469
+ readonly edgeId: string;
4470
+ /** New `sourceAnchorOpts`. Merged onto the edge's existing shape. */
4471
+ readonly sourceAnchorOpts?: Readonly<Record<string, unknown>>;
4472
+ /** New `targetAnchorOpts`. */
4473
+ readonly targetAnchorOpts?: Readonly<Record<string, unknown>>;
4474
+ /** New `waypoints`. Pass an empty array to clear. */
4475
+ readonly waypoints?: ReadonlyArray<{
4476
+ readonly x: number;
4477
+ readonly y: number;
4478
+ }>;
4479
+ }
4480
+ /** Settings the behaviour passes through to a distribution policy. */
4481
+ interface ParallelEdgeDistributeContext {
4482
+ readonly spacing: number;
4483
+ readonly basis: ParallelEdgeBasis;
4484
+ readonly anchorOffset: boolean;
4485
+ }
4486
+ /**
4487
+ * Pluggable distribution policy. Receives a group of co-located edges plus
4488
+ * the behaviour's settings and returns one patch per edge it wants to update.
4489
+ *
4490
+ * The default policy {@link centeredRanksPolicy} fans edges symmetrically
4491
+ * around rank zero — pass a custom function to implement one-sided fanout,
4492
+ * data-driven offsets, weighted spacing, etc.
4493
+ */
4494
+ type ParallelEdgeDistribute = (group: ParallelEdgeGroup, ctx: ParallelEdgeDistributeContext) => ReadonlyArray<ParallelEdgePatch>;
4495
+ /** Constructor options for {@link ParallelEdgeBehaviour}. */
4496
+ interface ParallelEdgeBehaviourOptions extends BehaviourOptions {
4497
+ /** Required — the `GraphLayer` id this behaviour drives. */
4498
+ layerId: string;
4499
+ /** Spacing between adjacent ranks in world units. Default `12`. */
4500
+ spacing?: number;
4501
+ /**
4502
+ * Basis the default distribution policy uses to translate a rank into a
4503
+ * waypoint / anchor-offset direction. Default `'auto'`.
4504
+ */
4505
+ basis?: ParallelEdgeBasis;
4506
+ /**
4507
+ * When `true` and an edge uses a port anchor (`'edge-port'` or
4508
+ * `'silhouette-port'`), the default policy writes
4509
+ * `sourceAnchorOpts: { side: 'auto', offset }` and the matching target
4510
+ * opts so endpoints fan along the host face. When `false`, the policy
4511
+ * only writes waypoints. Default `true`.
4512
+ */
4513
+ anchorOffset?: boolean;
4514
+ /**
4515
+ * Group key for an edge. Edges that produce the same key are bundled and
4516
+ * distributed together. Return `null` to exclude an edge. Default groups
4517
+ * by directed pair `${source}::${target}`.
4518
+ */
4519
+ groupBy?: (edge: GraphEdge) => string | null;
4520
+ /**
4521
+ * Distribution policy. Default {@link centeredRanksPolicy}.
4522
+ */
4523
+ distribute?: ParallelEdgeDistribute;
4524
+ }
4525
+ interface ResolvedOptions$1 {
4526
+ spacing: number;
4527
+ basis: ParallelEdgeBasis;
4528
+ anchorOffset: boolean;
4529
+ groupBy: (edge: GraphEdge) => string | null;
4530
+ distribute: ParallelEdgeDistribute;
4531
+ }
4532
+ /**
4533
+ * Default distribution policy — centres `N` ranks around zero, then for each
4534
+ * edge writes one midpoint waypoint plus (optionally) port-anchor offsets.
4535
+ *
4536
+ * Exported so callers can compose it (e.g. wrap with a filter) or call
4537
+ * directly when implementing a custom variant that wants to reuse the
4538
+ * default geometry for some edges.
4539
+ */
4540
+ declare const centeredRanksPolicy: ParallelEdgeDistribute;
4541
+ declare class ParallelEdgeBehaviour extends Behaviour {
4542
+ /** Bound target layer — resolved in `onRegister`. */
4543
+ private layer;
4544
+ private opts;
4545
+ /** Subscription disposers, called in `onDestroy`. */
4546
+ private subs;
4547
+ /** Re-entrancy guard: `true` while writing patches to the store. */
4548
+ private patching;
4549
+ constructor(opts: ParallelEdgeBehaviourOptions);
4550
+ protected onRegister(ctx: CanvasContext): void;
4551
+ protected onDestroy(): void;
4552
+ protected onEnable(): void;
4553
+ /** Read-only snapshot of resolved options. */
4554
+ get options(): Readonly<ResolvedOptions$1>;
4555
+ /** Runtime option update. Re-runs the distribution immediately if enabled. */
4556
+ setOptions(patch: Partial<ParallelEdgeBehaviourOptions>): void;
4557
+ /**
4558
+ * Force a recompute pass. Useful after bulk mutations performed inside a
4559
+ * `store.batch()` that callers want to flush through the behaviour
4560
+ * immediately.
4561
+ */
4562
+ recompute(): void;
4563
+ }
4564
+
4565
+ /**
4566
+ * `DegreeSizeBehaviour` — sizes nodes by their connection count.
4567
+ *
4568
+ * For each node, counts incident edges in the configured `direction`
4569
+ * (`'in'` / `'out'` / `'both'`), normalizes against the max observed degree,
4570
+ * and writes a `style.size` value scaled between `minSize` and `maxSize`.
4571
+ *
4572
+ * Because `style.size` flows through `GraphLayer.resolveNodeStyle` (which
4573
+ * rewrites the node's `shape` geometry before any consumer reads it), the
4574
+ * sizes are picked up uniformly by:
4575
+ * - the renderer (visual radius / width grows),
4576
+ * - `boundsOfNode` (ELK and other layouts that query bounds),
4577
+ * - D3ForceLayout's collide.radius callback (reads `style.shape.radius`
4578
+ * off the resolved style).
4579
+ *
4580
+ * The behaviour does NOT auto-rerun any layout — write sizes first, then
4581
+ * call `layout.apply(graph)` yourself. This matches the rest of the
4582
+ * behaviour catalogue: no behaviour runs layouts implicitly.
4583
+ *
4584
+ * Lifecycle:
4585
+ * - `onEnable()` — snapshot prior per-node `style.size`, compute,
4586
+ * apply.
4587
+ * - on store changes — recompute and reapply (microtask-debounced).
4588
+ * - `onDisable()` — restore the snapshotted prior `style.size`
4589
+ * values, clear snapshot.
4590
+ *
4591
+ * Defaults are `direction: 'both'`, `minSize: 8`, `maxSize: 32`,
4592
+ * `scale: 'sqrt'` — the sqrt curve dampens the long-tail effect typical of
4593
+ * real graphs (a few super-hubs would otherwise blow past the slider) while
4594
+ * still giving visually distinct sizing.
4595
+ *
4596
+ * @example
4597
+ * ```ts
4598
+ * canvas.behaviours.register(
4599
+ * new DegreeSizeBehaviour({
4600
+ * id: 'degree-size',
4601
+ * layerId: 'graph',
4602
+ * enabled: true,
4603
+ * direction: 'both',
4604
+ * minSize: 6,
4605
+ * maxSize: 40,
4606
+ * scale: 'sqrt',
4607
+ * }),
4608
+ * );
4609
+ * // ...after registering, run the layout:
4610
+ * void layout.apply(graph);
4611
+ * ```
4612
+ */
4613
+
4614
+ /** Scaling curve used to map raw degree → output size. */
4615
+ type DegreeSizeScale = 'linear' | 'sqrt' | 'log';
4616
+ /** Constructor options for `DegreeSizeBehaviour`. */
4617
+ interface DegreeSizeBehaviourOptions extends BehaviourOptions {
4618
+ /** Required — the `GraphLayer` id this behaviour drives. */
4619
+ layerId: string;
4620
+ /**
4621
+ * Edges to count when computing each node's degree.
4622
+ *
4623
+ * - `'in'` — only edges where the node is the target.
4624
+ * - `'out'` — only edges where the node is the source.
4625
+ * - `'both'` — sum of in + out. Default.
4626
+ */
4627
+ direction?: EdgeDirection;
4628
+ /** Output `style.size` for a node with degree === 0. Default `8`. */
4629
+ minSize?: number;
4630
+ /**
4631
+ * Output `style.size` for the node with the maximum observed degree.
4632
+ * Default `32`. Anything smaller than `minSize` is allowed but pointless.
4633
+ */
4634
+ maxSize?: number;
4635
+ /**
4636
+ * Curve mapping normalized degree (0..1) to a size between `minSize` and
4637
+ * `maxSize`. Default `'sqrt'`.
4638
+ *
4639
+ * - `'linear'` — size = min + (max - min) * (degree / maxDegree)
4640
+ * - `'sqrt'` — size = min + (max - min) * sqrt(degree / maxDegree)
4641
+ * dampens the long tail typical of real graphs
4642
+ * - `'log'` — size = min + (max - min) * log1p(degree) / log1p(maxDegree)
4643
+ * aggressive dampening; better for power-law graphs
4644
+ */
4645
+ scale?: DegreeSizeScale;
4646
+ /**
4647
+ * Optional override. When provided, supersedes `minSize` / `maxSize` /
4648
+ * `scale` and is called per-node with that node's degree plus the max
4649
+ * degree across the layer. Returns the literal `style.size` to write.
4650
+ */
4651
+ sizeFn?: (degree: number, maxDegree: number) => number;
4652
+ }
4653
+ interface ResolvedOptions {
4654
+ direction: EdgeDirection;
4655
+ minSize: number;
4656
+ maxSize: number;
4657
+ scale: DegreeSizeScale;
4658
+ sizeFn: ((degree: number, maxDegree: number) => number) | undefined;
4659
+ }
4660
+ declare class DegreeSizeBehaviour extends Behaviour {
4661
+ /** Bound target layer — resolved in `onRegister`. */
4662
+ private layer;
4663
+ private opts;
4664
+ /** Subscription disposers, called in `onDestroy`. */
4665
+ private readonly subs;
4666
+ /**
4667
+ * Snapshot of each touched node's prior `style.size`, captured on the
4668
+ * first write to that node. `undefined` means the node had no `size`
4669
+ * field before — restore by writing `undefined`. Cleared on `disable` /
4670
+ * `destroy`.
4671
+ */
4672
+ private readonly prior;
4673
+ /** Microtask debounce flag — coalesces bursts of store events. */
4674
+ private recomputeScheduled;
4675
+ /** Re-entrancy guard — set while writing patches so our own emits no-op. */
4676
+ private patching;
4677
+ constructor(opts: DegreeSizeBehaviourOptions);
4678
+ protected onRegister(ctx: CanvasContext): void;
4679
+ protected onEnable(): void;
4680
+ protected onDisable(): void;
4681
+ protected onDestroy(): void;
4682
+ /** Read-only snapshot of resolved options. */
4683
+ get options(): Readonly<ResolvedOptions>;
4684
+ /**
4685
+ * Runtime option update. Re-runs `applyAll()` immediately if enabled so
4686
+ * GUI slider changes are visible without an extra call.
4687
+ */
4688
+ setOptions(patch: Partial<DegreeSizeBehaviourOptions>): void;
4689
+ /**
4690
+ * Force a recompute + write pass. Useful after a bulk `store.batch()`
4691
+ * the caller wants reflected immediately (the microtask-debounced
4692
+ * subscription would otherwise fire on the next tick).
4693
+ */
4694
+ recompute(): void;
4695
+ private scheduleRecompute;
4696
+ /**
4697
+ * Compute degree for every node, map to size, and write back via
4698
+ * `store.updateNode` (merged with the prior `style` per the
4699
+ * `updateNode replaces style wholesale` contract).
4700
+ *
4701
+ * Nodes touched here have their original `style.size` captured into
4702
+ * `this.prior` on first write so `revertAll()` can restore them.
4703
+ */
4704
+ private applyAll;
4705
+ /** Restore each touched node's prior `style.size` and clear the snapshot. */
4706
+ private revertAll;
4707
+ }
4708
+
4709
+ /**
4710
+ * `ResponsiveThemeBehaviour` — keeps a `GraphLayer`'s theme-dependent styling in
4711
+ * sync with the host's colour scheme.
4712
+ *
4713
+ * It owns the *entire* theme-driven look of graph content via three `light` /
4714
+ * `dark` variant pairs:
4715
+ * - `node` — applied to every node through `setNodeDefaults` (shallow-merge into
4716
+ * the shared template, re-rendering every node).
4717
+ * - `edge` — applied to every edge through `setEdgeDefaults`.
4718
+ * - `group` — applied *only* to group nodes (those carrying `style.group`),
4719
+ * layered on top of `node`. Since the engine has no group template /
4720
+ * `setGroupDefaults`, group variants are written per-group-node via
4721
+ * `store.updateNode` and re-applied as groups are added.
4722
+ *
4723
+ * Background theming is deliberately *not* this behaviour's job — that stays with
4724
+ * `BackgroundLayer` (`{ light, dark }` colour props) / `ThemedBackgroundLayer`.
4725
+ *
4726
+ * Why a behaviour and not React state: the declarative `<GraphLayer>` wrapper
4727
+ * applies its `node`/`edge` style props only at mount, so React-state colour
4728
+ * changes wouldn't reach existing — or freshly-drawn — items. Patching the layer
4729
+ * template imperatively does, and new nodes inherit the patched template.
4730
+ *
4731
+ * `mode`:
4732
+ * - `'auto'` (default) — follows the host's `prefers-color-scheme`, flipping
4733
+ * live when the OS appearance changes.
4734
+ * - `'light'` / `'dark'` — pin a variant explicitly.
4735
+ *
4736
+ * Lifecycle:
4737
+ * - `onEnable()` — arm the media query (when `auto`) and apply the resolved
4738
+ * variant immediately.
4739
+ * - `onDisable()` — detach the media query. The layer keeps the last applied
4740
+ * defaults (no revert — there is no "un-themed" baseline to
4741
+ * restore to).
4742
+ *
4743
+ * @example
4744
+ * ```ts
4745
+ * canvas.behaviours.register(
4746
+ * new ResponsiveThemeBehaviour({
4747
+ * id: 'theme',
4748
+ * layerId: 'graph',
4749
+ * enabled: true,
4750
+ * node: {
4751
+ * light: { bgStrokeColor: 0xffffff },
4752
+ * dark: { bgStrokeColor: 0x0f172a },
4753
+ * },
4754
+ * edge: {
4755
+ * light: { strokeColor: 0xcbd5e1 },
4756
+ * dark: { strokeColor: 0x475569 },
4757
+ * },
4758
+ * group: {
4759
+ * light: { bgFill: 0xeef2ff, bgStrokeColor: 0x6b7fff },
4760
+ * dark: { bgFill: 0x1e293b, bgStrokeColor: 0x475569 },
4761
+ * },
4762
+ * }),
4763
+ * );
4764
+ * ```
4765
+ *
4766
+ * @remarks
4767
+ * The `group` pass tracks `node:add` / `flush`, so groups present at `setData`
4768
+ * and groups added afterwards are themed. A node that *becomes* a group later via
4769
+ * `node:update` is not re-themed (we don't watch `node:update`, to avoid a write
4770
+ * loop with our own `updateNode` calls).
4771
+ */
4772
+
4773
+ /** The concrete variant currently being applied after mode resolution. */
4774
+ type ThemeKind = 'light' | 'dark';
4775
+ /** Mode selector. `'auto'` follows `prefers-color-scheme`; the rest pin. */
4776
+ type ThemeMode = 'auto' | 'light' | 'dark';
4777
+ /** A light / dark pair of style patches; each side is optional. */
4778
+ interface ThemeVariants<S> {
4779
+ /** Patch applied when the resolved kind is `'light'`. */
4780
+ light?: Partial<S>;
4781
+ /** Patch applied when the resolved kind is `'dark'`. */
4782
+ dark?: Partial<S>;
4783
+ }
4784
+ /** Constructor options for `ResponsiveThemeBehaviour`. */
4785
+ interface ResponsiveThemeBehaviourOptions extends BehaviourOptions {
4786
+ /** Required — the `GraphLayer` id this behaviour themes. */
4787
+ layerId: string;
4788
+ /**
4789
+ * How light vs dark is decided. `'auto'` (default) follows the host's
4790
+ * `prefers-color-scheme`; `'light'` / `'dark'` pin a variant.
4791
+ */
4792
+ mode?: ThemeMode;
4793
+ /**
4794
+ * Node style applied per resolved kind via `setNodeDefaults` (shallow-merge
4795
+ * into the layer's node template). Omit a side to leave it unthemed.
4796
+ */
4797
+ node?: ThemeVariants<NodeStyle>;
4798
+ /**
4799
+ * Edge style applied per resolved kind via `setEdgeDefaults` (shallow-merge
4800
+ * into the layer's edge template). Omit a side to leave it unthemed.
4801
+ */
4802
+ edge?: ThemeVariants<EdgeStyle>;
4803
+ /**
4804
+ * Style applied to group nodes only (those with `style.group`), layered on top
4805
+ * of `node`. Full `NodeStyle` per resolved kind. Because the engine has no
4806
+ * group template, this is written per-group-node via `store.updateNode` and
4807
+ * re-applied as groups are added. Omit a side to leave it unthemed.
4808
+ */
4809
+ group?: ThemeVariants<NodeStyle>;
4810
+ }
4811
+ declare class ResponsiveThemeBehaviour extends Behaviour {
4812
+ /** Bound target layer — resolved in `onRegister`. */
4813
+ private layer;
4814
+ private mode;
4815
+ private node?;
4816
+ private edge?;
4817
+ private group?;
4818
+ private mediaQuery;
4819
+ private mediaListener;
4820
+ /** Store-event disposers for the group re-theme subscription. */
4821
+ private readonly subs;
4822
+ /** Microtask debounce flag — coalesces bursts of `node:add` / `flush`. */
4823
+ private groupApplyScheduled;
4824
+ /** Re-entrancy guard — set while our own `updateNode` writes are in flight. */
4825
+ private patching;
4826
+ constructor(opts: ResponsiveThemeBehaviourOptions);
4827
+ protected onRegister(ctx: CanvasContext): void;
4828
+ protected onEnable(): void;
4829
+ protected onDisable(): void;
4830
+ protected onDestroy(): void;
4831
+ /** Current mode setting. */
4832
+ getMode(): ThemeMode;
4833
+ /** Concrete kind currently resolved from the mode. */
4834
+ getResolvedKind(): ThemeKind;
4835
+ /**
4836
+ * Switch mode. `'auto'` re-arms the system listener; `'light'` / `'dark'`
4837
+ * detach it and pin. Re-applies immediately when enabled.
4838
+ */
4839
+ setMode(mode: ThemeMode): void;
4840
+ /**
4841
+ * Replace the light/dark style variants. Re-applies immediately when enabled
4842
+ * so live tweaking (e.g. from a GUI) is visible without an extra call.
4843
+ */
4844
+ setVariants(variants: {
4845
+ node?: ThemeVariants<NodeStyle>;
4846
+ edge?: ThemeVariants<EdgeStyle>;
4847
+ group?: ThemeVariants<NodeStyle>;
4848
+ }): void;
4849
+ /** Resolve the active kind and push its variants onto the layer. */
4850
+ private applyTheme;
4851
+ /**
4852
+ * Write the group variant onto every group node, layered over its current
4853
+ * style. There is no group template, so this targets instances directly; the
4854
+ * per-instance style wins over the node template, giving groups
4855
+ * `node[kind]` (base) + `group[kind]` (override).
4856
+ */
4857
+ private applyGroupTheme;
4858
+ /**
4859
+ * Microtask-debounced re-apply of the group pass when new group nodes appear.
4860
+ * Node/edge variants need no rescheduling — they live on the template, so new
4861
+ * nodes inherit them automatically.
4862
+ */
4863
+ private scheduleGroupApply;
4864
+ /**
4865
+ * Arm the `prefers-color-scheme` listener when in `'auto'` mode; detach it
4866
+ * otherwise. A system flip re-applies the resolved variant.
4867
+ */
4868
+ private wireMediaQuery;
4869
+ private detachMediaQuery;
4870
+ }
4871
+
4872
+ export { type ArcShapeOption, type ArrowShape, type BadgeEffects, type BadgeOrigin, type BadgePlacement, type BrushModifierKey, BrushSelectBehaviour, type BrushSelectBehaviourOptions, type BrushSelectElementType, type BrushSelectStyle, type BuiltInNodeShapeOptions, type CanonicalStateName, type CircleShapeOption, ClickInspectBehaviour, type ClickInspectBehaviourOptions, type ClickInspectEventMap, ClickSelectBehaviour, type ClickSelectBehaviourOptions, type ClickSelectEventMap, CollapseExpandBehaviour, type CollapseExpandBehaviourOptions, ContextMenuBehaviour, type ContextMenuBehaviourOptions, type ContextMenuEvent, type ContextMenuTargetType, CreateNodeBehaviour, type CreateNodeBehaviourOptions, type CustomShapeOption, DEFAULT_EDGE_STATES, DEFAULT_NODE_STATES, type DecorationSpecCommon, DegreeSizeBehaviour, type DegreeSizeBehaviourOptions, type DegreeSizeScale, DragNodeBehaviour, type DragNodeBehaviourOptions, DrawEdgeBehaviour, type DrawEdgeBehaviourOptions, type EdgeAnchor, type EdgeBadge, type EdgeBadgePlacement, type EdgeData, type EdgeDecorationSpec, type EdgeDirection, type EdgeInput, type EdgeOption, type EdgePathType, type EdgeShapeOptions, EdgeSizeLODBehaviour, type EdgeSizeLODBehaviourOptions, type EdgeSizeLODConfig, type EdgeStyle, EraseBehaviour, type EraseBehaviourOptions, type EraseTargetKind, type ErasedElement, GROUP_TOGGLE_SLOT, GraphClipboard, type GraphClipboardEventMap, type GraphClipboardOptions, type GraphData, type GraphDataOptions, type GraphEdge, GraphHistory, type GraphHistoryEventMap, type GraphHistoryOptions, GraphLayer, type GraphLayerEvents, type GraphLayerOptions, type GraphNode, GraphStore, type GraphStoreEventMap, type GraphStoreOptions, type GroupOptions, type HistoryEntry, type HistoryOp, type HistoryRecorder, HoverActivateBehaviour, type HoverActivateBehaviourOptions, type HoverDirection, type HoverableElement, type HoverableElementType, type InspectTarget, LabelCollisionBehaviour, type LabelCollisionBehaviourOptions, type LabelCollisionStrategy, type LabelPriorityResolver, LabelResolutionLODBehaviour, type LabelResolutionLODBehaviourOptions, type LassoModifierKey, LassoSelectBehaviour, type LassoSelectBehaviourOptions, type LassoSelectElementType, type LassoSelectStyle, MiniMapLayer, type MiniMapLayerOptions, type MiniMapPosition, type NodeBadge, type NodeData, type NodeDecorationSpec, type NodeEffects, type NodeIcon, type NodeImage, type NodeInput, type NodeOption, NodeResizeBehaviour, type NodeResizeBehaviourOptions, type NodeShapeOptions, NodeSizeLODBehaviour, type NodeSizeLODBehaviourOptions, type NodeSizeLODConfig, type NodeStyle, type ParallelEdgeBasis, ParallelEdgeBehaviour, type ParallelEdgeBehaviourOptions, type ParallelEdgeDistribute, type ParallelEdgeDistributeContext, type ParallelEdgeGroup, type ParallelEdgePatch, type PasteResult, type PolygonShapeOption, type RectShapeOption, type RegularPolygonShapeOption, type Resolvable, type ResolvableEdgeStyle, type ResolvableId, type ResolvableNodeStyle, ResponsiveThemeBehaviour, type ResponsiveThemeBehaviourOptions, type SelectDirection, type SelectModifierKey, type SelectableElement, type SelectableElementType, type SelectionSnapshot, type StarShapeOption, type ThemeKind, type ThemeMode, type ThemeVariants, type Vec2, centeredRanksPolicy, isBuiltInNodeShape, resolveField };