@weasel-js/core 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +413 -0
  2. package/dist/{DrawCommand-BKJ5JPkC.d.ts → DrawCommand-DCmiNkBj.d.ts} +14 -169
  3. package/dist/{chunk-GLZLSPGJ.js → chunk-6SHZHXXR.js} +46 -9
  4. package/dist/chunk-6SHZHXXR.js.map +1 -0
  5. package/dist/{chunk-DLV5F4NY.js → chunk-D2C6K6FO.js} +915 -547
  6. package/dist/chunk-D2C6K6FO.js.map +1 -0
  7. package/dist/chunk-DCIU3SRK.js +505 -0
  8. package/dist/chunk-DCIU3SRK.js.map +1 -0
  9. package/dist/chunk-IO5Z75X4.js +167 -0
  10. package/dist/chunk-IO5Z75X4.js.map +1 -0
  11. package/dist/{chunk-BPRHGTLQ.js → chunk-UB6A6L77.js} +3 -3
  12. package/dist/{chunk-BPRHGTLQ.js.map → chunk-UB6A6L77.js.map} +1 -1
  13. package/dist/{chunk-CMBE45MT.js → chunk-Z3KNFTTS.js} +6 -6
  14. package/dist/chunk-Z3KNFTTS.js.map +1 -0
  15. package/dist/{index-Clf_koyR.d.ts → index-ChM3ZJ2v.d.ts} +103 -90
  16. package/dist/index.d.ts +313 -32
  17. package/dist/index.js +6 -6
  18. package/dist/insert.js +5 -5
  19. package/dist/insert.js.map +1 -1
  20. package/dist/move.js +2 -2
  21. package/dist/paint-types-CnLIzqq1.d.ts +261 -0
  22. package/dist/patterns-builtin.d.ts +57 -2
  23. package/dist/patterns-builtin.js +1 -97
  24. package/dist/patterns-builtin.js.map +1 -1
  25. package/dist/renderer.d.ts +24 -7
  26. package/dist/renderer.js +6 -6
  27. package/dist/resize.js +2 -2
  28. package/dist/routing.d.ts +4 -4
  29. package/dist/routing.js +1 -1
  30. package/dist/{viewToMat3-DFMdZvVl.d.ts → viewToMat3-BvaWCN99.d.ts} +1 -1
  31. package/package.json +6 -6
  32. package/dist/chunk-6DAUIMTO.js +0 -233
  33. package/dist/chunk-6DAUIMTO.js.map +0 -1
  34. package/dist/chunk-CMBE45MT.js.map +0 -1
  35. package/dist/chunk-DLV5F4NY.js.map +0 -1
  36. package/dist/chunk-GLZLSPGJ.js.map +0 -1
  37. package/dist/chunk-Y52N27PF.js +0 -39
  38. package/dist/chunk-Y52N27PF.js.map +0 -1
  39. package/dist/registerTexture-BzHTLhD9.d.ts +0 -25
