@weasel-js/labkit 1.1.0 → 1.3.0-pre.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.
Files changed (226) hide show
  1. package/README.md +27 -0
  2. package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-DBB45NfN.d.ts} +244 -58
  3. package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
  4. package/dist/_dts/index-DgcNdEdh.d.ts +314 -0
  5. package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
  6. package/dist/_dts/types-AHlQxBNN.d.ts +162 -0
  7. package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
  8. package/dist/_dts/{useTrialState-DYe2vUwN.d.ts → useTrialState-CsGMjhu9.d.ts} +6 -92
  9. package/dist/canvas/index.d.ts +8 -3
  10. package/dist/canvas/index.js +2 -1
  11. package/dist/chrome/index.d.ts +86 -0
  12. package/dist/chrome/index.js +8 -0
  13. package/dist/chrome/index.js.map +1 -0
  14. package/dist/{chunk-3TYUJR7Z.js → chunk-4TMMVIDM.js} +9 -3
  15. package/dist/chunk-4TMMVIDM.js.map +1 -0
  16. package/dist/chunk-HFDVGF4X.js +170 -0
  17. package/dist/chunk-HFDVGF4X.js.map +1 -0
  18. package/dist/chunk-LN6JDUGB.js +106 -0
  19. package/dist/chunk-LN6JDUGB.js.map +1 -0
  20. package/dist/{chunk-574LJAV4.js → chunk-NS54R52R.js} +31 -21
  21. package/dist/chunk-NS54R52R.js.map +1 -0
  22. package/dist/{chunk-NRKWVTVT.js → chunk-O2BULFHX.js} +5 -14
  23. package/dist/chunk-O2BULFHX.js.map +1 -0
  24. package/dist/chunk-O7NVSCLN.js +266 -0
  25. package/dist/chunk-O7NVSCLN.js.map +1 -0
  26. package/dist/chunk-PO6L3NSH.js +366 -0
  27. package/dist/chunk-PO6L3NSH.js.map +1 -0
  28. package/dist/chunk-PQJ5B2U2.js +27 -0
  29. package/dist/chunk-PQJ5B2U2.js.map +1 -0
  30. package/dist/chunk-RVNN4CHQ.js +212 -0
  31. package/dist/chunk-RVNN4CHQ.js.map +1 -0
  32. package/dist/chunk-TLGRYALP.js +6151 -0
  33. package/dist/chunk-TLGRYALP.js.map +1 -0
  34. package/dist/{chunk-SFL7NFKN.js → chunk-V7PRSIRQ.js} +15 -7
  35. package/dist/chunk-V7PRSIRQ.js.map +1 -0
  36. package/dist/{chunk-DJLDIRFN.js → chunk-Z6HJ5FEM.js} +9626 -6050
  37. package/dist/chunk-Z6HJ5FEM.js.map +1 -0
  38. package/dist/chunk-ZZAYVX4X.js +505 -0
  39. package/dist/chunk-ZZAYVX4X.js.map +1 -0
  40. package/dist/controls/index.d.ts +19 -5
  41. package/dist/controls/index.js +3 -1
  42. package/dist/dragdrop/index.d.ts +5 -2
  43. package/dist/index.d.ts +307 -107
  44. package/dist/index.js +797 -540
  45. package/dist/index.js.map +1 -1
  46. package/dist/job/index.d.ts +13 -0
  47. package/dist/job/index.js +3 -0
  48. package/dist/job/index.js.map +1 -0
  49. package/dist/layers/index.d.ts +6 -3
  50. package/dist/layers/index.js +2 -1
  51. package/dist/passthrough/weasel-canvas.d.ts +69 -13
  52. package/dist/passthrough/weasel-canvas.js +1 -1
  53. package/dist/passthrough/weasel-ui.d.ts +3804 -3661
  54. package/dist/passthrough/weasel-ui.js +2 -2
  55. package/dist/primitives/index.d.ts +102 -8
  56. package/dist/primitives/index.js +4 -1
  57. package/dist/state/index.d.ts +3 -2
  58. package/dist/state/index.js +2 -2
  59. package/dist/styles.css +543 -144
  60. package/dist/surface/index.d.ts +77 -0
  61. package/dist/surface/index.js +4 -0
  62. package/dist/surface/index.js.map +1 -0
  63. package/dist/ui/layers/index.js +4 -3
  64. package/dist/undo/index.d.ts +5 -2
  65. package/package.json +24 -7
  66. package/src/canvas/AGENTS.md +12 -0
  67. package/src/canvas/CanvasStack.tsx +6 -2
  68. package/src/canvas/useLayerScheduler.ts +22 -15
  69. package/src/canvas/useOrbit.test.ts +71 -0
  70. package/src/canvas/useOrbit.ts +149 -0
  71. package/src/canvas/usePanZoom.test.ts +80 -0
  72. package/src/canvas/usePanZoom.ts +16 -2
  73. package/src/chrome/ChromeRegions.stories.tsx +106 -0
  74. package/src/chrome/builtins.test.ts +153 -0
  75. package/src/chrome/builtins.tsx +205 -0
  76. package/src/chrome/index.ts +25 -0
  77. package/src/chrome/merge.test.ts +51 -0
  78. package/src/chrome/merge.ts +52 -0
  79. package/src/chrome/regions/PaletteRegion.less +14 -0
  80. package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
  81. package/src/chrome/regions/PaletteRegion.tsx +41 -0
  82. package/src/chrome/regions/SidebarRegion.tsx +56 -0
  83. package/src/chrome/regions/StatusRegion.tsx +31 -0
  84. package/src/chrome/regions/TitleBarRegion.tsx +41 -0
  85. package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
  86. package/src/chrome/regions/ToolbarRegion.tsx +71 -0
  87. package/src/chrome/regions/ViewportRegion.less +42 -0
  88. package/src/chrome/regions/ViewportRegion.tsx +37 -0
  89. package/src/chrome/regions/regions.test.tsx +81 -0
  90. package/src/chrome/types.ts +127 -0
  91. package/src/config/builder.test.ts +87 -0
  92. package/src/config/builder.ts +224 -0
  93. package/src/config/fromConfigField.test.ts +93 -0
  94. package/src/config/fromConfigField.ts +74 -0
  95. package/src/config/index.ts +22 -0
  96. package/src/config/resolve.test.ts +120 -0
  97. package/src/config/resolve.ts +67 -0
  98. package/src/config/rules.ts +68 -0
  99. package/src/config/types.ts +108 -0
  100. package/src/config/useConfigSchema.ts +30 -0
  101. package/src/config/visible.ts +20 -0
  102. package/src/controls/ControlPanel.less +4 -58
  103. package/src/controls/ControlPanel.stories.tsx +86 -0
  104. package/src/controls/ControlPanel.test.tsx +223 -10
  105. package/src/controls/ControlPanel.tsx +202 -203
  106. package/src/dragdrop/DragDropRuntime.tsx +36 -18
  107. package/src/dragdrop/DragGhost.less +1 -1
  108. package/src/dragdrop/Palette.less +1 -1
  109. package/src/index.test.ts +18 -0
  110. package/src/index.ts +38 -3
  111. package/src/instrument/SineWave.smoke.test.tsx +9 -1
  112. package/src/instrument/defineInstrument.test.ts +38 -1
  113. package/src/instrument/defineInstrument.ts +31 -4
  114. package/src/instrument/index.ts +1 -0
  115. package/src/instrument/types.ts +38 -3
  116. package/src/instrument/validateConfigSchema.test.ts +13 -3
  117. package/src/instrument/validateConfigSchema.ts +7 -10
  118. package/src/job/index.ts +3 -0
  119. package/src/job/types.ts +47 -0
  120. package/src/job/useJob.test.tsx +210 -0
  121. package/src/job/useJob.ts +134 -0
  122. package/src/lab/Lab.chrome.test.tsx +65 -0
  123. package/src/lab/Lab.stories.tsx +1 -2
  124. package/src/lab/Lab.tsx +58 -15
  125. package/src/lab/LabContext.ts +6 -0
  126. package/src/lab/LabFullChrome.stories.tsx +308 -0
  127. package/src/lab/LabHeader.test.tsx +42 -0
  128. package/src/lab/LabHeader.tsx +57 -0
  129. package/src/lab/LabPalette.test.tsx +35 -0
  130. package/src/lab/LabPalette.tsx +34 -0
  131. package/src/lab/LabShell.less +50 -3
  132. package/src/lab/Workspace.less +1 -30
  133. package/src/lab/Workspace.surface.test.tsx +49 -0
  134. package/src/lab/Workspace.test.tsx +20 -10
  135. package/src/lab/Workspace.tsx +19 -9
  136. package/src/lab/index.ts +2 -0
  137. package/src/layers/AGENTS.md +1 -1
  138. package/src/layers/LayerList.less +31 -7
  139. package/src/layers/LayerList.tsx +17 -4
  140. package/src/passthrough/weasel-ui.ts +23 -0
  141. package/src/primitives/DragHandleGlyph.tsx +29 -0
  142. package/src/primitives/FloatingPanel.less +28 -0
  143. package/src/primitives/FloatingPanel.stories.tsx +46 -0
  144. package/src/primitives/FloatingPanel.test.tsx +240 -0
  145. package/src/primitives/FloatingPanel.tsx +190 -0
  146. package/src/primitives/FpsMeter.test.tsx +44 -0
  147. package/src/primitives/FpsMeter.tsx +20 -9
  148. package/src/primitives/JobProgress.less +69 -0
  149. package/src/primitives/JobProgress.stories.tsx +48 -0
  150. package/src/primitives/JobProgress.tsx +65 -0
  151. package/src/primitives/Legend.less +47 -0
  152. package/src/primitives/Legend.stories.tsx +32 -0
  153. package/src/primitives/Legend.test.tsx +57 -0
  154. package/src/primitives/Legend.tsx +40 -0
  155. package/src/primitives/Sidebar.less +32 -2
  156. package/src/primitives/StatusBar.less +7 -0
  157. package/src/primitives/StatusBar.tsx +10 -3
  158. package/src/primitives/Toolbar.less +50 -3
  159. package/src/primitives/Toolbar.test.tsx +67 -1
  160. package/src/primitives/Toolbar.tsx +72 -6
  161. package/src/primitives/ZoomControl.less +31 -0
  162. package/src/primitives/ZoomControl.tsx +76 -0
  163. package/src/primitives/floating.entry.test.tsx +14 -0
  164. package/src/primitives/index.ts +10 -2
  165. package/src/primitives/useRovingTabIndex.test.ts +27 -0
  166. package/src/primitives/useRovingTabIndex.ts +79 -0
  167. package/src/state/store.ts +16 -2
  168. package/src/state/toolSlot.test.ts +36 -0
  169. package/src/state/types.ts +8 -2
  170. package/src/state/view.test.ts +127 -0
  171. package/src/state/view.ts +18 -0
  172. package/src/styles.less +6 -0
  173. package/src/surface/AGENTS.md +64 -0
  174. package/src/surface/SurfaceContext.ts +5 -0
  175. package/src/surface/composeRects.test.ts +50 -0
  176. package/src/surface/composeRects.ts +19 -0
  177. package/src/surface/deviceRect.test.ts +40 -0
  178. package/src/surface/deviceRect.ts +19 -0
  179. package/src/surface/index.ts +7 -0
  180. package/src/surface/rect.ts +16 -0
  181. package/src/surface/useSurfaceTile.test.tsx +67 -0
  182. package/src/surface/useSurfaceTile.ts +32 -0
  183. package/src/surface/useTiledSurface.test.tsx +231 -0
  184. package/src/surface/useTiledSurface.ts +161 -0
  185. package/src/test-setup.ts +26 -0
  186. package/src/theme/base.less +19 -2
  187. package/src/theme/interstellar.tokens.json +6 -2
  188. package/src/tools/types.ts +24 -0
  189. package/src/trial/Trial.config.test.tsx +113 -0
  190. package/src/trial/Trial.job.test.tsx +73 -0
  191. package/src/trial/Trial.less +121 -4
  192. package/src/trial/Trial.stories.tsx +9 -1
  193. package/src/trial/Trial.test.tsx +67 -96
  194. package/src/trial/Trial.tsx +112 -45
  195. package/src/trial/TrialChrome.tsx +110 -64
  196. package/src/trial/TrialDragContext.ts +10 -0
  197. package/src/trial/TrialTitleBar.tsx +41 -0
  198. package/src/trial/index.ts +1 -15
  199. package/src/trial/trialChrome.borders.test.ts +38 -0
  200. package/src/ui/layers/LayerStack.less +9 -9
  201. package/src/ui/layers/LayerStack.tsx +1 -13
  202. package/src/ui/properties/CurveField.less +2 -2
  203. package/src/ui/properties/CurveField.test.tsx +11 -11
  204. package/src/ui/properties/PropertyGroup.less +2 -2
  205. package/src/ui/properties/PropertyPanel.less +55 -41
  206. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +4 -2
  207. package/src/ui/properties/storyLayouts.tsx +2 -2
  208. package/dist/_dts/index-iAP6XbH3.d.ts +0 -132
  209. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  210. package/dist/chunk-3TYUJR7Z.js.map +0 -1
  211. package/dist/chunk-574LJAV4.js.map +0 -1
  212. package/dist/chunk-5R2ATYPJ.js +0 -201
  213. package/dist/chunk-5R2ATYPJ.js.map +0 -1
  214. package/dist/chunk-C6GJKPUI.js +0 -5986
  215. package/dist/chunk-C6GJKPUI.js.map +0 -1
  216. package/dist/chunk-DJLDIRFN.js.map +0 -1
  217. package/dist/chunk-NRKWVTVT.js.map +0 -1
  218. package/dist/chunk-PMAU3SEE.js +0 -126
  219. package/dist/chunk-PMAU3SEE.js.map +0 -1
  220. package/dist/chunk-SFL7NFKN.js.map +0 -1
  221. package/src/instrument/capabilityDetector.test.ts +0 -64
  222. package/src/instrument/capabilityDetector.ts +0 -20
  223. package/src/trial/DefaultSidebar.tsx +0 -29
  224. package/src/trial/DefaultStatusBar.tsx +0 -19
  225. package/src/trial/DefaultToolbar.tsx +0 -92
  226. package/src/trial/slotTypes.ts +0 -53
