@weasel-js/core 1.4.2 → 1.4.3

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 (43) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/dist/{DrawCommand-Ch8W63oQ.d.ts → DrawCommand-CD-ug3d9.d.ts} +45 -1
  3. package/dist/{stroke-Dq8nL4A2.d.ts → builtins-CtGORLCG.d.ts} +51 -2
  4. package/dist/{chunk-2KKYDDDD.js → chunk-4RJP2N2L.js} +43 -21
  5. package/dist/chunk-4RJP2N2L.js.map +1 -0
  6. package/dist/{chunk-MXFSHJOM.js → chunk-D4AAWR64.js} +4 -4
  7. package/dist/{chunk-MXFSHJOM.js.map → chunk-D4AAWR64.js.map} +1 -1
  8. package/dist/{chunk-ZB7UYJVG.js → chunk-DOBSZOPR.js} +3 -3
  9. package/dist/{chunk-ZB7UYJVG.js.map → chunk-DOBSZOPR.js.map} +1 -1
  10. package/dist/{chunk-TORKRNFY.js → chunk-LEURURX3.js} +68 -5
  11. package/dist/chunk-LEURURX3.js.map +1 -0
  12. package/dist/{chunk-Y5KS66G7.js → chunk-ORZNKVXM.js} +3 -3
  13. package/dist/{chunk-Y5KS66G7.js.map → chunk-ORZNKVXM.js.map} +1 -1
  14. package/dist/{chunk-67KE7SDP.js → chunk-T3UQ3F6R.js} +4 -4
  15. package/dist/{chunk-67KE7SDP.js.map → chunk-T3UQ3F6R.js.map} +1 -1
  16. package/dist/{chunk-KIVJXUYE.js → chunk-WYPSGIYS.js} +1058 -503
  17. package/dist/chunk-WYPSGIYS.js.map +1 -0
  18. package/dist/clipboard.d.ts +2 -2
  19. package/dist/clipboard.js +3 -3
  20. package/dist/clone.d.ts +2 -2
  21. package/dist/clone.js +3 -3
  22. package/dist/{geometry-Dtt_k6Dq.d.ts → geometry-CHR36Ub_.d.ts} +3 -3
  23. package/dist/{grid-nnXU4VjN.d.ts → grid-Bw8cSde-.d.ts} +1 -1
  24. package/dist/index.d.ts +117 -232
  25. package/dist/index.js +7 -7
  26. package/dist/insert.d.ts +3 -3
  27. package/dist/move.d.ts +4 -4
  28. package/dist/move.js +2 -2
  29. package/dist/{options-CdFl510T.d.ts → options-DYfUlZxk.d.ts} +1 -1
  30. package/dist/{pointSnapToGrid-Dmthv94u.d.ts → pointSnapToGrid-D1bRdq-j.d.ts} +2 -2
  31. package/dist/{registry-BwY_DxJM.d.ts → registry-DnoTWGED.d.ts} +98 -8
  32. package/dist/renderer.d.ts +32 -4
  33. package/dist/renderer.js +7 -7
  34. package/dist/resize.d.ts +4 -4
  35. package/dist/routing.d.ts +6 -6
  36. package/dist/routing.js +1 -1
  37. package/dist/{types-miXHGrZM.d.ts → types-BJqsTlXl.d.ts} +1 -1
  38. package/dist/{types-XBcDEp3Y.d.ts → types-CoVTbo_y.d.ts} +9 -0
  39. package/dist/{types-DIQAisSG.d.ts → types-H7o6MaPo.d.ts} +20 -1
  40. package/package.json +9 -9
  41. package/dist/chunk-2KKYDDDD.js.map +0 -1
  42. package/dist/chunk-KIVJXUYE.js.map +0 -1
  43. package/dist/chunk-TORKRNFY.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,380 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 2de5a37: A canvas opting out of an action no longer takes it away from its siblings.
