@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.
- package/dist/index.d.ts +4872 -0
- package/dist/index.js +7685 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|