@weasel-js/core 1.4.0 → 1.4.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,144 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - bfb0595: Run the last four drags in the kit on `openPointerSession`. Capture, pointer identity, teardown and recovery from a release that never arrives are now decided in one place for every pointerdown-to-pointerup lifecycle in the kit.
8
+
9
+ Fix a drag that silently dropped its commit. `openPointerSession` treated `lostpointercapture` as the end of the gesture, and Chrome releases capture implicitly a beat *before* it delivers `pointerup` — so a release already on its way arrived after the session had torn down its listeners, and the gesture ended as a cancel instead of a commit. Roughly three drags in four were lost this way in one measured consumer. Losing capture now ends a session only once the origin has left the document, which is the case the rule was written for: the session listens on the document, so capture is what retargets events, not what delivers them.
10
+
11
+ The gesture dispatcher opens a session per held pointer instead of tracking pointers itself. Two behavior changes come with that: a drag released outside the canvas now ends, where before only pointer capture made that work; and a fresh press on a pointer still believed held cancels the stale gesture rather than committing it at the new press's coordinates, since where it actually ended is unknown.
12
+
13
+ Two small breaking changes. `ThresholdDragOptions.onCancel` now fires only when a gesture ends without a release — a release below the threshold calls the new `onClick`. And in labkit, `useDragDrop`'s `startDrag` and `Palette`'s `onDragStart` take the React pointerdown event in place of a `Point`; a cancelled palette drag now drops nothing, where before it had no cancel path at all.
14
+
15
+ `startThresholdDrag` also takes an `origin` element, for a list whose grabbed row unmounts mid-drag and drops capture with it. `useReorderDragList` uses it and no longer carries its own copy of the threshold logic.
16
+ - 3b07b13: A scene write repaints the canvas instead of re-rendering `SceneCanvas`.
17
+
18
+ `SceneCanvas` held a `useSyncExternalStore` on the scene's version whose only
19
+ effect was to force a render — it discarded the value. Every mutation therefore
20
+ committed the whole canvas subtree, including for hosts that had asked not to
21
+ be re-rendered: `useScene(..., { subscribe: false })` gated the host's own
22
+ subscription but was invisible to `SceneCanvas`, which subscribed anyway. A
23
+ frame loop writing poses paid a React render per write.
24
+
25
+ It now subscribes for a repaint, the way the pose-override channel beside it
26
+ already did. The layers read the scene at paint time and `contentVersion` is a
27
+ getter, so nothing about drawing the new content needed the commit.
28
+
29
+ **This changes when scene-derived DOM updates.** DOM now lands on the render of
30
+ whoever subscribed for it, rather than riding along on the canvas's. In practice
31
+ every consumer already subscribes: `useScene` does by default, `SelectionPanel`
32
+ subscribes itself, and panels an app hangs beside the canvas re-render with the
33
+ host that owns the scene. A consumer that renders node data as DOM while
34
+ holding no subscription of its own — reading a `Scene` it got from somewhere
35
+ other than `useScene`, and relying on the canvas to re-render it — needs to
36
+ subscribe. Selection is unaffected: it reaches the render body through
37
+ `useSelection`'s own store subscription and still commits.
38
+
39
+ `docs/concepts.md` said a scene change leads with DOM. It no longer does, and
40
+ that section is rewritten.
41
+ - 8e9eb1d: Export `SpritesDrawCommand` and `SPRITE_STRIDE` from the main barrel.
42
+
43
+ They shipped in 1.4.1 reachable only from the `@weasel-js/core/renderer` subpath, because `src/index.ts` re-exports the renderer by name rather than with a star and only the renderer's own barrel was updated. Every other `DrawCommand` variant is nameable from `@weasel-js/core`, and a consumer cannot pack a sprite run without the stride constant.
44
+
45
+ `index.barrel.test.ts` now asserts that every `*DrawCommand` the renderer barrel names is named on the main barrel too, so the next variant cannot land half-exported.
46
+ - Updated dependencies [c93aa91]
47
+ - @weasel-js/text@1.4.2
48
+ - @weasel-js/cursor@1.4.2
49
+ - @weasel-js/font@1.4.2
50
+ - @weasel-js/geom@1.4.2
51
+ - @weasel-js/gestures@1.4.2
52
+ - @weasel-js/history@1.4.2
53
+ - @weasel-js/modes@1.4.2
54
+ - @weasel-js/paint@1.4.2
55
+
56
+ ## 1.4.1
57
+
58
+ ### Patch Changes
59
+
60
+ - dcef92c: Restore a displaced action when the registrant that displaced it unregisters.
61
+
62
+ The actions registry held one `Action` per id, so two `<SceneCanvas>` instances under one `<ActionsProvider>` collided: the second to mount displaced the first's `viewport.wheelPan` / `viewport.zoom` / `viewport.pinchZoom`, and its teardown then deleted the entry outright rather than uncovering what it had displaced. The canvas still on screen was left with no viewport actions at all — wheel pan and Cmd+wheel / Cmd+- / Cmd+0 dead, with no error. The `vertex-widths`, `curve-lab` and `rotated-resize-math` demos all mount several canvases this way.
63
+
64
+ `register` now stacks registrants per id, newest live, and the unregister it returns takes its own entry out wherever that entry has since ended up. So a displaced registrant becomes live again when the one above it leaves, and a registrant that was already displaced still disturbs nothing when it goes. Last-writer-wins is unchanged while both are mounted.
65
+
66
+ This closes the same hole for every other hook that registers a fixed id into a shared registry — `useStandardActions`, `useToolActions`, `useKeybindings`, `useContributions` — and lets `useActionsPropResolver` drop its restore hack, which re-registered a stale snapshot on cleanup and never took it off again.
67
+
68
+ `unregister(id)` is unchanged and still drops every registrant of that id: it is the "this action should not exist" door, not a release.
69
+ - 73039aa: Collapse four duplicated helpers and drop three dead modules.
70
+
71
+ `Badge`'s shape-control table lived twice — once in `shapeControls.ts`, which nothing imported, and once re-declared inside the stories, which is the copy that rendered. The stories now import the module, so the badge shape defaults have one definition again. `Badge`, `Shield` and `Perforated` shared eleven identical lines of ResizeObserver measurement for the same viewBox-unit conversion; that is now `useSvgBox`.
72
+
73
+ `composeSelectionPose` and `makeContainerAwareBoundsResolver` each carried their own copy of the leaf walk, including the rule that keeps an empty container from contributing bounds — one function now, so the rule can be fixed in one place.
74
+ - b91a8dd: Coalesce consecutive image draws into one batch, and add `kind: 'sprites'` for a run handed over packed.
75
+
76
+ `drawImage` was one `drawElements` per command, so an atlas-backed wall of thumbnails paid a draw call per thumbnail: 20,000 of them cost 51ms a frame on an M2 Max, three frames' budget for one frame's work. Consecutive image quads now stage into an `ImageBatch` and flush as a single draw — 10.6ms for that frame, and 0.094ms where it was 3.05 at 512 quads.
77
+
78
+ Nothing changes for a consumer emitting `kind: 'image'`. A run merges across a group transform, a group alpha and a per-command opacity, because all three ride the vertices — opacity through a new `a_opacity` attribute, which is exact rather than conditional since `u_opacity` multiplies the alpha after the color matrix. A run breaks on a different bitmap, a different `sampling`, a clip boundary, or a color matrix.
79
+
80
+ `SpritesDrawCommand` is for the case where even a command object per quad is too much. It carries one bitmap and a `Float32Array` of `SPRITE_STRIDE` floats a sprite — `dx, dy, dw, dh, sx, sy, sw, sh, opacity`, source in bitmap pixels, a negative `sw`/`sh` mirroring that axis — and stages through the same run, so a packed run and the image commands around it merge into one draw. It takes 20,000 sprites to 0.79ms. Below a few thousand a plain run of image commands merges into the same draw and reads better.
81
+
82
+ Ring slots come in tiers sized to the flush. The driver's write hazard is per buffer object, so a one-quad flush into a slot sized for 256 waits on the whole thing — a frame of 20,000 quads that nothing merges cost 92ms against one slot size and 53ms against tiered ones, matching the unbatched path it replaces.
83
+ - caad52f: Wire the hand tool's `inertia` and `axis` options, which accepted a full
84
+ config and did nothing.
85
+
86
+ Both are now binding params on `viewport.dragPan`, so any consumer binding
87
+ that action gets them — not only `useHandTool`. `axis` drops one component of
88
+ every pan delta. `inertia` coasts the view after release through a new
89
+ optional `view.decay` dep, which `<SceneCanvas>` wires from `useDecayLoop`;
90
+ where no such dep is published the pan simply lands, as before.
91
+
92
+ `useVelocityTracker`'s logic is now also available hook-free as
93
+ `createVelocityTracker`, because an Action descriptor is a static object and
94
+ cannot call hooks. `InertiaConfig` moves next to `DecayLoopConfig` and
95
+ `PanBounds` in `useDecayLoop`; `useHandTool` re-exports it, so the
96
+ `HandToolInertiaConfig` alias on the barrel is unchanged.
97
+
98
+ `viewport.wheelPan` gains the same `axis` option through a
99
+ `makeViewportWheelPanAction` factory, matching its `makeViewportZoomAction` /
100
+ `makePinchZoomAction` siblings, and `<SceneCanvas viewport={{ pan: { axis } }}>`
101
+ reaches it. Two things fall out: `viewport={{ inertia: true }}` was documented
102
+ as "on with defaults" but produced no inertia — only the object form did — and
103
+ `ParallaxDemo` can drop the `setViewXOnly` commit clamp it used because the
104
+ axis options did not work.
105
+ - 0b0f13f: Put every drag in the kit on one pointer lifecycle, and recover the releases the DOM does not deliver.
106
+
107
+ Fourteen pointerdown-to-pointerup lifecycles each answered capture, pointer identity, teardown and lost-pointer recovery for themselves. They now run on `openPointerSession`: `Slider`, `BandEditor`, `Timeline`'s `Lane` and `Ruler`, `LayeredCurveEditor`, `ResizeHandle`, `useReorderDragList`, `MinimapCanvas`, labkit's `LayerList`, `usePanZoom`, `useOrbit` and `FloatingPanel`. A drag released over another window, or whose element unmounts mid-gesture, now ends instead of hanging in flight.
108
+
109
+ A third recovery rule joins the two that shipped with the primitive: a fresh press on a pointer still believed held reports `'superseded'`, because the release landed somewhere that never told us and the pointer never came back for the missed-release rule to see. Without it a stale session steers the next press. `useGestureDispatcher` applies the same rule to its own multi-pointer lifecycle.
110
+
111
+ Breaking: hooks that drove their drag through returned React props no longer return them, because the session owns the gesture from the press.
112
+
113
+ - `useReorderDragList`'s `containerProps` is `{ ref }` only; `onPointerMove` / `onPointerUp` / `onPointerCancel` are gone. It gains `onPress(id, mods)` — a press released without engaging a drag, fired for locked rows too, with the modifiers held at press. That is the click-vs-drag decision consumers previously had to reconstruct by sampling drag state before forwarding the pointerup, which no longer works now that the session ends first.
114
+ - labkit's `PanZoomHandlers` and `OrbitHandlers` lose `onPointerMove` / `onPointerUp`. `usePanZoom` gains `onTap` for the same reason.
115
+
116
+ The five `@weasel-js/ui` drag surfaces pass `capture: false` deliberately and now assert it: capture retargets `pointerup` to the capture element and kills the click on consumer-rendered content inside a slider thumb, a band body, or curve-editor chrome.
117
+ - 00af9ac: Add `openPointerSession`, and put the kit's drag lifecycles on it.
118
+
119
+ `useHandleDrag`, `startThresholdDrag` and `useDragHandle` each owned a pointerdown-to-pointerup lifecycle and each answered the same four questions differently. Capture: two took it untry'd, one never took it. Listeners: one on the element, two on `document`. Pointer identity: none of the three filtered by `pointerId`, so a second finger drove and could end a drag in progress. Teardown on unmount: one had none, one had it for half its lifecycle.
120
+
121
+ None of them — nor the dispatcher — handled `lostpointercapture`, and none read a `pointermove` with no button held as the release it missed. So a drag whose pointer left the element, or whose capturing element was removed mid-gesture, hung in flight with no end and no cancel.
122
+
123
+ `openPointerSession(origin, downEvent, callbacks)` now decides all of it once: capture on the origin, listeners on the document so a removed element cannot strand the gesture, every event filtered to its own pointer, `lostpointercapture` and the missed release both closing the session, and one `cancel()` for unmount, Escape or blur. The missed-release rule disarms itself when the press reports no button state, so synthesized events do not read as instant releases.
124
+
125
+ `useGestureDispatcher` keeps its own multi-pointer lifecycle — one canvas listener set keyed by `pointerId` is the right shape for multitouch — but takes both recovery rules from the same module, so there is one implementation of each rather than two that drift.
126
+
127
+ Breaking, in `useHandleDrag`: `onEnd` now fires only on a real release and receives `{ point, moved, event }` instead of a bare event; a cancelled gesture reports through the new `onCancel(reason)`. The old signature made every commit-on-end consumer sniff `e.type === 'pointercancel'` to tell an edit from an abandoned drag, and hold its own ref to recover the end position — `GradientEditor` does neither now.
128
+ - 9b9224c: Remove `space` from `ToolModifiers`.
129
+
130
+ Breaking for anyone constructing a `ToolModifiers` literal: the field is gone and an object still carrying it is now an excess property. Reading `ctx.modifiers.space` was already meaningless — `Canvas` hardcoded `false` at both construction sites, so the field never once reported a held space bar.
131
+
132
+ Nothing needed it. Space-for-hand is armed by the tool's own `hotkey: 'space'` declaration, which routes through `tool.offhand` and the dispatcher's key-held lifecycle and never consults `ToolCtx`. The field existed for a mid-gesture read that no tool ever wrote or performed.
133
+ - @weasel-js/cursor@1.4.1
134
+ - @weasel-js/font@1.4.1
135
+ - @weasel-js/geom@1.4.1
136
+ - @weasel-js/gestures@1.4.1
137
+ - @weasel-js/history@1.4.1
138
+ - @weasel-js/modes@1.4.1
139
+ - @weasel-js/paint@1.4.1
140
+ - @weasel-js/text@1.4.1
141
+
3
142
  ## 1.4.0