package/CHANGELOG.md CHANGED
@@ -1,5 +1,418 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ffd9713: Chrome hit-tests are axis-aware under non-uniform zoom.
8
+
9
+ Chrome declares its hit zones in screen pixels and paints in screen space, but
10
+ the hit-test converted those pixels to world units through `meanScale`, a
11
+ single geometric mean of the two axis scales. Under per-axis zoom that made the
12
+ pickable region larger than the painted one on the squashed axis and smaller on
13
+ the stretched one — an 8px handle at `scale: { x: 4, y: 1 }` was grabbable 16
14
+ screen px to the side and only 4 px above.
15
+
16
+ New primitives in `core/viewport/pxExtent`, all exported: `pxExtent(px, scale)`
17
+ for a per-axis world length, and `withinPxBox` / `withinPxRadius` to compare a
18
+ world-space delta in screen space directly, which can't drift from the paint at
19
+ all.
20
+
21
+ `AffordanceRegion`'s `point` hit is now that screen-space comparison, so a
22
+ handle's hit region is the square you see — which also fixes it under a rotated
23
+ target, where the old local-frame test was a tilted rectangle on screen. The
24
+ annulus band floor (`minBandPx`) and the annulus paint inset (`insetPx`) are
25
+ per-axis, keeping the ring you can see the ring you can grab. The pen's
26
+ close-hit radius is a screen-space circle. Snap-guide tolerances — the shared
27
+ guide strategy, insert, resize, and alignment — are per-axis, which is exact
28
+ rather than approximate there: a vertical guide is matched by a horizontal
29
+ distance, so it answers to `scale.x` alone.
30
+
31
+ `matchAlignment`'s `worldTolerance` parameter is now `{ x, y }` rather than a
32
+ number. Pass the same value twice for a world-space tolerance.
33
+
34
+ `meanScale` stays, for the two things that legitimately have no per-axis
35
+ answer: hairline stroke widths, where the renderer takes one width, and painted
36
+ chrome placement, whose per-axis form doesn't separate under a rotated target.
37
+ Its doc now says so.
38
+
39
+ - 8853e73: Affordance hits are claims, and an exclusive claim outranks the scope tier.
40
+
41
+ `AffordanceHit` gains `strength`, and `owner` naming what produced the hit. Kit
42
+ chrome claims `'shared'`, which is what it always did: compete on scope and
43
+ specificity. Registered layers, whose hits previously flattened to a bare kind
44
+ string and a payload, now return a `LayerHit` that can also carry `cursor` and
45
+ `strength`, so a consumer's own chrome says the things kit chrome already said
46
+ through `AffordanceRegion`. `owner` is groundwork — nothing binds on it yet, and
47
+ today only diagnostics read it; target it with `kindOf` or `affordance:<kind>`.
48
+
49
+ **Behavior change.** When a press carries an exclusive claim, only bindings
50
+ whose `target` consults the affordance — a `kindOf` predicate, or the
51
+ `affordance:<kind>` string form — are candidates. Scope ordering applies within
52
+ that filtered set, unchanged. Body-class targets (`'empty'`, `'selected-body'`,
53
+ `'unselected-body'`) and `kind:` targets resolve from the body classification
54
+ and never see the affordance, so they no longer win presses on chrome floating
55
+ over the body they name.
56
+
57
+ This is the dispatcher rule the previous release's changeset said was the real
58
+ fix. `select`'s marquee no longer needs its hand-written predicate declining
59
+ chrome affordances, and it is deleted; `rect`, `ellipse`, `polygon`, `star`,
60
+ `hand`, `pen` and `lasso` keep their bare `{ kind: 'drag' }` bindings and stop
61
+ swallowing drags on HUD chrome anyway, which is the point — seven copies of a
62
+ predicate was the alternative. (`select`'s _click_ binding keeps a predicate of
63
+ its own, for an unrelated reason: resize and rotate bind only drag, so a click
64
+ exactly on a handle would otherwise clear the selection.)
65
+
66
+ The filter is hard: if an exclusive claim leaves nothing eligible, the press
67
+ does nothing. A dev-only warning names the owner the first time that happens for
68
+ each owner, because the failure is otherwise silent.
69
+
70
+ **What a claim does not reach.** Only gestures that carry an affordance are
71
+ filtered, and keyboard events never do. Which pointer-family gestures carry one
72
+ is settled by the per-kind claim work later in this release — see "A widget
73
+ declares which gestures it consumes." The filter outranks hotkey scope as well
74
+ as active scope, so holding space to pan no longer pans while the pointer is
75
+ over HUD chrome.
76
+
77
+ `@weasel-js/hud` claims exclusively, and `Widget` gains `cursorAt(x, y)`, which
78
+ resolves a cursor per point rather than from hover state; `hud.window()`
79
+ implements it, so hovering a resize band shows `nwse-resize` instead of the
80
+ active tool's cursor. A widget can also stay transparent to input — `rect`,
81
+ `text`, `label` and `image` are decoration — so a backdrop widget no longer
82
+ eats presses meant for the canvas or for widgets beneath it. That occlusion
83
+ predates this release, but an exclusive claim would have widened it from "HUD
84
+ elements occlude each other" to "HUD elements kill every tool underneath."
85
+
86
+ `WindowWidget.cursor` is removed. Nothing read it; `cursorAt` replaces it.
87
+
88
+ - 43482ce: A registry entry declares what it contributes and when it is eligible.
89
+
90
+ `Contribution` is the entry type: `bindings`, `actions`, an `overlay`, a
91
+ `presentation`, each optional and independent. `Tool<TScratch>` is now the
92
+ **focus-declaring case** of one — it keeps only what a mode the user switches
93
+ into needs (`initScratch`, the lifecycle hooks, the preview hooks, a `cursor`
94
+ closing over its own scratch). An entry that only routes input declares only
95
+ bindings and actions, which is what `@weasel-js/hud` always was.
96
+
97
+ `Eligibility` is a set of conditions rather than one value, because one entry
98
+ holds several: the hand tool is palette-selectable _and_ space-held. `focus`,
99
+ `offhand: HotkeyTrigger`, `always`, `claimed`, plus `capabilities` as a modality
100
+ filter — `@weasel-js/modes` now reads its tags from there. The scope tier a
101
+ binding matches at is derived from whichever condition is live, ordered to match
102
+ the dispatcher's existing hotkey > active > ambient walk. Nothing about that
103
+ walk changed; what changed is that an entry lands in a tier because of what it
104
+ declares about itself rather than which argument a consumer passed it in.
105
+
106
+ `useContributions` is the assembly point, and `useTools` is a shim over it with
107
+ its shape unchanged.
108
+
109
+ **Breaking — `@weasel-js/hud`:** `createHudTool` / `useHudTool` are now
110
+ `createHudContribution` / `useHudContribution`, with no alias. The old names were
111
+ half of the same misstatement as the `as unknown as Tool<null>` cast they
112
+ required; keeping them would preserve exactly what this corrects.
113
+
114
+ **Breaking — `DispatcherContext.ambientToolIds` is removed.** A host driving the
115
+ dispatcher directly declares `eligibility: { always: true }` on always-on entries
116
+ instead. `useTools` consumers are unaffected; the shim sets it.
117
+
118
+ **Behavior — a declared held-key trigger now wires itself.** `ToolDef.hotkey` was
119
+ read by the inspector and wired nothing; the engagement lived in a host-side
120
+ registration keyed by tool id (`BUILTIN_OFFHAND_ACTIONS`, now gone). Assembly
121
+ registers the consolidated `tool.offhand` action from the declarations, so a tool
122
+ that wants space declares it — `useHandTool` does. A host that also registered
123
+ `tool.offhand` by hand should stop.
124
+
125
+ **Behavior — `useTools` returns shallow copies** for ambient entries and
126
+ `hotkey`-declaring tools, since it adds the declaration they were missing.
127
+ Registry tools from `defineTool` are returned unchanged.
128
+
129
+ **Behavior — route-conflict reporting now also sees action `defaultBinding`s.**
130
+ The dispatcher always matched against them; the reporter never saw them, so a
131
+ tool binding colliding with an action default went unreported. It no longer does.
132
+ Dispatch is unchanged.
133
+
134
+ Also added: `mergeContributions(...bundles)`, which concatenates and throws on a
135
+ duplicate id rather than silently dropping one — the recorded plugin/bundling v1,
136
+ whose deferral condition was "≥2 plugin-shaped features in flight."
137
+
138
+ **Not done, so the seam is stated rather than implied:**
139
+ `EligibilityState.heldTriggers` exists and `liveScope` honors it, but nothing
140
+ populates it. `tool.offhand`'s invoker still reports engagement by pushing a tool
141
+ _id_, which `engagedIds` reads, so the declaration registers the binding while
142
+ the id carries the tier. Retiring that means changing `tool.offhand`'s contract.
143
+
144
+ - 9ed1139: Gradient fills that stay attached to their shape, and an editor for them.
145
+
146
+ Gradient geometry was interpreted in screen space: `draw.ts` set the shader's
147
+ `u_worldInv` to the identity matrix, with a comment saying a later step would
148
+ wire the real view inverse. Nothing did. Every gradient therefore slid across
149
+ its own geometry under pan and zoom, which is why the gradient demo shipped
150
+ with pan and zoom disabled. Fine for a viewport-fixed wash, useless for a
151
+ paint on a shape.
152
+
153
+ Gradient paints now carry `units`, mirroring SVG's `gradientUnits`:
154
+
155
+ - `'bounds'` — fractions of the painted node's box, `0..1` per axis (SVG
156
+ `objectBoundingBox`). Resolved by the node painter, so the paint follows the
157
+ node through moves, resizes and rotation.
158
+ - `'local'` — the frame the geometry was handed to the renderer in.
159
+ - `'world'` — scene coordinates; the paint holds still while geometry moves
160
+ through it (SVG `userSpaceOnUse`).
161
+ - `'screen'` — surface pixels, and the default, so every existing gradient
162
+ keeps the behavior it had. WeaselDraw's workspace tint wants exactly this.
163
+
164
+ `WeaselRenderer.render` takes an optional view matrix for `'world'`, and falls
165
+ back to screen space without one. `fillInPoseFrame` resolves `'bounds'` against
166
+ a box and `fillToBoundsFrame` inverts it; `mat3.invert` is new alongside them.
167
+ Supporting helpers: `sampleGradientStops`, `withGradientKind`,
168
+ `gradientGeometry`, `gradientForBounds`.
169
+
170
+ `setFill` takes a whole `paint` as well as a `color`, so a gradient edit is one
171
+ undo entry like any color edit. A `color` no longer tries to inherit alpha from
172
+ a fill that is a gradient.
173
+
174
+ New in `@weasel-js/ui`: `<GradientEditor>` (kind switch, stop strip, per-stop
175
+ color) and `<GradientHandles>` (on-canvas endpoint / center / radius / angle
176
+ handles, positioned through consumer-supplied `toScreen` / `toLocal` so it
177
+ needs no view or scene of its own). Both split live `onInput` from committed
178
+ `onChange`.
179
+
180
+ Converting between kinds is lossy in ways the data makes unavoidable: a radial
181
+ gradient stores no angle, so a round trip through one leaves the segment
182
+ horizontal, and a conic stores no radius, so a round trip through one resets
183
+ the segment's length.
184
+
185
+ `'bounds'` is not a frame you can do polar math in: `x` and `y` are fractions
186
+ of two different lengths, so a circle in it is an ellipse on screen.
187
+ `<GradientHandles>` therefore takes a gradient already resolved by
188
+ `fillInPoseFrame`, and consumers convert edits back with `fillToBoundsFrame`.
189
+
190
+ - 6aaa469: Group resize scales a rotated child along its own axes.
191
+
192
+ Resizing a group applied the group's per-axis scale straight to each leaf's
193
+ axis-aligned `width` and `height` and carried `rotation` through untouched.
194
+ That is correct when `src`/`dst` are already in the leaf's own frame — the
195
+ single-leaf path, where the drag delta is projected into that frame before the
196
+ anchor math runs — but in a group they are world-frame, and a rotated leaf's
197
+ local axes are not the axes being scaled. At 90° a horizontal stretch grew the
198
+ leaf's `width`, when its world-horizontal extent is its `height`.
199
+
200
+ New `remapRotatedLeaf(pose, src, dst)` in `interactions/actions/resize/geometry`
201
+ applies the group affine to the leaf's local frame and drops the shear, which
202
+ the `{x, y, width, height, rotation}` pose model cannot represent. The centre
203
+ moves exactly; each local axis takes the length of its own image; the rotation
204
+ follows the image of the local x-axis. `resizeAction` uses it on the group path
205
+ for any leaf with a rotation, and nowhere else — the unrotated leaf and the
206
+ single-leaf path are byte-identical to before.
207
+
208
+ Rotation and `width` compose exactly under repeated application; only `height`
209
+ drifts, since the perpendicular of a mapped axis is not the image of the
210
+ perpendicular — that gap is exactly the dropped shear. It does not accumulate
211
+ during a drag: every move remaps from the gesture's start poses, not from the
212
+ previous preview.
213
+
214
+ - 40dd97d: A widget declares which gestures it consumes, and a claim bars only those.
215
+
216
+ A HUD widget could be pressed, dragged and hovered and nothing else. The gap
217
+ was in the dispatcher: `affordanceAt` ran on `pointerdown` and hover only, so
218
+ `doubleclick`, `contextmenu` and `wheel` carried no affordance and no binding
219
+ could gate on one. All four now do — `doubleclick` replays it from its press,
220
+ `contextmenu` classifies its own position (a secondary button never reaches
221
+ `onPointerDown`, so there is nothing to replay from) and gains world
222
+ coordinates it never carried, and `wheel` classifies the point under the
223
+ cursor. The immediate-invoker param bag, which forwarded position for `click`
224
+ and `pointerdown` only, now covers `contextmenu`, `longpress` and the
225
+ affordance on all four.
226
+
227
+ **Behavior change, and the reason for the rest of this entry.** An exclusive
228
+ claim previously meant _this pixel is mine_. Under that rule, giving `wheel` an
229
+ affordance would have killed scroll-to-zoom over every floating panel. It now
230
+ means _these gestures are mine_: `LayerHit` and `AffordanceHit` gain
231
+ `claimedKinds`, a set over the new `ClaimableGesture` union (`'pointer'` —
232
+ covering `pointerDown` / `click` / `drag`, one press protocol — plus
233
+ `'doubleClick'`, `'contextMenu'`, `'longPress'`, `'wheel'`). Omitting it bars
234
+ everything, which is what an exclusive claim did before.
235
+
236
+ `Widget.claims` is that set. Absent means every kind but `wheel`, so
237
+ right-clicking or double-clicking HUD chrome stops acting on the scene
238
+ underneath while scroll-to-zoom over a panel is unchanged; a widget that wants
239
+ the wheel asks for it. `claims: []` is decoration and replaces `claimsPointer`,
240
+ which is removed. `HudPointerEvent` gains `doubleclick`, `contextmenu`,
241
+ `longpress` and `wheel` arms.
242
+
243
+ `PointerClaim` and `onPointer`'s return type are removed. The return was
244
+ discarded at every call site, and it cannot be made live: the claim filter runs
245
+ at match time, before any widget is consulted, so there is no later moment at
246
+ which returning `'pass'` could restore a binding the filter already dropped.
247
+
248
+ **Two matcher fixes this depends on.** `doubleClick` and `contextMenu` passed
249
+ the DOM target to `matchTarget` where `click`, `drag` and `longPress` pass the
250
+ affordance; a `kindOf` predicate on either kind therefore received an element
251
+ no predicate in the kit expects. They now match the others. And the kit's body
252
+ predicates — `isBody`, `isSelectedBody`, `isUnselectedBody`, `isEmpty` — carry
253
+ `readsAffordance: false`, which `targetConsultsAffordance` honors. Each has a
254
+ `kindOf` but reads only `bodyTarget`, so the filter inferred that they consult
255
+ the hit and let them survive a claim; with `doubleclick` now carrying one,
256
+ `enterPathEdit`'s `kindOf: isBody` would otherwise have entered path-edit mode
257
+ on a double-click over chrome.
258
+
259
+ `WheelSpec` gains `target`, and `wheel` gains a route-grammar target slot —
260
+ a wheel binding could not gate on anything at all before.
261
+
262
+ - 531150f: A loupe, and the window primitive it needed.
263
+
264
+ `hud.window()` is a draggable, resizable frame drawn in WebGL over the canvas —
265
+ titlebar, eight resize bands, close box. It paints no interior. Interiors come
266
+ from a new optional `content` painter on `Widget`, which `attachHud` draws
267
+ beneath every widget frame in the same layer, clipped to `contentRect`. That
268
+ painter receives `HudContentCtx`, carrying the scene data and view the hud layer
269
+ was already handed; `HudDrawCtx` stays data-free, so widgets remain renderable
270
+ headlessly. Painter commands are in absolute canvas coordinates — the group
271
+ carries a clip, not a transform.
272
+
273
+ `createLoupe()` is the first consumer: a window whose interior shows either the
274
+ scene re-rendered through a magnified inner view, or the actual framebuffer read
275
+ back and magnified 1:1. Both modes exist because neither answers both questions
276
+ honestly — a re-render is crisp at any magnification but its antialiased edge
277
+ colors are not the colors on screen, so the hex readout samples the framebuffer
278
+ in either mode. The frame stays parked and the pointer aims it; content freezes
279
+ while the pointer is over the window, which is what keeps the borders reachable.
280
+ Aiming uses its own `pointermove` listener rather than hud hover, because hud
281
+ hover comes from the layer's `onUncapturedMove` and stops during a captured drag
282
+ — exactly when a magnifier is most wanted.
283
+
284
+ Also fixed in the HUD: `hud.drag` pumped **world** coordinates into widgets
285
+ while `hud.press` sent screen coordinates, because the dispatcher builds
286
+ move/end contexts with an empty dep bag and the `view` lookup silently fell
287
+ through. Invisible at zoom 1, and a window that jumped and tracked backwards at
288
+ any other zoom. The drag action now captures its deps at gesture start.
289
+
290
+ `ImageDrawCommand` gains `sampling: 'linear' | 'nearest'`, applied per draw at
291
+ bind time rather than at upload, since `GLImageCache` keys textures by bitmap
292
+ identity. Without `nearest`, magnifying a framebuffer readback comes back
293
+ blurred, which defeats the readback.
294
+
295
+ **Behavior change in `@weasel-js/core`:** the select tool's area-select drag now
296
+ declines presses that a registered layer's hit-test claimed. It bound
297
+ `{ kind: 'drag', target: 'empty' }`, and the string form of `target` resolves
298
+ from the body only — chrome floating over empty canvas read as empty canvas, so
299
+ area-select swallowed drags on HUD widgets. The adjacent `clearSelection` click
300
+ binding already had the correct shape. Other tools that bind a bare
301
+ `{ kind: 'drag' }` still have this hole.
302
+
303
+ - 596253e: The built-in path and shape painters accept a `FillStyle` in `data.fill`.
304
+
305
+ `kit:path` and `kit:shape` typed `data.fill` as a color string and emitted
306
+ `{ color }`, so a node could not carry a gradient or a pattern without the
307
+ consumer registering a painter of its own — even though the renderer has taken
308
+ every `FillStyle` variant since the paint model landed. The two painters were
309
+ the narrow point, not the renderer.
310
+
311
+ `data.fill` is now `string | FillStyle` (exported as `NodeFill`). A string
312
+ still means a solid color, `'none'` still skips the fill, `undefined` still
313
+ falls back to `data.color` and then to the default — and only when there is no
314
+ stroke, so a stroke-only pencil path stays unfilled. An object is used as-is.
315
+ `ink()` agrees with `paint()` on all of it, which the existing agreement test
316
+ plus a new one both check.
317
+
318
+ One behavior change beyond the widening: `kit:shape` used to paint `'none'` as
319
+ a literal color, since only `kit:path` special-cased it. It now skips the fill,
320
+ matching `kit:path` and matching what the string obviously means.
321
+
322
+ This is the kit half of gradient and texture fills. The app half — widening
323
+ WeaselDraw's own data shape, a fill-kind switch in the properties panel, a
324
+ gradient editor with on-canvas handles, and matching SVG `<linearGradient>` /
325
+ `<pattern>` export — is untouched.
326
+
327
+ - 22eafe6: Pattern fills tile, persist, and round-trip through SVG.
328
+
329
+ The `pattern` variant of `FillStyle` never tiled. `drawPathFillPattern`
330
+ borrowed the `imageFill` program while binding the path fill mesh VAO, which
331
+ enables `a_position` only — `a_uv` was never bound, so `v_uv` was the constant
332
+ `(0, 0)` and every fragment sampled texel (0, 0) of the tile. Textures also
333
+ uploaded with `CLAMP_TO_EDGE`, so correct UVs alone would have smeared rather
334
+ than repeated. The variant had no visual consumer, which is why it went
335
+ unnoticed.
336
+
337
+ `patternFill` is now its own program, taking `gradFill`'s vertex stage:
338
+ paint-space coordinates come from the screen position through `u_worldInv`
339
+ rather than a UV attribute, so the path mesh keeps its position-only layout.
340
+ `GLTextureCache.upload` takes a wrap argument and pattern textures bind
341
+ `REPEAT`.
342
+
343
+ Patterns pick up `units` alongside gradients. For a pattern it names the space
344
+ the tile's **origin and scale** live in — not geometry, which a pattern hasn't
345
+ got. `'bounds'` anchors the tile to the painted node's box, so dragging the
346
+ node carries the pattern and resizing reveals more tiles instead of stretching
347
+ them; `fillInPoseFrame` rebases it by translation only.
348
+
349
+ `TilePatternSpec` is the serializable payload — plain data naming a built-in
350
+ tile (`hatch`, `crosshatch`, `dots`, `chunks`) plus its parameters:
351
+
352
+ ```ts
353
+ { fill: 'pattern', pattern: { tile: 'hatch', color: '#0fb5a8', size: 8 }, units: 'bounds' }
354
+ ```
355
+
356
+ `resolvePatternSpec` turns one into a `TextureHandle` at paint time, memoized
357
+ on the spec's values so identical specs share a texture. The built-in painters
358
+ resolve it alongside `fillInPoseFrame`; a consumer emitting its own draw
359
+ commands resolves it at the same place it calls that one. A `TextureHandle`
360
+ payload still works untouched, but cannot be persisted or exported — prefer
361
+ the spec.
362
+
363
+ `@weasel-js/svg` serializes a tile spec as a `<pattern patternUnits=
364
+ "userSpaceOnUse">` whose children come from the same tile description that
365
+ rasterizes the texture, so the vector and raster forms cannot drift. The spec
366
+ rides along on `data-weasel-tile` for lossless re-import; a hand-authored
367
+ `<pattern>` without it is dropped with a warning rather than guessed at.
368
+ `SerializeOptions.onWarn` is new, and reports paint that SVG cannot express —
369
+ a conic gradient, or a pattern carrying a `TextureHandle`.
370
+
371
+ `tilePreviewSvg` / `tilePreviewCssUrl` render a single tile as a standalone
372
+ `<svg>`, for pickers that need to show a tile outside a document.
373
+
374
+ - cd23624: A text run can turn off a flag its node sets.
375
+
376
+ Run-level `bold` / `italic` / `underline` / `strikethrough` are additive over
377
+ the node's `TextStyle` — a run turns a flag on, never off. So "select a word
378
+ inside an underlined node and hit U" was unrepresentable, and the character bar
379
+ could only refuse.
380
+
381
+ New `setFlagOverRange(runs, style, start, end, key, value)` in
382
+ `features/text/runs/flagRange`, exported alongside `nodeHasFlag`. Turning a
383
+ flag on, or off in a node that doesn't set it, is the ordinary additive write
384
+ and returns the style untouched. Turning it off in a node that _does_ set it
385
+ clears the node flag and raises it on the runs outside the range: identical
386
+ rendered result, expressible edit, and `StyledRun` unchanged — so nothing a
387
+ document can already contain changes meaning.
388
+
389
+ That is the answer to the recorded question of tri-state versus
390
+ normalize-on-write, and it is not close. A tri-state run flag cannot cover
391
+ `bold` or `italic`: those are booleans on a run but `fontWeight` and
392
+ `fontStyle` on the node, so a run's `false` has no node-level boolean to
393
+ override. Tri-state fixes two of the four flags; this fixes all four, and
394
+ without widening the persisted shape.
395
+
396
+ One case is declined rather than approximated. `run.bold` resolves to exactly
397
+ 700 everywhere, so a node at `fontWeight: 900` cannot have its weight pushed
398
+ onto its runs without lightening the text that was _not_ edited. That returns
399
+ `applied: false` and writes nothing; a control should disable rather than
400
+ silently downgrade.
401
+
402
+ `useTextEdit` takes an optional `setStyle(id, style)` for this — the hook could
403
+ read the node style but had nowhere to write one back. Omit it and the toggle
404
+ declines exactly as it does today.
405
+
406
+ ### Patch Changes
407
+
408
+ - Updated dependencies [43482ce]
409
+ - Updated dependencies [40dd97d]
410
+ - @weasel-js/modes@1.0.0
411
+ - @weasel-js/gestures@1.0.0
412
+ - @weasel-js/font@1.0.0
413
+ - @weasel-js/geom@1.0.0
414
+ - @weasel-js/history@1.0.0
415
+
3
416
  ## 0.8.0