package/README.md CHANGED
@@ -59,6 +59,33 @@ import { ThemeProvider } from '@weasel-js/theme/react';
59
59
  `"light"` or `"dark"`. Only `styles.css` needs importing — the token values
60
60
  arrive through the provider.
61
61
 
62
+ ## Driving your own renderer
63
+
64
+ `CanvasStack` is 2D. For three.js or raw WebGL, take rects and dirtiness from
65
+ labkit and keep the GL yourself:
66
+
67
+ ```tsx
68
+ import { toDeviceRect, useSurfaceTile, useTiledSurface } from '@weasel-js/labkit/surface';
69
+ ```
70
+
71
+ A trial's `view` is opaque to labkit — it is persisted, restored on Reset and
72
+ handed to the instrument without being read into — so a 3D lab stores an orbit
73
+ there and gets all three. `useOrbit` is the 3D peer of `usePanZoom`.
74
+
75
+ See `src/surface/AGENTS.md` for the contract and the traps.
76
+
77
+ ## Long-running work
78
+
79
+ An instrument with work too slow for a render declares a `job`. labkit starts it,
80
+ aborts it on unmount and on a key change, discards results from a superseded run,
81
+ and renders progress and a cancel control into the trial chrome. Per-item failure
82
+ is an event rather than a thrown error, so a run with two failed items is a
83
+ partial success.
84
+
85
+ ```tsx
86
+ import type { JobCapability } from '@weasel-js/labkit/job';
87
+ ```
88
+
62
89
  ## Development