4
143
 
5
144
  ### Minor Changes
@@ -129,7 +129,7 @@ interface SolidPaint {
129
129
  opacity?: number;
130
130
  }
131
131
  /** DrawCommand variants implemented through step 6. */
132
- type DrawCommand = PathDrawCommand | GroupDrawCommand | TextDrawCommand | ImageDrawCommand | ShaderDrawCommand;
132
+ type DrawCommand = PathDrawCommand | GroupDrawCommand | TextDrawCommand | ImageDrawCommand | SpritesDrawCommand | ShaderDrawCommand;
133
133
  /** Draw a path, filled and/or stroked. The workhorse command: every shape the
134
134
  * kit draws that is not text, an image, or a custom shader is one of these. */
135
135
  interface PathDrawCommand {
@@ -230,6 +230,38 @@ interface ImageDrawCommand {
230
230
  flipX?: boolean;
231
231
  flipY?: boolean;
232
232
  }
233
+ /** Floats per sprite in `SpritesDrawCommand.sprites`. */
234
+ declare const SPRITE_STRIDE = 9;
235
+ /**
236
+ * Draw many quads sampling one bitmap — an atlas, a sprite sheet, a wall of
237
+ * thumbnails. The same picture as a run of `ImageDrawCommand`s the renderer
238
+ * would coalesce anyway, handed over already packed so it never walks a
239
+ * command object per quad.
240
+ *
241
+ * Reach for it past a few thousand sprites. Below that a plain run of image
242
+ * commands merges into the same single draw and reads better; the packed form
243
+ * exists because at 20,000 the object walk is about half the frame.
244
+ *
245
+ * The sprites are one run: they share a texture, a filter, and whatever group
246
+ * transform, alpha, color matrix and clip are live, exactly as a merged run of
247
+ * image commands would. Anything varying per sprite is in the array.
248
+ */
249
+ interface SpritesDrawCommand {
250
+ kind: 'sprites';
251
+ image: ImageBitmap;
252
+ /** Magnification filter for the whole run, as `ImageDrawCommand.sampling`. */
253
+ sampling?: 'linear' | 'nearest';
254
+ /**
255
+ * `SPRITE_STRIDE` floats per sprite:
256
+ * `dx, dy, dw, dh, sx, sy, sw, sh, opacity`.
257
+ *
258
+ * Destination is in the group's coordinates; source is in bitmap pixels,
259
+ * like `ImageDrawCommand.source`. A negative `sw` or `sh` mirrors that axis
260
+ * within the source rect, which is what `flipX` / `flipY` do. A trailing
261
+ * partial sprite is ignored.
262
+ */
263
+ sprites: Float32Array;
264
+ }
233
265
  /**
234
266
  * Custom shader draw command. The renderer generates a quad over `bounds`
235
267
  * and dispatches the consumer's fragment shader with the kit's vertex prelude.
@@ -253,4 +285,4 @@ interface ShaderDrawCommand {
253
285
  };
254
286
  }
255
287
 
256
- export { type DrawCommand as D, type GroupDrawCommand as G, type ImageDrawCommand as I, type Mat3 as M, type PathDrawCommand as P, type ShaderDrawCommand as S, type TextDrawCommand as T, type ShaderProgramHandle as a, type ShaderUniform as b, type SolidPaint as c, mat3 as m, registerProgram as r };
288
+ export { type DrawCommand as D, type GroupDrawCommand as G, type ImageDrawCommand as I, type Mat3 as M, type PathDrawCommand as P, SPRITE_STRIDE as S, type TextDrawCommand as T, type ShaderDrawCommand as a, type ShaderProgramHandle as b, type ShaderUniform as c, type SolidPaint as d, type SpritesDrawCommand as e, mat3 as m, registerProgram as r };