4
417
 
5
418
  ### Minor Changes
@@ -1,5 +1,5 @@
1
1
  import { P as Path } from './path-B6MMiodD.js';
2
- import { T as TextureHandle } from './registerTexture-BzHTLhD9.js';
2
+ import { F as FillStyle, S as Stroke, a as TextureHandle } from './paint-types-CnLIzqq1.js';
3
3
 
4
4
  /**
5
5
  * 2D affine matrix utilities. Column-major 9-element Float32Array, matching
@@ -19,6 +19,13 @@ declare function identity(): Mat3;
19
19
  declare function multiply(out: Mat3, m: Mat3): Mat3;
20
20
  declare function translate(m: Mat3, tx: number, ty: number): Mat3;
21
21
  declare function scale(m: Mat3, sx: number, sy: number): Mat3;
22
+ /**
23
+ * Inverse of an affine matrix. Returns identity for a singular matrix
24
+ * (determinant 0) — a degenerate transform collapses every point onto a
25
+ * line, so there is no meaningful inverse and callers get an unmapped
26
+ * space rather than NaNs propagating into a shader uniform.
27
+ */
28
+ declare function invert(m: Mat3): Mat3;
22
29
  declare function apply(m: Mat3, x: number, y: number): [number, number];
23
30
  /**
24
31
  * Map screen pixel coords (0..width on X, 0..height on Y, top-left origin)
@@ -31,177 +38,11 @@ declare const mat3: {
31
38
  multiply: typeof multiply;
32
39
  translate: typeof translate;
33
40
  scale: typeof scale;
41
+ invert: typeof invert;
34
42
  apply: typeof apply;
35
43
  screenToClip: typeof screenToClip;
36
44
  };
37
45
 
38
- /**
39
- * FillStyle and Stroke types — the unified shape for "what color or texture
40
- * paints these pixels," modeled on SVG's paint-server concept.
41
- *
42
- * - `FillStyle` is a tagged union: solid color, pattern, or gradient. Used
43
- * wherever a kit option previously took `fillStyle: string`.
44
- * - `Stroke` pairs a `FillStyle` with structural stroke parameters (width, dash,
45
- * line cap/join, alignment).
46
- * - These types are consumed by the GL renderer's DrawCommand path fills
47
- * and strokes.
48
- *
49
- * The 2D `applyPaint` / `applyStroke` / `renderFilledRegion` helpers that
50
- * formerly lived alongside these types were deleted with the 2D backend in
51
- * Step 10. `alignedStrokeRect` survives as a pure geometry helper used by
52
- * path tessellation and the selection overlay.
53
- */
54
-
55
- /**
56
- * Color/texture strategy for fills (and, via `Stroke.paint`, strokes).
57
- *
58
- * `fill` is optional and defaults to `'solid'` — `{ color: '#abc' }` is
59
- * equivalent to `{ fill: 'solid', color: '#abc' }`. Pattern paints must set
60
- * `fill: 'pattern'` explicitly.
61
- *
62
- * The `'pattern'` variant's payload is a `TextureHandle` (registered via
63
- * `registerTexture()`). The kit-level factory `createTilePattern` (and the
64
- * `patterns-builtin` catalog: `hatch`, `crosshatch`, `dots`, `chunks`)
65
- * produces these handles by rendering a tile to an `OffscreenCanvas` and
66
- * registering the resulting `ImageBitmap` as a GL texture.
67
- */
68
- type FillStyle = {
69
- fill?: 'solid';
70
- color: string;
71
- opacity?: number;
72
- } | {
73
- fill: 'pattern';
74
- pattern: TextureHandle;
75
- opacity?: number;
76
- } | {
77
- fill: 'linear-gradient';
78
- from: {
79
- x: number;
80
- y: number;
81
- };
82
- to: {
83
- x: number;
84
- y: number;
85
- };
86
- stops: GradStop[];
87
- opacity?: number;
88
- } | {
89
- fill: 'radial-gradient';
90
- center: {
91
- x: number;
92
- y: number;
93
- };
94
- radius: number;
95
- stops: GradStop[];
96
- opacity?: number;
97
- } | {
98
- fill: 'conic-gradient';
99
- center: {
100
- x: number;
101
- y: number;
102
- };
103
- angle: number;
104
- stops: GradStop[];
105
- opacity?: number;
106
- };
107
- /** A single color stop within a gradient. `offset` is in 0..1. */
108
- interface GradStop {
109
- offset: number;
110
- color: string;
111
- }
112
- /**
113
- * Where a stroke sits relative to the geometric edge it strokes.
114
- *
115
- * - `'center'` (default): canvas-native — half the stroke width sits inside
116
- * the geometry, half outside.
117
- * - `'inner'`: the entire stroke lies inside the geometry. The outer edge of
118
- * the stroke coincides with the geometric edge.
119
- * - `'outer'`: the entire stroke lies outside the geometry. The inner edge
120
- * of the stroke coincides with the geometric edge.
121
- *
122
- * Mirrors the (proposed) SVG `stroke-alignment` property. Honoring `inner`
123
- * or `outer` is the renderer's responsibility — for axis-aligned rects, the
124
- * kit shifts coordinates by `width / 2`. For arbitrary paths, renderers
125
- * typically use a stencil mask of the stroked path against the geometry.
126
- */
127
- type StrokeAlign = 'center' | 'inner' | 'outer';
128
- /** Stroke style: a FillStyle plus structural line parameters. */
129
- interface Stroke {
130
- paint: FillStyle;
131
- width?: number;
132
- /** Per `CanvasRenderingContext2D.setLineDash` — empty/omitted = solid. */
133
- dash?: number[];
134
- cap?: 'butt' | 'round' | 'square';
135
- join?: 'miter' | 'round' | 'bevel';
136
- /**
137
- * Miter join fallback threshold. When the miter length exceeds
138
- * `miterLimit * width / 2`, the join falls back to a bevel. Default 10
139
- * (matching Canvas2D). SVG's default is 4; consumers that want SVG
140
- * fidelity should set this explicitly when constructing strokes from
141
- * SVG sources where the attribute was omitted.
142
- */
143
- miterLimit?: number;
144
- /** Where the stroke sits relative to the geometric edge. Default `'center'`. */
145
- align?: StrokeAlign;
146
- /**
147
- * Per-anchor RGBA, flat (length = 4 × countPathAnchors(path)). Each
148
- * value in 0..1. Arc-length interpolated across the tessellated ribbon
149
- * between consecutive anchors. When set, `paint` is still required —
150
- * its `opacity` (and color, as a placeholder) flow through the shader.
151
- */
152
- vertexColors?: number[];
153
- /**
154
- * Per-anchor stroke width (length = `countPathAnchors(path)`). When set,
155
- * the tessellator interpolates half-widths along each segment to produce
156
- * a tapered ribbon. `width` is used as the fallback for any anchor whose
157
- * entry is missing or non-finite. Pressure-driven pencil strokes use
158
- * this; pair with `pressureToWidth` to derive widths from stylus input.
159
- *
160
- * Joins between adjacent segments whose widths differ by more than
161
- * `varyingWidthJoinThreshold` (default 1.5×) are forced to bevel
162
- * regardless of the `join` setting — miter math is unstable when widths
163
- * vary across the corner; smooth round joins with mismatched widths
164
- * are a future enhancement.
165
- */
166
- vertexWidths?: number[];
167
- /**
168
- * Max width ratio (greater / lesser) at which a non-bevel join is
169
- * preserved when `vertexWidths` causes adjacent segments to differ.
170
- * Beyond this ratio the join falls back to bevel. Default 1.5. Ignored
171
- * when `vertexWidths` is absent.
172
- */
173
- varyingWidthJoinThreshold?: number;
174
- }
175
- /**
176
- * Inflate (positive) or deflate (negative) a rect to honor `align` when
177
- * stroking it. Returns the rect to pass to a stroked-rect renderer. `width`
178
- * is the stroke width (defaults to 1 to match canvas).
179
- *
180
- * Pure geometry helper — no rendering side effects. Used by path
181
- * tessellation and the selection overlay to produce a rect whose
182
- * center-aligned stroke visually coincides with the requested
183
- * inner/outer-aligned stroke of the original rect.
184
- */
185
- declare function alignedStrokeRect(rect: {
186
- x: number;
187
- y: number;
188
- width: number;
189
- height: number;
190
- }, align: StrokeAlign, width?: number): {
191
- x: number;
192
- y: number;
193
- width: number;
194
- height: number;
195
- };
196
- /** Region a fill is clipped to. */
197
- interface Region {
198
- x: number;
199
- y: number;
200
- w: number;
201
- h: number;
202
- shape: 'rectangle' | 'circle';
203
- }
204
-
205
46
  /**
206
47
  * Typography for `TextPose` and friends. Every field is optional; consumers
207
48
  * pass `{}` or override the few they care about. Defaults live in
@@ -520,6 +361,10 @@ interface ImageDrawCommand {
520
361
  w: number;
521
362
  h: number;
522
363
  opacity?: number;
364
+ /** Magnification filter. `'linear'` (default) smooths; `'nearest'` shows
365
+ * device pixels as hard squares — required by anything magnifying a
366
+ * framebuffer readback, where blur destroys the point of the readback. */
367
+ sampling?: 'linear' | 'nearest';
523
368
  }