63
90
 
64
91
  ```bash
@@ -88,6 +88,8 @@ interface SerializedHistoryEntry {
88
88
  label: string;
89
89
  forwardOps: SerializedOp[];
90
90
  baseOps: SerializedOp[];
91
+ selectionBefore?: readonly string[];
92
+ selectionAfter?: readonly string[];
91
93
  }
92
94
  /** Snapshot of an entire `History` instance. Designed to live alongside the
93
95
  * scene snapshot in IDB so a reload restores the undo / redo stacks to
@@ -119,6 +121,10 @@ interface HistoryEntry {
119
121
  * contribute nothing. May be `undefined` for deserialized entries
120
122
  * restored from an older snapshot that predates this field. */
121
123
  touchedIds?: ReadonlySet<string>;
124
+ /** Selection restored when this entry is undone. */
125
+ selectionBefore?: readonly string[];
126
+ /** Selection restored when this entry is redone. */
127
+ selectionAfter?: readonly string[];
122
128
  }
123
129
  /** Op-batched undo/redo controller returned by `createHistory`. */
124
130
  interface History {
@@ -170,7 +176,7 @@ interface History {
170
176
  * Unlike `applyOps`, does NOT call `op.apply()`. Used by Journal.commit
171
177
  * to flush a session's net forward ops to the parent as one entry without
172
178
  * re-mutating the scene. */
173
- recordEntry(ops: Op[], label: string): void;
179
+ recordEntry(ops: Op[], label: string, options?: RecordEntryOptions): void;
174
180
  /** Concatenated forwardOps of every undo-stack entry, in order. Snapshot
175
181
  * of "what changes are currently applied via this history" — useful for
176
182
  * Journal.commit to flush to a parent, and for any caller that wants to
@@ -193,6 +199,32 @@ interface History {
193
199
  * to resume or discard before calling this. */
194
200
  resumeJournal(journal: Journal): void;
195
201
  }
202
+ /** Options for `recordEntry`. */
203
+ interface RecordEntryOptions {
204
+ /** Selection as of before the already-applied ops ran. `recordEntry` is
205
+ * called after the fact, so the live selection has moved on by then and
206
+ * the engine cannot sample it — a caller that wants undo to restore the
207
+ * selection captures it when the batch opens and passes it here. */
208
+ selectionBefore?: readonly string[];
209
+ }
210
+
211
+ /**
212
+ * Path data model. Vector-graphics primitive used kit-wide as the canonical
213
+ * shape (replacing per-shape ad-hoc poses). SVG-style command stream so we
214
+ * cover lines, polygons, beziers, and multi-contour shapes (think the inner
215
+ * hole of an "O") under one type.
216
+ *
217
+ * Storage: command codes in a `Uint8Array`, parameters in a `Float32Array`.
218
+ * Each command consumes a fixed number of float coords from the parameter
219
+ * array; the parser walks both arrays in lockstep. This keeps interaction
220
+ * hot loops monomorphic and keeps GC pressure low under heavy edits.
221
+ *
222
+ * Two path subtypes are distinguished at the type level so common-case
223
+ * machinery (selection AABBs, area-select intersection, hit-testing rect
224
+ * silhouettes) can short-circuit on `RectPath` without paying the polygon
225
+ * kernel cost. Polymorphic kernels accept either via the `Path` union and
226
+ * dispatch on `kind`.
227
+ */
196
228
 
197
229
  /** Fill rule used by polygon path hit-testing and `ctx.fill()`. */
198
230
  type PathFillRule = 'nonzero' | 'evenodd';
@@ -298,6 +330,20 @@ interface NodeBase<TData, TLayer extends string, TPose> {
298
330
  pose: TPose;
299
331
  data: TData;
300
332
  parent: NodeId | null;
333
+ /** Nodes whose poses this node's geometry is computed from. Fixed at add
334
+ * time. Absent or empty means the node's geometry is authored, which is the
335
+ * normal case. */
336
+ dependsOn?: readonly NodeId[];
337
+ /** Computes this node's path from its dependencies' poses, in `dependsOn`
338
+ * order. A dependency that has been removed arrives as `undefined`.
339
+ * Returning `null` means "nothing to draw right now". Re-evaluated when a
340
+ * dependency's world pose changes, never authored. Absolute-pose `Scene`
341
+ * makes that the dependency's own pose, and an ancestor's move reaches it as
342
+ * a `setPose` of its own from the container cascade.
343
+ * `node` is deliberately widened: naming `TData`/`TLayer` here puts them in
344
+ * a contravariant position, making `Scene` invariant in both and breaking
345
+ * assignment kit-wide. The cost is that a `derivePath` casts to read `node.data`. */
346
+ derivePath?: (node: Node<unknown, string, TPose>, deps: readonly (TPose | undefined)[]) => Path | null;
301
347
  }
302
348
  /** A node with no children — a shape, a label, an image. */
303
349
  interface LeafNode<TData, TLayer extends string, TPose = RectPose> extends NodeBase<TData, TLayer, TPose> {
@@ -352,6 +398,11 @@ interface AddNodeSpec<TData, TLayer extends string, TPose = RectPose> {
352
398
  /** Only meaningful when `kind === 'container'`. Attach a clip-path function
353
399
  * to the node; ignored for leaves. Mirrors `ContainerNode.clipFromPose`. */
354
400
  clipFromPose?: (pose: TPose) => Path | null;
401
+ /** Mirrors `SceneNode.dependsOn`. */
402
+ dependsOn?: readonly NodeId[];
403
+ /** Mirrors `SceneNode.derivePath`. Taken as a live function; its registry key is
404
+ * looked up from it, never passed in. */
405
+ derivePath?: (node: Node<unknown, string, TPose>, deps: readonly (TPose | undefined)[]) => Path | null;
355
406
  }
356
407
  /** A custom scene mutation registered with `Scene.registerOp`: how to apply
357
408
  * it and how to undo it. The pair is what makes it participate in history. */
@@ -399,6 +450,57 @@ interface SerializedNode<TData, TLayer extends string, TPose> {
399
450
  /** Registry key for the container's clip-path factory.
400
451
  * Containers only; omitted when the container has no clip. */
401
452
  clipFromPoseKey?: string;
453
+ /** Ids this node's geometry derives from. Omitted when it derives from nothing. */
454
+ dependsOn?: readonly string[];
455
+ /** Registry key for the node's `derivePath` function. Omitted when it has none. */
456
+ derivePathKey?: string;
457
+ }
458
+ /**
459
+ * One node's ephemeral presentation override — what a frame loop wants to say
460
+ * about a node without saying it about the document.
461
+ *
462
+ * Not document content: never recorded in history, never in `toJSON`, and
463
+ * writing one does not bump `Scene.getVersion()`. Hoist one entry per node and
464
+ * mutate it in place on a frame loop; `PoseOverrides.commit()` is what makes a
465
+ * mutation visible.
466
+ */
467
+ interface PoseOverride<TPose> {
468
+ /** Replaces the node's document pose everywhere the render and hit-test
469
+ * paths read one, including the clip a container derives from its pose.
470
+ * Resolved by `effectivePose` — reading `node.pose` directly is how those
471
+ * paths came to disagree about where a node is. */
472
+ pose?: TPose;
473
+ /** Multiplied into the node's painted alpha, on top of any `alphaFor`. */
474
+ alpha?: number;
475
+ }
476
+ /**
477
+ * The scene's ephemeral per-node overrides — see {@link PoseOverride}.
478
+ *
479
+ * The intended shape of a frame is: `set` each node once, mutate the entries
480
+ * in place per frame, `commit()` once. `commit` is not optional bookkeeping —
481
+ * the painter memo keys on pose *reference*, so a mutation without a commit
482
+ * paints the previous frame with no error.
483
+ *
484
+ * To promote a frame to document state (dropping a drag, baking an animation),
485
+ * write it once through `Scene.setPose` and `clear` the override.
486
+ */
487
+ interface PoseOverrides<TPose> {
488
+ /** Store `entry` for `id` **by reference**; the caller keeps mutating it. */
489
+ set(id: NodeId, entry: PoseOverride<TPose>): void;
490
+ get(id: NodeId): PoseOverride<TPose> | undefined;
491
+ has(id: NodeId): boolean;
492
+ /** The overridden ids, as a snapshot array. */
493
+ ids(): readonly NodeId[];
494
+ clear(id: NodeId): void;
495
+ clearAll(): void;
496
+ /** Publish this frame's in-place mutations: invalidate the painter memo for
497
+ * every overridden node, then notify subscribers. */
498
+ commit(): void;
499
+ /** Notified after every write. The canvas uses this to repaint without a
500
+ * scene version bump. */
501
+ subscribe(fn: () => void): () => void;
502
+ /** Monotonic write counter. A snapshot for observers that poll. */
503
+ getGeneration(): number;
402
504
  }
403
505
  /**
404
506
  * The kit-owned scene tree: nodes, layers, and the undo history over both.
@@ -428,10 +530,19 @@ interface Scene<TData, TLayer extends string, TPose = RectPose> {
428
530
  * not a live view, and not yours to mutate. */
429
531
  renderOrderNodes(): readonly Node<TData, TLayer, TPose>[];
430
532
  add(spec: AddNodeSpec<TData, TLayer, TPose>): NodeId;
431
- /** Delete `id` **and its entire subtree** every descendant is removed in
432
- * the same operation. Recorded as one undoable step; `undo()` restores the
433
- * whole subtree (root + descendants, child order intact). */
533
+ /** Delete `id`, its **entire subtree**, and **everything that derives from**
534
+ * any of those nodes a node listing one of them in `dependsOn` goes too,
535
+ * along with its own subtree, transitively. A dependent can live anywhere in
536
+ * the tree, so this deletes nodes the caller never named and may unlink
537
+ * several disjoint subtrees at once. Recorded as one undoable step; `undo()`
538
+ * restores every one of them where it was, child order intact. */
434
539
  remove(id: NodeId): void;
540
+ /** {@link remove} over several roots at once, as a **single** undoable step.
541
+ * Ids resolve against the tree as it stands at the call, so an id that
542
+ * another one would cascade away is absorbed rather than removed twice —
543
+ * which is what makes it safe to pass a whole selection. Throws if any id is
544
+ * not in the scene; an empty list does nothing and records no step. */
545
+ removeMany(ids: readonly NodeId[]): void;
435
546
  update(id: NodeId, patch: {
436
547
  data: TData;
437
548
  }): void;
@@ -467,6 +578,9 @@ interface Scene<TData, TLayer extends string, TPose = RectPose> {
467
578
  setLayerVisible(layer: TLayer, visible: boolean): void;
468
579
  setLayerLocked(layer: TLayer, locked: boolean): void;
469
580
  addLayer(spec: AddLayerSpec<TLayer>): void;
581
+ /** Drop a user layer and every node tagged to it, as one undoable step.
582
+ * Removal cascades, so this also deletes nodes **on other layers** that
583
+ * derive from a node on this one. */
470
584
  removeLayer(layer: TLayer): void;
471
585
  renameLayer(layer: TLayer, name: string): void;
472
586
  moveLayer(layer: TLayer, index: number): void;
@@ -500,6 +614,19 @@ interface Scene<TData, TLayer extends string, TPose = RectPose> {
500
614
  * `SceneCanvasAdapter`). Pass `this` from `sceneToAdapter` or a compatible
501
615
  * adapter. */
502
616
  applyBatch(ops: Op[], label: string, adapter: unknown): void;
617
+ /** The transient set of active ids — "operate on these N as a unit".
618
+ * Shared by every view over this scene unless a view supplies its own
619
+ * (see `CanvasView.selection`). Not document content: it never appears
620
+ * in `toJSON`. It does ride on history entries, so undo and redo put
621
+ * back the selection an edit was made under; changing it is never an
622
+ * undo step of its own. */
623
+ getSelection(): readonly NodeId[];
624
+ setSelection(ids: readonly NodeId[]): void;
625
+ /** Per-node pose / alpha overrides the render and hit-test paths read
626
+ * through. Like {@link Scene.getSelection} this is not document content:
627
+ * writes are never recorded, never serialized, and do not bump
628
+ * {@link Scene.getVersion}. See {@link PoseOverrides}. */
629
+ readonly overrides: PoseOverrides<TPose>;
503
630
  undo(): boolean;
504
631
  redo(): boolean;
505
632
  canUndo(): boolean;
@@ -609,29 +736,11 @@ interface ViewportDims {
609
736
  }
610
737
 
611
738
  /**
612
- * 2D affine matrix utilities. Column-major 9-element Float32Array, matching
613
- * `WebGL2RenderingContext.uniformMatrix3fv` byte order so we can pass the
614
- * array directly without a transpose flag.
739
+ * A registered texture, named by the registry that holds it.
615
740
  *
616
- * Layout (column-major):
617
- * [m00, m10, 0,
618
- * m01, m11, 0,
619
- * tx, ty, 1]
620
- *
621
- * `apply(m, x, y)` returns `[m * (x, y, 1)] = [m00*x + m01*y + tx,
622
- * m10*x + m11*y + ty]`.
623
- */
624
- type Mat3 = Float32Array;
625
-
626
- /**
627
- * registerTexture — accepts an image source, assigns an opaque id, stores in
628
- * a module-level registry. Actual GL upload happens lazily at draw time in
629
- * drawShader() via GLTextureCache.upload (which is idempotent).
630
- *
631
- * Lifecycle: textures live for the renderer's lifetime. No unregister in v1.
632
- *
633
- * Convention §9: this registry stores image data only — no per-renderer state.
634
- * Each WeaselRenderer's GLTextureCache does its own dedup via has(id).
741
+ * The handle is here rather than beside the registry because `FillStyle`'s
742
+ * pattern variant names it, and paint is a leaf: the registry lives in the
743
+ * renderer and imports this back.
635
744
  */
636
745
  interface TextureHandle {
637
746
  readonly id: string;
@@ -789,20 +898,53 @@ interface GradStop {
789
898
  * typically use a stencil mask of the stroked path against the geometry.
790
899
  */
791
900
  type StrokeAlign = 'center' | 'inner' | 'outer';
901
+ /**
902
+ * The kit's built-in marker vocabulary, open on the string the way
903
+ * `PaintKind` is so a consumer's registered key typechecks.
904
+ */
905
+ type KitMarkerKey = 'arrow' | 'arrow-open' | 'arrow-concave' | 'diamond' | 'diamond-hollow' | 'circle' | 'square' | 'bar';
906
+ type MarkerKey = KitMarkerKey | (string & {});
907
+ /**
908
+ * A marker on one end (or every interior vertex) of a stroke.
909
+ *
910
+ * `size` reuses `Stroke.width`'s unit system: a bare number scales with the
911
+ * resolved stroke width, `{ px }` pins screen pixels. That is SVG's
912
+ * `markerUnits` in the idiom this codebase already resolves at draw time.
913
+ * Omitted, the marker is one stroke width per unit.
914
+ */
915
+ type MarkerRef = MarkerKey | {
916
+ key: MarkerKey;
917
+ size?: number | {
918
+ px: number;
919
+ };
920
+ };
792
921
  /** Stroke style: a FillStyle plus structural line parameters. */
793
922
  interface Stroke {
794
923
  paint: FillStyle;
795
- width?: number;
924
+ /** World units, or `{ px }` for screen pixels — resolved against the
925
+ * accumulated transform scale at draw time, so it holds its on-screen
926
+ * thickness as the view zooms. */
927
+ width?: number | {
928
+ px: number;
929
+ };
796
930
  /** Per `CanvasRenderingContext2D.setLineDash` — empty/omitted = solid. */
797
931
  dash?: number[];
932
+ /** Marker at the first vertex of each open subpath, rotated to point back
933
+ * along the line (SVG's `auto-start-reverse`, as the only behavior). */
934
+ markerStart?: MarkerRef;
935
+ /** Marker at every interior authored vertex, on the bisector of the
936
+ * incoming and outgoing directions. Never insets the stroke. */
937
+ markerMid?: MarkerRef;
938
+ /** Marker at the last vertex of each open subpath. */
939
+ markerEnd?: MarkerRef;
798
940
  cap?: 'butt' | 'round' | 'square';
799
941
  join?: 'miter' | 'round' | 'bevel';
800
942
  /**
801
943
  * Miter join fallback threshold. When the miter length exceeds
802
- * `miterLimit * width / 2`, the join falls back to a bevel. Default 10
803
- * (matching Canvas2D). SVG's default is 4; consumers that want SVG
804
- * fidelity should set this explicitly when constructing strokes from
805
- * SVG sources where the attribute was omitted.
944
+ * `miterLimit * width / 2`, the join falls back to a bevel. Default 4,
945
+ * matching SVG which is also what the kit's own serializer implies when
946
+ * it omits the attribute for an unset field. Canvas2D's 10 lets an acute
947
+ * corner throw a spike four times the half-width.
806
948
  */
807
949
  miterLimit?: number;
808
950
  /** Where the stroke sits relative to the geometric edge. Default `'center'`. */
@@ -837,17 +979,41 @@ interface Stroke {
837
979
  varyingWidthJoinThreshold?: number;
838
980
  }
839
981
 
982
+ /**
983
+ * 2D affine matrix utilities. Column-major 9-element Float32Array, matching
984
+ * `WebGL2RenderingContext.uniformMatrix3fv` byte order so we can pass the
985
+ * array directly without a transpose flag.
986
+ *
987
+ * Layout (column-major):
988
+ * [m00, m10, 0,
989
+ * m01, m11, 0,
990
+ * tx, ty, 1]
991
+ *
992
+ * `apply(m, x, y)` returns `[m * (x, y, 1)] = [m00*x + m01*y + tx,
993
+ * m10*x + m11*y + ty]`.
994
+ */
995
+ type Mat3 = Float32Array;
996
+
840
997
  /**
841
998
  * Typography for `TextPose` and friends. Every field is optional; consumers
842
999
  * pass `{}` or override the few they care about. Defaults live in
843
1000
  * `DEFAULT_TEXT_STYLE` and are applied at render/measure time, never written
844
1001
  * back to the pose.
845
1002
  *
846
- * `fill` follows the kit-wide `FillStyle` model solid color or pattern. The
847
- * contenteditable edit overlay flattens non-solid fills to `'#000'` for CSS
848
- * since the browser can't paint with a texture handle.
1003
+ * Paint is not typography and does not live here. A text node carries its
1004
+ * fill and stroke in `data.fill` / `data.stroke`, the slots every other node
1005
+ * kind uses; `resolveTextStyle` takes them as its second argument and
1006
+ * `StyledRun.fill` / `.stroke` override them per range.
849
1007
  */
850
1008
 
1009
+ /**
1010
+ * Horizontal alignment. `start` / `end` resolve against the reading direction;
1011
+ * `left` / `right` are absolute. Same five values as CSS `text-align`, with
1012
+ * the same split between the relative pair and the absolute pair.
1013
+ */
1014
+ type TextAlign = 'left' | 'center' | 'right' | 'start' | 'end';
1015
+ /** Reading direction, which is what gives `start` / `end` their meaning. */
1016
+ type TextDirection = 'ltr' | 'rtl';
851
1017
  /** User-facing text style. All fields optional; defaults applied at render time via `resolveTextStyle`. */
852
1018
  interface TextStyle {
853
1019
  /** Font size in world units. Default 16. */
@@ -858,15 +1024,15 @@ interface TextStyle {
858
1024
  fontWeight?: number | string;
859
1025
  /** Default `'normal'`. */
860
1026
  fontStyle?: 'normal' | 'italic';
861
- /** Default `'left'`. */
862
- align?: 'left' | 'center' | 'right';
1027
+ /** Default `'left'`. Pass `'start'` to align by reading order instead. */
1028
+ align?: TextAlign;
1029
+ /** Reading direction, resolving `align: 'start' | 'end'`. Default `'ltr'`. */
1030
+ direction?: TextDirection;
863
1031
  /** Multiplier applied to `fontSize`. Default 1.2. */
864
1032
  lineHeight?: number;
865
- /** Default `{ fill: 'solid', color: '#000' }`. */
866
- fill?: FillStyle;
867
1033
  /**
868
- * Caret color used by the edit overlay. Defaults to the text color when
869
- * `fill` is solid; falls back to `#000` for non-solid paints.
1034
+ * Caret color used by the edit overlay. Defaults to the node's fill when
1035
+ * that fill is solid; falls back to `#000` for non-solid paints.
870
1036
  */
