@weasel-js/labkit 0.1.0
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/README.md +72 -0
- package/dist/_dts/fitViewToBounds-rWrBI4a6.d.ts +870 -0
- package/dist/_dts/index-C6Yze7sQ.d.ts +93 -0
- package/dist/_dts/types-Si4Fw-1F.d.ts +48 -0
- package/dist/_dts/useExperimentState-eSvpwV_P.d.ts +102 -0
- package/dist/canvas/index.d.ts +34 -0
- package/dist/canvas/index.js +4 -0
- package/dist/canvas/index.js.map +1 -0
- package/dist/chunk-2QNYYL3V.js +86 -0
- package/dist/chunk-2QNYYL3V.js.map +1 -0
- package/dist/chunk-54IQ2DX7.js +222 -0
- package/dist/chunk-54IQ2DX7.js.map +1 -0
- package/dist/chunk-6FO7G5BB.js +6095 -0
- package/dist/chunk-6FO7G5BB.js.map +1 -0
- package/dist/chunk-7BKDG73Z.js +54 -0
- package/dist/chunk-7BKDG73Z.js.map +1 -0
- package/dist/chunk-B5ZUYRKN.js +23330 -0
- package/dist/chunk-B5ZUYRKN.js.map +1 -0
- package/dist/chunk-HXZHVU4G.js +126 -0
- package/dist/chunk-HXZHVU4G.js.map +1 -0
- package/dist/chunk-PWC7AQZM.js +8 -0
- package/dist/chunk-PWC7AQZM.js.map +1 -0
- package/dist/chunk-SD3HDXR7.js +161 -0
- package/dist/chunk-SD3HDXR7.js.map +1 -0
- package/dist/chunk-SSMHPMDV.js +350 -0
- package/dist/chunk-SSMHPMDV.js.map +1 -0
- package/dist/chunk-VLAHRJOC.js +201 -0
- package/dist/chunk-VLAHRJOC.js.map +1 -0
- package/dist/controls/index.d.ts +13 -0
- package/dist/controls/index.js +3 -0
- package/dist/controls/index.js.map +1 -0
- package/dist/dragdrop/index.d.ts +3 -0
- package/dist/dragdrop/index.js +3 -0
- package/dist/dragdrop/index.js.map +1 -0
- package/dist/fonts/oswald-latin-variable.woff2 +0 -0
- package/dist/index.d.ts +410 -0
- package/dist/index.js +1425 -0
- package/dist/index.js.map +1 -0
- package/dist/layers/index.d.ts +17 -0
- package/dist/layers/index.js +3 -0
- package/dist/layers/index.js.map +1 -0
- package/dist/passthrough/weasel-canvas.d.ts +258 -0
- package/dist/passthrough/weasel-canvas.js +3 -0
- package/dist/passthrough/weasel-canvas.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +2826 -0
- package/dist/passthrough/weasel-ui.js +4 -0
- package/dist/passthrough/weasel-ui.js.map +1 -0
- package/dist/primitives/index.d.ts +57 -0
- package/dist/primitives/index.js +4 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/state/index.d.ts +33 -0
- package/dist/state/index.js +34 -0
- package/dist/state/index.js.map +1 -0
- package/dist/styles.css +1224 -0
- package/dist/theme-interstellar.css +25 -0
- package/dist/theme-light.css +13 -0
- package/dist/ui/layers/index.d.ts +42 -0
- package/dist/ui/layers/index.js +5 -0
- package/dist/ui/layers/index.js.map +1 -0
- package/dist/undo/index.d.ts +30 -0
- package/dist/undo/index.js +3 -0
- package/dist/undo/index.js.map +1 -0
- package/package.json +99 -0
- package/src/canvas/AGENTS.md +72 -0
- package/src/canvas/CanvasStack.less +22 -0
- package/src/canvas/CanvasStack.stories.tsx +63 -0
- package/src/canvas/CanvasStack.test.tsx +82 -0
- package/src/canvas/CanvasStack.tsx +108 -0
- package/src/canvas/CanvasStackContext.ts +8 -0
- package/src/canvas/canvasCoords.test.ts +30 -0
- package/src/canvas/canvasCoords.ts +15 -0
- package/src/canvas/index.ts +6 -0
- package/src/canvas/useLayerScheduler.ts +72 -0
- package/src/canvas/usePanZoom.ts +98 -0
- package/src/controls/ControlPanel.less +61 -0
- package/src/controls/ControlPanel.stories.tsx +77 -0
- package/src/controls/ControlPanel.test.tsx +149 -0
- package/src/controls/ControlPanel.tsx +249 -0
- package/src/controls/index.ts +12 -0
- package/src/controls/types.ts +61 -0
- package/src/dragdrop/DragDropRuntime.tsx +135 -0
- package/src/dragdrop/DragGhost.less +17 -0
- package/src/dragdrop/DragGhost.tsx +24 -0
- package/src/dragdrop/Palette.less +34 -0
- package/src/dragdrop/Palette.tsx +27 -0
- package/src/dragdrop/dragDrop.test.tsx +145 -0
- package/src/dragdrop/index.ts +1 -0
- package/src/fonts/oswald-latin-variable.woff2 +0 -0
- package/src/index.ts +70 -0
- package/src/instrument/SineWave.smoke.test.tsx +94 -0
- package/src/instrument/capabilityDetector.test.ts +64 -0
- package/src/instrument/capabilityDetector.ts +17 -0
- package/src/instrument/defineInstrument.test.ts +27 -0
- package/src/instrument/defineInstrument.ts +5 -0
- package/src/instrument/index.ts +19 -0
- package/src/instrument/types.ts +65 -0
- package/src/instrument/validateConfigSchema.test.ts +125 -0
- package/src/instrument/validateConfigSchema.ts +65 -0
- package/src/lab/Lab.less +3 -0
- package/src/lab/Lab.stories.tsx +51 -0
- package/src/lab/Lab.test.tsx +126 -0
- package/src/lab/Lab.tsx +196 -0
- package/src/lab/LabContext.test.ts +11 -0
- package/src/lab/LabContext.ts +28 -0
- package/src/lab/LabShell.less +45 -0
- package/src/lab/LabShell.stories.tsx +39 -0
- package/src/lab/LabShell.test.tsx +59 -0
- package/src/lab/LabShell.tsx +33 -0
- package/src/lab/WorkspaceGrid.less +8 -0
- package/src/lab/WorkspaceGrid.stories.tsx +61 -0
- package/src/lab/WorkspaceGrid.test.tsx +40 -0
- package/src/lab/WorkspaceGrid.tsx +20 -0
- package/src/lab/gridDims.test.ts +35 -0
- package/src/lab/gridDims.ts +11 -0
- package/src/lab/index.ts +10 -0
- package/src/layers/AGENTS.md +65 -0
- package/src/layers/LayerList.less +53 -0
- package/src/layers/LayerList.test.tsx +49 -0
- package/src/layers/LayerList.tsx +105 -0
- package/src/layers/index.ts +3 -0
- package/src/passthrough/weasel-canvas.test.ts +30 -0
- package/src/passthrough/weasel-canvas.ts +23 -0
- package/src/passthrough/weasel-ui.test.ts +79 -0
- package/src/passthrough/weasel-ui.ts +150 -0
- package/src/primitives/FpsMeter.less +14 -0
- package/src/primitives/FpsMeter.stories.tsx +14 -0
- package/src/primitives/FpsMeter.test.tsx +38 -0
- package/src/primitives/FpsMeter.tsx +36 -0
- package/src/primitives/ScaleIndicator.less +25 -0
- package/src/primitives/ScaleIndicator.stories.tsx +16 -0
- package/src/primitives/ScaleIndicator.test.tsx +46 -0
- package/src/primitives/ScaleIndicator.tsx +48 -0
- package/src/primitives/Sidebar.less +57 -0
- package/src/primitives/Sidebar.stories.tsx +38 -0
- package/src/primitives/Sidebar.test.tsx +74 -0
- package/src/primitives/Sidebar.tsx +32 -0
- package/src/primitives/StatusBar.less +21 -0
- package/src/primitives/StatusBar.stories.tsx +24 -0
- package/src/primitives/StatusBar.test.tsx +25 -0
- package/src/primitives/StatusBar.tsx +18 -0
- package/src/primitives/Toolbar.less +43 -0
- package/src/primitives/Toolbar.stories.tsx +36 -0
- package/src/primitives/Toolbar.test.tsx +65 -0
- package/src/primitives/Toolbar.tsx +44 -0
- package/src/primitives/fpsAverage.test.ts +21 -0
- package/src/primitives/fpsAverage.ts +5 -0
- package/src/primitives/index.ts +9 -0
- package/src/state/SingletonExperiment.test.tsx +101 -0
- package/src/state/SingletonExperiment.tsx +52 -0
- package/src/state/adapters.test.ts +94 -0
- package/src/state/adapters.ts +102 -0
- package/src/state/context.tsx +42 -0
- package/src/state/helpers.test.ts +89 -0
- package/src/state/helpers.ts +61 -0
- package/src/state/index.ts +40 -0
- package/src/state/store.test.ts +243 -0
- package/src/state/store.ts +225 -0
- package/src/state/types.ts +53 -0
- package/src/state/useExperimentState.test.tsx +102 -0
- package/src/state/useExperimentState.ts +28 -0
- package/src/styles.less +20 -0
- package/src/test-setup.ts +17 -0
- package/src/theme/Interstellar.stories.less +92 -0
- package/src/theme/Interstellar.stories.tsx +243 -0
- package/src/theme/base.less +122 -0
- package/src/theme/interstellar.less +34 -0
- package/src/theme/light.less +14 -0
- package/src/theme/tokens.less +85 -0
- package/src/ui/format.ts +19 -0
- package/src/ui/layers/LayerStack.less +143 -0
- package/src/ui/layers/LayerStack.stories.tsx +45 -0
- package/src/ui/layers/LayerStack.test.tsx +170 -0
- package/src/ui/layers/LayerStack.tsx +215 -0
- package/src/ui/layers/index.ts +1 -0
- package/src/ui/properties/CheckboxRow.stories.tsx +27 -0
- package/src/ui/properties/ColorRow.stories.tsx +106 -0
- package/src/ui/properties/CurveField.less +48 -0
- package/src/ui/properties/CurveField.stories.tsx +20 -0
- package/src/ui/properties/CurveField.test.tsx +127 -0
- package/src/ui/properties/CurveField.tsx +181 -0
- package/src/ui/properties/EffectCard.tsx +312 -0
- package/src/ui/properties/Gallery.stories.tsx +178 -0
- package/src/ui/properties/NumberRow.stories.tsx +65 -0
- package/src/ui/properties/PropertyGroup.less +50 -0
- package/src/ui/properties/PropertyGroup.stories.tsx +34 -0
- package/src/ui/properties/PropertyGroup.test.tsx +33 -0
- package/src/ui/properties/PropertyGroup.tsx +44 -0
- package/src/ui/properties/PropertyList.stories.tsx +83 -0
- package/src/ui/properties/PropertyPanel.less +592 -0
- package/src/ui/properties/PropertyPanel.stories.tsx +191 -0
- package/src/ui/properties/PropertyPanel.test.tsx +193 -0
- package/src/ui/properties/PropertyPanel.tsx +441 -0
- package/src/ui/properties/PropertyRow.stories.tsx +53 -0
- package/src/ui/properties/SelectRow.stories.tsx +58 -0
- package/src/ui/properties/SliderRow.stories.tsx +74 -0
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +396 -0
- package/src/ui/properties/TextRow.stories.tsx +62 -0
- package/src/ui/properties/ToggleRow.stories.tsx +57 -0
- package/src/ui/properties/index.ts +39 -0
- package/src/ui/properties/storyLayouts.tsx +36 -0
- package/src/undo/eventBus.test.ts +49 -0
- package/src/undo/eventBus.ts +32 -0
- package/src/undo/index.ts +5 -0
- package/src/undo/undoStack.test.ts +70 -0
- package/src/undo/undoStack.ts +38 -0
- package/src/workspace/DefaultSidebar.tsx +26 -0
- package/src/workspace/DefaultStatusBar.tsx +15 -0
- package/src/workspace/DefaultToolbar.tsx +88 -0
- package/src/workspace/Workspace.less +58 -0
- package/src/workspace/Workspace.stories.tsx +78 -0
- package/src/workspace/Workspace.test.tsx +186 -0
- package/src/workspace/Workspace.tsx +255 -0
- package/src/workspace/WorkspaceChrome.tsx +150 -0
- package/src/workspace/index.ts +24 -0
- package/src/workspace/slotTypes.ts +44 -0
- package/src/workspace/workspaceOps.test.ts +142 -0
- package/src/workspace/workspaceOps.ts +78 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An invertible mutation. Applied via an adapter; produces an inverse op
|
|
3
|
+
* that, when applied to the same adapter, undoes the original.
|
|
4
|
+
*
|
|
5
|
+
* Adapters are intentionally typed loosely here so different op types can
|
|
6
|
+
* require different adapter capabilities. Each op is responsible for
|
|
7
|
+
* narrowing the adapter via the methods it calls.
|
|
8
|
+
*
|
|
9
|
+
* Lives here rather than in `@weasel-js/core` because an invertible,
|
|
10
|
+
* replayable mutation is a history concept: this package is what pushes ops
|
|
11
|
+
* onto a stack, inverts them, coalesces them, and rebuilds them from a
|
|
12
|
+
* serialized snapshot. Nothing about the shape is core-specific — it names no
|
|
13
|
+
* scene, node, or pose type. Core re-exports it from `core/ops/types` so its
|
|
14
|
+
* own call sites read unchanged.
|
|
15
|
+
*/
|
|
16
|
+
interface Op {
|
|
17
|
+
/** Apply the mutation. Return `false` (or `'noop'`) to signal that
|
|
18
|
+
* nothing changed — the history layer then skips pushing an undo
|
|
19
|
+
* entry for the batch when *every* op reports no-op. Returning
|
|
20
|
+
* `undefined`/`void` means "mutated" (the common case; existing ops
|
|
21
|
+
* don't need to change). */
|
|
22
|
+
apply(adapter: unknown): void | boolean | 'noop';
|
|
23
|
+
invert(): Op;
|
|
24
|
+
label?: string;
|
|
25
|
+
coalesceKey?: string;
|
|
26
|
+
/** Stable factory name for op-registry lookup. Kit-emitted ops always
|
|
27
|
+
* set this; consumer ops without a name can't round-trip through
|
|
28
|
+
* `History.serialize()` and are dropped from persisted snapshots. */
|
|
29
|
+
name?: string;
|
|
30
|
+
/** Serializable args (JSON / structured-clone-safe) that, paired with
|
|
31
|
+
* `name`, reconstruct the op via the registry's `rebuildOp`. */
|
|
32
|
+
args?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Journal {
|
|
36
|
+
readonly targetId: string | undefined;
|
|
37
|
+
readonly forkedAtEntryId: number;
|
|
38
|
+
applyBatch(ops: Op[], label: string): void;
|
|
39
|
+
undo(): void;
|
|
40
|
+
redo(): void;
|
|
41
|
+
canUndo(): boolean;
|
|
42
|
+
canRedo(): boolean;
|
|
43
|
+
entries(): {
|
|
44
|
+
undo: HistoryEntry[];
|
|
45
|
+
redo: HistoryEntry[];
|
|
46
|
+
};
|
|
47
|
+
commit(label: string): void;
|
|
48
|
+
cancel(): void;
|
|
49
|
+
suspend(): void;
|
|
50
|
+
isActive(): boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Wire form of a single op inside a serialized history. The pair
|
|
54
|
+
* `(name, args)` reconstructs a live `Op` via the op-factory registry. */
|
|
55
|
+
interface SerializedOp {
|
|
56
|
+
name: string;
|
|
57
|
+
args: unknown;
|
|
58
|
+
}
|
|
59
|
+
/** Wire form of one history entry. `forwardOps` / `baseOps` mirror the
|
|
60
|
+
* in-memory entry's fields (see `Entry` above) but only carry the
|
|
61
|
+
* serializable `(name, args)` projection of each op. */
|
|
62
|
+
interface SerializedHistoryEntry {
|
|
63
|
+
id: number;
|
|
64
|
+
label: string;
|
|
65
|
+
forwardOps: SerializedOp[];
|
|
66
|
+
baseOps: SerializedOp[];
|
|
67
|
+
}
|
|
68
|
+
/** Snapshot of an entire `History` instance. Designed to live alongside the
|
|
69
|
+
* scene snapshot in IDB so a reload restores the undo / redo stacks to
|
|
70
|
+
* exactly where they were. */
|
|
71
|
+
interface SerializedHistory {
|
|
72
|
+
version: 1;
|
|
73
|
+
undoStack: SerializedHistoryEntry[];
|
|
74
|
+
/** Stored newest-first, mirroring the in-memory stack so a deserialized
|
|
75
|
+
* history matches the original's `entries().redo` ordering. */
|
|
76
|
+
redoStack: SerializedHistoryEntry[];
|
|
77
|
+
nextEntryId: number;
|
|
78
|
+
/** Entries dropped because at least one of their ops lacked a `name`
|
|
79
|
+
* and therefore couldn't round-trip through the op-factory registry.
|
|
80
|
+
* Always present (zero when nothing was dropped) so callers can detect
|
|
81
|
+
* loss without parsing the debug log. */
|
|
82
|
+
droppedEntries: number;
|
|
83
|
+
}
|
|
84
|
+
/** Read-only view of a history entry exposed via `History.entries()`. */
|
|
85
|
+
interface HistoryEntry {
|
|
86
|
+
/** Stable monotonic id (preserved across coalesce merges). */
|
|
87
|
+
id: number;
|
|
88
|
+
/** Human-readable label (the `label` arg passed to `applyOps`). */
|
|
89
|
+
label: string;
|
|
90
|
+
/** Push/last-coalesce timestamp (ms). */
|
|
91
|
+
timestamp: number;
|
|
92
|
+
/** Set of node ids touched by any op in this entry. Populated from ops
|
|
93
|
+
* whose `args` carry an `id` field (transform, setPath, reparent) or a
|
|
94
|
+
* `node.id` field (insert, delete). Ops without a recognisable id field
|
|
95
|
+
* contribute nothing. May be `undefined` for deserialized entries
|
|
96
|
+
* restored from an older snapshot that predates this field. */
|
|
97
|
+
touchedIds?: ReadonlySet<string>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Pose composition for hierarchical scene graphs.
|
|
102
|
+
*
|
|
103
|
+
* As of the nesting change, `getPose(id)` on adapters returns the
|
|
104
|
+
* **local** pose — relative to the object's direct parent. Anything in the
|
|
105
|
+
* kit that needs to draw, hit-test, snap, or otherwise reason about world
|
|
106
|
+
* coordinates routes through `composeWorldPose`, which walks the parent
|
|
107
|
+
* chain and folds local poses together via a consumer-supplied `compose`.
|
|
108
|
+
*
|
|
109
|
+
* Pose shape is generic, so the compose strategy is too. For the common
|
|
110
|
+
* `{x, y, width, height}` axis-aligned rect, use `composeRectPose` —
|
|
111
|
+
* translation only, child dimensions preserved. Custom pose shapes (paths,
|
|
112
|
+
* matrix transforms) supply their own.
|
|
113
|
+
*
|
|
114
|
+
* The inverse — `rebaseLocalPose` — converts a world-space pose into a
|
|
115
|
+
* local pose under a target parent. Used when reparenting so the visual
|
|
116
|
+
* world position of a child is preserved across the parent change.
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* Axis-aligned rectangle pose with optional rotation. The canonical pose
|
|
120
|
+
* shape used by `composeRectPose` and the `unionBounds` helper. Rotation is
|
|
121
|
+
* in radians, pivoted on the unrotated AABB center; absent === 0. Kit-side
|
|
122
|
+
* code that consumes rotation already reads `pose.rotation ?? 0`
|
|
123
|
+
* (`SceneCanvas.defaultDrawOne`, `rotate/handle.ts`, `pathInWorld.ts`), so
|
|
124
|
+
* the slot exists on every default scene whether or not the consumer
|
|
125
|
+
* populates it. Defined here in core/transforms so the compose helpers
|
|
126
|
+
* don't depend on features.
|
|
127
|
+
*/
|
|
128
|
+
interface RectPose {
|
|
129
|
+
x: number;
|
|
130
|
+
y: number;
|
|
131
|
+
width: number;
|
|
132
|
+
height: number;
|
|
133
|
+
/** Rotation in radians around the unrotated AABB center. Absent === 0. */
|
|
134
|
+
rotation?: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Fill rule used by polygon path hit-testing and `ctx.fill()`. */
|
|
138
|
+
type PathFillRule = 'nonzero' | 'evenodd';
|
|
139
|
+
/**
|
|
140
|
+
* Polygon path with arbitrary contours and optional bezier segments.
|
|
141
|
+
* Multi-contour: each `M` opens a new subpath; `Z` closes the current one.
|
|
142
|
+
* Open subpaths (no `Z`) render as polylines and don't contribute to fills.
|
|
143
|
+
*/
|
|
144
|
+
interface PolygonPath {
|
|
145
|
+
kind: 'polygon';
|
|
146
|
+
commands: Uint8Array;
|
|
147
|
+
coords: Float32Array;
|
|
148
|
+
fillRule: PathFillRule;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Axis-aligned rectangle. Fast path for the (very common) case where the
|
|
152
|
+
* shape is just a rect — preserves O(1) bounds and hit-test, avoids the
|
|
153
|
+
* polygon kernel entirely. Promote to `PolygonPath` only when the shape
|
|
154
|
+
* grows beyond what a rect can express.
|
|
155
|
+
*/
|
|
156
|
+
interface RectPath {
|
|
157
|
+
kind: 'rect';
|
|
158
|
+
x: number;
|
|
159
|
+
y: number;
|
|
160
|
+
width: number;
|
|
161
|
+
height: number;
|
|
162
|
+
}
|
|
163
|
+
/** Canonical path shape — either an axis-aligned rect (fast path) or a polygon command stream. */
|
|
164
|
+
type Path = PolygonPath | RectPath;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* # SceneNode — the thing in the scene
|
|
168
|
+
*
|
|
169
|
+
* A `SceneNode` is the single canonical unit of a weasel scene. Everything
|
|
170
|
+
* the user sees on the canvas — a shape, a group, an annotation, a tile —
|
|
171
|
+
* is one of these. Containers and leaves are both nodes; the kit has no
|
|
172
|
+
* other concept of "scene element."
|
|
173
|
+
*
|
|
174
|
+
* ## Three orthogonal slots
|
|
175
|
+
*
|
|
176
|
+
* Every node carries three independent slots, plus its tree position:
|
|
177
|
+
*
|
|
178
|
+
* - **`data: TData`** — app-defined payload. The kit never inspects it.
|
|
179
|
+
* Color, label, kind, glyph, sample-rate, whatever the app's domain
|
|
180
|
+
* calls for. Mutated via `Scene.update(id, { data })`.
|
|
181
|
+
*
|
|
182
|
+
* - **`pose: TPose`** — local transform, relative to the node's direct
|
|
183
|
+
* parent (or world, for root nodes). Default `RectPose` is
|
|
184
|
+
* `{ x, y, width, height }`, but `TPose` is fully generic so apps can
|
|
185
|
+
* use rotated rects, paths, ellipses, etc. The kit composes world
|
|
186
|
+
* poses via `composeWorldPose` when rendering / hit-testing / snapping.
|
|
187
|
+
*
|
|
188
|
+
* - **`layer: TLayer`** — a string tag associating the node with a
|
|
189
|
+
* visual `RenderLayer` at draw time. Separate from `LayerRecord` (the
|
|
190
|
+
* per-layer visible/locked metadata held by the `Scene`).
|
|
191
|
+
*
|
|
192
|
+
* Tree position lives on the node itself: every node has a `parent` (or
|
|
193
|
+
* `null` for roots), and `ContainerNode` adds an ordered `children: NodeId[]`.
|
|
194
|
+
*
|
|
195
|
+
* ## Identity is by `NodeId`, not by reference
|
|
196
|
+
*
|
|
197
|
+
* Nodes are addressed by `NodeId` everywhere outside the scene tree:
|
|
198
|
+
* selection is `NodeId[]`, ops reference `NodeId`s, adapter methods accept
|
|
199
|
+
* `string` ids and look up the node on demand. The `Node` object itself is
|
|
200
|
+
* a snapshot of current state — don't hold references to it across scene
|
|
201
|
+
* updates; look up by id when you need the latest.
|
|
202
|
+
*
|
|
203
|
+
* Picking helpers (`pickBest`, `pickEvery`) likewise return ids, not nodes —
|
|
204
|
+
* they're hit-testing primitives that stay ignorant of node payload shape.
|
|
205
|
+
*
|
|
206
|
+
* ## Vocabulary
|
|
207
|
+
*
|
|
208
|
+
* The kit-internal name is `Node`; the public re-export is `SceneNode`
|
|
209
|
+
* (avoids collision with DOM `Node` at call sites). Adapter methods speak
|
|
210
|
+
* the same vocabulary: `getNode`, `getNodes`, `insertNode`, `removeNode`,
|
|
211
|
+
* `cloneNode`, `addNode`. Older code, demos, and comments may still say
|
|
212
|
+
* "object" or "item" — those are historical aliases for the same concept.
|
|
213
|
+
*/
|
|
214
|
+
/** Opaque branded id. Treat as opaque outside the kit. */
|
|
215
|
+
type NodeId = string & {
|
|
216
|
+
readonly __brand: 'NodeId';
|
|
217
|
+
};
|
|
218
|
+
interface NodeBase<TData, TLayer extends string, TPose> {
|
|
219
|
+
id: NodeId;
|
|
220
|
+
layer: TLayer;
|
|
221
|
+
pose: TPose;
|
|
222
|
+
data: TData;
|
|
223
|
+
parent: NodeId | null;
|
|
224
|
+
}
|
|
225
|
+
interface LeafNode<TData, TLayer extends string, TPose = RectPose> extends NodeBase<TData, TLayer, TPose> {
|
|
226
|
+
kind: 'leaf';
|
|
227
|
+
}
|
|
228
|
+
interface ContainerNode<TData, TLayer extends string, TPose = RectPose> extends NodeBase<TData, TLayer, TPose> {
|
|
229
|
+
kind: 'container';
|
|
230
|
+
children: NodeId[];
|
|
231
|
+
/** Optional clip-path source. Re-evaluated each render. Returning `null`
|
|
232
|
+
* means "no clip for this container right now"; an empty / zero-area path
|
|
233
|
+
* means "clip everything out" (children render nowhere). When set, the
|
|
234
|
+
* renderer rasterizes the returned path into the stencil buffer and
|
|
235
|
+
* paints descendants only where it covers. */
|
|
236
|
+
clipFromPose?: (pose: TPose) => Path | null;
|
|
237
|
+
}
|
|
238
|
+
type Node<TData, TLayer extends string, TPose = RectPose> = LeafNode<TData, TLayer, TPose> | ContainerNode<TData, TLayer, TPose>;
|
|
239
|
+
interface LayerRecordBase<TLayer extends string> {
|
|
240
|
+
id: TLayer;
|
|
241
|
+
visible: boolean;
|
|
242
|
+
locked: boolean;
|
|
243
|
+
}
|
|
244
|
+
interface SystemLayerRecord<TLayer extends string> extends LayerRecordBase<TLayer> {
|
|
245
|
+
kind: 'system';
|
|
246
|
+
}
|
|
247
|
+
interface UserLayerRecord<TLayer extends string> extends LayerRecordBase<TLayer> {
|
|
248
|
+
kind: 'user';
|
|
249
|
+
name: string;
|
|
250
|
+
}
|
|
251
|
+
type LayerRecord<TLayer extends string> = SystemLayerRecord<TLayer> | UserLayerRecord<TLayer>;
|
|
252
|
+
interface AddNodeSpec<TData, TLayer extends string, TPose = RectPose> {
|
|
253
|
+
kind: 'leaf' | 'container';
|
|
254
|
+
layer: TLayer;
|
|
255
|
+
pose: TPose;
|
|
256
|
+
data: TData;
|
|
257
|
+
parent?: NodeId | null;
|
|
258
|
+
index?: number;
|
|
259
|
+
/** Explicit id wins over the Scene's `generateId` and the kit default. */
|
|
260
|
+
id?: NodeId;
|
|
261
|
+
/** Only meaningful when `kind === 'container'`. Attach a clip-path function
|
|
262
|
+
* to the node; ignored for leaves. Mirrors `ContainerNode.clipFromPose`. */
|
|
263
|
+
clipFromPose?: (pose: TPose) => Path | null;
|
|
264
|
+
}
|
|
265
|
+
interface RegisteredOp<P> {
|
|
266
|
+
apply: (payload: P) => void;
|
|
267
|
+
revert: (payload: P) => void;
|
|
268
|
+
}
|
|
269
|
+
interface SystemLayerSpec<TLayer extends string> {
|
|
270
|
+
id: TLayer;
|
|
271
|
+
visible?: boolean;
|
|
272
|
+
locked?: boolean;
|
|
273
|
+
}
|
|
274
|
+
/** Argument to `Scene.addLayer`. Always produces a `UserLayerRecord`
|
|
275
|
+
* (`kind: 'user'`). */
|
|
276
|
+
interface AddLayerSpec<TLayer extends string> {
|
|
277
|
+
id: TLayer;
|
|
278
|
+
name: string;
|
|
279
|
+
/** Default `true`. */
|
|
280
|
+
visible?: boolean;
|
|
281
|
+
/** Default `false`. */
|
|
282
|
+
locked?: boolean;
|
|
283
|
+
/** Render-stack position. Default: top of stack (highest render index). */
|
|
284
|
+
index?: number;
|
|
285
|
+
}
|
|
286
|
+
/** JSON-serializable shape of a Scene's current state. Produced by
|
|
287
|
+
* `scene.toJSON()`; consumed by `sceneFromJSON()`. Function fields
|
|
288
|
+
* (e.g., `clipFromPose`) appear as string keys (`clipFromPoseKey`) and
|
|
289
|
+
* are resolved through `SceneRegistry` at load time. */
|
|
290
|
+
interface SerializedScene<TData, TLayer extends string, TPose> {
|
|
291
|
+
version: 1;
|
|
292
|
+
systemLayers: readonly SystemLayerSpec<TLayer>[];
|
|
293
|
+
nodes: readonly SerializedNode<TData, TLayer, TPose>[];
|
|
294
|
+
}
|
|
295
|
+
/** JSON-serializable shape of a single node. Mirrors `AddNodeSpec` but
|
|
296
|
+
* with function fields replaced by registry keys. */
|
|
297
|
+
interface SerializedNode<TData, TLayer extends string, TPose> {
|
|
298
|
+
id: string;
|
|
299
|
+
kind: 'leaf' | 'container';
|
|
300
|
+
layer: TLayer;
|
|
301
|
+
pose: TPose;
|
|
302
|
+
data: TData;
|
|
303
|
+
/** Parent id; omitted for roots. */
|
|
304
|
+
parent?: string;
|
|
305
|
+
/** Registry key for the container's clip-path factory.
|
|
306
|
+
* Containers only; omitted when the container has no clip. */
|
|
307
|
+
clipFromPoseKey?: string;
|
|
308
|
+
}
|
|
309
|
+
interface Scene<TData, TLayer extends string, TPose = RectPose> {
|
|
310
|
+
readonly nodes: ReadonlyMap<NodeId, Node<TData, TLayer, TPose>>;
|
|
311
|
+
readonly roots: readonly NodeId[];
|
|
312
|
+
readonly layers: readonly LayerRecord<TLayer>[];
|
|
313
|
+
get(id: NodeId): Node<TData, TLayer, TPose> | undefined;
|
|
314
|
+
childrenOf(id: NodeId): readonly NodeId[];
|
|
315
|
+
ancestorsOf(id: NodeId): readonly NodeId[];
|
|
316
|
+
renderOrder(): Iterable<NodeId>;
|
|
317
|
+
add(spec: AddNodeSpec<TData, TLayer, TPose>): NodeId;
|
|
318
|
+
/** Delete `id` **and its entire subtree** — every descendant is removed in
|
|
319
|
+
* the same operation. Recorded as one undoable step; `undo()` restores the
|
|
320
|
+
* whole subtree (root + descendants, child order intact). */
|
|
321
|
+
remove(id: NodeId): void;
|
|
322
|
+
update(id: NodeId, patch: {
|
|
323
|
+
data: TData;
|
|
324
|
+
}): void;
|
|
325
|
+
setPose(id: NodeId, pose: TPose): void;
|
|
326
|
+
/** Retag `id` to `layer`. On a **container this cascades**: every descendant
|
|
327
|
+
* is moved to the same layer, recorded as a **single** undo step.
|
|
328
|
+
*
|
|
329
|
+
* Invariants:
|
|
330
|
+
* - **Layer floor** — a child may not render below its parent, so retagging
|
|
331
|
+
* to a layer *below* the node's parent throws. Retagging to the parent's
|
|
332
|
+
* layer or any higher one is allowed; a node with no parent is
|
|
333
|
+
* unconstrained.
|
|
334
|
+
* - **No-op elision** — setting the layer a node already has does nothing
|
|
335
|
+
* and pushes **no** history entry. */
|
|
336
|
+
setLayer(id: NodeId, layer: TLayer): void;
|
|
337
|
+
/** Reparent `id` under `parent` (or to a root when `parent` is `null`) at
|
|
338
|
+
* `index` within the new sibling list, appending when `index` is omitted.
|
|
339
|
+
* Siblings are reindexed. Recorded as one undoable step.
|
|
340
|
+
*
|
|
341
|
+
* Rejected (throws) when:
|
|
342
|
+
* - `parent` exists but is a **leaf**, not a container;
|
|
343
|
+
* - the move would form a **cycle** — `parent` is `id` itself or one of
|
|
344
|
+
* `id`'s own descendants;
|
|
345
|
+
* - it would drop `id` **below its new parent's layer** (child may not
|
|
346
|
+
* render below its parent).
|
|
347
|
+
*
|
|
348
|
+
* `move(id, null)` — detaching to a root — is always allowed regardless of
|
|
349
|
+
* layer, since a root has no parent to render beneath. */
|
|
350
|
+
move(id: NodeId, parent: NodeId | null, index?: number): void;
|
|
351
|
+
/** Shift `id` to `index` within its **current** parent's child list. Unlike
|
|
352
|
+
* {@link move}, the parent never changes — only sibling order. */
|
|
353
|
+
reorder(id: NodeId, index: number): void;
|
|
354
|
+
setLayerVisible(layer: TLayer, visible: boolean): void;
|
|
355
|
+
setLayerLocked(layer: TLayer, locked: boolean): void;
|
|
356
|
+
addLayer(spec: AddLayerSpec<TLayer>): void;
|
|
357
|
+
removeLayer(layer: TLayer): void;
|
|
358
|
+
renameLayer(layer: TLayer, name: string): void;
|
|
359
|
+
moveLayer(layer: TLayer, index: number): void;
|
|
360
|
+
registerOp<P>(kind: string, handler: RegisteredOp<P>): void;
|
|
361
|
+
recordOp<P>(op: {
|
|
362
|
+
kind: string;
|
|
363
|
+
payload: P;
|
|
364
|
+
}): void;
|
|
365
|
+
/** Install (or clear) the active-journal accessor after scene construction.
|
|
366
|
+
* Useful when the journal source (typically a mode machine) is built
|
|
367
|
+
* with `scene.history` as a dependency — a chicken-and-egg situation
|
|
368
|
+
* where the accessor can't be passed in via `UseSceneOptions`.
|
|
369
|
+
*
|
|
370
|
+
* Pass `null` to detach. Overrides any `getActiveJournal` set in
|
|
371
|
+
* `UseSceneOptions`. */
|
|
372
|
+
setActiveJournalAccessor(fn: (() => Journal | null) | null): void;
|
|
373
|
+
/** Apply a batch of ops with journal-aware routing.
|
|
374
|
+
*
|
|
375
|
+
* - **Without active journal** (or no `getActiveJournal` in options):
|
|
376
|
+
* the ops themselves are recorded as one undo entry on the scene's own
|
|
377
|
+
* history, rebound to `adapter` — undo replays each op's `invert()`
|
|
378
|
+
* against that same adapter. Consecutive `applyBatch` entries can
|
|
379
|
+
* coalesce via matching op `coalesceKey`s when the scene opts into
|
|
380
|
+
* `coalesceWindowMs`.
|
|
381
|
+
* - **With active journal**: routes ops to `journal.applyBatch(ops, label)`.
|
|
382
|
+
* The scene's history recording is suppressed for the duration so the
|
|
383
|
+
* journal's inner history — not the scene's undo stack — tracks the batch.
|
|
384
|
+
* Mutations still happen on `adapter` / scene state.
|
|
385
|
+
*
|
|
386
|
+
* `adapter` must be the same adapter the ops expect (typically a
|
|
387
|
+
* `SceneCanvasAdapter`). Pass `this` from `sceneToAdapter` or a compatible
|
|
388
|
+
* adapter. */
|
|
389
|
+
applyBatch(ops: Op[], label: string, adapter: unknown): void;
|
|
390
|
+
undo(): boolean;
|
|
391
|
+
redo(): boolean;
|
|
392
|
+
canUndo(): boolean;
|
|
393
|
+
canRedo(): boolean;
|
|
394
|
+
batch<T>(label: string, fn: () => T): T;
|
|
395
|
+
/** Read-only snapshot of every history entry currently reachable from
|
|
396
|
+
* the present state. Oldest applied first, then redoable entries in
|
|
397
|
+
* the order they'd be re-applied. Each entry id is stable. */
|
|
398
|
+
historyEntries(): readonly {
|
|
399
|
+
id: string;
|
|
400
|
+
label: string;
|
|
401
|
+
}[];
|
|
402
|
+
/** Index of the "current state". Equals the count of applied entries;
|
|
403
|
+
* `0` means "nothing applied" (initial). */
|
|
404
|
+
historyIndex(): number;
|
|
405
|
+
/** Jump to the given history index by calling undo/redo repeatedly.
|
|
406
|
+
* Clamps to [0, total]. Returns true if any movement occurred. */
|
|
407
|
+
jumpToHistoryIndex(index: number): boolean;
|
|
408
|
+
/** Snapshot the undo/redo history in a JSON-serializable form (the
|
|
409
|
+
* engine's `SerializedHistory`). Entries containing any nameless op are
|
|
410
|
+
* dropped (hand-rolled anonymous ops passed to `applyBatch`); kit and
|
|
411
|
+
* consumer-registered ops always carry names. Payload JSON-safety
|
|
412
|
+
* (e.g. typed arrays inside poses) is the caller's concern. Do not call
|
|
413
|
+
* mid-`batch` — the open batch's ops are not yet recorded. */
|
|
414
|
+
serializeHistory(): SerializedHistory;
|
|
415
|
+
/** Replace the undo/redo history from a `serializeHistory()` snapshot.
|
|
416
|
+
* Call on a scene whose node/layer state already matches the snapshot's
|
|
417
|
+
* head state (i.e. right after `loadState` from the paired scene
|
|
418
|
+
* snapshot); node state is NOT mutated. Ops re-registered via
|
|
419
|
+
* `registerOp` before this call round-trip; unknown kinds become no-op
|
|
420
|
+
* placeholders; external ops rebuild via the global op-factory registry
|
|
421
|
+
* and replay against the `setHistoryAdapter` accessor. Restored entries
|
|
422
|
+
* never coalesce with new ones. Notifies once. Do not call mid-`batch`:
|
|
423
|
+
* the stacks are replaced underneath the open batch, whose eventual
|
|
424
|
+
* flush would graft onto (and evict against) the restored stacks. */
|
|
425
|
+
restoreHistory(snapshot: SerializedHistory): void;
|
|
426
|
+
/** Install (or clear with `null`) the accessor for the adapter that
|
|
427
|
+
* RESTORED external ops (recorded via `applyBatch`, rebuilt from a
|
|
428
|
+
* `restoreHistory` snapshot) apply against on undo/redo. Resolved lazily
|
|
429
|
+
* at each apply, so wiring order relative to `restoreHistory` doesn't
|
|
430
|
+
* matter. Live `applyBatch` entries are unaffected (they bind their
|
|
431
|
+
* call-site adapter). If unset when a restored op applies, the op is a
|
|
432
|
+
* debug-warned no-op. */
|
|
433
|
+
setHistoryAdapter(fn: (() => unknown) | null): void;
|
|
434
|
+
/** Snapshot the current scene state to a JSON-serializable shape.
|
|
435
|
+
* History (undo/redo stacks) is NOT captured. Function fields like
|
|
436
|
+
* `ContainerNode.clipFromPose` are translated to string keys via the
|
|
437
|
+
* scene's registry; throws if any function field has no matching key. */
|
|
438
|
+
toJSON(): SerializedScene<TData, TLayer, TPose>;
|
|
439
|
+
/** Replace this scene's entire node + layer state in place from a snapshot
|
|
440
|
+
* produced by `toJSON()`. Unlike `sceneFromJSON`, the existing Scene
|
|
441
|
+
* instance is preserved — holders such as `<SceneCanvas>` keep their
|
|
442
|
+
* reference. History (undo/redo) is cleared, matching `sceneFromJSON`.
|
|
443
|
+
* Bumps `getVersion()` and notifies subscribers exactly once.
|
|
444
|
+
*
|
|
445
|
+
* Throws on an unsupported version or unknown registry/layer ids; on a
|
|
446
|
+
* malformed snapshot the scene is left empty or partially populated (callers should treat a
|
|
447
|
+
* `loadState` throw as fatal and reload). Snapshots from `toJSON()` are
|
|
448
|
+
* always well-formed. */
|
|
449
|
+
loadState(json: SerializedScene<TData, TLayer, TPose>): void;
|
|
450
|
+
subscribe(listener: () => void): () => void;
|
|
451
|
+
/** Monotonically increasing version. Snapshot for `useSyncExternalStore`. */
|
|
452
|
+
getVersion(): number;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Viewport state. `(view.x, view.y)` is the **world point currently
|
|
457
|
+
* rendered at the canvas top-left**; `view.scale.x` / `view.scale.y` is
|
|
458
|
+
* pixels per world unit on each axis (default `{ x: 1, y: 1 }`). So:
|
|
459
|
+
*
|
|
460
|
+
* screenX = (worldX - view.x) * view.scale.x
|
|
461
|
+
* screenY = (worldY - view.y) * view.scale.y
|
|
462
|
+
* worldX = screenX / view.scale.x + view.x
|
|
463
|
+
* worldY = screenY / view.scale.y + view.y
|
|
464
|
+
*
|
|
465
|
+
* `scale` is always a 2-vector. Input convenience types
|
|
466
|
+
* {@link ZoomFactor} and {@link ZoomBound} let callers pass a scalar
|
|
467
|
+
* when they want both axes treated the same.
|
|
468
|
+
*/
|
|
469
|
+
interface View {
|
|
470
|
+
x: number;
|
|
471
|
+
y: number;
|
|
472
|
+
scale: {
|
|
473
|
+
x: number;
|
|
474
|
+
y: number;
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 2D affine matrix utilities. Column-major 9-element Float32Array, matching
|
|
480
|
+
* `WebGL2RenderingContext.uniformMatrix3fv` byte order so we can pass the
|
|
481
|
+
* array directly without a transpose flag.
|
|
482
|
+
*
|
|
483
|
+
* Layout (column-major):
|
|
484
|
+
* [m00, m10, 0,
|
|
485
|
+
* m01, m11, 0,
|
|
486
|
+
* tx, ty, 1]
|
|
487
|
+
*
|
|
488
|
+
* `apply(m, x, y)` returns `[m * (x, y, 1)] = [m00*x + m01*y + tx,
|
|
489
|
+
* m10*x + m11*y + ty]`.
|
|
490
|
+
*/
|
|
491
|
+
type Mat3 = Float32Array;
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* registerTexture — accepts an image source, assigns an opaque id, stores in
|
|
495
|
+
* a module-level registry. Actual GL upload happens lazily at draw time in
|
|
496
|
+
* drawShader() via GLTextureCache.upload (which is idempotent).
|
|
497
|
+
*
|
|
498
|
+
* Lifecycle: textures live for the renderer's lifetime. No unregister in v1.
|
|
499
|
+
*
|
|
500
|
+
* Convention §9: this registry stores image data only — no per-renderer state.
|
|
501
|
+
* Each WeaselRenderer's GLTextureCache does its own dedup via has(id).
|
|
502
|
+
*/
|
|
503
|
+
interface TextureHandle {
|
|
504
|
+
readonly id: string;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* FillStyle and Stroke types — the unified shape for "what color or texture
|
|
509
|
+
* paints these pixels," modeled on SVG's paint-server concept.
|
|
510
|
+
*
|
|
511
|
+
* - `FillStyle` is a tagged union: solid color, pattern, or gradient. Used
|
|
512
|
+
* wherever a kit option previously took `fillStyle: string`.
|
|
513
|
+
* - `Stroke` pairs a `FillStyle` with structural stroke parameters (width, dash,
|
|
514
|
+
* line cap/join, alignment).
|
|
515
|
+
* - These types are consumed by the GL renderer's DrawCommand path fills
|
|
516
|
+
* and strokes.
|
|
517
|
+
*
|
|
518
|
+
* The 2D `applyPaint` / `applyStroke` / `renderFilledRegion` helpers that
|
|
519
|
+
* formerly lived alongside these types were deleted with the 2D backend in
|
|
520
|
+
* Step 10. `alignedStrokeRect` survives as a pure geometry helper used by
|
|
521
|
+
* path tessellation and the selection overlay.
|
|
522
|
+
*/
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Color/texture strategy for fills (and, via `Stroke.paint`, strokes).
|
|
526
|
+
*
|
|
527
|
+
* `fill` is optional and defaults to `'solid'` — `{ color: '#abc' }` is
|
|
528
|
+
* equivalent to `{ fill: 'solid', color: '#abc' }`. Pattern paints must set
|
|
529
|
+
* `fill: 'pattern'` explicitly.
|
|
530
|
+
*
|
|
531
|
+
* The `'pattern'` variant's payload is a `TextureHandle` (registered via
|
|
532
|
+
* `registerTexture()`). The kit-level factory `createTilePattern` (and the
|
|
533
|
+
* `patterns-builtin` catalog: `hatch`, `crosshatch`, `dots`, `chunks`)
|
|
534
|
+
* produces these handles by rendering a tile to an `OffscreenCanvas` and
|
|
535
|
+
* registering the resulting `ImageBitmap` as a GL texture.
|
|
536
|
+
*/
|
|
537
|
+
type FillStyle = {
|
|
538
|
+
fill?: 'solid';
|
|
539
|
+
color: string;
|
|
540
|
+
opacity?: number;
|
|
541
|
+
} | {
|
|
542
|
+
fill: 'pattern';
|
|
543
|
+
pattern: TextureHandle;
|
|
544
|
+
opacity?: number;
|
|
545
|
+
} | {
|
|
546
|
+
fill: 'linear-gradient';
|
|
547
|
+
from: {
|
|
548
|
+
x: number;
|
|
549
|
+
y: number;
|
|
550
|
+
};
|
|
551
|
+
to: {
|
|
552
|
+
x: number;
|
|
553
|
+
y: number;
|
|
554
|
+
};
|
|
555
|
+
stops: GradStop[];
|
|
556
|
+
opacity?: number;
|
|
557
|
+
} | {
|
|
558
|
+
fill: 'radial-gradient';
|
|
559
|
+
center: {
|
|
560
|
+
x: number;
|
|
561
|
+
y: number;
|
|
562
|
+
};
|
|
563
|
+
radius: number;
|
|
564
|
+
stops: GradStop[];
|
|
565
|
+
opacity?: number;
|
|
566
|
+
} | {
|
|
567
|
+
fill: 'conic-gradient';
|
|
568
|
+
center: {
|
|
569
|
+
x: number;
|
|
570
|
+
y: number;
|
|
571
|
+
};
|
|
572
|
+
angle: number;
|
|
573
|
+
stops: GradStop[];
|
|
574
|
+
opacity?: number;
|
|
575
|
+
};
|
|
576
|
+
/** A single color stop within a gradient. `offset` is in 0..1. */
|
|
577
|
+
interface GradStop {
|
|
578
|
+
offset: number;
|
|
579
|
+
color: string;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Where a stroke sits relative to the geometric edge it strokes.
|
|
583
|
+
*
|
|
584
|
+
* - `'center'` (default): canvas-native — half the stroke width sits inside
|
|
585
|
+
* the geometry, half outside.
|
|
586
|
+
* - `'inner'`: the entire stroke lies inside the geometry. The outer edge of
|
|
587
|
+
* the stroke coincides with the geometric edge.
|
|
588
|
+
* - `'outer'`: the entire stroke lies outside the geometry. The inner edge
|
|
589
|
+
* of the stroke coincides with the geometric edge.
|
|
590
|
+
*
|
|
591
|
+
* Mirrors the (proposed) SVG `stroke-alignment` property. Honoring `inner`
|
|
592
|
+
* or `outer` is the renderer's responsibility — for axis-aligned rects, the
|
|
593
|
+
* kit shifts coordinates by `width / 2`. For arbitrary paths, renderers
|
|
594
|
+
* typically use a stencil mask of the stroked path against the geometry.
|
|
595
|
+
*/
|
|
596
|
+
type StrokeAlign = 'center' | 'inner' | 'outer';
|
|
597
|
+
/** Stroke style: a FillStyle plus structural line parameters. */
|
|
598
|
+
interface Stroke {
|
|
599
|
+
paint: FillStyle;
|
|
600
|
+
width?: number;
|
|
601
|
+
/** Per `CanvasRenderingContext2D.setLineDash` — empty/omitted = solid. */
|
|
602
|
+
dash?: number[];
|
|
603
|
+
cap?: 'butt' | 'round' | 'square';
|
|
604
|
+
join?: 'miter' | 'round' | 'bevel';
|
|
605
|
+
/**
|
|
606
|
+
* Miter join fallback threshold. When the miter length exceeds
|
|
607
|
+
* `miterLimit * width / 2`, the join falls back to a bevel. Default 10
|
|
608
|
+
* (matching Canvas2D). SVG's default is 4; consumers that want SVG
|
|
609
|
+
* fidelity should set this explicitly when constructing strokes from
|
|
610
|
+
* SVG sources where the attribute was omitted.
|
|
611
|
+
*/
|
|
612
|
+
miterLimit?: number;
|
|
613
|
+
/** Where the stroke sits relative to the geometric edge. Default `'center'`. */
|
|
614
|
+
align?: StrokeAlign;
|
|
615
|
+
/**
|
|
616
|
+
* Per-anchor RGBA, flat (length = 4 × countPathAnchors(path)). Each
|
|
617
|
+
* value in 0..1. Arc-length interpolated across the tessellated ribbon
|
|
618
|
+
* between consecutive anchors. When set, `paint` is still required —
|
|
619
|
+
* its `opacity` (and color, as a placeholder) flow through the shader.
|
|
620
|
+
*/
|
|
621
|
+
vertexColors?: number[];
|
|
622
|
+
/**
|
|
623
|
+
* Per-anchor stroke width (length = `countPathAnchors(path)`). When set,
|
|
624
|
+
* the tessellator interpolates half-widths along each segment to produce
|
|
625
|
+
* a tapered ribbon. `width` is used as the fallback for any anchor whose
|
|
626
|
+
* entry is missing or non-finite. Pressure-driven pencil strokes use
|
|
627
|
+
* this; pair with `pressureToWidth` to derive widths from stylus input.
|
|
628
|
+
*
|
|
629
|
+
* Joins between adjacent segments whose widths differ by more than
|
|
630
|
+
* `varyingWidthJoinThreshold` (default 1.5×) are forced to bevel
|
|
631
|
+
* regardless of the `join` setting — miter math is unstable when widths
|
|
632
|
+
* vary across the corner; smooth round joins with mismatched widths
|
|
633
|
+
* are a future enhancement.
|
|
634
|
+
*/
|
|
635
|
+
vertexWidths?: number[];
|
|
636
|
+
/**
|
|
637
|
+
* Max width ratio (greater / lesser) at which a non-bevel join is
|
|
638
|
+
* preserved when `vertexWidths` causes adjacent segments to differ.
|
|
639
|
+
* Beyond this ratio the join falls back to bevel. Default 1.5. Ignored
|
|
640
|
+
* when `vertexWidths` is absent.
|
|
641
|
+
*/
|
|
642
|
+
varyingWidthJoinThreshold?: number;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Typography for `TextPose` and friends. Every field is optional; consumers
|
|
647
|
+
* pass `{}` or override the few they care about. Defaults live in
|
|
648
|
+
* `DEFAULT_TEXT_STYLE` and are applied at render/measure time, never written
|
|
649
|
+
* back to the pose.
|
|
650
|
+
*
|
|
651
|
+
* `fill` follows the kit-wide `FillStyle` model — solid color or pattern. The
|
|
652
|
+
* contenteditable edit overlay flattens non-solid fills to `'#000'` for CSS
|
|
653
|
+
* since the browser can't paint with a texture handle.
|
|
654
|
+
*/
|
|
655
|
+
|
|
656
|
+
/** User-facing text style. All fields optional; defaults applied at render time via `resolveTextStyle`. */
|
|
657
|
+
interface TextStyle {
|
|
658
|
+
/** Font size in world units. Default 16. */
|
|
659
|
+
fontSize?: number;
|
|
660
|
+
/** Default `'sans-serif'`. */
|
|
661
|
+
fontFamily?: string;
|
|
662
|
+
/** Default 400. */
|
|
663
|
+
fontWeight?: number | string;
|
|
664
|
+
/** Default `'normal'`. */
|
|
665
|
+
fontStyle?: 'normal' | 'italic';
|
|
666
|
+
/** Default `'left'`. */
|
|
667
|
+
align?: 'left' | 'center' | 'right';
|
|
668
|
+
/** Multiplier applied to `fontSize`. Default 1.2. */
|
|
669
|
+
lineHeight?: number;
|
|
670
|
+
/** Default `{ fill: 'solid', color: '#000' }`. */
|
|
671
|
+
fill?: FillStyle;
|
|
672
|
+
/**
|
|
673
|
+
* Caret color used by the edit overlay. Defaults to the text color when
|
|
674
|
+
* `fill` is solid; falls back to `#000` for non-solid paints.
|
|
675
|
+
*/
|
|
676
|
+
caretColor?: string;
|
|
677
|
+
/**
|
|
678
|
+
* Selection background color used by the edit overlay's `::selection`
|
|
679
|
+
* pseudo-element. Defaults to a 25%-opacity tint of `caretColor` via CSS
|
|
680
|
+
* `color-mix`. Pass `'none'` to fall back to the browser-native highlight.
|
|
681
|
+
*/
|
|
682
|
+
selectionBackground?: string;
|
|
683
|
+
/** Selection text color paired with `selectionBackground`. Default: inherits text color. */
|
|
684
|
+
selectionColor?: string;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Apply node-level `ResolvedTextStyle` defaults to each `StyledRun`,
|
|
689
|
+
* producing a fully-resolved run with every styling field set. Downstream
|
|
690
|
+
* layout and draw never re-resolve defaults — `ResolvedRun` is the
|
|
691
|
+
* canonical shape the renderer consumes.
|
|
692
|
+
*
|
|
693
|
+
* `bold`/`italic` toggles on a run are folded into `fontWeight`/`fontStyle`:
|
|
694
|
+
* `bold: true` → fontWeight 700, `italic: true` → fontStyle 'italic'.
|
|
695
|
+
* Explicit `fontFamily` / `fontSize` / `fill` on the run override the
|
|
696
|
+
* node-level value.
|
|
697
|
+
*/
|
|
698
|
+
|
|
699
|
+
interface ResolvedRun {
|
|
700
|
+
text: string;
|
|
701
|
+
fontFamily: string;
|
|
702
|
+
fontSize: number;
|
|
703
|
+
fontWeight: number;
|
|
704
|
+
fontStyle: 'normal' | 'italic';
|
|
705
|
+
fill: FillStyle;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Box vertical alignment for text draw commands. Given the command's box
|
|
710
|
+
* `height` and the laid-out text block's height, returns the Y offset to
|
|
711
|
+
* apply to every quad. `'top'` (or an undefined `align`, or a missing box
|
|
712
|
+
* `height`) is the legacy behavior: offset 0.
|
|
713
|
+
*/
|
|
714
|
+
type TextVerticalAlign = 'top' | 'center' | 'bottom';
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* registerProgram — public API for registering custom shader programs.
|
|
718
|
+
*
|
|
719
|
+
* Stores raw GLSL source strings in a module-level registry. GL compilation
|
|
720
|
+
* happens on each WeaselRenderer via WeaselRenderer.registerProgram(), which
|
|
721
|
+
* calls getProgramSource() and compiles the result. This keeps registerProgram
|
|
722
|
+
* GL-context-agnostic — identical pattern to registerFont storing ImageBitmap.
|
|
723
|
+
*
|
|
724
|
+
* Convention §9: module-level state = source strings only; compiled GL
|
|
725
|
+
* programs live on each renderer's programRegistry (Map<id, ShaderProgram>).
|
|
726
|
+
*
|
|
727
|
+
* Lifecycle: program sources live for the module lifetime. No unregister in v1.
|
|
728
|
+
*/
|
|
729
|
+
|
|
730
|
+
/** Opaque handle to a compiled custom shader program. */
|
|
731
|
+
interface ShaderProgramHandle {
|
|
732
|
+
readonly id: string;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Scalar and vector uniform types accepted by the custom shader uniform binder.
|
|
736
|
+
*
|
|
737
|
+
* | TS type | GL call |
|
|
738
|
+
* |-------------------------|--------------------------------------|
|
|
739
|
+
* | number | uniform1f |
|
|
740
|
+
* | [n, n] | uniform2fv |
|
|
741
|
+
* | [n, n, n] | uniform3fv |
|
|
742
|
+
* | [n, n, n, n] | uniform4fv |
|
|
743
|
+
* | Float32Array length 9 | uniformMatrix3fv (column-major) |
|
|
744
|
+
* | Float32Array length 16 | uniformMatrix4fv (column-major) |
|
|
745
|
+
* | TextureHandle | bind to next tex unit + uniform1i |
|
|
746
|
+
*/
|
|
747
|
+
type ShaderUniform = number | [number, number] | [number, number, number] | [number, number, number, number] | Float32Array | TextureHandle;
|
|
748
|
+
|
|
749
|
+
/** DrawCommand variants implemented through step 6. */
|
|
750
|
+
type DrawCommand = PathDrawCommand | GroupDrawCommand | TextDrawCommand | ImageDrawCommand | ShaderDrawCommand;
|
|
751
|
+
interface PathDrawCommand {
|
|
752
|
+
kind: 'path';
|
|
753
|
+
path: Path;
|
|
754
|
+
/** Any `FillStyle` variant: solid, pattern, or gradient (linear/radial/conic). */
|
|
755
|
+
fill?: FillStyle;
|
|
756
|
+
/** Stroke spec. Only solid `paint` supported through step 4. */
|
|
757
|
+
stroke?: Stroke;
|
|
758
|
+
/**
|
|
759
|
+
* Optional flat RGBA-per-path-anchor color array (length =
|
|
760
|
+
* `4 × countPathAnchors(path)`, floats in 0..1). The renderer
|
|
761
|
+
* arc-length-interpolates these per-anchor colors across the
|
|
762
|
+
* flattened/triangulated mesh between consecutive anchors using the
|
|
763
|
+
* mesh's `anchorA` / `anchorB` / `anchorT` parameterization.
|
|
764
|
+
*
|
|
765
|
+
* **`fill` must also be set when using `vertexColors`.** The renderer
|
|
766
|
+
* only enters the per-vertex shader path when the command has a fill
|
|
767
|
+
* (the fill provides the opacity uniform; the vertex colors override
|
|
768
|
+
* the fill's color). Pass any solid `fill` (e.g. `{ color: '#fff' }`)
|
|
769
|
+
* as the placeholder; the per-vertex colors win in the shader.
|
|
770
|
+
*/
|
|
771
|
+
vertexColors?: number[];
|
|
772
|
+
}
|
|
773
|
+
interface GroupDrawCommand {
|
|
774
|
+
kind: 'group';
|
|
775
|
+
transform?: Mat3;
|
|
776
|
+
alpha?: number;
|
|
777
|
+
/**
|
|
778
|
+
* Optional 4×5 color matrix (row-major, 20 numbers) — `out = M₄ₓ₄ * in + bias`.
|
|
779
|
+
* Accumulated multiplicatively down the group stack. Defaults to identity.
|
|
780
|
+
*/
|
|
781
|
+
colorMatrix?: number[];
|
|
782
|
+
/** Optional clip path. When set, the renderer rasterizes this path into
|
|
783
|
+
* the stencil buffer before drawing `children`; the children paint only
|
|
784
|
+
* where the clip covers. Nested groups with clips intersect — a child
|
|
785
|
+
* cannot escape an ancestor's clip. Max 7 nesting levels; the renderer
|
|
786
|
+
* throws if exceeded. */
|
|
787
|
+
clip?: Path;
|
|
788
|
+
children: DrawCommand[];
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Text draw command. Renders one or more runs at (`x`, `y`) in screen
|
|
792
|
+
* space, optionally word-wrapping at `maxWidth`. The renderer resolves
|
|
793
|
+
* each run's `(fontFamily, fontWeight, fontStyle)` to an MSDF atlas via
|
|
794
|
+
* `resolveFontVariant` and bucket-draws by atlas + color group.
|
|
795
|
+
*
|
|
796
|
+
* `style` carries node-level defaults (`lineHeight`, anti-alias width)
|
|
797
|
+
* that don't belong on individual runs.
|
|
798
|
+
*/
|
|
799
|
+
interface TextDrawCommand {
|
|
800
|
+
kind: 'text';
|
|
801
|
+
x: number;
|
|
802
|
+
y: number;
|
|
803
|
+
runs: ResolvedRun[];
|
|
804
|
+
maxWidth?: number;
|
|
805
|
+
align?: 'left' | 'center' | 'right';
|
|
806
|
+
style: TextStyle;
|
|
807
|
+
/** Box height for vertical alignment. When set with `verticalAlign`,
|
|
808
|
+
* the laid-out block shifts within `[y, y+height]`. */
|
|
809
|
+
height?: number;
|
|
810
|
+
/** Default 'top' — the legacy top-anchored behavior. */
|
|
811
|
+
verticalAlign?: TextVerticalAlign;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Image draw command — renders `image` at screen-space rect (x, y, w, h).
|
|
815
|
+
* The image is stretched to fit; no tiling. Use a pattern FillStyle on a path
|
|
816
|
+
* for tiling.
|
|
817
|
+
*/
|
|
818
|
+
interface ImageDrawCommand {
|
|
819
|
+
kind: 'image';
|
|
820
|
+
image: ImageBitmap;
|
|
821
|
+
x: number;
|
|
822
|
+
y: number;
|
|
823
|
+
w: number;
|
|
824
|
+
h: number;
|
|
825
|
+
opacity?: number;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Custom shader draw command. The renderer generates a quad over `bounds`
|
|
829
|
+
* and dispatches the consumer's fragment shader with the kit's vertex prelude.
|
|
830
|
+
*
|
|
831
|
+
* `uniforms` keys must match names declared in the consumer's fragment shader.
|
|
832
|
+
* The kit automatically sets `u_bounds`, `u_view`, and `u_proj` — do not
|
|
833
|
+
* declare those in `uniforms`.
|
|
834
|
+
*
|
|
835
|
+
* @experimental API may change before v2.
|
|
836
|
+
*/
|
|
837
|
+
interface ShaderDrawCommand {
|
|
838
|
+
kind: 'shader';
|
|
839
|
+
program: ShaderProgramHandle;
|
|
840
|
+
uniforms: Record<string, ShaderUniform>;
|
|
841
|
+
/** Screen-space bounding rect in CSS pixels. */
|
|
842
|
+
bounds: {
|
|
843
|
+
x: number;
|
|
844
|
+
y: number;
|
|
845
|
+
w: number;
|
|
846
|
+
h: number;
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Axis-aligned rectangle in world space. Kit-wide `Bounds` shape used by
|
|
852
|
+
* selection, group, and viewport helpers. The optional `rotation` field
|
|
853
|
+
* (radians, around the AABB center) lets selection chrome attach a rotated
|
|
854
|
+
* orientation to an otherwise axis-aligned rect without needing a parallel
|
|
855
|
+
* type.
|
|
856
|
+
*/
|
|
857
|
+
interface Bounds {
|
|
858
|
+
x: number;
|
|
859
|
+
y: number;
|
|
860
|
+
width: number;
|
|
861
|
+
height: number;
|
|
862
|
+
rotation?: number;
|
|
863
|
+
}
|
|
864
|
+
/** Pixel dimensions of the canvas viewport. */
|
|
865
|
+
interface ViewportDims {
|
|
866
|
+
width: number;
|
|
867
|
+
height: number;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export type { Bounds as B, DrawCommand as D, Node as N, Op as O, PathDrawCommand as P, Scene as S, View as V, ViewportDims as a, NodeId as b };
|