524
369
  /**
525
370
  * Custom shader draw command. The renderer generates a quad over `bounds`
@@ -544,4 +389,4 @@ interface ShaderDrawCommand {
544
389
  };
545
390
  }
546
391
 
547
- export { type DrawCommand as D, type FillStyle as F, type GradStop as G, type ImageDrawCommand as I, type Mat3 as M, type PathDrawCommand as P, type ResolvedTextStyle as R, type ShaderProgramHandle as S, type TextDrawCommand as T, type Stroke as a, type GroupDrawCommand as b, type ShaderDrawCommand as c, type ShaderUniform as d, type SolidPaint as e, type StyledRun as f, type TextStyle as g, type TextVerticalAlign as h, DEFAULT_TEXT_STYLE as i, type Region as j, type ResolvedRun as k, type StrokeAlign as l, mat3 as m, alignedStrokeRect as n, fontString as o, markdownToRuns as p, resolveRuns as q, registerProgram as r, resolveTextStyle as s, runsToMarkdown as t, runsToPlainText as u, toRuns as v, verticalAlignOffset as w };
392
+ export { type DrawCommand as D, type GroupDrawCommand as G, type ImageDrawCommand as I, type Mat3 as M, type PathDrawCommand as P, type ResolvedTextStyle as R, type ShaderProgramHandle as S, type TextDrawCommand as T, type ShaderDrawCommand as a, type ShaderUniform as b, type SolidPaint as c, type StyledRun as d, type TextStyle as e, type TextVerticalAlign as f, DEFAULT_TEXT_STYLE as g, type ResolvedRun as h, fontString as i, markdownToRuns as j, resolveRuns as k, resolveTextStyle as l, mat3 as m, runsToMarkdown as n, runsToPlainText as o, registerProgram as r, toRuns as t, verticalAlignOffset as v };