871
1037
  caretColor?: string;
872
1038
  /**
@@ -883,19 +1049,8 @@ interface TextStyle {
883
1049
  underline?: boolean;
884
1050
  /** Default `false`. */
885
1051
  strikethrough?: boolean;
886
- /**
887
- * Outline painted over the glyph fill. Omitted (the default) means no
888
- * outline — there is no such thing as a default text stroke.
889
- *
890
- * Only glyphs on the outline tier are stroked: above
891
- * `textOutlineMinScreenSize` a glyph is a real `PolygonPath`, so it gets
892
- * the ordinary tessellated ribbon with real joins, caps and miters, in any
893
- * paint. Below it a glyph is a sampled distance field with no geometry to
894
- * stroke, and it renders unstroked rather than approximated. `width` is in
895
- * world units, like every other stroke in the kit — it does not scale with
896
- * `fontSize`.
897
- */
898
- stroke?: Stroke;
1052
+ /** Default `false`. */
1053
+ overline?: boolean;
899
1054
  }
900
1055
 
901
1056
  /**
@@ -910,10 +1065,15 @@ interface TextStyle {
910
1065
  * override the node-level value (`letterSpacing: 0` on a run is an override,
911
1066
  * not an absence — it zeroes inherited tracking).
912
1067
  *
913
- * `underline` / `strikethrough` are *additive*, like `bold`/`italic`: a run
914
- * can turn a decoration on but never off, so they resolve as
915
- * `run.x || style.x` and not `run.x ?? style.x`. See the header of
1068
+ * `underline` / `strikethrough` / `overline` are *additive*, like
1069
+ * `bold`/`italic`: a run can turn a decoration on but never off, so they
1070
+ * resolve as `run.x || style.x` and not `run.x ?? style.x`. See the header of
916
1071
  * `runs/rangeStyle.ts` for why the model collapses the tri-state.
1072
+ *
1073
+ * `script` is folded the same way the toggles are, into the two primitives it
1074
+ * is a preset over: `baselineShift` and `fontScale`. Both come out as one
1075
+ * world-unit `baselineShift` and a final `fontSize`, so layout never learns
1076
+ * that superscripts exist — it places a run against a baseline and an offset.
917
1077
  */
