@weasel-js/core 1.4.2 → 1.4.4

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 (49) hide show
  1. package/CHANGELOG.md +742 -0
  2. package/dist/{DrawCommand-Ch8W63oQ.d.ts → DrawCommand-CD-ug3d9.d.ts} +45 -1
  3. package/dist/{pointSnapToGrid-Dmthv94u.d.ts → autoPoseDescriptor-DF1SnnSx.d.ts} +18 -3
  4. package/dist/{stroke-Dq8nL4A2.d.ts → builtins-BXFBXegF.d.ts} +251 -102
  5. package/dist/{chunk-Y5KS66G7.js → chunk-2VXGHUVL.js} +4 -4
  6. package/dist/{chunk-Y5KS66G7.js.map → chunk-2VXGHUVL.js.map} +1 -1
  7. package/dist/{chunk-2KKYDDDD.js → chunk-4RJP2N2L.js} +43 -21
  8. package/dist/chunk-4RJP2N2L.js.map +1 -0
  9. package/dist/{chunk-TORKRNFY.js → chunk-BL65SHCX.js} +83 -8
  10. package/dist/chunk-BL65SHCX.js.map +1 -0
  11. package/dist/{chunk-MXFSHJOM.js → chunk-D4AAWR64.js} +4 -4
  12. package/dist/{chunk-MXFSHJOM.js.map → chunk-D4AAWR64.js.map} +1 -1
  13. package/dist/{chunk-ZB7UYJVG.js → chunk-DOBSZOPR.js} +3 -3
  14. package/dist/{chunk-ZB7UYJVG.js.map → chunk-DOBSZOPR.js.map} +1 -1
  15. package/dist/{chunk-BLJNRMKB.js → chunk-PRGBGMH3.js} +3 -3
  16. package/dist/{chunk-BLJNRMKB.js.map → chunk-PRGBGMH3.js.map} +1 -1
  17. package/dist/{chunk-KIVJXUYE.js → chunk-R3AWPTLZ.js} +2779 -1660
  18. package/dist/chunk-R3AWPTLZ.js.map +1 -0
  19. package/dist/{chunk-67KE7SDP.js → chunk-T3UQ3F6R.js} +4 -4
  20. package/dist/{chunk-67KE7SDP.js.map → chunk-T3UQ3F6R.js.map} +1 -1
  21. package/dist/{chunk-SBFC6J3C.js → chunk-WPM42WJP.js} +3 -3
  22. package/dist/{chunk-SBFC6J3C.js.map → chunk-WPM42WJP.js.map} +1 -1
  23. package/dist/clipboard.d.ts +2 -2
  24. package/dist/clipboard.js +3 -3
  25. package/dist/clone.d.ts +2 -2
  26. package/dist/clone.js +3 -3
  27. package/dist/{geometry-Dtt_k6Dq.d.ts → geometry-6fCNhAux.d.ts} +3 -3
  28. package/dist/{grid-nnXU4VjN.d.ts → grid-0Pbn5B2C.d.ts} +1 -14
  29. package/dist/index.d.ts +330 -288
  30. package/dist/index.js +9 -9
  31. package/dist/insert.d.ts +3 -3
  32. package/dist/insert.js +1 -1
  33. package/dist/move.d.ts +4 -4
  34. package/dist/move.js +3 -3
  35. package/dist/{options-CdFl510T.d.ts → options-DbYLImvq.d.ts} +1 -1
  36. package/dist/{registry-BwY_DxJM.d.ts → registry-BY-wI9gm.d.ts} +98 -8
  37. package/dist/renderer.d.ts +32 -4
  38. package/dist/renderer.js +9 -9
  39. package/dist/resize.d.ts +5 -4
  40. package/dist/resize.js +2 -2
  41. package/dist/routing.d.ts +6 -6
  42. package/dist/routing.js +1 -1
  43. package/dist/{types-XBcDEp3Y.d.ts → types-DEALFt5F.d.ts} +10 -6
  44. package/dist/{types-DIQAisSG.d.ts → types-bcc7jcUy.d.ts} +116 -15
  45. package/dist/{types-miXHGrZM.d.ts → types-ei3UMl9R.d.ts} +1 -1
  46. package/package.json +9 -9
  47. package/dist/chunk-2KKYDDDD.js.map +0 -1
  48. package/dist/chunk-KIVJXUYE.js.map +0 -1
  49. package/dist/chunk-TORKRNFY.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,747 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 9ce6f00: Fix solid fills coming out multiplied by whatever bitmap shared their batch.
