@invana/canvas 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2034 @@
1
+ import { E as EventMap, a as EventEmitter, b as EventSource, C as CanvasEventBus, c as Camera, P as PrimitivesRenderer } from './index-D0Z3YfJv.js';
2
+ export { br as AnchorCtx, bs as AnchorShapeRef, bq as AnchorSpec, L as ArcShape, bF as ArcSpec, U as ArrowMarker, W as ArrowMarkerSpec, b2 as BadgeOptions, b3 as BadgePlacement, bH as BaseConnectorSpec, bz as BaseShapeSpec, aZ as BreathingConnectorEffect, a_ as BreathingConnectorEffectStyle, aX as BreathingEffect, aY as BreathingEffectStyle, l as CameraOptions, f as CanvasEvent, k as CanvasEventBusOptions, h as CanvasGlobalEvents, G as CircleShape, bA as CircleSpec, O as CompositePart, M as CompositeShape, N as CompositeSpec, Q as Connector, b4 as ConnectorBadgePlacement, r as ConnectorBase, t as ConnectorDecorationBase, bU as ConnectorDecorationCtor, bM as ConnectorDecorationHostInfo, v as ConnectorEffectBase, bI as ConnectorEndpointSpec, bK as ConnectorHostInfo, cg as ConnectorLabelPlacement, ch as ConnectorLabelStyle, by as ConnectorPaintStyle, D as DEFAULT_TAP_EXCLUDE, bX as DecorationSpec, bV as DecorationTarget, F as Easing, u as EffectBase, c5 as EffectSpec, bY as EffectTarget, bZ as EffectTargetKind, bg as Endpoint, d as EventHandler, g as EventSourceKind, a$ as FadeInConnectorEffect, b0 as FadeInConnectorEffectStyle, b1 as FadeInEasingName, at as FlowParticlesConnectorDecoration, au as FlowParticlesConnectorDecorationStyle, ar as FlyMarkerConnectorDecoration, as as FlyMarkerConnectorDecorationStyle, av as GlowConnectorDecoration, aw as GlowConnectorDecorationStyle, ad as GlowDecoration, ae as GlowDecorationStyle, c6 as HitResult, ci as HtmlTagStyle, bn as IAnchor, bO as IConnector, bR as IConnectorDecoration, bP as IDecorationBase, c1 as IEffectBase, bl as IPathStyle, bk as IRouter, bN as IShape, bQ as IShapeDecoration, c2 as IShapeEffect, bv as InsetAnchor, a8 as LOOP_CURVE_PRESETS, ca as LabelBackground, aG as LabelConnectorDecoration, c9 as LabelContent, aF as LabelDecoration, cd as LabelStyleCommon, cc as LabelVisibility, cb as LabelWrap, ah as LiquidFillDecoration, ai as LiquidFillDecorationStyle, a9 as LoopCurvePresetName, an as MarchingAntsConnectorDecoration, ao as MarchingAntsConnectorDecorationStyle, aj as MarchingAntsDecoration, ak as MarchingAntsDecorationStyle, bG as MarkerShapeSpec, b5 as NamedBadgePlacement, bo as Obstacle, bh as Path, bi as PathCommand, bm as PathStyleEndpoints, n as Point, I as PolygonShape, bC as PolygonSpec, bj as Polyline, q as PrimitiveBase, c7 as PrimitivesRendererEventMap, p as PrimitivesRendererOptions, af as PulseRingDecoration, ag as PulseRingDecorationStyle, R as Rect, H as RectShape, bB as RectSpec, bW as RegisterDecorationOptions, c4 as RegisterEffectOptions, J as RegularPolygonShape, bD as RegularPolygonSpec, c8 as RenderStats, aL as ResizeHandleDecoration, aM as ResizeHandleDecorationStyle, aO as ResizeHandleHitGeometry, aN as ResizeHandlePlacement, az as RevealConnectorDecoration, aA as RevealConnectorDecorationStyle, aB as RevealDirection, aC as RevealEasingName, aD as RevealHostStroke, aE as RevealRepeat, ap as RingConnectorDecoration, aq as RingConnectorDecorationStyle, al as RingDecoration, am as RingDecorationStyle, ax as RippleConnectorDecoration, ay as RippleConnectorDecorationStyle, bp as RouterCtx, aT as SelectionFrameBorderStyle, aP as SelectionFrameDecoration, aQ as SelectionFrameDecorationStyle, aS as SelectionFrameHandleHit, aU as SelectionFrameHandleShape, aR as SelectionFramePlacement, aV as ShakeEffect, aW as ShakeEffectStyle, S as ShapeBase, bS as ShapeCtor, s as ShapeDecorationBase, bT as ShapeDecorationCtor, bL as ShapeDecorationHostInfo, c3 as ShapeEffectCtor, c0 as ShapeEffectHostInfo, bt as ShapeFill, bu as ShapeFillLayer, bJ as ShapeHostInfo, ce as ShapeLabelPlacement, cf as ShapeLabelStyle, bx as ShapePaintStyle, bw as ShapeStroke, K as StarShape, bE as StarSpec, b$ as StyleOverride, T as TapHandler, j as TapOptions, o as TextureRegistry, aH as ToggleDecoration, aI as ToggleDecorationStyle, aK as ToggleHitGeometry, aJ as TogglePlacement, b_ as TransformDelta, w as Tween, x as TweenOptions, bf as Vec2, V as arrowMarkerSpec, a3 as bezierPathStyle, ab as boundaryAnchor, a5 as bumpRadialPathStyle, aa as centerAnchor, be as distanceToPolylineSq, B as easeInOutCubic, z as easeInOutSine, A as easeOutCubic, $ as erRouter, i as isExcludedFromTap, y as linear, a7 as loopCurvePathStyle, m as makeCanvasEvent, e as makeEventType, Z as manhattanRouter, _ as metroRouter, b8 as mirrorPlacement, a1 as normalPathStyle, a0 as oneSideRouter, b7 as originToBadgeLocal, Y as orthRouter, bd as pathBounds, ac as perpendicularAnchor, b6 as placementToHostAnchor, a4 as quadraticPathStyle, b9 as resolveBadgePosition, a2 as roundedPathStyle, ba as samplePath, bb as samplePathAt, a6 as smoothPathStyle, X as straightRouter, bc as tangentAt } from './index-D0Z3YfJv.js';
3
+ import { StoreApi } from 'zustand/vanilla';
4
+ export { StoreApi } from 'zustand/vanilla';
5
+ import { Container, Graphics, Application } from 'pixi.js';
6
+ export { Graphics } from 'pixi.js';
7
+ import 'pixi-viewport';
8
+
9
+ /**
10
+ * `SourceEmitter` — typed event emitter that auto-forwards every emit to a
11
+ * `CanvasEventBus.tap()` channel as a structured envelope.
12
+ *
13
+ * Architecture: see `architecture-proposal.md` §2.5.
14
+ *
15
+ * Each source (a `Layer`, a `Behaviour`, a `Layout`, the `Canvas` itself)
16
+ * holds one of these. Local subscribers see the plain payload (`on(name, fn)`);
17
+ * the bus's tap subscribers see the envelope (`{ type, timestamp, source, payload }`).
18
+ *
19
+ * The forward path is a single method call (`bus.publish(envelope)`). Tap
20
+ * filtering (exclude / sampleRate) lives in the bus, not here, so the emit
21
+ * cost stays constant whether 0 or 100 taps are subscribed.
22
+ *
23
+ * In dev, payloads are run through `assertSerialisableInDev` so violations
24
+ * surface immediately with the offending path.
25
+ *
26
+ * @example
27
+ * class GraphLayer {
28
+ * readonly events: SourceEmitter<{ 'node:click': { id: string } }>;
29
+ *
30
+ * constructor(id: string, ctx: CanvasContext) {
31
+ * this.events = new SourceEmitter({ kind: 'layer', id }, ctx.events);
32
+ * }
33
+ *
34
+ * onShapeClick(id: string) {
35
+ * // local handlers + bus tap, both fired:
36
+ * this.events.emit('node:click', { id });
37
+ * }
38
+ * }
39
+ */
40
+
41
+ declare class SourceEmitter<E extends EventMap = EventMap> extends EventEmitter<E> {
42
+ private readonly source;
43
+ /**
44
+ * @param source — `{ kind: 'layer' | 'behaviour' | 'layout' | 'canvas' | 'store', id }`.
45
+ * Identity of this emitter; used as the `source` field of each envelope.
46
+ * @param bus — Optional. When present, every `emit()` also publishes a
47
+ * `CanvasEvent` envelope to this bus's tap channel. Pass `undefined`
48
+ * for emitters that should be local-only (rare; mostly tests).
49
+ */
50
+ private bus?;
51
+ constructor(source: EventSource, bus?: CanvasEventBus);
52
+ /**
53
+ * Attach (or detach) the bus this emitter forwards to.
54
+ *
55
+ * Use case: a `Layer` is constructed before it knows which `Canvas` it'll be
56
+ * mounted on. The Layer creates its `SourceEmitter` upfront with no bus,
57
+ * then `mount(ctx)` calls `events.setBus(ctx.events)` to start forwarding.
58
+ * Pass `undefined` to detach (e.g. on unmount).
59
+ */
60
+ setBus(bus: CanvasEventBus | undefined): void;
61
+ /**
62
+ * Emit to local subscribers AND publish to the bus's tap channel.
63
+ * Order: local handlers run first (synchronous, in registration order),
64
+ * then the envelope is published. A throwing local handler is caught
65
+ * (logged via `console.error` per `EventEmitter`) and does not block the
66
+ * tap publish.
67
+ */
68
+ emit<K extends keyof E>(event: K, payload: E[K]): void;
69
+ /** Convenience: source identity (read-only). */
70
+ get sourceInfo(): EventSource;
71
+ }
72
+
73
+ /**
74
+ * Dev-mode walker that asserts an event payload is serialisable.
75
+ *
76
+ * Architecture: see `architecture-proposal.md` §2.5 (Serialisability discipline).
77
+ *
78
+ * Once the canvas advertises `tap()` as telemetry-ready, payloads must be
79
+ * serialisable: only ids, numbers, strings, plain objects, arrays, and
80
+ * `Map`/`Set` containing the same. PixiJS objects, DOM nodes, function refs,
81
+ * and class instances must NOT appear in payloads — they break:
82
+ *
83
+ * - JSON-based telemetry sinks (Datadog, log shippers)
84
+ * - structured-clone-based sinks (BroadcastChannel, postMessage to workers)
85
+ * - devtools time-travel
86
+ * - test snapshots
87
+ *
88
+ * The walker is **dev-only**. The exported `assertSerialisable` is
89
+ * unconditionally callable, but `assertSerialisableInDev` is the public
90
+ * entry point that the bus uses — it inlines a build-time NODE_ENV check
91
+ * so production bundlers tree-shake the entire walker out.
92
+ *
93
+ * @example violation log
94
+ * [canvas] payload at 'node.shape' is not serialisable: BaseShape (class instance)
95
+ */
96
+ /**
97
+ * Walk `value`, returning a list of human-readable violation messages.
98
+ * Empty array means "fully serialisable".
99
+ *
100
+ * The walker is iterative-ish: it uses recursion but with explicit cycle
101
+ * detection so a self-referencing payload doesn't blow the stack.
102
+ */
103
+ declare function findSerialisationViolations(value: unknown, rootPath?: string): string[];
104
+ /**
105
+ * Convenience: assert a payload is serialisable. In dev, logs warnings via
106
+ * `console.warn` for each violation (with offending path). In production,
107
+ * compiles to a no-op via `process.env.NODE_ENV` substitution.
108
+ *
109
+ * Pass a `context` string so the warning includes which event triggered it,
110
+ * e.g. `assertSerialisableInDev(payload, "emit('node:click')")`.
111
+ */
112
+ declare function assertSerialisableInDev(value: unknown, context: string): void;
113
+
114
+ /**
115
+ * `Store<T>` — typed alias of `zustand/vanilla` `StoreApi<T>` plus a thin
116
+ * factory that pre-composes the middleware stack we use everywhere.
117
+ *
118
+ * Architecture: see `architecture-proposal.md` §2.1 (state vs. data — bifurcated).
119
+ *
120
+ * **Use this only for small, observable interaction state** — hover, selection,
121
+ * drag intent, decoration overrides, view modes. The cardinality should stay
122
+ * comfortably below a few thousand items per slice; typical state size is
123
+ * tens of fields, not megabytes.
124
+ *
125
+ * For bulk hot data (positions, attributes for tens of thousands or millions
126
+ * of items), use `ColumnStore` instead. Immer + Map at 500k entries clones the
127
+ * whole Map on every mutation (5–50 ms each); typed-array columns are 10 ns.
128
+ *
129
+ * Middleware stack (outer → inner):
130
+ *
131
+ * devtools → subscribeWithSelector → immer → state creator
132
+ *
133
+ * - **immer** lets recipes mutate a draft; the produced state is structurally
134
+ * shared with the previous state (untouched branches kept by reference).
135
+ * - **subscribeWithSelector** adds the `subscribe(selector, listener, opts?)`
136
+ * overload so consumers can subscribe to a slice instead of the whole state.
137
+ * - **devtools** is enabled only in non-production builds (Redux DevTools
138
+ * extension). Tree-shaken / no-op in production.
139
+ *
140
+ * @example
141
+ * type GraphLayerState = {
142
+ * hoveredId: string | null;
143
+ * selectedIds: ReadonlySet<string>;
144
+ * haloIds: ReadonlySet<string>;
145
+ * };
146
+ *
147
+ * const store = createLayerStore<GraphLayerState>(
148
+ * { hoveredId: null, selectedIds: new Set(), haloIds: new Set() },
149
+ * { name: 'GraphLayer:graph-1' }
150
+ * );
151
+ *
152
+ * // immer-style mutation
153
+ * store.setState((draft) => { draft.hoveredId = 'n-42'; });
154
+ *
155
+ * // selector subscription — fires only when haloIds changes
156
+ * const off = store.subscribe(
157
+ * (s) => s.haloIds,
158
+ * (curr, prev) => { ... },
159
+ * { equalityFn: Object.is }
160
+ * );
161
+ */
162
+
163
+ /**
164
+ * The store API surface exposed to consumers.
165
+ *
166
+ * After middleware composition, the runtime store has:
167
+ * - `setState(recipe)` — immer recipe that mutates a draft (immer middleware).
168
+ * - `setState(partial)` and `setState(updater)` — vanilla forms (still work).
169
+ * - `subscribe(listener)` — vanilla zustand API.
170
+ * - `subscribe(selector, listener, opts?)` — added by `subscribeWithSelector`.
171
+ *
172
+ * The `setState` overload union mirrors what the immer middleware produces at
173
+ * runtime; `Store<T>` is what `createLayerStore<T>()` returns.
174
+ */
175
+ type Store<T> = Omit<StoreApi<T>, 'setState' | 'subscribe'> & {
176
+ setState: {
177
+ /** Immer recipe form — mutate the draft, return nothing. Preferred. */
178
+ (recipe: (draft: T) => void): void;
179
+ /** Direct partial / replacement / updater forms — also accepted. */
180
+ (partial: T | Partial<T> | ((state: T) => T | Partial<T> | void), replace?: false): void;
181
+ /** Replace-state form (second arg = true). */
182
+ (state: T, replace: true): void;
183
+ };
184
+ subscribe: StoreApi<T>['subscribe'] & {
185
+ <U>(selector: (state: T) => U, listener: (selectedState: U, previousSelectedState: U) => void, options?: {
186
+ equalityFn?: (a: U, b: U) => boolean;
187
+ fireImmediately?: boolean;
188
+ }): () => void;
189
+ };
190
+ };
191
+ interface CreateLayerStoreOptions {
192
+ /**
193
+ * Devtools display name. Used as the "store" name in Redux DevTools.
194
+ * Convention: `<ClassName>:<id>` (e.g. `'GraphLayer:graph-1'`).
195
+ */
196
+ name?: string;
197
+ /**
198
+ * Force devtools on/off. Default: enabled when `process.env.NODE_ENV !== 'production'`.
199
+ * High-frequency mutation sites can pass `enableDevtools: false` per-store
200
+ * to avoid devtools serialisation cost in dev too.
201
+ */
202
+ enableDevtools?: boolean;
203
+ }
204
+ /**
205
+ * Create a `Store<T>` with our standard middleware stack.
206
+ *
207
+ * Pass either an initial state object, or a creator function that takes the
208
+ * zustand `set` / `get` (already wrapped with immer) and returns initial state.
209
+ * The creator form is useful when initial state needs to reference itself or
210
+ * close over imperative setup; the object form is the common case.
211
+ */
212
+ declare function createLayerStore<T extends object>(initial: T, opts?: CreateLayerStoreOptions): Store<T>;
213
+ declare function createLayerStore<T extends object>(creator: (set: (recipe: (draft: T) => void) => void, get: () => T) => T, opts?: CreateLayerStoreOptions): Store<T>;
214
+
215
+ /**
216
+ * `ColumnStore<TSchema>` — typed-array column store for **bulk hot data**.
217
+ *
218
+ * Architecture: see `architecture-proposal.md` §2.1.
219
+ *
220
+ * Designed to scale to millions of items at machine-rate mutation (1000s/sec
221
+ * from external feeds). Where `Store<T>` (zustand+immer) makes per-mutation
222
+ * structural-sharing trade-offs that cap out around 5–10k for hot data,
223
+ * `ColumnStore` mutates typed-array slots in place at ~10 ns per write.
224
+ *
225
+ * **Mental model**
226
+ *
227
+ * One id-keyed object would be: `{ id: 'n-42', x: 100, y: 50, color: 0x... }`
228
+ * In a ColumnStore that's: slot 17 across N parallel typed-array columns.
229
+ *
230
+ * Lookup goes id → slot (`Map<string, number>`), then any number of columns
231
+ * are read/written by indexing slot. Slots are recycled when items are removed,
232
+ * so the column buffers stay compact under churn.
233
+ *
234
+ * **Performance characteristics at 500k items**
235
+ *
236
+ * | Op | Cost |
237
+ * |---|---|
238
+ * | `add(id, row)` | ~100 ns (Map.set + N TypedArray writes) |
239
+ * | `set(id, col, value)` | ~50 ns (Map.get + TypedArray write) |
240
+ * | `column(name)[slot] = value` (fast-path) | ~10 ns (single TypedArray write) |
241
+ * | `addBulk(rows)` for 500k | ~5–20 ms |
242
+ * | Memory for 500k × 5 columns × 4 bytes | ~10 MB |
243
+ *
244
+ * Compare immer + Map<string, object> at 500k: ~5–50 ms per single-field
245
+ * mutation (clones the entire 500k Map every time), ~150–250 MB memory.
246
+ *
247
+ * @example
248
+ * type NodeSchema = { x: 'f32'; y: 'f32'; color: 'u32'; size: 'f32' };
249
+ *
250
+ * class GraphNodeStore extends ColumnStore<NodeSchema> {
251
+ * constructor() {
252
+ * super({ x: 'f32', y: 'f32', color: 'u32', size: 'f32' }, { initialCapacity: 1024 });
253
+ * }
254
+ * }
255
+ *
256
+ * const nodes = new GraphNodeStore();
257
+ * nodes.add('n-1', { x: 10, y: 20, color: 0x3b82f6, size: 8 });
258
+ * nodes.set('n-1', 'x', 30); // typesafe per-field setter
259
+ *
260
+ * // Renderer fast path — hold refs once, write directly:
261
+ * const xCol = nodes.column('x');
262
+ * const slot = nodes.slot('n-1')!;
263
+ * xCol[slot] = 40; // ~10 ns per write
264
+ * nodes.touch(); // bump version after fast-path writes
265
+ */
266
+ /**
267
+ * Numeric type tags for typed-array columns. Each maps to a JS TypedArray ctor.
268
+ *
269
+ * - `i8 / u8` — small integers (1 byte). Use for booleans, bitfields, packed enums.
270
+ * - `i16 / u16` — medium integers (2 bytes). Use for short integer ids, type-tags.
271
+ * - `i32 / u32` — large integers (4 bytes). Use for slot refs, packed colors, hashes.
272
+ * - `f32` — single-precision floats (4 bytes). Default for coordinates, weights.
273
+ * - `f64` — double-precision floats (8 bytes). Use only when precision matters.
274
+ */
275
+ type ColumnType = 'i8' | 'u8' | 'i16' | 'u16' | 'i32' | 'u32' | 'f32' | 'f64';
276
+ type ColumnSchema = Record<string, ColumnType>;
277
+ type ColumnValue<T extends ColumnType> = T extends 'f32' | 'f64' ? number : number;
278
+ type ColumnArray<T extends ColumnType> = T extends 'i8' ? Int8Array : T extends 'u8' ? Uint8Array : T extends 'i16' ? Int16Array : T extends 'u16' ? Uint16Array : T extends 'i32' ? Int32Array : T extends 'u32' ? Uint32Array : T extends 'f32' ? Float32Array : T extends 'f64' ? Float64Array : never;
279
+ type RowOf<TSchema extends ColumnSchema> = {
280
+ [K in keyof TSchema]: ColumnValue<TSchema[K]>;
281
+ };
282
+ interface ColumnStoreOptions {
283
+ /** Initial slot capacity. Doubles on overflow. Default 256. */
284
+ initialCapacity?: number;
285
+ /** Max capacity. Throws on overflow. Default 16_777_216 (~16M). */
286
+ maxCapacity?: number;
287
+ }
288
+ declare class ColumnStore<TSchema extends ColumnSchema = ColumnSchema> {
289
+ private readonly schema;
290
+ private readonly columnNames;
291
+ private readonly maxCapacity;
292
+ /** id → slot. The only object-keyed lookup on the hot path. */
293
+ private readonly idIndex;
294
+ /** slot → id. Filled slots have a string; recycled holes have `undefined`. */
295
+ private readonly idReverse;
296
+ /** Stack of recycled slots. `add()` pops from here before extending. */
297
+ private readonly freeSlots;
298
+ /** TypedArray per column. Replaced on grow (new buffer with copied data). */
299
+ private columns;
300
+ /** Current capacity (length of each TypedArray). */
301
+ private _capacity;
302
+ /** High-water mark — largest slot index ever assigned + 1. Not necessarily filled. */
303
+ private _highWater;
304
+ /** Mutation version. Increments on any add/remove/set or `touch()`. */
305
+ private _version;
306
+ constructor(schema: TSchema, opts?: ColumnStoreOptions);
307
+ /** Number of items currently stored. */
308
+ get size(): number;
309
+ /** Current allocated capacity. Grows automatically when filled. */
310
+ get capacity(): number;
311
+ /** Mutation counter — bumps on any change. Subscribers diff this. */
312
+ get version(): number;
313
+ /** True iff `id` has been added. */
314
+ has(id: string): boolean;
315
+ /** Returns the slot for `id`, or `undefined`. Useful for the renderer fast path. */
316
+ slot(id: string): number | undefined;
317
+ /** Returns the id at `slot`, or `undefined` if the slot is free. */
318
+ idAt(slot: number): string | undefined;
319
+ /**
320
+ * Direct access to a column's TypedArray. **Holds a stable reference until
321
+ * the column is grown** (then the underlying buffer is replaced).
322
+ *
323
+ * Use the version field to detect grow events:
324
+ * const v = store.version; const col = store.column('x');
325
+ * // if (store.version !== v) the col reference may be stale
326
+ *
327
+ * Renderer fast path: cache `column(name)` and `slot(id)` once per frame
328
+ * and write directly. Bump `touch()` after batched fast-path writes so
329
+ * subscribers know.
330
+ */
331
+ column<K extends keyof TSchema>(name: K): ColumnArray<TSchema[K]>;
332
+ /** Read a single value. ~50 ns: Map.get + TypedArray read. */
333
+ get<K extends keyof TSchema>(id: string, name: K): ColumnValue<TSchema[K]> | undefined;
334
+ /** Materialise a full row by id. Allocates an object — avoid in hot loops. */
335
+ row(id: string): RowOf<TSchema> | undefined;
336
+ /**
337
+ * Add a new item. Throws if `id` already exists.
338
+ * Reuses a recycled slot when available; otherwise extends (and grows).
339
+ */
340
+ add(id: string, row: RowOf<TSchema>): number;
341
+ /**
342
+ * Bulk add. Grows once if needed (cheaper than N individual grows).
343
+ * Throws if any id already exists.
344
+ */
345
+ addBulk(items: ReadonlyArray<{
346
+ id: string;
347
+ row: RowOf<TSchema>;
348
+ }>): void;
349
+ /**
350
+ * Set a single field. ~50 ns: Map.get + TypedArray write.
351
+ * No-op if id doesn't exist.
352
+ */
353
+ set<K extends keyof TSchema>(id: string, name: K, value: ColumnValue<TSchema[K]>): void;
354
+ /**
355
+ * Update multiple fields of one item in one call. Avoids N version bumps.
356
+ */
357
+ update(id: string, partial: Partial<RowOf<TSchema>>): void;
358
+ /**
359
+ * Remove an item. Recycles the slot. No-op if id doesn't exist.
360
+ */
361
+ remove(id: string): void;
362
+ /** Bulk remove. */
363
+ removeBulk(ids: readonly string[]): void;
364
+ /**
365
+ * Mark the store as mutated without actually changing anything via the API.
366
+ * Use this after batches of fast-path writes via `column(...)[slot] = ...`
367
+ * so version-bump-driven subscribers know to re-read.
368
+ */
369
+ touch(): void;
370
+ /**
371
+ * Drop all items and recycled slots. Keeps the current capacity (no shrink).
372
+ * Cheap reset for repopulating from a feed.
373
+ */
374
+ clear(): void;
375
+ /**
376
+ * Iterate over (id, slot) pairs in insertion order of currently-live ids.
377
+ * O(idIndex.size) — does NOT walk holes.
378
+ */
379
+ forEach(cb: (id: string, slot: number) => void): void;
380
+ /** Iterator over live ids only. */
381
+ ids(): IterableIterator<string>;
382
+ private allocSlot;
383
+ /**
384
+ * Grow each column's TypedArray to at least `target` capacity.
385
+ * Doubles past target until met (so we don't grow once per add in a tight loop).
386
+ */
387
+ private grow;
388
+ }
389
+
390
+ /**
391
+ * `DirtyBatcher<TBucket>` — accumulates "this id changed" signals between
392
+ * frames, deduplicates them, and hands a per-frame snapshot to a flush callback.
393
+ *
394
+ * Architecture: see `architecture-proposal.md` §2.1 (Render projection).
395
+ *
396
+ * **Single responsibility:** absorb high-frequency dirty signals; nothing else.
397
+ * No rendering, no PixiJS knowledge, no `requestAnimationFrame` of its own.
398
+ * Canvas owns the single RAF (proposal §2.1) and calls `flush()` once per tick.
399
+ *
400
+ * **Performance properties (the contract that makes the architecture work)**
401
+ *
402
+ * - `mark(bucket, id)`: O(1) — Map.get + Set.add. No allocation in steady state.
403
+ * - `markAll(bucket)`: O(1) — sets a flag.
404
+ * - `flush()`: O(1) — Map swap, no copy.
405
+ * - Per-frame work for the consumer is proportional to `changed` ids,
406
+ * never total scene size.
407
+ * - Sets are reused across frames (cleared after swap-out, not reallocated).
408
+ * Zero GC pressure in steady state.
409
+ *
410
+ * **Double buffering & mid-flush mutations**
411
+ *
412
+ * `flush()` returns a snapshot whose Sets are the previous frame's accumulated
413
+ * marks. The "active" Sets are swapped to the empty buffer, so any `mark()`
414
+ * call made *during* the consumer's flush handler lands in the next frame's
415
+ * bucket — never corrupts the iteration in flight, never loops.
416
+ *
417
+ * @example
418
+ * type GraphDirty = 'shape' | 'halo' | 'edge';
419
+ * const dirty = new DirtyBatcher<GraphDirty>();
420
+ *
421
+ * // anywhere — mutation site
422
+ * dirty.mark('shape', 'n-42');
423
+ *
424
+ * // canvas tick
425
+ * if (dirty.hasPending()) {
426
+ * const snap = dirty.flush();
427
+ * for (const id of snap.buckets.shape) renderer.updateShape(id, ...);
428
+ * for (const id of snap.buckets.halo) renderer.setDecoration(id, 'halo', ...);
429
+ * }
430
+ */
431
+ /**
432
+ * The frozen snapshot handed to the flush handler.
433
+ *
434
+ * `buckets` is a ReadonlyMap keyed by bucket name; each value is the Set of
435
+ * dirty ids for that bucket this frame. Buckets that have never been touched
436
+ * are absent (use `snap.buckets.get(bucket) ?? EMPTY_SET` to handle missing).
437
+ *
438
+ * `rebuildAll` is the Set of buckets the consumer marked with `markAll()`.
439
+ * For those buckets, the consumer should iterate the underlying data
440
+ * (not the per-id Set) — usually meaning "rebuild everything in this category."
441
+ */
442
+ interface DirtySnapshot<TBucket extends string = string> {
443
+ readonly buckets: ReadonlyMap<TBucket, ReadonlySet<string>>;
444
+ readonly rebuildAll: ReadonlySet<TBucket>;
445
+ }
446
+ declare class DirtyBatcher<TBucket extends string = string> {
447
+ private active;
448
+ private buffer;
449
+ /** True iff at least one mark has landed since the last flush. */
450
+ private _dirty;
451
+ /**
452
+ * Mark a single id as dirty in a bucket. O(1), no allocation in steady state.
453
+ *
454
+ * Bucket Sets are created lazily on first mark and reused thereafter.
455
+ * If the bucket is currently flagged `markAll`, this call is redundant
456
+ * (consumer will iterate all data anyway) but cheap and harmless.
457
+ */
458
+ mark(bucket: TBucket, id: string): void;
459
+ /**
460
+ * Flag a whole bucket as needing rebuild. The consumer's flush handler
461
+ * should iterate its full data set for this bucket, ignoring the per-id Set.
462
+ *
463
+ * Use for bulk events: theme change, LOD swap, "everything moved",
464
+ * data feed wholesale replace.
465
+ */
466
+ markAll(bucket: TBucket): void;
467
+ /** Cheap check the canvas tick uses to decide whether to call `flush()`. */
468
+ hasPending(): boolean;
469
+ /**
470
+ * Swap buffers and return the previous frame's snapshot. After this call:
471
+ * - The returned snapshot is stable for the duration of the consumer's
472
+ * handler (any new marks land in the freshly-cleared other buffer).
473
+ * - `hasPending()` returns false until the next mark.
474
+ *
475
+ * The handed-out Sets are still owned by the batcher — the consumer must
476
+ * **not retain references past the flush call**. The next `flush()` will
477
+ * reuse and clear them.
478
+ */
479
+ flush(): DirtySnapshot<TBucket>;
480
+ /**
481
+ * Drop both buffers. Call on layer unmount. After reset(), the batcher is
482
+ * usable again from a clean state.
483
+ */
484
+ reset(): void;
485
+ /** Number of dirty ids in a bucket. Returns 0 if bucket has never been touched. */
486
+ bucketSize(bucket: TBucket): number;
487
+ /** True iff the bucket has been flagged for rebuild this frame. */
488
+ isRebuildAll(bucket: TBucket): boolean;
489
+ }
490
+
491
+ /**
492
+ * `Layer` — base class for everything composable onto `canvas.layers`.
493
+ *
494
+ * Architecture: see `architecture-proposal.md` §2.1.
495
+ *
496
+ * **What every Layer owns:**
497
+ * - `id` — stable identifier; used by registries, events, telemetry envelopes.
498
+ * - `options` — construction-time, mostly-immutable config.
499
+ * - `state` — UI / interaction state (`Store<T>` zustand+immer; small, observable).
500
+ * - `events` — typed `SourceEmitter` that auto-forwards to the canvas tap.
501
+ * - `dirty` — `DirtyBatcher` for per-frame batched flush.
502
+ * - `visible` / `hittable` / `zIndex` / `cullable` — composition flags.
503
+ *
504
+ * **Lifecycle:** `mount(ctx)` → … → `unmount()`. `Canvas` calls these via the
505
+ * `LayerRegistry`. `flush()` is called once per Canvas tick when
506
+ * `dirty.hasPending()` is true.
507
+ *
508
+ * **Bulk hot data (`data`)** is NOT on the base class — it lives on subclasses
509
+ * that need it (e.g. `GraphLayer` ships `GraphNodeStore`). See
510
+ * `architecture-proposal.md` §2.1 for the bifurcated state/data model.
511
+ *
512
+ * **What subclasses provide:**
513
+ * - `createState()` — initial UI state.
514
+ * - `applyDirty(snap)` — translate dirty buckets → renderer commands.
515
+ * - `onMount()` / `onUnmount()` — domain-specific setup/teardown
516
+ * (subscribe to feeds, register decorations, etc.).
517
+ * - `WorldLayer` / `ScreenLayer` add `hitTest(coord, coord)`.
518
+ */
519
+
520
+ /**
521
+ * The subset of `Layer` the `LayerRegistry` and `Canvas.tick` interact with.
522
+ * Lets the registry stay decoupled from the abstract class implementation.
523
+ */
524
+ interface ILayer {
525
+ readonly id: string;
526
+ visible: boolean;
527
+ hittable: boolean;
528
+ zIndex: number;
529
+ cullable: boolean;
530
+ mount(ctx: CanvasContext): void;
531
+ unmount(): void;
532
+ flush(): void;
533
+ hasPending(): boolean;
534
+ }
535
+ interface LayerOptions<TOptions = unknown> {
536
+ id: string;
537
+ options: TOptions;
538
+ visible?: boolean;
539
+ hittable?: boolean;
540
+ zIndex?: number;
541
+ /**
542
+ * Off-screen culling participation. Default `true`. Set `false` for
543
+ * full-canvas effect layers (background gradient, overlay) that should
544
+ * always render regardless of camera visibility.
545
+ */
546
+ cullable?: boolean;
547
+ /** Optional: name shown in devtools. Default `'<ClassName>:<id>'`. */
548
+ devtoolsName?: string;
549
+ }
550
+ declare abstract class Layer<TOptions = unknown, TState extends object = object, TEvents extends EventMap = EventMap, TDirtyBucket extends string = string> implements ILayer {
551
+ readonly id: string;
552
+ readonly options: TOptions;
553
+ readonly state: Store<TState>;
554
+ readonly events: SourceEmitter<TEvents>;
555
+ readonly dirty: DirtyBatcher<TDirtyBucket>;
556
+ /** Backing field for the `visible` accessor. */
557
+ private _visible;
558
+ hittable: boolean;
559
+ zIndex: number;
560
+ cullable: boolean;
561
+ /**
562
+ * Whether this layer renders. Setting `false` hides the layer's pixi
563
+ * container (via `onVisibleChange`, overridden by `WorldLayer` /
564
+ * `ScreenLayer`) and the Canvas tick skips its flush.
565
+ */
566
+ get visible(): boolean;
567
+ set visible(value: boolean);
568
+ /** Set by `mount(ctx)`; cleared by `unmount()`. */
569
+ protected ctx?: CanvasContext;
570
+ /** True between `mount` and `unmount`. */
571
+ get mounted(): boolean;
572
+ constructor(opts: LayerOptions<TOptions>);
573
+ mount(ctx: CanvasContext): void;
574
+ unmount(): void;
575
+ /** Convenience accessor; throws when called pre-mount. */
576
+ protected get context(): CanvasContext;
577
+ /** Whether `flush()` has work to do this frame. */
578
+ hasPending(): boolean;
579
+ /**
580
+ * Called by Canvas tick when `hasPending()` is true. Swaps the dirty
581
+ * snapshot, hands it to `applyDirty`. Subclasses normally don't override.
582
+ */
583
+ flush(): void;
584
+ /** Build the initial UI / interaction state. Called once in the constructor. */
585
+ protected abstract createState(): TState;
586
+ /**
587
+ * Translate a dirty snapshot into renderer / pixi commands.
588
+ * Default: no-op. Override when the layer batches work via `dirty.mark(...)`.
589
+ */
590
+ protected applyDirty(_snap: DirtySnapshot<TDirtyBucket>): void;
591
+ /** Domain-specific mount setup (subscribe to peers, attach renderer, etc.). */
592
+ protected onMount(_ctx: CanvasContext): void;
593
+ /** Domain-specific unmount teardown. */
594
+ protected onUnmount(_ctx: CanvasContext): void;
595
+ /**
596
+ * Called whenever `visible` changes (setter only — not on initial
597
+ * construction). Subclasses override to keep their pixi container's
598
+ * `.visible` in sync. Default: no-op.
599
+ */
600
+ protected onVisibleChange(_value: boolean): void;
601
+ }
602
+
603
+ /**
604
+ * `LayerRegistry` — stores the Layers added to a Canvas.
605
+ *
606
+ * Architecture: see `architecture-proposal.md` §2.4 (CanvasContext.layers).
607
+ *
608
+ * **Responsibilities**
609
+ * - Add / remove (with mount / unmount lifecycle).
610
+ * - Typed `get<T>(id)`.
611
+ * - `byZOrder()` iteration — used by the Canvas tick.
612
+ * - Fires `'layer:added'` / `'layer:removed'` on the bus.
613
+ *
614
+ * **Lifecycle wiring**
615
+ *
616
+ * The registry doesn't itself construct the `CanvasContext` — it would be
617
+ * circular (the registry is a field of the context). Instead the Canvas
618
+ * passes a `getContext()` thunk; `add(layer)` resolves it at the moment of
619
+ * mount. This keeps the registry decoupled from the context's full shape.
620
+ */
621
+
622
+ interface LayerRegistryOptions {
623
+ /**
624
+ * Resolves the `CanvasContext` at the moment of mount. The Canvas creates
625
+ * its registries before the context object exists, so this thunk lets the
626
+ * registry defer the context lookup.
627
+ */
628
+ getContext: () => CanvasContext;
629
+ /** Bus for `layer:added` / `layer:removed` events. */
630
+ bus: CanvasEventBus;
631
+ }
632
+ declare class LayerRegistry {
633
+ private readonly layers;
634
+ private readonly getContext;
635
+ private readonly bus;
636
+ /** Cached z-sorted view; invalidated on add/remove/setZIndex. */
637
+ private zOrderCache;
638
+ constructor(opts: LayerRegistryOptions);
639
+ /** Number of registered layers. */
640
+ get size(): number;
641
+ /**
642
+ * Add a Layer to the canvas. Calls `layer.mount(ctx)` and fires `layer:added`.
643
+ * Throws if `id` is already registered.
644
+ */
645
+ add(layer: ILayer): void;
646
+ /**
647
+ * Remove a Layer. Calls `layer.unmount()` and fires `layer:removed`.
648
+ * No-op if `id` isn't registered.
649
+ */
650
+ remove(id: string): void;
651
+ /** Typed get by id. Returns `undefined` if not found. */
652
+ get<T extends ILayer = ILayer>(id: string): T | undefined;
653
+ has(id: string): boolean;
654
+ /** Snapshot of all layers in insertion order. */
655
+ list(): readonly ILayer[];
656
+ /**
657
+ * Iterate layers in z-order (low → high). The Canvas tick walks layers in
658
+ * z-order to flush dirty work; rendering order is then determined by
659
+ * pixi's child order (handled by `SurfaceManager.setWorldLayerZ`).
660
+ *
661
+ * The result is cached and reused until `add` / `remove` / `setZIndex` invalidates.
662
+ */
663
+ byZOrder(): readonly ILayer[];
664
+ /**
665
+ * Update a layer's `zIndex` and propagate to surfaces. Invalidates the
666
+ * z-order cache. No-op if the layer isn't registered.
667
+ */
668
+ setZIndex(id: string, zIndex: number): void;
669
+ /**
670
+ * Tear down every registered layer. Called on Canvas destroy.
671
+ * Iteration is over a snapshot so unmount-triggered side effects don't
672
+ * corrupt the loop.
673
+ */
674
+ clear(): void;
675
+ }
676
+
677
+ /**
678
+ * `Behaviour` — input subscriber that translates user input into state mutations.
679
+ *
680
+ * Architecture: see `architecture-proposal.md` §2.2.
681
+ *
682
+ * Behaviours own neither rendering output nor source-of-truth data. They
683
+ * subscribe to layer events (`'node:hover'`, `'shape:click'`) or canvas events
684
+ * (`'pointerdown'`) and mutate the appropriate `state` slice.
685
+ *
686
+ * **Default `enabled: false`.** Registration wires the behaviour up; the
687
+ * developer explicitly enables it. Matches the rule that no input behaviour
688
+ * is auto-active (`architecture-proposal.md` §2.2 + repo CLAUDE.md rule 7).
689
+ *
690
+ * **`shortcuts`** is advisory metadata — used by `BehaviourRegistry` to log
691
+ * conflict warnings when two enabled behaviours claim the same gesture
692
+ * (e.g. lasso vs. pan both wanting `'shift+drag'`). The framework warns;
693
+ * it does not enforce — that's the developer's job.
694
+ */
695
+
696
+ /** What `BehaviourRegistry` sees. */
697
+ interface IBehaviour {
698
+ readonly id: string;
699
+ readonly enabled: boolean;
700
+ readonly scope: 'layer' | 'canvas';
701
+ readonly layerId?: string;
702
+ readonly shortcuts?: readonly string[];
703
+ register(ctx: CanvasContext): void;
704
+ destroy(): void;
705
+ enable(): void;
706
+ disable(): void;
707
+ }
708
+ interface BehaviourOptions {
709
+ id: string;
710
+ /**
711
+ * Layer-scoped behaviours target a specific Layer by id. Canvas-scoped
712
+ * behaviours have no `layerId` and `scope: 'canvas'`.
713
+ */
714
+ layerId?: string;
715
+ /** Default `false` — the developer explicitly enables. */
716
+ enabled?: boolean;
717
+ /**
718
+ * Gesture identifiers this behaviour claims. Used by `BehaviourRegistry`
719
+ * for conflict warnings. Format is convention-free (`'shift+drag'`,
720
+ * `'wheel+ctrl'`, `'rclick'`); registries match strings as-is.
721
+ */
722
+ shortcuts?: readonly string[];
723
+ }
724
+ declare abstract class Behaviour implements IBehaviour {
725
+ readonly id: string;
726
+ readonly layerId?: string;
727
+ readonly shortcuts?: readonly string[];
728
+ /**
729
+ * `'layer'` if `layerId` is set, otherwise `'canvas'`. Set automatically
730
+ * from the constructor — subclasses don't need to re-declare.
731
+ */
732
+ readonly scope: 'layer' | 'canvas';
733
+ protected _enabled: boolean;
734
+ protected ctx?: CanvasContext;
735
+ constructor(opts: BehaviourOptions);
736
+ get enabled(): boolean;
737
+ /** Called by `BehaviourRegistry.register(behaviour)`. Subscribes to inputs. */
738
+ register(ctx: CanvasContext): void;
739
+ /** Called by `BehaviourRegistry.unregister(id)`. Drops subscriptions. */
740
+ destroy(): void;
741
+ enable(): void;
742
+ disable(): void;
743
+ /** Subscribe to events / setup any handler resources. */
744
+ protected abstract onRegister(ctx: CanvasContext): void;
745
+ /** Cleanup on destroy. Default no-op. */
746
+ protected onDestroy(_ctx: CanvasContext): void;
747
+ /** Hook fired when the developer enables the behaviour. */
748
+ protected onEnable(): void;
749
+ /** Hook fired on disable. */
750
+ protected onDisable(): void;
751
+ /**
752
+ * Convenience `if (!enabled) return;` for use inside event handlers
753
+ * (without rebinding `this` cost).
754
+ */
755
+ protected get isEnabled(): boolean;
756
+ }
757
+
758
+ /**
759
+ * `BehaviourRegistry` — stores Behaviours and toggles their enabled state.
760
+ *
761
+ * Architecture: see `architecture-proposal.md` §2.2.
762
+ *
763
+ * **Responsibilities**
764
+ * - `register` / `unregister` (with register / destroy lifecycle).
765
+ * - `setEnabled(id, enabled)` — toggles + fires `'behaviour:enabled'` /
766
+ * `'behaviour:disabled'`.
767
+ * - Typed `get<T>(id)`.
768
+ * - **Gesture-conflict warning**: when two enabled behaviours claim the same
769
+ * `shortcut`, log a `console.warn`. Doesn't enforce — the developer
770
+ * decides whether two behaviours can coexist on the same gesture.
771
+ */
772
+
773
+ interface BehaviourRegistryOptions {
774
+ getContext: () => CanvasContext;
775
+ bus: CanvasEventBus;
776
+ }
777
+ declare class BehaviourRegistry {
778
+ private readonly behaviours;
779
+ private readonly getContext;
780
+ private readonly bus;
781
+ constructor(opts: BehaviourRegistryOptions);
782
+ get size(): number;
783
+ /**
784
+ * Register a Behaviour. Calls `behaviour.register(ctx)` + fires
785
+ * `'behaviour:registered'`. If `behaviour.enabled` is `true` at construction
786
+ * time (the developer opted in via `enabled: true` option), also fires
787
+ * `'behaviour:enabled'` and runs the conflict-warning check.
788
+ *
789
+ * Throws on duplicate id.
790
+ */
791
+ register(behaviour: IBehaviour): void;
792
+ /** Remove a behaviour. Calls `destroy()`. No-op if not registered. */
793
+ unregister(id: string): void;
794
+ /** Enable / disable a behaviour. Fires the corresponding bus event. */
795
+ setEnabled(id: string, enabled: boolean): void;
796
+ get<T extends IBehaviour = IBehaviour>(id: string): T | undefined;
797
+ has(id: string): boolean;
798
+ list(): readonly IBehaviour[];
799
+ /** Tear down all behaviours. Called on Canvas destroy. */
800
+ clear(): void;
801
+ private warnOnShortcutConflict;
802
+ }
803
+
804
+ /**
805
+ * `CanvasContext` — the shared service surface every Layer / Behaviour /
806
+ * Layout receives at mount/register time.
807
+ *
808
+ * Architecture: see `architecture-proposal.md` §2.4.
809
+ *
810
+ * **One context, three audiences.** Per the proposal, there is no separate
811
+ * `LayerContext` / `BehaviourContext` / `LayoutContext` — the same shape is
812
+ * handed to every participant so cross-cutting access (read peer layers,
813
+ * fire camera moves, tap telemetry) doesn't need three parallel context types.
814
+ *
815
+ * The `Canvas` builds a concrete object that satisfies this interface and
816
+ * passes it down. Tests can construct a stub by satisfying these fields.
817
+ */
818
+
819
+ interface CanvasContext {
820
+ /** Layer registry — `add / remove / get<T>(id) / list / byZOrder`. */
821
+ readonly layers: LayerRegistry;
822
+ /**
823
+ * Behaviour registry — `register / setEnabled / get<T>(id) / list`.
824
+ * Behaviours never auto-enable; the developer registers + enables explicitly
825
+ * (`architecture-proposal.md` §2.2).
826
+ */
827
+ readonly behaviours: BehaviourRegistry;
828
+ /** Camera — pan/zoom/projection. Wraps a `pixi-viewport` `Viewport`. */
829
+ readonly camera: Camera;
830
+ /** Canvas-wide event bus + telemetry tap channel. */
831
+ readonly events: CanvasEventBus;
832
+ /**
833
+ * The world container — a `pixi-viewport` `Viewport` instance. Camera-
834
+ * transformed; `WorldLayer.mount` attaches its root sub-layer container
835
+ * here. Typed as `Container` so domain code doesn't depend on
836
+ * `pixi-viewport`; reach for the `Viewport`-specific API via
837
+ * `camera.viewport`.
838
+ */
839
+ readonly world: Container;
840
+ /**
841
+ * The pixi `app.stage` (or test stage) — the renderer root. `ScreenLayer.mount`
842
+ * attaches its root container here, as a sibling of `world`. Pixi's child
843
+ * order = draw order: `world` is added first (bottom), each `ScreenLayer`'s
844
+ * root is added after (above). No screen-wrapper container exists.
845
+ */
846
+ readonly stage: Container;
847
+ /**
848
+ * The underlying HTMLCanvasElement when running in DOM mode (`Canvas.init`).
849
+ * Undefined for `Canvas.initWithStage` (headless / test path). Layers that
850
+ * overlay DOM content above the canvas — `DevInfoLayer`, tooltips, popovers —
851
+ * read this to find a parent element and to attach native DOM listeners.
852
+ */
853
+ readonly canvasElement?: HTMLCanvasElement;
854
+ }
855
+
856
+ /**
857
+ * `WorldLayer` — abstract base for layers that live in **world coordinate space**.
858
+ *
859
+ * Architecture: see `architecture-proposal.md` §2.1.
860
+ *
861
+ * - Camera-affected: pans / zooms with the camera.
862
+ * - Owns a root pixi `Container` (RenderGroup) attached to `surfaces.world`.
863
+ * - `hitTest(worldX, worldY)` — input is in world coordinates.
864
+ *
865
+ * Subclasses call `this.createGraphics(label?)` or `this.createContainer(label?)`
866
+ * to add pixi display objects, and override `onMount(ctx)` to wire up renderers.
867
+ * For stacked draw-order (e.g. edges below nodes), use separate Layer instances.
868
+ *
869
+ * The type-distinct `hitTest` signature (vs. `ScreenLayer`'s) is what stops
870
+ * consumers passing screen coords to a world layer or vice versa.
871
+ */
872
+
873
+ interface WorldLayerHit {
874
+ /** Whatever the subclass chooses to return — a node id, a sub-region, etc. */
875
+ readonly id: string;
876
+ readonly subId?: string;
877
+ readonly kind?: string;
878
+ }
879
+ declare abstract class WorldLayer<TOptions = unknown, TState extends object = object, TEvents extends EventMap = EventMap, TDirtyBucket extends string = string, THit extends WorldLayerHit = WorldLayerHit> extends Layer<TOptions, TState, TEvents, TDirtyBucket> {
880
+ /** Backing field — assigned in `mount`, cleared in `unmount`. */
881
+ protected _container?: Container;
882
+ /**
883
+ * Root pixi `Container` (RenderGroup) for this layer. Available from
884
+ * `onMount(ctx)` for the layer's lifetime. Throws before mount / after unmount.
885
+ *
886
+ * Pass to `ShapesRenderer` as the `container` option when wiring up a renderer
887
+ * inside `onMount`. Subclass-only — not part of the external layer API.
888
+ */
889
+ protected get container(): Container;
890
+ constructor(opts: LayerOptions<TOptions>);
891
+ mount(ctx: CanvasContext): void;
892
+ /** Keep the pixi container in sync when `layer.visible` is toggled. */
893
+ protected onVisibleChange(value: boolean): void;
894
+ unmount(): void;
895
+ /**
896
+ * Create a pixi `Graphics` attached to this layer's root container. The
897
+ * sanctioned way for layer authors to obtain a `Graphics` for direct
898
+ * painting via `@invana/canvas/draw` primitives — keeps pixi internal
899
+ * (no `new Graphics()` in user code).
900
+ */
901
+ createGraphics(label?: string): Graphics;
902
+ /**
903
+ * Create a plain pixi `Container` attached to this layer's root container.
904
+ * Useful as a parent for mounted display objects (e.g. text sprites).
905
+ */
906
+ createContainer(label?: string): Container;
907
+ /**
908
+ * Update this layer's z-order relative to its peers. Keeps the iteration
909
+ * field (`this.zIndex`, used by `LayerRegistry.byZOrder()`) and the pixi
910
+ * container's `zIndex` in sync, and flips `surfaces.world` into sorted mode
911
+ * so the change renders.
912
+ */
913
+ setZIndex(z: number): void;
914
+ /**
915
+ * Return the world-space AABB of everything currently rendered on this layer.
916
+ * Delegates to Pixi's `getLocalBounds()` — a one-shot scene-graph traversal.
917
+ * Suitable for "fit to content" calls; do not call every frame.
918
+ */
919
+ getBounds(): {
920
+ x: number;
921
+ y: number;
922
+ width: number;
923
+ height: number;
924
+ };
925
+ /**
926
+ * Hit-test in world coordinates. Returns the topmost hit or `null`.
927
+ * Concrete layers implement this against their own data + spatial index.
928
+ *
929
+ * The `Canvas`-level hit-test orchestration (top-down by z-order, stop on
930
+ * first hit, screen-layers-before-world per proposal Q6) calls this.
931
+ */
932
+ abstract hitTest(worldX: number, worldY: number): THit | null;
933
+ }
934
+
935
+ /**
936
+ * `ScreenLayer` — abstract base for layers that live in **screen / viewport coordinate space**.
937
+ *
938
+ * Architecture: see `architecture-proposal.md` §2.1.
939
+ *
940
+ * - Viewport-fixed: NOT camera-affected. Pans / zooms do not transform it.
941
+ * - Owns a root pixi `Container` attached directly to `ctx.stage`.
942
+ * Plain `Container` (not a RenderGroup) — screen-space content is typically
943
+ * lightweight HUD-style rendering that doesn't need its own GPU batch boundary.
944
+ * - `hitTest(screenX, screenY)` — input is in screen pixels.
945
+ *
946
+ * Examples: `MiniMapLayer`, `DevInfoLayer`, HUD, tool palettes.
947
+ *
948
+ * The type-distinct `hitTest` signature (vs. `WorldLayer`'s) is what stops
949
+ * consumers passing world coords to a screen layer or vice versa.
950
+ */
951
+
952
+ interface ScreenLayerHit {
953
+ readonly id: string;
954
+ readonly subId?: string;
955
+ readonly kind?: string;
956
+ }
957
+ declare abstract class ScreenLayer<TOptions = unknown, TState extends object = object, TEvents extends EventMap = EventMap, TDirtyBucket extends string = string, THit extends ScreenLayerHit = ScreenLayerHit> extends Layer<TOptions, TState, TEvents, TDirtyBucket> {
958
+ /** Backing field — assigned in `mount`, cleared in `unmount`. */
959
+ protected _container?: Container;
960
+ /**
961
+ * Root pixi `Container` for this screen-space layer. Available from
962
+ * `onMount(ctx)` for the layer's lifetime. Throws before mount / after unmount.
963
+ *
964
+ * Subclass-only — not part of the external layer API.
965
+ */
966
+ protected get container(): Container;
967
+ constructor(opts: LayerOptions<TOptions>);
968
+ mount(ctx: CanvasContext): void;
969
+ /** Keep the pixi container in sync when `layer.visible` is toggled. */
970
+ protected onVisibleChange(value: boolean): void;
971
+ unmount(): void;
972
+ /**
973
+ * Create a pixi `Graphics` attached to this layer's root container. The
974
+ * sanctioned way for layer authors to obtain a `Graphics` for direct
975
+ * painting via `@invana/canvas/draw` primitives.
976
+ */
977
+ createGraphics(label?: string): Graphics;
978
+ /**
979
+ * Create a plain pixi `Container` attached to this layer's root container.
980
+ * Useful as a parent for mounted display objects.
981
+ */
982
+ createContainer(label?: string): Container;
983
+ /**
984
+ * Update this layer's z-order relative to its peers. Keeps the iteration
985
+ * field (`this.zIndex`) and the pixi container's `zIndex` in sync, and
986
+ * flips `ctx.stage` into sorted mode so the change renders.
987
+ */
988
+ setZIndex(z: number): void;
989
+ /** Hit-test in screen / viewport coordinates. Top-most hit or `null`. */
990
+ abstract hitTest(screenX: number, screenY: number): THit | null;
991
+ }
992
+
993
+ /**
994
+ * `DevInfoLayer` — developer overlay that continuously displays:
995
+ * - Canvas display size
996
+ * - Camera position (x, y) and zoom scale
997
+ * - Visible world bounds
998
+ * - Pointer position (screen and world coords)
999
+ * - Frame rate (FPS)
1000
+ *
1001
+ * Implemented as a `ScreenLayer` whose visible artifact is a plain
1002
+ * absolutely-positioned HTML `<div>` layered above the canvas (so it never
1003
+ * interferes with pointer events on the scene). The pixi `container` from
1004
+ * `ScreenLayer` is unused — overlay rendering is pure DOM.
1005
+ *
1006
+ * Headless / offscreen mode: when `ctx.canvasElement` is undefined (i.e.
1007
+ * `Canvas.initWithStage`), the layer mounts cleanly but renders nothing.
1008
+ *
1009
+ * @example
1010
+ * ```ts
1011
+ * import { DevInfoLayer } from '@invana/canvas/toolkit';
1012
+ *
1013
+ * const devInfo = new DevInfoLayer({ corner: 'top-right' });
1014
+ * canvas.layers.add(devInfo);
1015
+ *
1016
+ * // Toggle at runtime
1017
+ * devInfo.setEnabled(false);
1018
+ * ```
1019
+ */
1020
+
1021
+ type DevInfoCorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
1022
+ interface DevInfoLayerOptions {
1023
+ /** Which corner to anchor the overlay. Default: 'bottom-left' */
1024
+ corner?: DevInfoCorner;
1025
+ /** Show the overlay. Can be toggled at runtime via setEnabled(). Default: true */
1026
+ enabled?: boolean;
1027
+ /** Font size in px. Default: 11 */
1028
+ fontSize?: number;
1029
+ /** Panel opacity 0–1. Default: 0.92 */
1030
+ opacity?: number;
1031
+ /** Overlay background CSS color. Default: 'rgba(10,10,10,0.82)' */
1032
+ backgroundColor?: string;
1033
+ /** Text color. Default: '#c8d3e0' */
1034
+ textColor?: string;
1035
+ /** Accent / header color. Default: '#4fc3f7' */
1036
+ accentColor?: string;
1037
+ }
1038
+ interface DevInfoLayerCtorOptions extends DevInfoLayerOptions {
1039
+ /** Layer id. Default: 'dev-info'. */
1040
+ id?: string;
1041
+ /** Pixi z-index inside the screen stage. Default: 9999 (top). */
1042
+ zIndex?: number;
1043
+ }
1044
+ interface DevInfoState {
1045
+ enabled: boolean;
1046
+ }
1047
+ declare class DevInfoLayer extends ScreenLayer<DevInfoLayerOptions, DevInfoState> {
1048
+ private _opts;
1049
+ private _overlay;
1050
+ private _pointerScreen;
1051
+ private _pointerWorld;
1052
+ private _onPointerMove;
1053
+ private _unsubs;
1054
+ private _rafId;
1055
+ private _fps;
1056
+ private _frameCount;
1057
+ private _lastFpsTimestamp;
1058
+ constructor(opts?: DevInfoLayerCtorOptions);
1059
+ protected createState(): DevInfoState;
1060
+ /** Overlay is DOM with `pointer-events:none` — never participates in hit-testing. */
1061
+ hitTest(_screenX: number, _screenY: number): ScreenLayerHit | null;
1062
+ protected onMount(): void;
1063
+ protected onUnmount(): void;
1064
+ /** Show or hide the overlay at runtime without removing the layer. */
1065
+ setEnabled(enabled: boolean): void;
1066
+ enable(): void;
1067
+ disable(): void;
1068
+ /** Update display options (corner, colors, font size, …) at runtime. */
1069
+ setOptions(partial: Partial<DevInfoLayerOptions>): void;
1070
+ private _mountOverlay;
1071
+ private _unmountOverlay;
1072
+ private _applyStyles;
1073
+ private _startFpsTicker;
1074
+ private _stopFpsTicker;
1075
+ private _update;
1076
+ }
1077
+
1078
+ /**
1079
+ * `BackgroundLayer` — solid colour or tiled pattern fill behind the world.
1080
+ *
1081
+ * Architecture: see `architecture-proposal.md` §2.1.
1082
+ *
1083
+ * Implemented as a `ScreenLayer` for two practical reasons:
1084
+ *
1085
+ * 1. The pattern needs to cover the *viewport*, not the world bounds. A
1086
+ * WorldLayer would require sizing an infinite rectangle.
1087
+ * 2. `TilingSprite` lets us mimic camera-following cheaply by adjusting
1088
+ * `tileScale` + `tilePosition` on each pan/zoom — no per-frame geometry
1089
+ * rebuild needed.
1090
+ *
1091
+ * When `followCamera` is `true` (default), the pattern shifts and scales with
1092
+ * the camera so the background feels like part of the world ("graph paper").
1093
+ * When `false`, the pattern is fixed to the screen.
1094
+ *
1095
+ * @example
1096
+ * ```ts
1097
+ * canvas.layers.add(new BackgroundLayer({
1098
+ * id: 'bg',
1099
+ * options: { type: 'pattern', patternType: 'dots', backgroundColor: 0x0f172a },
1100
+ * }));
1101
+ * ```
1102
+ */
1103
+
1104
+ /** Top-level background style. `'solid'` skips the pattern texture entirely. */
1105
+ type BackgroundType = 'solid' | 'pattern';
1106
+ /** Pattern texture kind. */
1107
+ type BackgroundPatternType = 'dots' | 'grid' | 'lines';
1108
+ /**
1109
+ * Mode selector for light/dark colour resolution. `'auto'` follows the host's
1110
+ * `prefers-color-scheme` media query; `'light'` / `'dark'` pin explicitly.
1111
+ */
1112
+ type BackgroundMode = 'auto' | 'light' | 'dark';
1113
+ /** The concrete kind currently being rendered after mode resolution. */
1114
+ type BackgroundKind = 'light' | 'dark';
1115
+ /**
1116
+ * A colour input. Pass a `number` / CSS string for a single colour, or a
1117
+ * `{ light, dark }` pair to swap based on the layer's `mode`.
1118
+ */
1119
+ type BackgroundColor = number | string | {
1120
+ light: number | string;
1121
+ dark: number | string;
1122
+ };
1123
+ /** Construction-time options for `BackgroundLayer`. */
1124
+ interface BackgroundLayerOptions {
1125
+ /** `'solid'` paints a flat fill; `'pattern'` overlays a tiled texture. Default `'solid'`. */
1126
+ type?: BackgroundType;
1127
+ /** Tile texture kind when `type === 'pattern'`. Default `'dots'`. */
1128
+ patternType?: BackgroundPatternType;
1129
+ /**
1130
+ * Pattern foreground colour (dot / line / grid colour). Accepts `0xRRGGBB`,
1131
+ * a CSS string, or a `{ light, dark }` pair resolved against `mode`.
1132
+ */
1133
+ color?: BackgroundColor;
1134
+ /** Solid-fill colour painted behind the pattern. Same accepted forms as `color`. */
1135
+ backgroundColor?: BackgroundColor;
1136
+ /** Dot radius / line thickness, in *texture pixels*. Default `1`. */
1137
+ size?: number;
1138
+ /** Tile cell spacing, in *texture pixels*. Default `12`. */
1139
+ spacing?: number;
1140
+ /** Pattern alpha 0–1. Default `0.6`. */
1141
+ alpha?: number;
1142
+ /**
1143
+ * `true` (default): pattern shifts + scales with the camera. `false`: pattern
1144
+ * stays fixed to the screen regardless of camera state.
1145
+ */
1146
+ followCamera?: boolean;
1147
+ /**
1148
+ * How `{ light, dark }` colour variants are resolved. `'auto'` (default)
1149
+ * follows `prefers-color-scheme`; `'light'` / `'dark'` pin explicitly. Has
1150
+ * no effect when both colours are plain scalars.
1151
+ */
1152
+ mode?: BackgroundMode;
1153
+ }
1154
+ interface BackgroundLayerState {
1155
+ readonly _placeholder?: never;
1156
+ }
1157
+ declare class BackgroundLayer extends ScreenLayer<BackgroundLayerOptions, BackgroundLayerState, Record<string, never>, never, ScreenLayerHit> {
1158
+ private opts;
1159
+ private tiling;
1160
+ private patternTexture;
1161
+ /** DPR baked into the current pattern texture — used to compensate `tileScale`. */
1162
+ private textureDpr;
1163
+ private resizeObserver;
1164
+ private offCameraPan;
1165
+ private offCameraZoom;
1166
+ private modeMediaQuery;
1167
+ private modeMediaListener;
1168
+ private camX;
1169
+ private camY;
1170
+ private camScale;
1171
+ constructor(opts: LayerOptions<BackgroundLayerOptions>);
1172
+ protected createState(): BackgroundLayerState;
1173
+ protected onMount(ctx: CanvasContext): void;
1174
+ protected onUnmount(): void;
1175
+ /**
1176
+ * Hit tests on the background always miss — clicks fall through to the
1177
+ * world layer beneath, which is what users expect for a bg.
1178
+ */
1179
+ hitTest(): ScreenLayerHit | null;
1180
+ /** Merge-update options + re-render. */
1181
+ setOptions(changes: Partial<BackgroundLayerOptions>): void;
1182
+ /** Snapshot of the resolved options. */
1183
+ getOptions(): Required<BackgroundLayerOptions>;
1184
+ /**
1185
+ * Set the colour-resolution mode. `'auto'` re-arms the system listener;
1186
+ * `'light'` / `'dark'` pin explicitly. No-op when mode is unchanged.
1187
+ */
1188
+ setMode(mode: BackgroundMode): void;
1189
+ /** Current mode setting. */
1190
+ getMode(): BackgroundMode;
1191
+ /** Concrete kind currently being rendered after mode resolution. */
1192
+ getResolvedKind(): BackgroundKind;
1193
+ private viewportSize;
1194
+ private render;
1195
+ private syncTileTransform;
1196
+ private createPatternTexture;
1197
+ private resolveColor;
1198
+ private hasVariantColor;
1199
+ private wireModeMediaQuery;
1200
+ private detachModeMediaQuery;
1201
+ }
1202
+
1203
+ /**
1204
+ * Mode selector. `'auto'` follows `prefers-color-scheme`; `'light'` / `'dark'`
1205
+ * pin explicitly.
1206
+ */
1207
+ type ThemedBackgroundMode = 'auto' | 'light' | 'dark';
1208
+ /** The concrete variant currently being rendered after mode resolution. */
1209
+ type ThemedBackgroundKind = 'light' | 'dark';
1210
+ /** A named look bundling both a light and dark variant. */
1211
+ interface ThemedBackgroundTheme {
1212
+ /** Stable identifier — referenced by `setTheme(id)` and `defaultTheme`. */
1213
+ id: string;
1214
+ /** Optional human-friendly label for UIs. */
1215
+ label?: string;
1216
+ /** Style applied when the resolved kind is `'light'`. */
1217
+ light: BackgroundLayerOptions;
1218
+ /** Style applied when the resolved kind is `'dark'`. */
1219
+ dark: BackgroundLayerOptions;
1220
+ }
1221
+ /** Construction-time options for `ThemedBackgroundLayer`. */
1222
+ interface ThemedBackgroundLayerOptions {
1223
+ /** Named themes. Must contain at least one entry. */
1224
+ themes: ThemedBackgroundTheme[];
1225
+ /** Id of the theme to start with. Defaults to `themes[0].id`. */
1226
+ defaultTheme?: string;
1227
+ /** Initial mode. Defaults to `'auto'`. */
1228
+ mode?: ThemedBackgroundMode;
1229
+ }
1230
+ /** Layer-event map fired by `ThemedBackgroundLayer.events`. */
1231
+ interface ThemedBackgroundLayerEvents {
1232
+ 'theme:switched': {
1233
+ theme: ThemedBackgroundTheme;
1234
+ resolvedKind: ThemedBackgroundKind;
1235
+ source: 'initial' | 'manual';
1236
+ };
1237
+ 'mode:updated': {
1238
+ mode: ThemedBackgroundMode;
1239
+ previousMode: ThemedBackgroundMode;
1240
+ resolvedKind: ThemedBackgroundKind;
1241
+ source: 'manual' | 'system';
1242
+ };
1243
+ [event: string]: unknown;
1244
+ }
1245
+ interface ThemedBackgroundLayerState {
1246
+ readonly _placeholder?: never;
1247
+ }
1248
+ declare class ThemedBackgroundLayer extends BackgroundLayer {
1249
+ private readonly themes;
1250
+ private activeId;
1251
+ private currentMode;
1252
+ private mediaQuery;
1253
+ private mediaListener;
1254
+ readonly events: EventEmitter<ThemedBackgroundLayerEvents> & BackgroundLayer['events'];
1255
+ constructor(opts: LayerOptions<ThemedBackgroundLayerOptions>);
1256
+ protected createState(): ThemedBackgroundLayerState;
1257
+ protected onMount(ctx: CanvasContext): void;
1258
+ protected onUnmount(): void;
1259
+ /**
1260
+ * Switch to a theme by id. Mode is preserved. Throws on unknown id.
1261
+ * Emits `'theme:switched'`.
1262
+ */
1263
+ setTheme(id: string): void;
1264
+ /**
1265
+ * Set the mode. `'auto'` re-arms the system listener; `'light'` / `'dark'`
1266
+ * pin explicitly. Emits `'mode:updated'` when the mode actually changes.
1267
+ */
1268
+ setMode(mode: ThemedBackgroundMode): void;
1269
+ /** Currently active theme. */
1270
+ getActiveTheme(): ThemedBackgroundTheme;
1271
+ /** Current mode setting. */
1272
+ getMode(): ThemedBackgroundMode;
1273
+ /** Concrete kind currently being rendered. */
1274
+ getResolvedKind(): ThemedBackgroundKind;
1275
+ /** Snapshot of the configured themes. */
1276
+ getThemes(): readonly ThemedBackgroundTheme[];
1277
+ private applyResolved;
1278
+ private emitThemeSwitched;
1279
+ private emitModeUpdated;
1280
+ private wireMediaQuery;
1281
+ private detachMediaQuery;
1282
+ }
1283
+
1284
+ /**
1285
+ * `LayersPanelLayer` — developer overlay that lists every layer currently
1286
+ * mounted on the canvas and exposes a checkbox per row to toggle that
1287
+ * layer's `visible` flag.
1288
+ *
1289
+ * Implemented as a `ScreenLayer` whose visible artifact is a plain
1290
+ * absolutely-positioned HTML `<div>` layered above the canvas — same pattern
1291
+ * as `DevInfoLayer`. Unlike `DevInfoLayer`, the overlay receives pointer
1292
+ * events (so the checkboxes are clickable); the layer itself still opts out
1293
+ * of engine hit-testing via `hittable: false`.
1294
+ *
1295
+ * The panel re-renders on `'layer:added'` / `'layer:removed'`. The panel's
1296
+ * own row is filtered out so the user can't hide it via itself.
1297
+ *
1298
+ * Headless / offscreen mode: when `ctx.canvasElement` is undefined (i.e.
1299
+ * `Canvas.initWithStage`), the layer mounts cleanly but renders nothing.
1300
+ *
1301
+ * @example
1302
+ * ```ts
1303
+ * import { LayersPanelLayer } from '@invana/canvas';
1304
+ *
1305
+ * const panel = new LayersPanelLayer({ corner: 'top-right' });
1306
+ * canvas.layers.add(panel);
1307
+ *
1308
+ * // Toggle the panel itself at runtime
1309
+ * panel.setEnabled(false);
1310
+ * ```
1311
+ */
1312
+
1313
+ type LayersPanelCorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
1314
+ interface LayersPanelLayerOptions {
1315
+ /** Which corner to anchor the overlay. Default: 'top-right' */
1316
+ corner?: LayersPanelCorner;
1317
+ /** Show the overlay. Toggle at runtime via setEnabled(). Default: true */
1318
+ enabled?: boolean;
1319
+ /** Font size in px. Default: 11 */
1320
+ fontSize?: number;
1321
+ /** Panel opacity 0–1. Default: 0.92 */
1322
+ opacity?: number;
1323
+ /** Overlay background CSS color. Default: 'rgba(10,10,10,0.82)' */
1324
+ backgroundColor?: string;
1325
+ /** Text color. Default: '#c8d3e0' */
1326
+ textColor?: string;
1327
+ /** Accent / header color. Default: '#4fc3f7' */
1328
+ accentColor?: string;
1329
+ /**
1330
+ * Layer ids to hide from the list. The panel's own id is always hidden
1331
+ * regardless of this option.
1332
+ */
1333
+ hideIds?: readonly string[];
1334
+ }
1335
+ interface LayersPanelLayerCtorOptions extends LayersPanelLayerOptions {
1336
+ /** Layer id. Default: 'layers-panel'. */
1337
+ id?: string;
1338
+ /** Pixi z-index inside the screen stage. Default: 9998 (just below DevInfoLayer). */
1339
+ zIndex?: number;
1340
+ }
1341
+ interface LayersPanelState {
1342
+ enabled: boolean;
1343
+ }
1344
+ declare class LayersPanelLayer extends ScreenLayer<LayersPanelLayerOptions, LayersPanelState> {
1345
+ private _opts;
1346
+ private _overlay;
1347
+ private _onChange;
1348
+ private _unsubs;
1349
+ constructor(opts?: LayersPanelLayerCtorOptions);
1350
+ protected createState(): LayersPanelState;
1351
+ /** Overlay is DOM — never participates in the engine's hit-testing. */
1352
+ hitTest(_screenX: number, _screenY: number): ScreenLayerHit | null;
1353
+ protected onMount(): void;
1354
+ protected onUnmount(): void;
1355
+ /** Show or hide the panel at runtime without removing the layer. */
1356
+ setEnabled(enabled: boolean): void;
1357
+ enable(): void;
1358
+ disable(): void;
1359
+ /** Update display options (corner, colors, font size, …) at runtime. */
1360
+ setOptions(partial: Partial<LayersPanelLayerOptions>): void;
1361
+ /**
1362
+ * Force a re-render of the panel. Call this if external code mutates
1363
+ * `layer.visible` on a registered layer and you want the checkboxes to
1364
+ * reflect the new state. (The engine does not emit an event for visibility
1365
+ * mutations.)
1366
+ */
1367
+ refresh(): void;
1368
+ private _mountOverlay;
1369
+ private _unmountOverlay;
1370
+ private _applyStyles;
1371
+ private _render;
1372
+ }
1373
+
1374
+ /**
1375
+ * `DragPanBehaviour` — pointer-drag panning via the pixi-viewport `drag` plugin.
1376
+ *
1377
+ * An optional `modifier` key restricts the gesture so you can reserve plain
1378
+ * drag for other behaviours (e.g. lasso, rubber-band select):
1379
+ *
1380
+ * - `'none'` (default) — any left-button drag pans.
1381
+ * - `'space'` — Space + drag (Figma / Sketch style).
1382
+ * - `'shift'` — Shift + drag.
1383
+ * - `'alt'` — Alt/Option + drag.
1384
+ *
1385
+ * A decelerate plugin is added alongside by default, giving momentum after
1386
+ * the pointer lifts. Disable with `decelerate: false`.
1387
+ *
1388
+ * The canvas cursor swaps to `dragCursor` (`'grabbing'` by default) the moment
1389
+ * a qualifying pointer is pressed — so it reads as "holding the canvas, ready
1390
+ * to drag" before any movement happens — and restores on release. The press is
1391
+ * matched against the configured `mouseButtons` and `modifier`; the `space`
1392
+ * modifier can't be read off a pointer event, so for that mode the swap falls
1393
+ * back to pixi-viewport's `drag-start` (fires once the gesture actually moves).
1394
+ * The idle cursor is left untouched, so this never fights the renderer's hover
1395
+ * cursor.
1396
+ */
1397
+
1398
+ type DragModifier = 'none' | 'space' | 'shift' | 'alt';
1399
+ interface DragPanBehaviourOptions extends BehaviourOptions {
1400
+ /** Which modifier key must be held during drag. Default `'none'`. */
1401
+ modifier?: DragModifier;
1402
+ /** Allowed mouse buttons. Default `'left'`. Forwarded to pixi-viewport. */
1403
+ mouseButtons?: 'all' | 'left' | 'right' | 'middle';
1404
+ /** Add momentum deceleration after pointer lift. Default `true`. */
1405
+ decelerate?: boolean;
1406
+ /**
1407
+ * Cursor applied to the canvas while the pan pointer is held. Set on
1408
+ * pointer-press (matching `mouseButtons` / `modifier`), restored to the
1409
+ * previous value on release. Default `'grabbing'`.
1410
+ */
1411
+ dragCursor?: string;
1412
+ }
1413
+ declare class DragPanBehaviour extends Behaviour {
1414
+ private readonly modifier;
1415
+ private readonly mouseButtons;
1416
+ private readonly withDecelerate;
1417
+ private readonly dragCursor;
1418
+ /** Canvas the cursor swap targets; `null` on headless / custom stages. */
1419
+ private canvasEl;
1420
+ /** Cursor saved when the pan pointer is pressed, restored on release. */
1421
+ private prevCursor;
1422
+ constructor(opts: DragPanBehaviourOptions);
1423
+ protected onRegister(ctx: CanvasContext): void;
1424
+ protected onEnable(): void;
1425
+ protected onDisable(): void;
1426
+ private readonly onPointerDown;
1427
+ /** Swap to the drag cursor, saving the prior value. No-op if already armed. */
1428
+ private readonly armCursor;
1429
+ /** Restore the saved cursor and detach the release listeners. */
1430
+ private readonly restoreCursor;
1431
+ /** Does this pointer button match the configured `mouseButtons`? */
1432
+ private buttonAllowed;
1433
+ /**
1434
+ * Is the configured modifier satisfied for this press? `shift` / `alt` read
1435
+ * off the event; `none` is always true; `space` returns `false` here (not
1436
+ * detectable on a pointer event) and is handled by the `drag-start` fallback.
1437
+ */
1438
+ private modifierHeld;
1439
+ }
1440
+
1441
+ /**
1442
+ * `DragShapeBehaviour` — pointer-drag move for individual shapes managed by
1443
+ * a `PrimitivesRenderer`. Layer-scoped: constructed with a specific renderer
1444
+ * reference; the same canvas can host multiple layers, each with its own
1445
+ * drag behaviour.
1446
+ *
1447
+ * Default `enabled: false` — register, then explicitly enable. Matches the
1448
+ * project rule that no behaviour auto-activates.
1449
+ *
1450
+ * What happens on drag:
1451
+ * 1. `shape:pointerdown` from the renderer → drag start. Records the
1452
+ * pointer's world position and the shape's current `(spec.x, spec.y)`.
1453
+ * 2. The viewport's pan plugin is paused so the camera doesn't pan while
1454
+ * you're moving a shape.
1455
+ * 3. Window-level `pointermove` updates the shape via
1456
+ * `renderer.updateShape(id, { x, y })` so the click point stays under
1457
+ * the cursor. Window events are used (rather than pixi container events)
1458
+ * so the drag continues smoothly even when the pointer slides off the
1459
+ * original shape or off the canvas momentarily.
1460
+ * 4. When `reRouteConnectors` is `true` (default), every connector is
1461
+ * re-routed after each move — useful when the moved shape is an
1462
+ * obstacle for an obstacle-aware router. Set `false` if you're moving
1463
+ * a node whose edges should re-route via a smarter graph-level signal
1464
+ * (or if you have thousands of edges and the cost matters).
1465
+ * 5. `pointerup` / `pointercancel` → drag end. Viewport pan resumes.
1466
+ *
1467
+ * The behaviour observes the renderer's public surface only: subscribes to
1468
+ * `shape:pointerdown`, calls `getShapePosition` / `updateShape` /
1469
+ * `reRouteAllConnectors`. No private access.
1470
+ */
1471
+
1472
+ interface DragShapeBehaviourOptions extends BehaviourOptions {
1473
+ /** The renderer whose shapes this behaviour can drag. */
1474
+ readonly renderer: PrimitivesRenderer;
1475
+ /**
1476
+ * Optional predicate to restrict which shape ids are draggable. Returning
1477
+ * `false` ignores the pointerdown. Default = every shape is draggable.
1478
+ */
1479
+ readonly filter?: (id: string) => boolean;
1480
+ /**
1481
+ * Re-route every connector after each move. Default `true` — needed for
1482
+ * obstacle-aware routers (`manhattan` etc.) so they recompute when
1483
+ * obstacles move. Set `false` to avoid the per-move re-route cost.
1484
+ */
1485
+ readonly reRouteConnectors?: boolean;
1486
+ /**
1487
+ * Optional cursor while dragging. Applied on drag start and cleared on
1488
+ * drag end. Default `'grabbing'`.
1489
+ */
1490
+ readonly dragCursor?: string;
1491
+ }
1492
+ declare class DragShapeBehaviour extends Behaviour {
1493
+ private readonly renderer;
1494
+ private readonly filter?;
1495
+ private readonly reRouteConnectors;
1496
+ private readonly dragCursor;
1497
+ private state;
1498
+ private offShapeDown?;
1499
+ private viewport?;
1500
+ private canvasEl;
1501
+ private prevCursor;
1502
+ constructor(opts: DragShapeBehaviourOptions);
1503
+ protected onRegister(ctx: CanvasContext): void;
1504
+ protected onDestroy(_ctx: CanvasContext): void;
1505
+ protected onDisable(): void;
1506
+ private startDrag;
1507
+ private endDrag;
1508
+ private readonly onWindowPointerMove;
1509
+ private readonly onWindowPointerUp;
1510
+ /** Convert a window-level `(clientX, clientY)` to canvas-relative screen coords. */
1511
+ private clientToScreen;
1512
+ }
1513
+
1514
+ /**
1515
+ * `WheelZoomBehaviour` — scroll-wheel zooming via the pixi-viewport `wheel` plugin.
1516
+ *
1517
+ * By default, any scroll wheel event zooms. Set `requireCtrl: true` to
1518
+ * restrict to Ctrl+scroll (frees plain scroll for page scrolling — good
1519
+ * for accessibility contexts where the canvas is inline on a scrollable page).
1520
+ *
1521
+ * `trackpadPinch: true` is enabled so two-finger trackpad pinches zoom
1522
+ * instead of scroll. Pair with `PinchZoomBehaviour` for touch devices.
1523
+ */
1524
+
1525
+ interface WheelZoomBehaviourOptions extends BehaviourOptions {
1526
+ /**
1527
+ * If `true`, only Ctrl+scroll triggers zoom; plain scroll falls through
1528
+ * to the browser. Good for inline canvas embeds. Default `false`.
1529
+ */
1530
+ requireCtrl?: boolean;
1531
+ /** Zoom speed per wheel tick, as a fraction. Default `0.1` (10%). */
1532
+ percent?: number;
1533
+ /**
1534
+ * Smooth-scroll frame count. `false` = instant snap. Default `false`.
1535
+ * Set to e.g. `8` for an ease-out feel.
1536
+ */
1537
+ smooth?: false | number;
1538
+ }
1539
+ declare class WheelZoomBehaviour extends Behaviour {
1540
+ private readonly requireCtrl;
1541
+ private readonly percent;
1542
+ private readonly smooth;
1543
+ constructor(opts: WheelZoomBehaviourOptions);
1544
+ protected onRegister(_ctx: CanvasContext): void;
1545
+ protected onEnable(): void;
1546
+ protected onDisable(): void;
1547
+ }
1548
+
1549
+ /**
1550
+ * `PinchZoomBehaviour` — two-finger pinch-to-zoom via the pixi-viewport `pinch` plugin.
1551
+ *
1552
+ * Designed for touch screens and trackpads. Works alongside
1553
+ * `WheelZoomBehaviour` (which handles trackpad pinch-as-scroll separately via
1554
+ * its `trackpadPinch` flag); this behaviour handles native touch pinch events.
1555
+ *
1556
+ * Set `noDrag: true` if you want pinch to only zoom, not also pan (useful
1557
+ * when you have a separate `DragPanBehaviour` and don't want conflicts).
1558
+ */
1559
+
1560
+ interface PinchZoomBehaviourOptions extends BehaviourOptions {
1561
+ /**
1562
+ * If `true`, suppress the implicit pan that accompanies a pinch gesture.
1563
+ * Default `false` — pinch both zooms and centres the viewport on the
1564
+ * midpoint between the two fingers.
1565
+ */
1566
+ noDrag?: boolean;
1567
+ /** Zoom speed multiplier. Default `0.1`. */
1568
+ percent?: number;
1569
+ }
1570
+ declare class PinchZoomBehaviour extends Behaviour {
1571
+ private readonly noDrag;
1572
+ private readonly percent;
1573
+ constructor(opts: PinchZoomBehaviourOptions);
1574
+ protected onRegister(_ctx: CanvasContext): void;
1575
+ protected onEnable(): void;
1576
+ protected onDisable(): void;
1577
+ }
1578
+
1579
+ /**
1580
+ * `KeyboardCameraInputBehaviour` — keyboard pan and zoom for accessibility.
1581
+ *
1582
+ * Default keymap (all configurable via `keymap` option):
1583
+ *
1584
+ * Pan up/down/left/right → Arrow keys
1585
+ * Zoom in → `+` / `=` / `NumpadAdd`
1586
+ * Zoom out → `-` / `NumpadSubtract`
1587
+ * Reset zoom to 1:1 → `0` / `Numpad0`
1588
+ *
1589
+ * Events attach to `document` so the canvas does not need to be
1590
+ * individually focused. Input fields, textareas, and selects are
1591
+ * excluded automatically — keyboard events whose `target` is an editable
1592
+ * element fall through unhandled.
1593
+ *
1594
+ * Arrow key direction follows the "scroll" metaphor: ArrowUp pans the
1595
+ * viewport so you see content *above* the current view.
1596
+ */
1597
+
1598
+ interface KeyboardCameraKeymap {
1599
+ panUp: string[];
1600
+ panDown: string[];
1601
+ panLeft: string[];
1602
+ panRight: string[];
1603
+ zoomIn: string[];
1604
+ zoomOut: string[];
1605
+ resetZoom: string[];
1606
+ }
1607
+ interface KeyboardCameraInputBehaviourOptions extends BehaviourOptions {
1608
+ /** Pan distance per key press in screen pixels. Default `40`. */
1609
+ panStep?: number;
1610
+ /**
1611
+ * Zoom multiplier per key press. `1.1` = 10% in/out per press.
1612
+ * Default `1.1`.
1613
+ */
1614
+ zoomFactor?: number;
1615
+ /** Override individual key groups. Merged with the defaults. */
1616
+ keymap?: Partial<KeyboardCameraKeymap>;
1617
+ }
1618
+ declare class KeyboardCameraInputBehaviour extends Behaviour {
1619
+ private readonly panStep;
1620
+ private readonly zoomFactor;
1621
+ private readonly keymap;
1622
+ private _handler?;
1623
+ constructor(opts: KeyboardCameraInputBehaviourOptions);
1624
+ protected onRegister(_ctx: CanvasContext): void;
1625
+ protected onEnable(): void;
1626
+ protected onDisable(): void;
1627
+ private _onKeyDown;
1628
+ private _match;
1629
+ }
1630
+
1631
+ /**
1632
+ * `ElementSizeLODBehaviour` — abstract base for zoom-driven "keep this
1633
+ * element at a fixed screen-pixel size" behaviours.
1634
+ *
1635
+ * Sits in the same family as `LabelResolutionLODBehaviour`: both react
1636
+ * to `camera:zoom`, both adapt how some kind of entity renders as the
1637
+ * camera scale changes. This base owns the shared plumbing — event
1638
+ * subscription, RAF coalescing of bursts, enable/disable lifecycle —
1639
+ * and leaves the *what to rescale* to concrete subclasses.
1640
+ *
1641
+ * ## Why a base + subclass split
1642
+ *
1643
+ * The "screen-constant size" need shows up across domains: graph nodes
1644
+ * and edges today; swimlane lane headers, annotation pins, ER table
1645
+ * decorations tomorrow. Putting the camera-zoom plumbing in canvas (which
1646
+ * already owns the camera and the behaviour base) and the per-element
1647
+ * rescaling in domain packages means:
1648
+ *
1649
+ * - Each domain ships its own subclass next to its data model. No need
1650
+ * to modify an upstream "knows about everything" class to add a new
1651
+ * element kind.
1652
+ * - The browser RAF callback batches every behaviour's scheduled
1653
+ * callback into the same frame, so registering multiple subclasses
1654
+ * has effectively the same per-frame cost as one monolith doing N
1655
+ * passes.
1656
+ *
1657
+ * ## Concrete subclass contract
1658
+ *
1659
+ * Override `onResolveTargets(ctx)` once at register to resolve layer
1660
+ * references. Override `apply(scale)` to walk those targets and write
1661
+ * the rescaled geometry through the renderer's fast paths
1662
+ * (`updateShape`, `setConnectorStroke`, etc.).
1663
+ *
1664
+ * `disable()` calls `apply(1)` — your apply function should be
1665
+ * idempotent at scale 1 (which is what "restore to world-unit sizing"
1666
+ * means).
1667
+ *
1668
+ * ## MapLibre note
1669
+ *
1670
+ * `MapLayer` writes the pixi-viewport transform directly to mirror
1671
+ * MapLibre's camera. It re-emits `camera:zoom` on the canvas event bus
1672
+ * after each move so subclasses of this behaviour react under MapLibre
1673
+ * gestures the same as under `WheelZoomBehaviour`. Without that bridge
1674
+ * these behaviours would silently no-op under MapLibre.
1675
+ */
1676
+
1677
+ /** A static value or a getter — getters are re-read on every `apply`. */
1678
+ type NumberOrGetter = number | (() => number);
1679
+ /** Coerce a {@link NumberOrGetter} to its current numeric value, or `undefined`. */
1680
+ declare function resolveNumberOrGetter(v: NumberOrGetter | undefined): number | undefined;
1681
+ interface ElementSizeLODBehaviourOptions extends BehaviourOptions {
1682
+ /**
1683
+ * Skip `apply` when the relative scale change since the last applied
1684
+ * frame is below this threshold (`|scale - lastScale| / lastScale`).
1685
+ * Set to `0` to disable the skip. Default `0.005` (0.5%) — sub-pixel
1686
+ * stroke / size deltas at typical screen DPIs, which the user can't
1687
+ * perceive but a wheel-zoom gesture fires 60×/sec of.
1688
+ */
1689
+ scaleEpsilon?: number;
1690
+ /**
1691
+ * When `> 0`, switch from per-frame RAF apply to a trailing-edge
1692
+ * debounce: skip work during a continuous gesture and run one final
1693
+ * `apply` after `settleMs` of zoom silence. Useful for expensive
1694
+ * passes (e.g. thousands of connector redraws) where mid-gesture
1695
+ * visual drift is preferable to a frame-rate collapse. Default `0`
1696
+ * (RAF mode).
1697
+ */
1698
+ settleMs?: number;
1699
+ }
1700
+ declare abstract class ElementSizeLODBehaviour extends Behaviour {
1701
+ private readonly subs;
1702
+ /**
1703
+ * Pending `requestAnimationFrame` handle. Non-null while a reflow is
1704
+ * scheduled but hasn't fired yet — collapses bursts of `camera:zoom`
1705
+ * events (the wheel-zoom gesture fires 100+/sec) into one `apply`
1706
+ * call per animation frame. Critical for keeping fps above 60 during
1707
+ * a continuous zoom over thousands of entities.
1708
+ */
1709
+ private rafHandle;
1710
+ /**
1711
+ * Settle timer (debounce) handle. Used instead of `rafHandle` when
1712
+ * `settleMs > 0`. Re-armed on every `camera:zoom`; firing triggers a
1713
+ * single `apply` at the latest scale.
1714
+ */
1715
+ private settleTimer;
1716
+ /**
1717
+ * Scale at the last `apply` call. Drives the `scaleEpsilon` skip:
1718
+ * the next scheduled apply bails if the current scale is within
1719
+ * epsilon of this value. `null` means "no prior apply, never skip".
1720
+ */
1721
+ private lastAppliedScale;
1722
+ private readonly scaleEpsilon;
1723
+ private readonly settleMs;
1724
+ constructor(opts: ElementSizeLODBehaviourOptions);
1725
+ protected onRegister(ctx: CanvasContext): void;
1726
+ protected onDestroy(): void;
1727
+ protected onEnable(): void;
1728
+ protected onDisable(): void;
1729
+ /**
1730
+ * Force an immediate reflow at the current camera scale. Useful after
1731
+ * tuning a config knob (e.g. moving a GUI slider that a `NumberOrGetter`
1732
+ * reads from) — push the new sizes without waiting for the next zoom.
1733
+ *
1734
+ * Bypasses the epsilon skip and the settle debounce — explicit calls
1735
+ * are always treated as "apply now."
1736
+ */
1737
+ reflow(): void;
1738
+ /**
1739
+ * Called once on register. Resolve layer references from `ctx.layers`
1740
+ * and stash them on `this` for later `apply` calls. Throw a descriptive
1741
+ * error if a required layer isn't present — the canvas guarantees
1742
+ * `ctx.layers` is fully populated before behaviours register.
1743
+ */
1744
+ protected abstract onResolveTargets(ctx: CanvasContext): void;
1745
+ /** Optional teardown hook — drop layer refs / caches. Default no-op. */
1746
+ protected onReleaseTargets(): void;
1747
+ /**
1748
+ * Apply rescaling at the given camera scale. Called by `onEnable`,
1749
+ * each `camera:zoom` (RAF coalesced), and by `onDisable` with
1750
+ * `scale = 1` to restore world-unit sizing.
1751
+ *
1752
+ * Implementations should be idempotent — calling twice with the same
1753
+ * scale is a no-op visually.
1754
+ */
1755
+ protected abstract apply(scale: number): void;
1756
+ /**
1757
+ * Route a `camera:zoom` to either the RAF path (default) or the
1758
+ * trailing-edge debounce path (`settleMs > 0`). Both eventually call
1759
+ * {@link tryApply}, which honours the epsilon skip.
1760
+ */
1761
+ private scheduleReflow;
1762
+ /**
1763
+ * Apply at the current camera scale if (a) still enabled, (b) the
1764
+ * scale has moved by more than `scaleEpsilon` since the last apply.
1765
+ * Updates {@link lastAppliedScale} only on a real apply, so cumulative
1766
+ * sub-epsilon drift is eventually caught.
1767
+ */
1768
+ private tryApply;
1769
+ private applyAndRemember;
1770
+ private cancelScheduledReflow;
1771
+ }
1772
+
1773
+ /**
1774
+ * `Layout` — function from data to positions.
1775
+ *
1776
+ * Architecture: see `architecture-proposal.md` §2.3.
1777
+ *
1778
+ * Per the proposal:
1779
+ * - A Layout does NOT register with the canvas.
1780
+ * - It does NOT render.
1781
+ * - It does NOT subscribe to input.
1782
+ * - You instantiate it and call it against a layer.
1783
+ *
1784
+ * const layout = new D3ForceLayout({ charge: -300 });
1785
+ * layout.events.on('end', () => canvas.camera.fitContent(...));
1786
+ * await layout.apply(graphLayer);
1787
+ *
1788
+ * Continuous-running cases (e.g. always-relax force simulation) are handled
1789
+ * by a thin wrapper Behaviour that calls `apply()` on a tick — keeps the
1790
+ * Layout API clean while supporting the rare continuous case.
1791
+ *
1792
+ * Whether two layouts conflict is a domain concern (don't apply two layouts
1793
+ * to the same data) — not enforced here.
1794
+ *
1795
+ * ## Lifecycle events
1796
+ *
1797
+ * Every layout owns a typed `events` emitter and fires three lifecycle
1798
+ * events around `apply()`:
1799
+ *
1800
+ * - `start` — emitted once, synchronously, after the layout has set up
1801
+ * its internal state and just before it begins producing positions.
1802
+ * - `tick` — emitted whenever the layout writes a fresh batch of positions.
1803
+ * One-shot layouts (e.g. ELK) fire it once. Iterative layouts (force
1804
+ * sims) fire it on every iteration. High-frequency; subscribe sparingly.
1805
+ * - `end` — emitted once when the run terminates. `reason` distinguishes
1806
+ * a natural settle from an external `stop()` call.
1807
+ *
1808
+ * Subscribe to these events to drive camera fits, progress UI, etc. —
1809
+ * instead of listening to per-tick `data:changed` on the layer, which
1810
+ * conflates "structure changed" with "positions updated".
1811
+ */
1812
+
1813
+ /**
1814
+ * Why the run ended.
1815
+ *
1816
+ * - `completed` — the layout settled / finished on its own.
1817
+ * - `stopped` — `stop()` (or a second `apply()`) cancelled it.
1818
+ */
1819
+ type LayoutEndReason = 'completed' | 'stopped';
1820
+ /**
1821
+ * Lifecycle events fired by every `Layout`.
1822
+ *
1823
+ * Subclass-specific telemetry (e.g. d3-force's `alpha`) belongs on a
1824
+ * subclass-specific event map, not here.
1825
+ */
1826
+ type LayoutEvents = {
1827
+ start: Record<string, never>;
1828
+ tick: Record<string, never>;
1829
+ end: {
1830
+ reason: LayoutEndReason;
1831
+ };
1832
+ };
1833
+ declare abstract class Layout<TLayer extends Layer<any, any, any, any> = Layer<any, any, any, any>> {
1834
+ /**
1835
+ * Lifecycle event bus. See class docs for the event vocabulary.
1836
+ * Subclasses with richer telemetry can declare their own typed
1837
+ * emitter on top (`override readonly events = new EventEmitter<MyEvents>()`).
1838
+ */
1839
+ readonly events: EventEmitter<LayoutEvents>;
1840
+ /**
1841
+ * Run the layout against `layer`. Resolves when the run terminates
1842
+ * (either a natural settle or an external `stop()`).
1843
+ *
1844
+ * Calling `apply()` again on the same instance must cancel any in-flight
1845
+ * run first.
1846
+ */
1847
+ abstract apply(layer: TLayer): Promise<void>;
1848
+ }
1849
+
1850
+ /**
1851
+ * `Canvas` — the engine root.
1852
+ *
1853
+ * Architecture: see `architecture-proposal.md` (whole document) and
1854
+ * `decorations-plan.md` §11.9 (RenderGroups + Ticker integration).
1855
+ *
1856
+ * **What it owns**
1857
+ * - The pixi `Application` (created by `init`) and its `Ticker`.
1858
+ * - The `CanvasEventBus` (typed canvas-wide events + tap channel).
1859
+ * - The `SurfaceManager` (world + screen RenderGroups).
1860
+ * - The `Camera` (pan/zoom/projection).
1861
+ * - The `LayerRegistry` and `BehaviourRegistry`.
1862
+ * - The `CanvasContext` object handed to every Layer / Behaviour.
1863
+ *
1864
+ * **What it does per tick** (single RAF, delegated to pixi `Ticker`):
1865
+ * 1. Walk layers in z-order.
1866
+ * 2. Skip invisible layers.
1867
+ * 3. If a layer has pending dirty work, call `layer.flush()`.
1868
+ * 4. Pixi auto-renders the stage at end of tick.
1869
+ *
1870
+ * Animation runner (Tweens) and per-renderer animation ticks land in later
1871
+ * steps; this Canvas implementation has the hook points but doesn't yet
1872
+ * orchestrate them.
1873
+ *
1874
+ * **Two init paths**
1875
+ * - `init(opts)` — the production path. Creates a pixi `Application`,
1876
+ * mounts its canvas into the DOM container, hooks the ticker.
1877
+ * - `initWithStage(stage, sw, sh)` — the test / headless path. Skips pixi
1878
+ * `Application` entirely. Caller provides a `Container` (which can be
1879
+ * a freshly-constructed pixi `Container()`) and viewport dimensions.
1880
+ * Useful for unit tests that want to exercise the layer pipeline without
1881
+ * standing up a renderer.
1882
+ */
1883
+
1884
+ interface CanvasOptions {
1885
+ /**
1886
+ * Stable identifier for this Canvas instance. Used as the source id on
1887
+ * envelopes published by the bus's own `emit()`. Default: `'canvas'`.
1888
+ * Override when running multiple Canvas instances in one document.
1889
+ */
1890
+ id?: string;
1891
+ /** DOM element pixi mounts its `<canvas>` into. Required by `init()`. */
1892
+ container?: HTMLElement;
1893
+ /** Preferred backend. Default `'webgpu'`. Pixi falls back via its own logic. */
1894
+ preference?: 'webgpu' | 'webgl' | 'canvas';
1895
+ /** Viewport width in CSS pixels. Default = `container.clientWidth`. */
1896
+ width?: number;
1897
+ /** Viewport height in CSS pixels. Default = `container.clientHeight`. */
1898
+ height?: number;
1899
+ /** Device pixel ratio. Default `window.devicePixelRatio`. */
1900
+ resolution?: number;
1901
+ /** GPU MSAA. Default `true`. Auto-disabled on the Canvas backend. */
1902
+ antialias?: boolean;
1903
+ /** `true` → opaque scene, `backgroundAlpha = 1` (skips per-frame blend). */
1904
+ opaque?: boolean;
1905
+ /** Background colour. Default `0` (black, but only visible when `opaque: true`). */
1906
+ backgroundColor?: number;
1907
+ /** GPU power preference. Default `'high-performance'`. */
1908
+ powerPreference?: 'high-performance' | 'low-power';
1909
+ /** Suppress pixi's "PixiJS X.X.X" startup log. Default `true`. */
1910
+ hello?: boolean;
1911
+ /**
1912
+ * Automatically resize the renderer and camera when the container element
1913
+ * changes size. Covers both window resize and programmatic expand/collapse.
1914
+ * Uses `ResizeObserver` internally. Default `false`.
1915
+ */
1916
+ autoResize?: boolean;
1917
+ /**
1918
+ * Suppress the browser's native right-click context menu on the canvas
1919
+ * element. Diagram apps typically want to show their own menu UI via the
1920
+ * `shape:contextmenu` / `connector:contextmenu` events. Default `true`.
1921
+ *
1922
+ * Set to `false` if the app wants the OS context menu (e.g. for
1923
+ * accessibility / dev tooling on right-click).
1924
+ */
1925
+ suppressBrowserContextMenu?: boolean;
1926
+ }
1927
+ declare class Canvas {
1928
+ readonly id: string;
1929
+ readonly options: CanvasOptions;
1930
+ /**
1931
+ * Public surface — populated by `init()` / `initWithStage()`. Accessing
1932
+ * before init throws (definite-assignment via `!`). Use `isInitialised`
1933
+ * to guard if needed.
1934
+ */
1935
+ readonly events: CanvasEventBus;
1936
+ /**
1937
+ * The world container — a `pixi-viewport` `Viewport` instance attached to
1938
+ * `app.stage`. Camera-transformed; `WorldLayer`s mount their roots here.
1939
+ * Typed as `Container` so consumers don't depend on `pixi-viewport`; reach
1940
+ * for the `Viewport`-specific API via `camera.viewport`.
1941
+ */
1942
+ world: Container;
1943
+ /**
1944
+ * The pixi `Application.stage` (or, for `initWithStage`, the caller-
1945
+ * provided stage). `ScreenLayer`s mount their roots directly here, as
1946
+ * siblings of `world` — `world` is added first (bottom), each ScreenLayer
1947
+ * after (above). No "screen" wrapper container.
1948
+ */
1949
+ stage: Container;
1950
+ camera: Camera;
1951
+ layers: LayerRegistry;
1952
+ behaviours: BehaviourRegistry;
1953
+ context: CanvasContext;
1954
+ private app?;
1955
+ private _isInitialised;
1956
+ private _onRendererResize?;
1957
+ constructor(opts?: CanvasOptions);
1958
+ get isInitialised(): boolean;
1959
+ /** Pixi `Application`, available after `init()` (not `initWithStage`). */
1960
+ get application(): Application | undefined;
1961
+ /**
1962
+ * Production init: create a pixi `Application`, mount its canvas into the
1963
+ * supplied DOM container, wire the ticker, and emit
1964
+ * `'renderer:initialised'` on the bus.
1965
+ *
1966
+ * The selected backend (and capabilities) flows through the bus event so
1967
+ * consumers see which renderer pixi resolved.
1968
+ */
1969
+ init(opts: CanvasOptions): Promise<void>;
1970
+ /**
1971
+ * Headless / test init. Caller provides a pre-built stage `Container`
1972
+ * and viewport dimensions; we skip pixi's `Application` setup entirely.
1973
+ *
1974
+ * Use case: unit tests of the layer / behaviour / dirty / state pipeline
1975
+ * that don't need an actual GPU renderer.
1976
+ */
1977
+ initWithStage(stage: Container, screenWidth: number, screenHeight: number): void;
1978
+ /**
1979
+ * Run one tick manually with a fixed delta. Useful in tests; in production
1980
+ * pixi's ticker calls `tick` automatically.
1981
+ */
1982
+ tickOnce(deltaMs?: number): void;
1983
+ /** Pixi ticker callback. Bound via `add(this.tick, this)`. */
1984
+ private tick;
1985
+ /**
1986
+ * Tear down everything: ticker callback, registries (which unmount their
1987
+ * Layers / destroy their Behaviours and any ScreenLayer roots they own),
1988
+ * the world subtree, bus subscriptions, pixi Application. Idempotent.
1989
+ */
1990
+ destroy(): void;
1991
+ private _wireScene;
1992
+ private _detectBackend;
1993
+ private _capabilities;
1994
+ }
1995
+
1996
+ /**
1997
+ * `loadIconFont` — inject an icon-font stylesheet at runtime, then await
1998
+ * font readiness so the very first paint rasterises against the real
1999
+ * webfont (not a fallback with the wrong metrics).
2000
+ *
2001
+ * The mechanic:
2002
+ *
2003
+ * 1. Attaching `<link rel="stylesheet" href=…>` kicks off:
2004
+ * stylesheet download → CSS parse → `@font-face` registration → WOFF
2005
+ * fetch.
2006
+ * 2. The browser's `FontFaceSet` only knows about the family **after**
2007
+ * the `@font-face` declaration is parsed — calling
2008
+ * `document.fonts.load(…)` before that returns an empty result.
2009
+ * 3. So we wait for the link's `load` event first, then ask the
2010
+ * `FontFaceSet` to actually load the face.
2011
+ *
2012
+ * Vendor-agnostic: takes any stylesheet URL and any font-family name. The
2013
+ * canvas library does not know about Font Awesome / Material Symbols /
2014
+ * Phosphor / Heroicons / etc. — consumers point this at whichever icon
2015
+ * font (or regular webfont) they want.
2016
+ *
2017
+ * @example
2018
+ * ```ts
2019
+ * await loadIconFont(
2020
+ * 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css',
2021
+ * 'Font Awesome 6 Free',
2022
+ * );
2023
+ * // now safe to render `{ kind: 'glyph', char: '', fontFamily: 'Font Awesome 6 Free', fontWeight: 900 }`.
2024
+ * ```
2025
+ *
2026
+ * Idempotent: subsequent calls with the same `stylesheetUrl` reuse the
2027
+ * existing `<link>` element. Safe to call from N stories that all use the
2028
+ * same icon font.
2029
+ *
2030
+ * SSR-safe: a no-op when `document` is undefined.
2031
+ */
2032
+ declare function loadIconFont(stylesheetUrl: string, fontFamilyToProbe?: string, fontWeightToProbe?: number | string, fontStyleToProbe?: 'normal' | 'italic'): Promise<void>;
2033
+
2034
+ export { BackgroundLayer, type BackgroundLayerOptions, type BackgroundPatternType, type BackgroundType, Behaviour, type BehaviourOptions, BehaviourRegistry, type BehaviourRegistryOptions, Camera, Canvas, type CanvasContext, CanvasEventBus, type CanvasOptions, type ColumnArray, type ColumnSchema, ColumnStore, type ColumnStoreOptions, type ColumnType, type ColumnValue, type CreateLayerStoreOptions, type DevInfoCorner, DevInfoLayer, type DevInfoLayerCtorOptions, type DevInfoLayerOptions, DirtyBatcher, type DirtySnapshot, type DragModifier, DragPanBehaviour, type DragPanBehaviourOptions, DragShapeBehaviour, type DragShapeBehaviourOptions, ElementSizeLODBehaviour, type ElementSizeLODBehaviourOptions, EventEmitter, EventMap, EventSource, type IBehaviour, type ILayer, KeyboardCameraInputBehaviour, type KeyboardCameraInputBehaviourOptions, type KeyboardCameraKeymap, Layer, type LayerOptions, LayerRegistry, type LayerRegistryOptions, type LayersPanelCorner, LayersPanelLayer, type LayersPanelLayerCtorOptions, type LayersPanelLayerOptions, Layout, type LayoutEndReason, type LayoutEvents, type NumberOrGetter, PinchZoomBehaviour, type PinchZoomBehaviourOptions, PrimitivesRenderer, type RowOf, ScreenLayer, type ScreenLayerHit, SourceEmitter, type Store, type ThemedBackgroundKind, ThemedBackgroundLayer, type ThemedBackgroundLayerEvents, type ThemedBackgroundLayerOptions, type ThemedBackgroundMode, type ThemedBackgroundTheme, WheelZoomBehaviour, type WheelZoomBehaviourOptions, WorldLayer, type WorldLayerHit, assertSerialisableInDev, createLayerStore, findSerialisationViolations, loadIconFont, resolveNumberOrGetter };