8
+
9
+ `useViewportActions` answered `pinchZoom: false` with
10
+ `reg.unregister('viewport.pinchZoom')`, which drops *every* registrant of that
11
+ id — so one canvas opting out killed pinch-zoom on a sibling that asked for it,
12
+ and nothing put it back when the opting-out canvas unmounted.
13
+ `actions={{ id: null }}` went through the same door. Registration has been
14
+ per-registrant since the registrant stack landed; suppression was not, and a
15
+ shared registry had nowhere to hang "not for me".
16
+
17
+ `ActionsScope` is that place. It is a view of the registry in scope with its own
18
+ mute set: `register`, `unregister` and the dispatcher / dep-registry slots pass
19
+ straight through to the shared store, so cross-canvas sharing is untouched,
20
+ while `list`, `trigger` and `begin` skip what this scope muted. Scopes nest.
21
+ `<ActionsProvider>` is itself a scope, so a lone canvas needs no wrapper, and a
22
+ `<SceneCanvas>` deferring to a host provider now mounts one.
23
+
24
+ `ActionsRegistry.mute(id)` returns a release; a scope drops everything it muted
25
+ when it unmounts. `unregister` keeps its old meaning — "this action should not
26
+ exist" — and is unchanged.
27
+
28
+ Breaking for anyone implementing `ActionsRegistry` themselves: `mute` is a new
29
+ required method.
30
+ - 10e1ab6: A bare `<Canvas>` now repaints when its selection changes, without a wrapper
31
+ asking it to.
32
+
33
+ The redraw tripwire — the layout effect whose dep array is meant to name every
34
+ paint input that arrives on a render — did not name the overlay-aware state the
35
+ layer helpers expose. Selection, preview poses and the chrome derived from them
36
+ were written during render into a ref, so changing the selection prop repainted
37
+ nothing. It looked correct only because `<SceneCanvas>` calls `requestRedraw()`
38
+ by hand for its own data sources.
39
+
40
+ The tripwire now carries the memoized chrome state, which re-derives on exactly
41
+ the selection, bounds and preview inputs the helpers read, plus the chrome-caps
42
+ predicate. A render that changed none of them still paints nothing.
43
+ - eb0d6ce: Undo of a Delete now brings back everything the delete cascaded — a node's
44
+ dependents, and their own subtrees — not just the node that was named.
45
+
46
+ Deleting a node takes everything deriving from it, so deleting a box takes the
47
+ edges drawn from it. The delete op only ever snapshotted the subtree, so undo
48
+ re-inserted the box alone and the edges stayed gone. The op now snapshots the
49
+ whole set before removing, and re-inserts it parent-first, each node at the slot
50
+ it held.
51
+
52
+ New public read: `scene.removalClosure(ids)` answers what `removeMany(ids)`
53
+ would take, without taking it. The scene owns the cascade relations, so a caller
54
+ that rebuilds the walk from `dependsOn` and `children` goes stale the moment a
55
+ relation is added — `buildDeleteOps` asked its own copy of that question and is
56
+ now on this one.
57
+
58
+ The delete op reads it through an optional `getRemovalClosure(ids)` on the
59
+ adapter, alongside `getChildren`. An adapter that cascades along nothing but the
60
+ subtree can leave it out and behaves as before; one that cascades further has to
61
+ answer, or the nodes it takes are absent from the snapshot. The scene-backed
62
+ adapters answer it.
63
+
64
+ `insertNode` on the scene-backed adapters now forwards the function-valued
65
+ fields — `dependsOn`, `derivePath` and `clipFromPose` — so a restored edge
66
+ derives again instead of coming back as a static path, and a restored container
67
+ still clips. The delete op's serialized `descendants` argument is now
68
+ `cascaded`, and carries the node itself alongside what went with it.
69
+ - 75969f6: Full-screen effect passes: a group's children render into a texture, shader
70
+ passes run over it, and the result composites back where the group sits.
71
+
72
+ Until now the renderer could draw over the frame but never transform it —
73
+ nothing sampled what had already been drawn, so blur, bloom and distortion were
74
+ unreachable and the only recourse was a CSS `filter` on the `<canvas>`, which is
75
+ the browser compositing on the kit's behalf.
76
+
77
+ `GroupDrawCommand.effects` is the primitive. It is the one field there that does
78
+ not accumulate down the group stack: it is a render-target boundary. The
79
+ children draw into a buffer with a stencil of its own, each effect reads the
80
+ previous one's output, and the composite applies the group's `transform`,
81
+ `alpha` and `colorMatrix` plus whatever clip encloses it — so the enclosing clip
82
+ clips the result rather than the pixels an effect reads, and nested clips inside
83
+ start from a fresh depth budget.
84
+
85
+ `RenderLayer.effects` is that field folded in at `drawOneLayer`, which is where
86
+ every layer already gets wrapped in a group. A blur there blurs one layer and
87
+ leaves the chrome drawn above it sharp, which is the thing the CSS filter cannot
88
+ do.
89
+
90
+ `blur({ radius })` and `vignette({ amount })` ship as built-ins; both return
91
+ `Effect[]` because a separable blur is honestly two passes.
92
+ `registerEffect(id, frag)` registers your own — it is `registerProgram` with the
93
+ effect vertex shader, and using the custom-shader one instead compiles, runs,
94
+ and samples the source upside down.
95
+
96
+ Nothing is allocated until a group declares an effect, so a canvas without them
97
+ carries no offscreen buffer. Buffers are drawing-buffer sized, pooled per
98
+ renderer, and dropped on resize, dispose and context loss.
99
+
100
+ Demo: "Full-screen effect passes", with visual baselines blurred and sharp.
101
+ - 0d40f94: A layer whose `draw` throws now paints nothing and names itself in the console,
102
+ instead of taking the frame down with it.
103
+
104
+ The paint runs on the frame loop, so a throw out of `draw` surfaced as an
105
+ uncaught `requestAnimationFrame` error on the window: the canvas went blank and
106
+ stayed stale until something unrelated asked for a redraw. `drawOneLayer` — the
107
+ one path both the canvas and a viewport's inner pass go through — catches it,
108
+ drops that layer for the frame, and paints the rest.
109
+
110
+ `drawLayers` and `drawOneLayer` take an `onLayerError` callback for a consumer
111
+ that wants to route the failure somewhere of its own; the default reports the
112
+ layer id and the error to `console.error`. The layer's cached commands are
113
+ dropped with it, so the next frame is a real re-attempt rather than a stale tree
114
+ served under fresh deps.
115
+ - 713f98a: A layout strategy decides what happens to a child released outside every
116
+ container.
117
+
118
+ When no container accepted a drag, the move action committed the child wherever
119
+ the pointer stopped, and the source layout had no say — a grid could not close
120
+ the gap, and a container that only means to arrange its own children could not
121
+ take one back.
122
+
123
+ `LayoutStrategy.releaseDrop` is that say. It sees the source container, its
124
+ remaining children and the dragged child in world coordinates, and returns ops
125
+ that replace the free-space commit. An empty array leaves the container alone,
126
+ which snaps the child home, since the drag only ever wrote previews. `null` —
127
+ and a strategy that does not implement the method — leaves the drop where the
128
+ pointer left it, exactly as before.
129
+ - 85f4a21: Dragging several selected nodes into a layout container now runs the layout,
130
+ instead of falling through to a plain translate. Three nodes dropped on a grid
131
+ fill three cells.
132
+
133
+ The selection lands as a unit. One container is chosen, from the center of the
134
+ whole dragged group rather than each child's own — hit-testing per child would
135
+ scatter a selection straddling two containers — and it has to accept every
136
+ member: `acceptsDrop` is asked once per dragged child, and one refusal takes the
137
+ container out of the running. The children are then placed one at a time, in
138
+ selection order, and each placement sees the container state the previous one
139
+ produced. A strategy that packs, stacks or displaces therefore sees the group
140
+ arrive the same way it would see three separate drags. If any child has no
141
+ target, the whole drop is refused rather than split between two homes.
142
+
143
+ Each child is snapped at its own position, displaced by however far the pointer
144
+ sits from the selection's center, so the group keeps its shape as it lands. A
145
+ single-node drag still probes at the pointer exactly as before.
146
+
147
+ The rest follows the selection: the live preview reflows the destination and
148
+ every source container the selection left, one pass per container with all of
149
+ its departing children withdrawn at once; and the commit emits a reparent op per
150
+ child that changed parent, every reparent before every drop.
151
+
152
+ `releaseDrop` — a container's say over its own child released into open space —
153
+ is now per source container too, so a mixed selection resolves per child: the
154
+ grid's own child goes home, and a free node in the same selection keeps the
155
+ translate it would have had on its own.
156
+ - 4bb0341: A parallax plane draws its source layers through `drawOneLayer`, so their
157
+ `space` means something.
158
+
159
+ `createParallaxLayer` called `layer.draw(...)` directly where every other path
160
+ through a view — the canvas itself, a viewport node's inner pass — goes through
161
+ `drawOneLayer`. A `space: 'world'` source therefore came out unprojected, and the
162
+ only way to see anything was for the source to pre-project by hand while
163
+ declaring a space it did not draw in. ParallaxDemo's four layers did exactly
164
+ that, and their labels were lies.
165
+
166
+ Now a world-space source is wrapped in the plane's inner view and a screen-space
167
+ one is passed through, the same rule that holds everywhere else. The plane
168
+ itself stays `space: 'screen'` — its children carry whatever transform they
169
+ need, and the outer canvas must add none.
170
+
171
+ The demo drops its `project` helper and emits world coordinates; the committed
172
+ `parallax` visual baseline passes unchanged.
173
+ - e0d5580: `pathHitTest` reads curves and holes instead of throwing on one and ignoring
174
+ the other.
175
+
176
+ Its vertex extractor walked `M` and `L` only and threw on any bezier command,
177
+ and it stopped at the first `Z`, so a path's second contour was never
178
+ considered. The throw was reachable in ordinary use: `sceneAdapter`'s
179
+ `nodeBoundsPassClips` calls `pathIntersectsRect` on every ancestor clip, so a
180
+ container with a curved `clipFromPose` crashed the hit-test walk.
181
+
182
+ `pathContainsRect`, `pathIntersectsRect`, `pathContainsPolygon` and
183
+ `pathIntersectsPolygon` now treat a `PolygonPath` as its filled region. Whether
184
+ a point is inside comes from `pointInPath`, so beziers flatten and `fillRule`
185
+ decides — a donut's hole is outside the shape under `evenodd` and inside it
186
+ under `nonzero`, and the four predicates agree with `pathContainsPoint` on
187
+ which. Each takes the same optional flattening tolerance `pointInPath` does.
188
+
189
+ Two answers change for paths that already worked. A rect sitting in a hole is
190
+ no longer reported as contained or intersecting, and containment now fails when
191
+ a contour reaches into the rect at all rather than only when it crosses an
192
+ edge.
193
+ - edf99d5: Undoing a removal from a persisted history brings `derivePath` and
194
+ `clipFromPose` back.
195
+
196
+ `kit:remove`'s snapshot cloned each node with its function-valued fields
197
+ attached, which works in-session and disappears the moment the history is
198
+ serialized. `dependsOn` survived the round-trip and repopulated the dependency
199
+ index, so a restored derived node looked wired up and never painted; a
200
+ container came back unclipped the same way.
201
+
202
+ The snapshot now carries the registry keys beside the nodes, and revert
203
+ re-resolves them exactly as `kit:add` does — warning, not throwing, when a key
204
+ is absent from the scene's registry.
205
+ - 2723cc7: `selectAll` no longer selects nodes on a hidden layer, so Cmd+A then Delete
206
+ cannot take content the user cannot see.
207
+
208
+ It walked `renderOrder()`, which is every node in the scene regardless of what
209
+ its layer's `visible` flag says. It now walks `renderOrderNodes()` when any
210
+ layer is hidden — the same sequence, carrying the layer each node sits on — and
211
+ keeps the cheaper id walk when every layer is visible.
212
+
213
+ A `scene` dep that answers neither `layers` nor `renderOrderNodes` behaves
214
+ exactly as before.
215
+ - 0ca0aca: Layer groups: several consecutive layers render into one buffer and share one
216
+ effect pass.
217
+
218
+ `RenderLayer.effects` runs over a single layer, which is the wrong picture as
219
+ soon as a pass reads neighboring pixels — `blur(A over B)` is not `blur(A) over
220
+ blur(B)` — and it costs a buffer and a pass chain per layer. A consumer wanting
221
+ the world blurred and the HUD sharp had no way to say that the world was more
222
+ than one layer.
223
+
224
+ `LayerGroup` is that surface: `{ id, layers, effects?, alpha?, colorMatrix? }`,
225
+ passed to `<Canvas>` / `<SceneCanvas>` as `layerGroups`. Members are named by
226
+ `RenderLayer.id`, the same names `layerOrder` and `layerVisibility` use, so a
227
+ group can take in kit-built layers as readily as consumer ones. `drawLayers`
228
+ brackets each run of consecutive members in one `kind: 'group'` command; the
229
+ renderer's existing offscreen path does the rest.
230
+
231
+ Only consecutive members share a buffer, because anything drawn between two of
232
+ them has to land between them. A group split across the render order is drawn as
233
+ one bracket per run, with a warning — the picture is right, the declaration
234
+ probably is not. A hidden member, and a member that paints nothing this frame,
235
+ break no run.
236
+
237
+ `effects` also accepts a thunk, re-read on every frame the canvas paints, so an
238
+ animating radius costs no React render. A group that declares no effects, alpha
239
+ or color matrix emits no wrapper at all, and a frame with no groups allocates
240
+ nothing.
241
+
242
+ The side-scroller load test now blurs its six world layers on a head knock and
243
+ leaves its HUD, callouts and ending card sharp; the CSS `filter` it used to
244
+ reach for is gone.
245
+ - 3583ca3: `useStandardActions` takes an `exclude` list, so a consumer can bind its own.
246
+
247
+ The hook registered a fixed descriptor list, which left no way to suppress an
248
+ individual kit action. A consumer wanting its own align or distribute
249
+ keybindings got the kit's as well, and the two competed for the same keys.
250
+
251
+ `exclude` names ids to leave unregistered; an id naming no kit action is
252
+ ignored, and changing the list re-registers. `KIT_STANDARD_ACTION_IDS` is the
253
+ full list in registration order, so the ids are discoverable rather than
254
+ something to read out of the source.
255
+ - fc16cac: `Stroke.paint` is optional, and a stroke without one paints nothing everywhere
256
+ rather than throwing.
257
+
258
+ Such a stroke is real: a property panel that writes one field onto a node with
259
+ no stroke — a width, a cap — materializes a whole stroke around it, and
260
+ documents written before that was fixed still hold them. The painters already
261
+ read one as no stroke. Every other reader dereferenced `paint` unguarded, so a
262
+ document holding one threw on SVG export, on copy, and out of any consumer
263
+ painter or overlay whose command reached the renderer directly.
264
+
265
+ The type says so now, which is what stops the next reader from assuming
266
+ otherwise. What each one does with an unpainted stroke:
267
+
268
+ - The renderer skips the stroke pass and paints the fill.
269
+ - The SVG serializer emits no `stroke` attributes at all, the way it already
270
+ does for an absent or zero-width stroke.
271
+ - Text layout keys it as no stroke, so an unpainted run groups with unstroked
272
+ ones instead of splitting a draw call, and does not get pulled onto the
273
+ outline tier to stroke nothing.
274
+ - `setStrokeOpacity` seeds the default stroke color to have something to set an
275
+ opacity on, keeping the width and joins already there.
276
+ - 6d4bbeb: WeaselDraw's SVG export drops what a hidden layer holds, matching what the
277
+ pixel path draws. It walked the whole container tree with no visibility gate,
278
+ so hiding a layer and exporting produced a file with the hidden content in it.
279
+
280
+ `SceneSource` gains an optional `isPainted(id)`; a node it refuses is skipped
281
+ along with everything under it, under an explicit `roots` override too — a
282
+ selection naming a hidden node still must not export it. A source that omits
283
+ the predicate emits everything, exactly as before.
284
+ - 995fde2: A text node's `data.fill: null` is now an explicit no-fill, so stroked-but-
285
+ unfilled text — outline-only display type — renders as such.
286
+
287
+ Every other node kind already read `null` that way. Text resolved it back to the
288
+ default black, because a `ResolvedRun` had to name a concrete `FillStyle` and
289
+ nothing downstream could skip the fill pass. `ResolvedRun.fill`,
290
+ `ResolvedTextStyle.fill` and `LaidOutGroup.fill` are now `FillStyle | null`, and
291
+ `TextPaint.fill: null` carries through to all three. Absent still means the
292
+ default black.
293
+
294
+ An unfilled run paints through its stroke alone, which only the outline tier can
295
+ lay down, so layout emits no atlas quads for one and the renderer skips the
296
+ glyph-fill mesh — an unfilled, unstroked run emits nothing at all, not even its
297
+ outline geometry. Underline, strikethrough and overline follow the fill: a rule
298
+ is a solid rect with no stroked counterpart, so an unfilled run draws none.
299
+ Nothing changes for text that has a fill.
300
+
301
+ Picking deliberately does not follow. `kit:text` still reports `filled: true`
302
+ for `fill: null`, because a text node's silhouette is its line boxes rather than
303
+ its glyph ink — reporting it unfilled would leave a word grabbable within a
304
+ stroke width of a box edge and nowhere near the letters.
305
+
306
+ `@weasel-js/svg` reads and writes SVG's own spelling of this: `<text
307
+ fill="none">` parses to `fill: null` instead of being dropped as absent, and a
308
+ text node with `fill: null` serializes as `fill="none"` rather than as SVG's
309
+ default black. `SvgTextNode.fill` widens to `FillStyle | null`.
310
+
311
+ The "Text outlines" demo has a Fill checkbox alongside its Stroke one; the two
312
+ off together is a node with no glyph paint at all.
313
+ - 6e4fb4d: A user layer survives `toJSON()` — its name, and the fact that it is a user
314
+ layer at all.
315
+
316
+ Every layer was written to a snapshot as `{ id, visible, locked }` and read back
317
+ as `kind: 'system'`, so reloading a document renamed nothing, showed nothing in
318
+ a layer list, and made `renameLayer` throw "cannot rename system layer" on a
319
+ layer the user had just created.
320
+
321
+ `SerializedLayer` is the snapshot's layer shape: the fields it always had, plus
322
+ an optional `kind` and `name`. A snapshot written before this carries neither
323
+ and loads as system layers, which is what every layer in it was.
324
+
325
+ `sceneFromJSON` now builds an empty scene and calls `loadState`, so the layer
326
+ stack is rebuilt by the one reader that knows how instead of by `createScene`,
327
+ which mints system layers only.
328
+ - b0fba6a: One wheel convention across the kit. Breaking: `computeWheelAction` changes
329
+ shape, and `useZoom` / `usePinchZoomTool` are gone.
330
+
331
+ Three public entry points answered the wheel and disagreed with each other.
332
+ `viewport.wheelPan` + `viewport.zoom` panned on a bare wheel and zoomed on
333
+ Cmd/Ctrl+wheel; `computeWheelAction` did the opposite, zooming on a bare wheel
334
+ and treating Cmd+wheel as a vertical scroll; `useZoom` zoomed on a bare wheel
335
+ and never panned. The two reducers also panned in screen pixels against a
336
+ `{ zoom, panX, panY }` state that is not a `View` and cannot be handed to
337
+ `view.set`.
338
+
339
+ The surviving convention is the one the dispatcher already ships: bare wheel
340
+ pans, shift+wheel pans horizontally, Cmd/Ctrl+wheel zooms under the pointer,
341
+ and a trackpad pinch (which browsers deliver as ctrl+wheel) zooms. Coordinates
342
+ are `View` throughout — a pan delta arrives in screen pixels and is divided by
343
+ `View.scale` before it lands, and a zoom anchor is canvas-local.
344
+
345
+ `computeWheelAction(view, input, clamp?)` now takes and returns a `View`. Its
346
+ halves, `wheelPan` and `wheelZoom`, are exported alongside `wheelZoomFactor`,
347
+ and `viewport.wheelPan` / `viewport.zoom` call them rather than restating the
348
+ math — so the wired path and the pure one cannot drift again. `WheelState` and
349
+ `ZoomBounds` are removed; `WheelInput` names its anchor `x`/`y` instead of
350
+ `mouseX`/`mouseY` and reads `ctrlKey`. Zoom is now `1.1^(-deltaY/100)` on
351
+ every path, which is reciprocal: scrolling a distance and back returns to the
352
+ scale you started from, where the old `1.1`/`0.9` pair did not.
353
+
354
+ A pinch anchors under the fingers in canvas-local coordinates. The dispatcher
355
+ converts the multitouch centroid the same way it already converted the wheel
356
+ anchor; on a canvas offset from the viewport top-left, `viewport.pinchZoom`
357
+ was anchoring on raw client coordinates and drifting by that offset.
358
+
359
+ `useZoom`, `UseZoomOptions` and `UseZoomReturn` are removed. They were
360
+ deprecated, had no consumer, and were the third convention.
361
+
362
+ `usePinchZoomTool` and `PinchZoomToolOpts` are removed, with the `viewport`
363
+ prop on the unexported `<Canvas>` primitive and the `ViewportConfig` type that
364
+ typed it. `viewport.pinchZoom` on `<SceneCanvas>` is unaffected — it is the
365
+ action, and it is now the kit's only pinch path. `usePinchGesture`, the raw
366
+ two-finger listener underneath, stays.
367
+ - Updated dependencies [fc16cac]
368
+ - Updated dependencies [995fde2]
369
+ - @weasel-js/paint@1.4.3
370
+ - @weasel-js/text@1.4.3
371
+ - @weasel-js/cursor@1.4.3
372
+ - @weasel-js/font@1.4.3
373
+ - @weasel-js/geom@1.4.3
374
+ - @weasel-js/gestures@1.4.3
375
+ - @weasel-js/history@1.4.3
376
+ - @weasel-js/modes@1.4.3
377
+
3
378
  ## 1.4.2
4
379
 
5
380
  ### Patch Changes
@@ -116,6 +116,35 @@ type ShaderUniform = number | [number, number] | [number, number, number] | [num
116
116
  */
117
117
  declare function registerProgram(id: string, vert: string, frag: string): ShaderProgramHandle;
118
118
 
119
+ /**
120
+ * One full-screen pass over what a group has already drawn.
121
+ *
122
+ * The renderer runs a group's effects in order, each reading the previous
123
+ * one's output through `u_source` and writing a whole new buffer — so an
124
+ * effect is free to read neighbouring pixels, which is the entire point and
125
+ * the one thing `colorMatrix` can never do.
126
+ *
127
+ * `uniforms` are the effect's own; `u_source`, `u_resolution` and `u_texel`
128
+ * come from the renderer and must not be passed here.
129
+ */
130
+ interface Effect {
131
+ program: ShaderProgramHandle;
132
+ uniforms?: Record<string, ShaderUniform>;
133
+ }
134
+ /**
135
+ * Register a fragment shader as an effect. Sugar over `registerProgram` with
136
+ * the effect vertex shader, and the reason a consumer never imports the
137
+ * prelude: an effect that registers with the *custom-shader* vertex shader
138
+ * compiles, runs, and samples its source upside down.
139
+ *
140
+ * The fragment shader reads `v_uv` and `u_source`, and may declare
141
+ * `u_resolution` / `u_texel`. See `effectPrelude.ts` for the full contract,
142
+ * including the premultiplied-alpha requirement.
143
+ *
144
+ * @experimental
145
+ */
146
+ declare function registerEffect(id: string, frag: string): ShaderProgramHandle;
147
+
119
148
  /**
120
149
  * Solid-fill paint variant (subset of the full `FillStyle` union from
121
150
  * `@weasel-js/core`). Kept for back-compat with step-1/2 consumers and
@@ -172,6 +201,21 @@ interface GroupDrawCommand {
172
201
  * cannot escape an ancestor's clip. Max 7 nesting levels; the renderer
173
202
  * throws if exceeded. */
174
203
  clip?: Path;
204
+ /**
205
+ * Full-screen passes run over this group's own pixels, in order, before it
206
+ * is composited into its parent.
207
+ *
208
+ * Unlike every other field here, this does not accumulate down the group
209
+ * stack — it is a render-target boundary. The children draw into a buffer of
210
+ * their own, each effect reads the previous one's output, and the result is
211
+ * composited back under this group's `transform`, `alpha`, `colorMatrix` and
212
+ * whatever clip encloses it. So a blur here blurs this group and nothing
213
+ * around it, which is what a CSS `filter` on the canvas cannot do.
214
+ *
215
+ * An empty or absent list costs nothing: no buffer is allocated until a
216
+ * group asks for one.
217
+ */
218
+ effects?: readonly Effect[];
175
219
  children: DrawCommand[];
176
220
  }
177
221
  /**
@@ -285,4 +329,4 @@ interface ShaderDrawCommand {
285
329
  };
286
330
  }
287
331
 
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 };
332
+ export { type DrawCommand as D, type Effect as E, 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, registerProgram as f, mat3 as m, registerEffect as r };
@@ -1,5 +1,5 @@
1
1
  import { GradStop, Stroke } from '@weasel-js/paint';
2
- import { M as Mat3, b as ShaderProgramHandle, D as DrawCommand } from './DrawCommand-Ch8W63oQ.js';
2
+ import { M as Mat3, b as ShaderProgramHandle, D as DrawCommand, E as Effect } from './DrawCommand-CD-ug3d9.js';
3
3
  import { P as Path } from './path-JEV2c5If.js';
4
4
 
5
5
  /**
@@ -279,6 +279,22 @@ declare class GroupState {
279
279
  get alpha(): number;
280
280
  get colorMatrix(): Float32Array;
281
281
  push(frame: GroupFrame): void;
282
+ /**
283
+ * Push a frame whose children paint into a surface of their own.
284
+ *
285
+ * The transform still accumulates — the children draw where they would have
286
+ * drawn. Alpha and colour do not: they describe how the finished surface
287
+ * joins the frame, and applying them on the way in as well would fade the
288
+ * pixels an effect is about to read, then fade them again on the way out.
289
+ *
290
+ * Returns the pair the caller must apply at composite time — what `push`
291
+ * would have left on the stack — so the composition rules live here and not
292
+ * in the dispatcher.
293
+ */
294
+ pushIsolated(frame: GroupFrame): {
295
+ alpha: number;
296
+ colorMatrix: Float32Array;
297
+ };
282
298
  /** Drop every pushed frame, leaving the root. A frame that throws part-way
283
299
  * down the tree never pops, and the next frame would draw under leftovers. */
284
300
  reset(): void;
@@ -491,6 +507,9 @@ declare class WeaselRenderer {
491
507
  private dpr;
492
508
  private canvas;
493
509
  private target;
510
+ /** Offscreen buffers for group effects. Allocates nothing until a group
511
+ * with effects asks, so a canvas without them pays no memory. */
512
+ private readonly effectTargets;
494
513
  private readonly imageMinification;
495
514
  private readonly flattenTolerance?;
496
515
  private readonly bakeBudget;
@@ -688,4 +707,34 @@ declare function resolveStrokeWidth(width: number | {
688
707
  */
689
708
  declare function tessellateStroke(path: Path, stroke: Stroke, opts?: StrokeOptions): Mesh;
690
709
 
691
- export { IDENTITY_COLOR_MATRIX as I, type Mesh as M, type RenderTarget as R, ShaderCompileError as S, type View as V, WeaselRenderer as W, type ImageMinification as a, type SpriteSheet as b, type StrokeOptions as c, type WeaselRendererOptions as d, buildGradientRamp as e, frameRect as f, ShaderProgram as g, resolveStrokeWidth as r, tessellateStroke as t, viewToMat3 as v };
710
+ /**
711
+ * Effects that ship with the kit.
712
+ *
713
+ * Each is a function returning `Effect[]`, not a single `Effect`, because a
714
+ * separable kernel is genuinely two passes and hiding that behind one entry
715
+ * would make the cost invisible at the callsite:
716
+ *
717
+ * effects={[...blur({ radius: 4 }), ...vignette({ amount: 0.6 })]}
718
+ */
719
+
720
+ /**
721
+ * Gaussian blur, as a horizontal pass followed by a vertical one.
722
+ *
723
+ * `radius` is in device pixels and is the distance of the outermost tap, not a
724
+ * standard deviation — 0 is a copy, and the falloff is the same nine-tap
725
+ * kernel at every radius, so a large one is a wide blur rather than a better
726
+ * one. Two passes at O(9) beat one at O(81) and look the same.
727
+ */
728
+ declare function blur({ radius }: {
729
+ radius: number;
730
+ }): Effect[];
731
+ /**
732
+ * Darken toward the corners. `amount` is how dark the corner gets (0..1) and
733
+ * `feather` how much of the radius the falloff occupies.
734
+ */
735
+ declare function vignette({ amount, feather }: {
736
+ amount: number;
737
+ feather?: number;
738
+ }): Effect[];
739
+
740
+ export { IDENTITY_COLOR_MATRIX as I, type Mesh as M, type RenderTarget as R, ShaderCompileError as S, type View as V, WeaselRenderer as W, type ImageMinification as a, type SpriteSheet as b, type StrokeOptions as c, type WeaselRendererOptions as d, blur as e, buildGradientRamp as f, frameRect as g, vignette as h, ShaderProgram as i, resolveStrokeWidth as r, tessellateStroke as t, viewToMat3 as v };
@@ -55,27 +55,49 @@ function createInsertOp(args) {
55
55
  registerOpFactory("insert", (args) => createInsertOp(args));
56
56
 
57
57
  // src/core/ops/delete.ts
58
- function captureDescendants(a, rootId) {
58
+ function indexIn(a, parentId, id) {
59
+ return a.getChildren?.(parentId)?.indexOf(id) ?? -1;
60
+ }
61
+ function captureCascade(a, rootId) {
59
62
  const { getChildren, getNode } = a;
60
63
  if (!getChildren || !getNode) return null;
64
+ const closure = a.getRemovalClosure?.([rootId]);
65
+ const taken = /* @__PURE__ */ new Map();
66
+ if (closure) {
67
+ for (const id of closure) {
68
+ const node = getNode.call(a, id);
69
+ if (node) taken.set(id, node);
70
+ }
71
+ } else {
72
+ const walk = (id) => {
73
+ const node = getNode.call(a, id);
74
+ if (!node) return;
75
+ taken.set(id, node);
76
+ for (const kid of getChildren.call(a, id)) walk(kid);
77
+ };
78
+ walk(rootId);
79
+ }
80
+ const detachRoots = [...taken.values()].filter((n) => {
81
+ const p = parentOf(n);
82
+ return p === null || !taken.has(p);
83
+ }).map((n) => ({ node: n, index: indexIn(a, parentOf(n), n.id) })).sort((l, r) => l.index - r.index);
61
84
  const out = [];
62
- const walk = (parentId) => {
63
- const kids = getChildren.call(a, parentId);
85
+ const emitSubtree = (entry) => {
86
+ out.push(entry);
87
+ const kids = getChildren.call(a, entry.node.id);
64
88
  for (let i = 0; i < kids.length; i++) {
65
- const child = getNode.call(a, kids[i]);
66
- if (!child) continue;
67
- out.push({ node: child, index: i });
68
- walk(kids[i]);
89
+ const child = taken.get(kids[i]);
90
+ if (child) emitSubtree({ node: child, index: i });
69
91
  }
70
92
  };
71
- walk(rootId);
93
+ for (const entry of detachRoots) emitSubtree(entry);
72
94
  return out;
73
95
  }
74
96
  function createDeleteOp(args) {
75
97
  const { node, label } = args;
76
98
  let slot = args.slot ?? slotFromIndex(args.index);
77
- const argsForSerial = { node, label, slot, descendants: args.descendants };
78
- let captured = args.descendants ?? [];
99
+ const argsForSerial = { node, label, slot, cascaded: args.cascaded };
100
+ let captured = args.cascaded ?? [];
79
101
  return {
80
102
  name: "delete",
81
103
  args: argsForSerial,
@@ -87,29 +109,29 @@ function createDeleteOp(args) {
87
109
  slot = observed;
88
110
  argsForSerial.slot = observed;
89
111
  }
90
- const snapshot = captureDescendants(a, node.id);
112
+ const snapshot = captureCascade(a, node.id);
91
113
  if (snapshot) {
92
114
  captured = snapshot;
93
- argsForSerial.descendants = snapshot;
115
+ argsForSerial.cascaded = snapshot;
94
116
  }
95
117
  a.removeNode(node.id);
96
118
  },
97
119
  invert() {
98
- const subtree = captured;
120
+ const cascaded = captured;
99
121
  const restoredSlot = slot;
100
- const reinsert = createInsertOp({ node, label, slot: restoredSlot });
101
- if (subtree.length === 0) return reinsert;
122
+ const reinsertRoot = createInsertOp({ node, label, slot: restoredSlot });
123
+ if (cascaded.length === 0) return reinsertRoot;
102
124
  return {
103
125
  label,
104
126
  apply(adapter) {
105
- reinsert.apply(adapter);
106
127
  const a = adapter;
107
- for (const entry of subtree) {
128
+ for (const entry of cascaded) {
108
129
  if (a.getNode?.(entry.node.id)) continue;
109
- a.insertNode(entry.node, entry.index);
130
+ if (entry.node.id === node.id) reinsertRoot.apply(adapter);
131
+ else a.insertNode(entry.node, entry.index);
110
132
  }
111
133
  },
112
- invert: () => createDeleteOp({ node, label, slot: restoredSlot, descendants: subtree })
134
+ invert: () => createDeleteOp({ node, label, slot: restoredSlot, cascaded })
113
135
  };
114
136
  }
115
137
  };
@@ -117,5 +139,5 @@ function createDeleteOp(args) {
117
139
  registerOpFactory("delete", (args) => createDeleteOp(args));
118
140
 
119
141
  export { captureSlot, createDeleteOp, createInsertOp, rebuildOp, registerOpFactory, registeredOpNames, resolveSlot, slotFromIndex };
120
- //# sourceMappingURL=chunk-2KKYDDDD.js.map
121
- //# sourceMappingURL=chunk-2KKYDDDD.js.map
142
+ //# sourceMappingURL=chunk-4RJP2N2L.js.map
143
+ //# sourceMappingURL=chunk-4RJP2N2L.js.map