@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.
- package/dist/index-D0Z3YfJv.d.ts +5075 -0
- package/dist/index.d.ts +2034 -0
- package/dist/index.js +10697 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives/index.d.ts +3 -0
- package/dist/primitives/index.js +7972 -0
- package/dist/primitives/index.js.map +1 -0
- package/package.json +68 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|