918
1078
 
919
1079
  /** A run with every style resolved against the node's text style — no
@@ -934,6 +1094,17 @@ interface ResolvedRun {
934
1094
  underline: boolean;
935
1095
  /** Draw a rule through this run's x-height. Additive over the node style. */
936
1096
  strikethrough: boolean;
1097
+ /** Draw a rule above this run's ascent. Additive over the node style. */
1098
+ overline: boolean;
1099
+ /**
1100
+ * How far this run sits off the line's shared baseline, in world units;
1101
+ * positive raises. 0 for ordinary text.
1102
+ *
1103
+ * Already multiplied out against the inherited font size, and already
1104
+ * carrying whatever `script` asked for — layout adds it to a baseline and
1105
+ * asks nothing about where it came from.
1106
+ */
1107
+ baselineShift: number;
937
1108
  }
938
1109
 
939
1110
  /**
@@ -952,7 +1123,7 @@ type TextVerticalAlign = 'top' | 'center' | 'bottom';
952
1123
  * calls getProgramSource() and compiles the result. This keeps registerProgram
953
1124
  * GL-context-agnostic — identical pattern to registerFont storing ImageBitmap.
954
1125
  *
955
- * Convention §9: module-level state = source strings only; compiled GL
1126
+ * Module-level state = source strings only; compiled GL
956
1127
  * programs live on each renderer's programRegistry (Map<id, ShaderProgram>).
957
1128
  *
958
1129
  * Lifecycle: program sources live for the module lifetime. No unregister in v1.
@@ -1063,6 +1234,21 @@ interface ImageDrawCommand {
1063
1234
  * device pixels as hard squares — required by anything magnifying a
1064
1235
  * framebuffer readback, where blur destroys the point of the readback. */