8
+ A solid vertex carried the UV of a 1x1 white texel, but the flush bound the
9
+ run's *image*, so any solid staged alongside an image quad sampled that bitmap
10
+ at its middle texel instead. A wall of thumbnails is a ground rect under an
11
+ atlas quad per cell — the shape the merged batch exists for — so its grounds
12
+ came out tinted by the atlas: white drew olive. Reported from outside the repo
13
+ against a canvas2d reference; no visual baseline caught it, because in every
14
+ demo the quad covers its ground.
15
+
16
+ Every batch vertex now names the texture slot it samples. Slot 0 is always the
17
+ white texel, so a solid's `texture() * a_vertexColor` is the vertex color
18
+ exactly whatever else joins its run, and bitmaps take the slots above it.
19
+
20
+ That also lets one run hold up to seven distinct bitmaps. A document with a
21
+ handful of loose images used to break its run on every change of bitmap; now it
22
+ breaks only when the slots run out, or when one bitmap is drawn at two
23
+ MAG_FILTERs, which is state on the texture object and cannot be had both ways
24
+ in one draw.
25
+
26
+ `tests/visual/batch-pixels.spec.ts` reads the framebuffer channel by channel
27
+ rather than diffing a screenshot, which is what it takes to see a run's
28
+ composition at all.
29
+ - 6f876a7: Two canvases under one `DepRegistryProvider` no longer take each other's deps
30
+ down.
31
+
32
+ `register` set one source per name and its release did a bare `delete`. Mount a
33
+ second canvas under a shared provider and it displaced the first's `view` /
34
+ `scene` / `selection`; unmount either one and the name went dark for the canvas
35
+ still on screen.
36
+
37
+ Sources now stack per name, newest live, and a release removes its own entry
38
+ wherever it sits — so a displaced source comes back when the one above it
39
+ leaves, and a displaced source leaving disturbs nothing. Same shape as the
40
+ `ActionsProvider` registrant stack, one layer down.
41
+ - ed400a3: A derivation now receives its dependencies as `{ node, pose }` rather than as
42
+ bare poses. Breaking for anything with a `derivePath` or `derivePose`: read
43
+ `deps[0]?.pose` where it read `deps[0]`.
44
+
45
+ A connector legitimately reads more than a box — one that thickens with its
46
+ endpoint's weight, or routes only to nodes on a given layer, is answering off
47
+ `data` and `layer`. The scene has invalidated dependents on `kit:setData` and
48
+ `kit:setLayer` since those landed, and `scene.ts` said in a comment that a
49
+ derivation "is handed its dependencies' nodes, not only their poses". It was
50
+ not: the invalidation was paying for a read the signature could not perform.
51
+
52
+ `scenePoseLookup` is now `sceneDepLookup` and answers `DerivedDep` for the same
53
+ reason. `DerivedDep` is exported.
54
+ - fc00dae: Hand a derivation its dependencies' paths, and label an edge with one.
55
+
56
+ `DerivedDep` is now `{ node, pose, path }`. The path resolves on first read and
57
+ memoizes, so a route costs the same whether one node reads it or five, and a
58
+ dependency nobody asks about costs nothing. `resolveDerivedPath` moves beside
59
+ `derivedPose` in `core/scene` — a pose can now derive from a dependency's path —
60
+ and picks up the cycle guard the pose side already had.
61
+
62
+ `pointAlongPath(path, t)` is the new geometry primitive underneath: where a path
63
+ is at a fraction of its length, and which way it heads there, measured along the
64
+ flattened arc.
65
+
66
+ In `@weasel-js/diagram`, an edge label is an ordinary leaf node with
67
+ `dependsOn: [edge]` and `LABEL_DERIVE_POSE`. Its trait says where it sits —
68
+ `at: 'start' | 'mid' | 'end'` or a fraction, plus an `offset` perpendicular to
69
+ the route — and it reads the edge's resolved path rather than routing again, so
70
+ a label and its arrowhead can never disagree about where the edge went.
71
+ - 730da55: A derived node now recomputes when a dependency's `data` or `layer` changes.
72
+
73
+ `derivePath` is handed its dependencies' nodes, not only their poses — so a
74
+ connector that thickens with a node's weight, or draws only for nodes on a given
75
+ layer, is answering off `data` and `layer`. `kit:setPose` and `kit:move`
76
+ invalidated the dependent's memo; `kit:setData` and `kit:setLayer` did not, so
77
+ the derived geometry kept the old answer with nothing on screen to show it was
78
+ stale. Undo and redo were wrong the same way.
79
+
80
+ Both ops now invalidate dependents on `apply` and on `revert`, matching the two
81
+ that already did.
82
+ - 60ba9d9: A stroke marker on a derived path is drawn.
83
+
84
+ `markerStart` / `markerMid` / `markerEnd` reached the `kit:derived` painter
85
+ intact and were then dropped: the painter emitted its stroke command and
86
+ returned, where `kit:path` follows with a marker pass. So an arrowhead on a
87
+ diagram edge — the whole reason markers and derived geometry landed in the same
88
+ release — silently drew nothing. Its own `ink` had been reserving the hit-test
89
+ reach for the marker all along, which is the shape of the bug: the pointer could
90
+ already grab past the end of a line with no head on it.
91
+
92
+ A connect-authored edge now carries `markerEnd: 'arrow'` by default. An edge
93
+ runs *from* one node *to* another and a plain line does not say so;
94
+ `DEFAULT_EDGE_STROKE` is exported for a consumer overriding `commit` who wants
95
+ the rest of it.
96
+ - 5732951: The minimap's scene fit and the scene text editor read a derived pose rather
97
+ than the placeholder a derived node authors. `computeFitView`'s `'scene'` fit
98
+ frames on `documentPose`, so a derived node is framed where it actually is
99
+ while a drag still leaves the framing alone; `useSceneTextEdit` resolves both
100
+ its double-click hit test and the overlay's own projection through
101
+ `effectivePose`, so double-clicking a derived label opens the editor on it and
102
+ the box lands on the text.
103
+
104
+ Removes `UseMoveOptions.cascadeWorldPose`. Nothing has read it since the move
105
+ action started walking `scene.childrenOf` for its own cascade — it was a
106
+ documented option that silently did nothing, and two doc comments described the
107
+ behavior it used to drive.
108
+ - 2ff4824: A scene node can now derive its **pose** from its dependencies, the way it
109
+ already derived its path. New API; a group's bounds change behavior.
110
+
111
+ `Node.derivePose` takes the same `dependsOn` list as `derivePath`, serializes
112
+ through `SceneRegistry.derivePose` by key, and rides the same push
113
+ invalidation. Where a derived path is resolved at paint time and reaches only
114
+ the painter, a derived pose is what the node *is* at — it feeds bounds,
115
+ hit-testing, selection chrome, snapping and layout.
116
+
117
+ `dependsOn` gains a second form, `'children'`: "my own children, in child
118
+ order", which a fixed id list cannot express because reparenting would have to
119
+ maintain it. The two forms differ in lifetime as well as membership — deleting
120
+ a node still deletes everything that names it in `dependsOn`, but a container
121
+ outlives the children it derives from, because an emptied group is still a
122
+ group.
123
+
124
+ `groupAction` uses it, which fixes the group-bounds defect: a container's union
125
+ AABB was computed once at creation and never re-derived, so moving a member
126
+ left the group's bounds, selection chrome and hit area behind. The kit
127
+ registers the union function under `kit:unionOfChildren` and merges its own
128
+ registry entries under the consumer's, so a grouped document round-trips
129
+ through `toJSON` in any scene.
130
+
131
+ `effectivePose(scene, node)` is the one rule — override, else derived, else
132
+ authored — and now takes the scene rather than the override table alone. The
133
+ three render walks, the pick walk, and the scene, commit and gesture adapters
134
+ all resolve through it or through `documentPose`, the same answer minus the
135
+ override step for a reader that must not see an in-flight gesture. Both are
136
+ exported.
137
+
138
+ `clipFromPose`, `derivePath` and `derivePose` now share one table-driven
139
+ serialization path (`core/scene/nodeFnFields.ts`) instead of a copy per field.
140
+ - 3d89141: Layout: `layered`, `tree` and `force`, and the action that runs one.
141
+
142
+ A layout is a plain function of the graph — no scene, no ops, no history. It
143
+ hands back the new top-left for every node that **moves**, and a node already
144
+ standing where the layout wants it is absent from the answer, so re-running a
145
+ layout on an arrangement it produced writes nothing and pushes no undo entry.
146
+
147
+ Three rules keep a re-layout from scrambling a diagram someone has arranged.
148
+ There is no RNG anywhere in the path, so the same graph always lays out the same
149
+ way. Within-rank order is seeded from where the nodes already sit on the cross
150
+ axis rather than from crossing-minimization, so two branches an author dragged
151
+ into an order come back in it. And a node carrying `pinned: true` never moves,
152
+ with the rest of the layout translated to sit around it — with no pin, the
153
+ layout lands on the diagram's own bounding box rather than at the origin.
154
+
155
+ `layered` ranks by longest path, breaking cycles with a depth-first walk in node
156
+ order so a loop draws as an edge running back up the page. `tree` centers a
157
+ parent over its children's block; a graph that is not a tree still lays out,
158
+ since roots are the nodes nothing points at and anything the walk cannot reach
159
+ becomes a root of its own. `force` is an iterative relaxation seeded from the
160
+ current positions — **the one layout that is not idempotent**, since re-running
161
+ it keeps relaxing.
162
+
163
+ `buildGraph` reads the adjacency index from the same participant source the port
164
+ affordance takes, per invocation rather than maintaining one. `createLayoutAction`
165
+ rebuilds it on each press and writes the whole move as a single `scene.batch`,
166
+ carrying a container's whole subtree — `setPose` does not cascade, and a built
167
+ body would otherwise walk out from under its own label rows.
168
+
169
+ In core, `createSimulation` is the velocity-Verlet integrator with no clock
170
+ attached: `tick()` is the only thing that moves a node, so a pure function can
171
+ run a whole relaxation and read the result. `useSimulation` is now that
172
+ integrator on a frame loop and is otherwise unchanged. `SimulationCore` and
173
+ `SimulationOptions` name the halves, and forces can be handed a seeded `random`
174
+ in place of `Math.random`.
175
+ - 4a128c4: New package `@weasel-js/diagram` — the skeleton of weasel's node-link
176
+ diagramming: flowcharts, pipelines, code-flow diagrams, simple visual
177
+ programming. Arc 3 of the diagram design; edges, routing, the connect gesture
178
+ and layout are still to come.
179
+
180
+ **`DiagramNode` is a trait on an existing scene node, not a node type the
181
+ package mints.** A text block, an image, a path, a group or a plain rect all
182
+ become participants by carrying it, and nothing has to be authored through this
183
+ package to take part. Two ways to attach it, both answered by the default
184
+ reader: on the node's own `data.diagram`, or by kind — `createDiagramNodes`
185
+ takes predicates over `data` the way `createNodeRouting` does, so a class of
186
+ node takes part without being stamped one at a time.
187
+
188
+ **Ports default to the perimeter, so a node needs to say nothing to be
189
+ connectable.** `portsOf(node, pose)` resolves every port to a world point and
190
+ an outward normal, reading bounds through the pose descriptor and carrying the
191
+ node's rotation. Anchors are normalized against the bounds — `{ u, v }` from
192
+ the top-left — so a port stays where it was put when the node is resized.
193
+
194
+ **The body builder is optional**, for nodes that should look like a flowchart
195
+ box: `rect`, `diamond`, `stadium` and `parallelogram` outlines plus a `Row[]`
196
+ body of labels, fields, port rows and slots. Rows measure a floor, and the
197
+ authored pose is maxed against it rather than set to it — adding a row can grow
198
+ a node, nothing shrinks one back — which is what keeps resize, align,
199
+ distribute, guides, snapping and undo free of a special case. Text measurement
200
+ is a seam rather than an import; `canvasMeasure` adapts a 2D context.
201
+
202
+ Rows lay out in the shape's **content box**, not its bounding box.
203
+ `contentBox` reports the largest axis-aligned box inside an outline — a
204
+ diamond's inscribed rect, a parallelogram minus its lean, the flat span between
205
+ a stadium's ends — and `boxForContent` inverts it so the floor grows to suit.
206
+ Without it a diamond's label is placed against the bounding box, lands outside
207
+ the diamond, and the silhouette clip removes it: the label simply vanishes.
208
+
209
+ `registerDiagramShape` paints a node whose trait names an outline, and reports
210
+ the outline as its silhouette so picking and clipping follow the diamond rather
211
+ than its box. Rows are not painted there — a built body's rows are ordinary
212
+ scene nodes, so the kit's own text painter draws them and text editing,
213
+ selection and styling work on them unchanged.
214
+
215
+ Core exports `AUTO_POSE_DESCRIPTOR` and `isPathLike`, which were already
216
+ general-purpose but reachable only from inside the package. A peer package
217
+ computing a node's bounds needs the kit's own default descriptor rather than a
218
+ second copy of it.
219
+ - aee9d92: `createPoseOverrides` is public.
220
+
221
+ `Scene` is public and its `overrides: PoseOverrides<TPose>` is mandatory — and
222
+ load-bearing, since every ongoing gesture writes a frame to it. The factory that
223
+ builds one was internal, so a consumer assembling a scene-like object by hand
224
+ had to reimplement the table from its type. It is now exported alongside
225
+ `createScene`.
226
+ - c067221: Put every baked gradient ramp in one texture, a row each, rather than a texture
227
+ each. `GradientRampAtlas` replaces `GradientRampCache`: a stop list is baked
228
+ once into a 256-texel strip and written to a row, and the fragment shader picks
229
+ its row with `u_rampV`. Every gradient in a frame now samples the same texture
230
+ unit, which is what a gradient needs before it can take a batch texture slot
231
+ the way a bitmap or a font atlas already does.
232
+
233
+ The atlas doubles from 16 rows and stops at 1024, recycling the least recently
234
+ used row past that. An animating gradient mints a new stop list every frame, so
235
+ the old cache grew a GL texture per frame and freed none of them; the cap is
236
+ what bounds that.
237
+
238
+ `PaintBindContext.bindRamp` now returns the `v` its ramp sits at. A registered
239
+ paint kind that samples the ramp at a constant `v` reads whatever gradient
240
+ happens to own that row, so it must sample at the returned value.
241
+ - 26d40bf: Fold gradients into the shared batch. A gradient fill used to bind its own
242
+ program and break the run of solid geometry, image quads and glyphs around it;
243
+ now it stages alongside them, so a page of gradient-filled shapes is one draw
244
+ rather than one per shape.
245
+
246
+ All three kinds go, and for the same reason — not that the ramp position is
247
+ affine in position, which is true only of a linear gradient, but that the
248
+ *coordinate* the ramp position is computed from is affine in all three. So a
249
+ vertex carries that and the rasterizer's interpolation across a triangle is
250
+ exact. A linear gradient's coordinate is the ramp position itself, which is why
251
+ it needs no paint mode of its own; a radial or conic one carries a
252
+ gradient-space point, with its atlas row where a plain vertex keeps its alpha,
253
+ and the shader takes a `length` or an `atan` of it behind a branch on the flat
254
+ paint mode. Fill opacity and group alpha ride the vertices the way a solid's
255
+ already did, and fills, stroke ribbons and glyph-outline meshes all take the
256
+ route.
257
+
258
+ Per-vertex-colored and even-odd fills still take their own draw, as do patterns
259
+ and shaders.
260
+
261
+ The ramp atlas is what makes the slot arithmetic work: every gradient in a run
262
+ shares one texture slot, so a document full of them costs the same one slot a
263
+ single gradient does. Growing the atlas moves every row, so a bake that would
264
+ grow it — or recycle a row — flushes the run first.
265
+
266
+ Gradient fills now apply the group's color matrix. `gradFill` was the only
267
+ paint program that did not, and since the batch program applies it to
268
+ everything in a run, leaving it out would have made a linear gradient and a
269
+ radial one under the same group paint differently.
270
+ - b8d2940: Layout you can watch, and push against.
271
+
272
+ `usePoseRun` is the transport: each frame it asks a producer for poses,
273
+ publishes them to the scene's ephemeral override channel — the one a drag
274
+ already writes to, which `effectivePose`, derived geometry and the pick source
275
+ read — and commits the lot as one batch when the producer says it is done or
276
+ the consumer stops it. Cancel drops the frames and the document is untouched.
277
+ It runs behind `useVisibleRaf`, and it knows nothing about layout.
278
+
279
+ A node carrying an override the run did not publish belongs to another gesture:
280
+ the run never writes it, never commits it, and reports it to the producer as
281
+ pinned. Dragging a box mid-run is therefore the consumer's ordinary move tool,
282
+ with no gesture contributed by the diagram package.
283
+
284
+ `useLiveLayout` in `@weasel-js/diagram` drives it. `force` relaxes one tick a
285
+ frame off the same force list the one-shot `force` runs, holding a pinned node
286
+ with `fx`/`fy` while its neighbors answer; `layered` and `tree` ease into a
287
+ target computed once. A node or edge appearing or disappearing re-heats the run.
288
+
289
+ `SceneNode.pickable: false` makes a node transparent to the hit-test walk, so a
290
+ press lands on what is behind it. Without it the innermost hit wins and dragging
291
+ a labeled box pulls the label out of the box.
292
+ - b5e2cd9: Solid geometry and image quads now share one batch, so a wall of thumbnails —
293
+ a ground rect under an atlas quad, per cell — draws in one call rather than
294
+ one per command. The two batches used to be exclusive: staging a solid drained
295
+ the image run and staging an image drained the solid one, so a shape that
296
+ batches perfectly in either half alone paid a flush per command. Solid vertices
297
+ carry the UV of a 1x1 white texel, which makes `texture() * a_vertexColor` the
298
+ vertex color exactly, so the merge is pixel-identical rather than close.
299
+
300
+ Measured over a viewport-filling grid of those cells on an M2 Max via ANGLE
301
+ (`tests/perf/atlas-wall.spec.ts`): 600 draw commands 2.83 -> 0.10 ms, 1,650
302
+ 11.37 -> 0.20, 5,400 40.50 -> 0.58, 15,000 126.15 -> 1.50. A run still breaks
303
+ on what a run cannot carry — a second bitmap, a different MAG_FILTER, a clip
304
+ depth, a color matrix.
305
+ - 89276ee: `usePoseRun` and `useSimulation` hand their injected clock straight to
306
+ `useVisibleRaf` instead of defaulting it themselves. The gate already falls
307
+ back to `requestAnimationFrame`, so both were defaulting it twice — and the
308
+ copy in `usePoseRun` was a bare `requestAnimationFrame` in kit source, which
309
+ `check:frame-loops` fails the build on. The allowlist is back down to the gate
310
+ itself.
311
+ - 36950d8: `scene.setDependsOn(id, dependsOn)` retargets a node's dependencies as one
312
+ undoable step, so dragging an existing edge's end onto a different node no
313
+ longer means removing the edge and adding another one. Switching a container
314
+ between an id list and `'children'` goes through the same call.
315
+
316
+ Both indices move with it — the reverse dependents index that drives cascade
317
+ delete, and the `'children'` set that drives the ancestor walk — and the
318
+ retargeted node's own memo is dropped alongside its dependents', since its
319
+ derivation now reads different sources. Order is significant, because a
320
+ derivation reads its dependencies positionally; declaring what a node already
321
+ declares records no history entry.
322
+ - 4f8c6b2: Text no longer breaks a batched run. Glyphs, the rules under underlined words,
323
+ and tessellated glyph outlines all stage into the same draw as the solid
324
+ geometry and image quads around them, so a wall of captioned thumbnails is one
325
+ draw where every label used to cost two.
326
+
327
+ The batch shader carries the glyph math behind a paint mode, which packs into
328
+ the texture-slot attribute the vertex already had, so the vertex does not grow
329
+ and a wall of thumbnails costs what it did before. It runs that math on every
330
+ fragment, glyph or not, because `fwidth` in non-uniform control flow is
331
+ undefined and the derivative has to be taken before anything selects on the
332
+ mode — priced at about 1.4% of a fragment that is not a glyph. A synthetic
333
+ oblique now shears on the CPU as the batch places its corners, rather than in a
334
+ vertex shader that read the baseline from a vertex attribute.
335
+
336
+ Three things a run used to break on are gone: a second text color in the same
337
+ paragraph, a decoration whose fill differs from the glyphs it sits under, and
338
+ the difference between a baked MSDF atlas and a runtime canvas bake. What still
339
+ breaks a run is a change of synthetic-bold threshold, which is a uniform — that
340
+ one is a fallback path, since a registered bold face never sets it.
341
+
342
+ **Breaking for anyone importing the text shader sources.** `TEXT_VERT_SRC`,
343
+ `TEXT_FRAG_SRC`, `TEXT_FRAG_R8_SRC`, `TEXT_SDF_UNIFORMS` and
344
+ `TEXT_SDF_ATTRIBUTES` are removed from `@weasel-js/font`: text has no program of
345
+ its own any more. What replaces them is `GLYPH_COVERAGE_GLSL`, the snippet a
346
+ program pastes in to turn an atlas sample into coverage, alongside
347
+ `GLYPH_MODE_MSDF` and `GLYPH_MODE_R8` naming the two channel layouts.
348
+ - 1240956: `kit:text` nodes take a `verticalAlign`, and picking follows it.
349
+
350
+ Centering a glyph in its box meant nudging `pose.y` by hand and re-deriving the
351
+ nudge whenever the font size changed. The painter forwarded the pose's height
352
+ but never an alignment, so the box the renderer aligned within was always
353
+ resolved as `'top'`.
354
+
355
+ `data.verticalAlign` — `'top' | 'center' | 'bottom'`, the same spelling the draw
356
+ command already took — now reaches both halves: the paint command and the
357
+ silhouette `textLineBoxes` builds, so a centered block is grabbable where it
358
+ draws rather than where a top-aligned one would have. A node that names none
359
+ paints exactly where it did before.
360
+ - Updated dependencies [4f8c6b2]
361
+ - @weasel-js/font@1.4.4
362
+ - @weasel-js/text@1.4.4
363
+ - @weasel-js/cursor@1.4.4
364
+ - @weasel-js/geom@1.4.4
365
+ - @weasel-js/gestures@1.4.4
366
+ - @weasel-js/history@1.4.4
367
+ - @weasel-js/modes@1.4.4
368
+ - @weasel-js/paint@1.4.4
369
+
370
+ ## 1.4.3
371
+
372
+ ### Patch Changes
373
+
374
+ - 2de5a37: A canvas opting out of an action no longer takes it away from its siblings.
375
+
376
+ `useViewportActions` answered `pinchZoom: false` with
377
+ `reg.unregister('viewport.pinchZoom')`, which drops *every* registrant of that
378
+ id — so one canvas opting out killed pinch-zoom on a sibling that asked for it,
379
+ and nothing put it back when the opting-out canvas unmounted.
380
+ `actions={{ id: null }}` went through the same door. Registration has been
381
+ per-registrant since the registrant stack landed; suppression was not, and a
382
+ shared registry had nowhere to hang "not for me".
383
+
384
+ `ActionsScope` is that place. It is a view of the registry in scope with its own
385
+ mute set: `register`, `unregister` and the dispatcher / dep-registry slots pass
386
+ straight through to the shared store, so cross-canvas sharing is untouched,
387
+ while `list`, `trigger` and `begin` skip what this scope muted. Scopes nest.
388
+ `<ActionsProvider>` is itself a scope, so a lone canvas needs no wrapper, and a
389
+ `<SceneCanvas>` deferring to a host provider now mounts one.
390
+
391
+ `ActionsRegistry.mute(id)` returns a release; a scope drops everything it muted
392
+ when it unmounts. `unregister` keeps its old meaning — "this action should not
393
+ exist" — and is unchanged.
394
+
395
+ Breaking for anyone implementing `ActionsRegistry` themselves: `mute` is a new
396
+ required method.
397
+ - 10e1ab6: A bare `<Canvas>` now repaints when its selection changes, without a wrapper
398
+ asking it to.
399
+
400
+ The redraw tripwire — the layout effect whose dep array is meant to name every
401
+ paint input that arrives on a render — did not name the overlay-aware state the
402
+ layer helpers expose. Selection, preview poses and the chrome derived from them
403
+ were written during render into a ref, so changing the selection prop repainted
404
+ nothing. It looked correct only because `<SceneCanvas>` calls `requestRedraw()`
405
+ by hand for its own data sources.
406
+
407
+ The tripwire now carries the memoized chrome state, which re-derives on exactly
408
+ the selection, bounds and preview inputs the helpers read, plus the chrome-caps
409
+ predicate. A render that changed none of them still paints nothing.
410
+ - eb0d6ce: Undo of a Delete now brings back everything the delete cascaded — a node's
411
+ dependents, and their own subtrees — not just the node that was named.
412
+
413
+ Deleting a node takes everything deriving from it, so deleting a box takes the
414
+ edges drawn from it. The delete op only ever snapshotted the subtree, so undo
415
+ re-inserted the box alone and the edges stayed gone. The op now snapshots the
416
+ whole set before removing, and re-inserts it parent-first, each node at the slot
417
+ it held.
418
+
419
+ New public read: `scene.removalClosure(ids)` answers what `removeMany(ids)`
420
+ would take, without taking it. The scene owns the cascade relations, so a caller
421
+ that rebuilds the walk from `dependsOn` and `children` goes stale the moment a
422
+ relation is added — `buildDeleteOps` asked its own copy of that question and is
423
+ now on this one.
424
+
425
+ The delete op reads it through an optional `getRemovalClosure(ids)` on the
426
+ adapter, alongside `getChildren`. An adapter that cascades along nothing but the
427
+ subtree can leave it out and behaves as before; one that cascades further has to
428
+ answer, or the nodes it takes are absent from the snapshot. The scene-backed
429
+ adapters answer it.
430
+
431
+ `insertNode` on the scene-backed adapters now forwards the function-valued
432
+ fields — `dependsOn`, `derivePath` and `clipFromPose` — so a restored edge
433
+ derives again instead of coming back as a static path, and a restored container
434
+ still clips. The delete op's serialized `descendants` argument is now
435
+ `cascaded`, and carries the node itself alongside what went with it.
436
+ - 75969f6: Full-screen effect passes: a group's children render into a texture, shader
437
+ passes run over it, and the result composites back where the group sits.
438
+
439
+ Until now the renderer could draw over the frame but never transform it —
440
+ nothing sampled what had already been drawn, so blur, bloom and distortion were
441
+ unreachable and the only recourse was a CSS `filter` on the `<canvas>`, which is
442
+ the browser compositing on the kit's behalf.
443
+
444
+ `GroupDrawCommand.effects` is the primitive. It is the one field there that does
445
+ not accumulate down the group stack: it is a render-target boundary. The
446
+ children draw into a buffer with a stencil of its own, each effect reads the
447
+ previous one's output, and the composite applies the group's `transform`,
448
+ `alpha` and `colorMatrix` plus whatever clip encloses it — so the enclosing clip
449
+ clips the result rather than the pixels an effect reads, and nested clips inside
450
+ start from a fresh depth budget.
451
+
452
+ `RenderLayer.effects` is that field folded in at `drawOneLayer`, which is where
453
+ every layer already gets wrapped in a group. A blur there blurs one layer and
454
+ leaves the chrome drawn above it sharp, which is the thing the CSS filter cannot
455
+ do.
456
+
457
+ `blur({ radius })` and `vignette({ amount })` ship as built-ins; both return
458
+ `Effect[]` because a separable blur is honestly two passes.
459
+ `registerEffect(id, frag)` registers your own — it is `registerProgram` with the
460
+ effect vertex shader, and using the custom-shader one instead compiles, runs,
461
+ and samples the source upside down.
462
+
463
+ Nothing is allocated until a group declares an effect, so a canvas without them
464
+ carries no offscreen buffer. Buffers are drawing-buffer sized, pooled per
465
+ renderer, and dropped on resize, dispose and context loss.
466
+
467
+ Demo: "Full-screen effect passes", with visual baselines blurred and sharp.
468
+ - 0d40f94: A layer whose `draw` throws now paints nothing and names itself in the console,
469
+ instead of taking the frame down with it.
470
+
471
+ The paint runs on the frame loop, so a throw out of `draw` surfaced as an
472
+ uncaught `requestAnimationFrame` error on the window: the canvas went blank and
473
+ stayed stale until something unrelated asked for a redraw. `drawOneLayer` — the
474
+ one path both the canvas and a viewport's inner pass go through — catches it,
475
+ drops that layer for the frame, and paints the rest.
476
+
477
+ `drawLayers` and `drawOneLayer` take an `onLayerError` callback for a consumer
478
+ that wants to route the failure somewhere of its own; the default reports the
479
+ layer id and the error to `console.error`. The layer's cached commands are
480
+ dropped with it, so the next frame is a real re-attempt rather than a stale tree
481
+ served under fresh deps.
482
+ - 713f98a: A layout strategy decides what happens to a child released outside every
483
+ container.
484
+
485
+ When no container accepted a drag, the move action committed the child wherever
486
+ the pointer stopped, and the source layout had no say — a grid could not close
487
+ the gap, and a container that only means to arrange its own children could not
488
+ take one back.
489
+
490
+ `LayoutStrategy.releaseDrop` is that say. It sees the source container, its
491
+ remaining children and the dragged child in world coordinates, and returns ops
492
+ that replace the free-space commit. An empty array leaves the container alone,
493
+ which snaps the child home, since the drag only ever wrote previews. `null` —
494
+ and a strategy that does not implement the method — leaves the drop where the
495
+ pointer left it, exactly as before.
496
+ - 85f4a21: Dragging several selected nodes into a layout container now runs the layout,
497
+ instead of falling through to a plain translate. Three nodes dropped on a grid
498
+ fill three cells.
499
+
500
+ The selection lands as a unit. One container is chosen, from the center of the
501
+ whole dragged group rather than each child's own — hit-testing per child would
502
+ scatter a selection straddling two containers — and it has to accept every
503
+ member: `acceptsDrop` is asked once per dragged child, and one refusal takes the
504
+ container out of the running. The children are then placed one at a time, in
505
+ selection order, and each placement sees the container state the previous one
506
+ produced. A strategy that packs, stacks or displaces therefore sees the group
507
+ arrive the same way it would see three separate drags. If any child has no
508
+ target, the whole drop is refused rather than split between two homes.
509
+
510
+ Each child is snapped at its own position, displaced by however far the pointer
511
+ sits from the selection's center, so the group keeps its shape as it lands. A
512
+ single-node drag still probes at the pointer exactly as before.
513
+
514
+ The rest follows the selection: the live preview reflows the destination and
515
+ every source container the selection left, one pass per container with all of
516
+ its departing children withdrawn at once; and the commit emits a reparent op per
517
+ child that changed parent, every reparent before every drop.
518
+
519
+ `releaseDrop` — a container's say over its own child released into open space —
520
+ is now per source container too, so a mixed selection resolves per child: the
521
+ grid's own child goes home, and a free node in the same selection keeps the
522
+ translate it would have had on its own.
523
+ - 4bb0341: A parallax plane draws its source layers through `drawOneLayer`, so their
524
+ `space` means something.
525
+
526
+ `createParallaxLayer` called `layer.draw(...)` directly where every other path
527
+ through a view — the canvas itself, a viewport node's inner pass — goes through
528
+ `drawOneLayer`. A `space: 'world'` source therefore came out unprojected, and the
529
+ only way to see anything was for the source to pre-project by hand while
530
+ declaring a space it did not draw in. ParallaxDemo's four layers did exactly
531
+ that, and their labels were lies.
532
+
533
+ Now a world-space source is wrapped in the plane's inner view and a screen-space
534
+ one is passed through, the same rule that holds everywhere else. The plane
535
+ itself stays `space: 'screen'` — its children carry whatever transform they
536
+ need, and the outer canvas must add none.
537
+
538
+ The demo drops its `project` helper and emits world coordinates; the committed
539
+ `parallax` visual baseline passes unchanged.
540
+ - e0d5580: `pathHitTest` reads curves and holes instead of throwing on one and ignoring
541
+ the other.
542
+
543
+ Its vertex extractor walked `M` and `L` only and threw on any bezier command,
544
+ and it stopped at the first `Z`, so a path's second contour was never
545
+ considered. The throw was reachable in ordinary use: `sceneAdapter`'s
546
+ `nodeBoundsPassClips` calls `pathIntersectsRect` on every ancestor clip, so a
547
+ container with a curved `clipFromPose` crashed the hit-test walk.
548
+
549
+ `pathContainsRect`, `pathIntersectsRect`, `pathContainsPolygon` and
550
+ `pathIntersectsPolygon` now treat a `PolygonPath` as its filled region. Whether
551
+ a point is inside comes from `pointInPath`, so beziers flatten and `fillRule`
552
+ decides — a donut's hole is outside the shape under `evenodd` and inside it
553
+ under `nonzero`, and the four predicates agree with `pathContainsPoint` on
554
+ which. Each takes the same optional flattening tolerance `pointInPath` does.
555
+
556
+ Two answers change for paths that already worked. A rect sitting in a hole is
557
+ no longer reported as contained or intersecting, and containment now fails when
558
+ a contour reaches into the rect at all rather than only when it crosses an
559
+ edge.
560
+ - edf99d5: Undoing a removal from a persisted history brings `derivePath` and
561
+ `clipFromPose` back.
562
+
563
+ `kit:remove`'s snapshot cloned each node with its function-valued fields
564
+ attached, which works in-session and disappears the moment the history is
565
+ serialized. `dependsOn` survived the round-trip and repopulated the dependency
566
+ index, so a restored derived node looked wired up and never painted; a
567
+ container came back unclipped the same way.
568
+
569
+ The snapshot now carries the registry keys beside the nodes, and revert
570
+ re-resolves them exactly as `kit:add` does — warning, not throwing, when a key
571
+ is absent from the scene's registry.
572
+ - 2723cc7: `selectAll` no longer selects nodes on a hidden layer, so Cmd+A then Delete
573
+ cannot take content the user cannot see.
574
+
575
+ It walked `renderOrder()`, which is every node in the scene regardless of what
576
+ its layer's `visible` flag says. It now walks `renderOrderNodes()` when any
577
+ layer is hidden — the same sequence, carrying the layer each node sits on — and
578
+ keeps the cheaper id walk when every layer is visible.
579
+
580
+ A `scene` dep that answers neither `layers` nor `renderOrderNodes` behaves
581
+ exactly as before.
582
+ - 0ca0aca: Layer groups: several consecutive layers render into one buffer and share one
583
+ effect pass.
584
+
585
+ `RenderLayer.effects` runs over a single layer, which is the wrong picture as
586
+ soon as a pass reads neighboring pixels — `blur(A over B)` is not `blur(A) over
587
+ blur(B)` — and it costs a buffer and a pass chain per layer. A consumer wanting
588
+ the world blurred and the HUD sharp had no way to say that the world was more
589
+ than one layer.
590
+
591
+ `LayerGroup` is that surface: `{ id, layers, effects?, alpha?, colorMatrix? }`,
592
+ passed to `<Canvas>` / `<SceneCanvas>` as `layerGroups`. Members are named by
593
+ `RenderLayer.id`, the same names `layerOrder` and `layerVisibility` use, so a
594
+ group can take in kit-built layers as readily as consumer ones. `drawLayers`
595
+ brackets each run of consecutive members in one `kind: 'group'` command; the
596
+ renderer's existing offscreen path does the rest.
597
+
598
+ Only consecutive members share a buffer, because anything drawn between two of
599
+ them has to land between them. A group split across the render order is drawn as
600
+ one bracket per run, with a warning — the picture is right, the declaration
601
+ probably is not. A hidden member, and a member that paints nothing this frame,
602
+ break no run.
603
+
604
+ `effects` also accepts a thunk, re-read on every frame the canvas paints, so an
605
+ animating radius costs no React render. A group that declares no effects, alpha
606
+ or color matrix emits no wrapper at all, and a frame with no groups allocates
607
+ nothing.
608
+
609
+ The side-scroller load test now blurs its six world layers on a head knock and
610
+ leaves its HUD, callouts and ending card sharp; the CSS `filter` it used to
611
+ reach for is gone.
612
+ - 3583ca3: `useStandardActions` takes an `exclude` list, so a consumer can bind its own.
613
+
614
+ The hook registered a fixed descriptor list, which left no way to suppress an
615
+ individual kit action. A consumer wanting its own align or distribute
616
+ keybindings got the kit's as well, and the two competed for the same keys.
617
+
618
+ `exclude` names ids to leave unregistered; an id naming no kit action is
619
+ ignored, and changing the list re-registers. `KIT_STANDARD_ACTION_IDS` is the
620
+ full list in registration order, so the ids are discoverable rather than
621
+ something to read out of the source.
622
+ - fc16cac: `Stroke.paint` is optional, and a stroke without one paints nothing everywhere
623
+ rather than throwing.
624
+
625
+ Such a stroke is real: a property panel that writes one field onto a node with
626
+ no stroke — a width, a cap — materializes a whole stroke around it, and
627
+ documents written before that was fixed still hold them. The painters already
628
+ read one as no stroke. Every other reader dereferenced `paint` unguarded, so a
629
+ document holding one threw on SVG export, on copy, and out of any consumer
630
+ painter or overlay whose command reached the renderer directly.
631
+
632
+ The type says so now, which is what stops the next reader from assuming
633
+ otherwise. What each one does with an unpainted stroke:
634
+
635
+ - The renderer skips the stroke pass and paints the fill.
636
+ - The SVG serializer emits no `stroke` attributes at all, the way it already
637
+ does for an absent or zero-width stroke.
638
+ - Text layout keys it as no stroke, so an unpainted run groups with unstroked
639
+ ones instead of splitting a draw call, and does not get pulled onto the
640
+ outline tier to stroke nothing.
641
+ - `setStrokeOpacity` seeds the default stroke color to have something to set an
642
+ opacity on, keeping the width and joins already there.
643
+ - 6d4bbeb: WeaselDraw's SVG export drops what a hidden layer holds, matching what the
644
+ pixel path draws. It walked the whole container tree with no visibility gate,
645
+ so hiding a layer and exporting produced a file with the hidden content in it.
646
+
647
+ `SceneSource` gains an optional `isPainted(id)`; a node it refuses is skipped
648
+ along with everything under it, under an explicit `roots` override too — a
649
+ selection naming a hidden node still must not export it. A source that omits
650
+ the predicate emits everything, exactly as before.
651
+ - 995fde2: A text node's `data.fill: null` is now an explicit no-fill, so stroked-but-
652
+ unfilled text — outline-only display type — renders as such.
653
+
654
+ Every other node kind already read `null` that way. Text resolved it back to the
655
+ default black, because a `ResolvedRun` had to name a concrete `FillStyle` and
656
+ nothing downstream could skip the fill pass. `ResolvedRun.fill`,
657
+ `ResolvedTextStyle.fill` and `LaidOutGroup.fill` are now `FillStyle | null`, and
658
+ `TextPaint.fill: null` carries through to all three. Absent still means the
659
+ default black.
660
+
661
+ An unfilled run paints through its stroke alone, which only the outline tier can
662
+ lay down, so layout emits no atlas quads for one and the renderer skips the
663
+ glyph-fill mesh — an unfilled, unstroked run emits nothing at all, not even its
664
+ outline geometry. Underline, strikethrough and overline follow the fill: a rule
665
+ is a solid rect with no stroked counterpart, so an unfilled run draws none.
666
+ Nothing changes for text that has a fill.
667
+
668
+ Picking deliberately does not follow. `kit:text` still reports `filled: true`
669
+ for `fill: null`, because a text node's silhouette is its line boxes rather than
670
+ its glyph ink — reporting it unfilled would leave a word grabbable within a
671
+ stroke width of a box edge and nowhere near the letters.
672
+
673
+ `@weasel-js/svg` reads and writes SVG's own spelling of this: `<text
674
+ fill="none">` parses to `fill: null` instead of being dropped as absent, and a
675
+ text node with `fill: null` serializes as `fill="none"` rather than as SVG's
676
+ default black. `SvgTextNode.fill` widens to `FillStyle | null`.
677
+
678
+ The "Text outlines" demo has a Fill checkbox alongside its Stroke one; the two
679
+ off together is a node with no glyph paint at all.
680
+ - 6e4fb4d: A user layer survives `toJSON()` — its name, and the fact that it is a user
681
+ layer at all.
682
+
683
+ Every layer was written to a snapshot as `{ id, visible, locked }` and read back
684
+ as `kind: 'system'`, so reloading a document renamed nothing, showed nothing in
685
+ a layer list, and made `renameLayer` throw "cannot rename system layer" on a
686
+ layer the user had just created.
687
+
688
+ `SerializedLayer` is the snapshot's layer shape: the fields it always had, plus
689
+ an optional `kind` and `name`. A snapshot written before this carries neither
690
+ and loads as system layers, which is what every layer in it was.
691
+
692
+ `sceneFromJSON` now builds an empty scene and calls `loadState`, so the layer
693
+ stack is rebuilt by the one reader that knows how instead of by `createScene`,
694
+ which mints system layers only.
695
+ - b0fba6a: One wheel convention across the kit. Breaking: `computeWheelAction` changes
696
+ shape, and `useZoom` / `usePinchZoomTool` are gone.
697
+
698
+ Three public entry points answered the wheel and disagreed with each other.
699
+ `viewport.wheelPan` + `viewport.zoom` panned on a bare wheel and zoomed on
700
+ Cmd/Ctrl+wheel; `computeWheelAction` did the opposite, zooming on a bare wheel
701
+ and treating Cmd+wheel as a vertical scroll; `useZoom` zoomed on a bare wheel
702
+ and never panned. The two reducers also panned in screen pixels against a
703
+ `{ zoom, panX, panY }` state that is not a `View` and cannot be handed to
704
+ `view.set`.
705
+
706
+ The surviving convention is the one the dispatcher already ships: bare wheel
707
+ pans, shift+wheel pans horizontally, Cmd/Ctrl+wheel zooms under the pointer,
708
+ and a trackpad pinch (which browsers deliver as ctrl+wheel) zooms. Coordinates
709
+ are `View` throughout — a pan delta arrives in screen pixels and is divided by
710
+ `View.scale` before it lands, and a zoom anchor is canvas-local.
711
+
712
+ `computeWheelAction(view, input, clamp?)` now takes and returns a `View`. Its
713
+ halves, `wheelPan` and `wheelZoom`, are exported alongside `wheelZoomFactor`,
714
+ and `viewport.wheelPan` / `viewport.zoom` call them rather than restating the
715
+ math — so the wired path and the pure one cannot drift again. `WheelState` and
716
+ `ZoomBounds` are removed; `WheelInput` names its anchor `x`/`y` instead of
717
+ `mouseX`/`mouseY` and reads `ctrlKey`. Zoom is now `1.1^(-deltaY/100)` on
718
+ every path, which is reciprocal: scrolling a distance and back returns to the
719
+ scale you started from, where the old `1.1`/`0.9` pair did not.
720
+
721
+ A pinch anchors under the fingers in canvas-local coordinates. The dispatcher
722
+ converts the multitouch centroid the same way it already converted the wheel
723
+ anchor; on a canvas offset from the viewport top-left, `viewport.pinchZoom`
724
+ was anchoring on raw client coordinates and drifting by that offset.
725
+
726
+ `useZoom`, `UseZoomOptions` and `UseZoomReturn` are removed. They were
727
+ deprecated, had no consumer, and were the third convention.
728
+
729
+ `usePinchZoomTool` and `PinchZoomToolOpts` are removed, with the `viewport`
730
+ prop on the unexported `<Canvas>` primitive and the `ViewportConfig` type that
731
+ typed it. `viewport.pinchZoom` on `<SceneCanvas>` is unaffected — it is the
732
+ action, and it is now the kit's only pinch path. `usePinchGesture`, the raw
733
+ two-finger listener underneath, stays.
734
+ - Updated dependencies [fc16cac]
735
+ - Updated dependencies [995fde2]
736
+ - @weasel-js/paint@1.4.3
737
+ - @weasel-js/text@1.4.3
738
+ - @weasel-js/cursor@1.4.3
739
+ - @weasel-js/font@1.4.3
740
+ - @weasel-js/geom@1.4.3
741
+ - @weasel-js/gestures@1.4.3
742
+ - @weasel-js/history@1.4.3
743
+ - @weasel-js/modes@1.4.3
744
+
3
745
  ## 1.4.2
4
746
 
5
747
  ### Patch Changes