1065
1236
  sampling?: 'linear' | 'nearest';
1237
+ /** Sub-rectangle of `image` to draw, in bitmap pixels from the top-left.
1238
+ * Omitted draws the whole bitmap. Not range-checked: a rect past the edge
1239
+ * samples outside [0..1], which CLAMP_TO_EDGE smears. With
1240
+ * `sampling: 'linear'` the filter reaches half a texel beyond `source`, so
1241
+ * atlas frames need a gutter (see `SpriteSheet.spacing`) or `'nearest'`. */
1242
+ source?: {
1243
+ x: number;
1244
+ y: number;
1245
+ w: number;
1246
+ h: number;
1247
+ };
1248
+ /** Mirror the sampled region within the destination rect. The quad does not
1249
+ * move — a flipped draw covers exactly the pixels an unflipped one does. */
1250
+ flipX?: boolean;
1251
+ flipY?: boolean;
1066
1252
  }
1067
1253
  /**
1068
1254
  * Custom shader draw command. The renderer generates a quad over `bounds`
@@ -1087,4 +1273,4 @@ interface ShaderDrawCommand {
1087
1273
  };
1088
1274
  }
1089
1275
 
1090
- export type { Bounds as B, DrawCommand as D, History as H, Node as N, Op as O, PathDrawCommand as P, Scene as S, View as V, ViewportDims as a, NodeId as b, Path as c };
1276
+ export type { Bounds as B, DrawCommand as D, History as H, Node as N, Op as O, Path as P, Scene as S, View as V, ViewportDims as a, PathDrawCommand as b, NodeId as c };