@weasel-js/core 1.4.4 → 1.5.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.
- package/CHANGELOG.md +937 -2202
- package/README.md +118 -75
- package/dist/{autoPoseDescriptor-DF1SnnSx.d.ts → autoPoseDescriptor-CvjflWJK.d.ts} +29 -26
- package/dist/{chunk-2VXGHUVL.js → chunk-BDWAA634.js} +4 -22
- package/dist/chunk-BDWAA634.js.map +1 -0
- package/dist/{chunk-R3AWPTLZ.js → chunk-MG7OXCAI.js} +2760 -4491
- package/dist/chunk-MG7OXCAI.js.map +1 -0
- package/dist/{chunk-PRGBGMH3.js → chunk-MQI4PIX3.js} +3 -3
- package/dist/chunk-MQI4PIX3.js.map +1 -0
- package/dist/{chunk-WPM42WJP.js → chunk-UCPV7JXC.js} +201 -256
- package/dist/chunk-UCPV7JXC.js.map +1 -0
- package/dist/clipboard.d.ts +2 -3
- package/dist/clone.d.ts +3 -2
- package/dist/depSchema-nMqj_qTM.d.ts +3490 -0
- package/dist/{grid-0Pbn5B2C.d.ts → grid-BrIa38gG.d.ts} +7 -10
- package/dist/index.d.ts +1996 -1229
- package/dist/index.js +4 -5
- package/dist/insert.d.ts +4 -4
- package/dist/insert.js +1 -1
- package/dist/move.d.ts +5 -6
- package/dist/move.js +3 -6
- package/dist/move.js.map +1 -1
- package/dist/{options-DbYLImvq.d.ts → options-BDyCnrp8.d.ts} +3 -2
- package/dist/poseDescriptor-CGOgIgf8.d.ts +134 -0
- package/dist/renderer.d.ts +10 -4
- package/dist/renderer.js +4 -5
- package/dist/resize.d.ts +10 -12
- package/dist/resize.js +2 -2
- package/dist/routing.d.ts +1 -142
- package/dist/routing.js +1 -1
- package/dist/routing.js.map +1 -1
- package/dist/{types-ei3UMl9R.d.ts → types-DMyo7dnM.d.ts} +12 -41
- package/dist/{types-DEALFt5F.d.ts → types-DtjCJA5r.d.ts} +9 -3
- package/package.json +13 -10
- package/dist/DrawCommand-CD-ug3d9.d.ts +0 -332
- package/dist/builtins-BXFBXegF.d.ts +0 -840
- package/dist/chunk-2VXGHUVL.js.map +0 -1
- package/dist/chunk-BL65SHCX.js +0 -573
- package/dist/chunk-BL65SHCX.js.map +0 -1
- package/dist/chunk-PRGBGMH3.js.map +0 -1
- package/dist/chunk-R3AWPTLZ.js.map +0 -1
- package/dist/chunk-WPM42WJP.js.map +0 -1
- package/dist/geometry-6fCNhAux.d.ts +0 -114
- package/dist/path-JEV2c5If.d.ts +0 -48
- package/dist/registry-BY-wI9gm.d.ts +0 -4003
- package/dist/types-BHK2dkMu.d.ts +0 -172
- package/dist/types-bcc7jcUy.d.ts +0 -594
- package/dist/view-DSQgxBJB.d.ts +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,586 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9190fc9: Follow-ups a 3D lab turned up while driving core's dispatcher over a WebGL
|
|
8
|
+
viewport. Each one is a place the kit assumed its own 2D renderer.
|
|
9
|
+
|
|
10
|
+
**`classifyTarget` and `affordanceAt` now take the world point their types
|
|
11
|
+
promise.** Both were handed the raw client point at every dispatcher call site,
|
|
12
|
+
so `<SceneCanvas>` and `<CanvasView>` each wrapped their thunk in the same
|
|
13
|
+
`clientToWorld` they also passed the dispatcher, and a consumer hit-tested in
|
|
14
|
+
one space while reading `ctx.world` in another. The conversion happens once now,
|
|
15
|
+
where the event arrives. Behavior-identical for both kit consumers; a consumer
|
|
16
|
+
passing no `clientToWorld` sees identity. **If you pass either option to
|
|
17
|
+
`useGestureDispatcher` yourself and convert coordinates inside it, remove your
|
|
18
|
+
conversion.**
|
|
19
|
+
|
|
20
|
+
**`InvocationCtx.screen` carries a screen point, and is optional.** It was
|
|
21
|
+
filled from the same field as `ctx.world`, so it had never been screen-space.
|
|
22
|
+
It now comes from the event's client coordinates and is absent where the event
|
|
23
|
+
carries none — a keystroke, a UI-driven trigger, a synthetic probe. A
|
|
24
|
+
view-mutating drag still wants `drag.screenDelta`. A click's `ctx.world` is the
|
|
25
|
+
click's own position rather than the origin.
|
|
26
|
+
|
|
27
|
+
**`scene.history` publishes the `History` a `Scene` already owned.** The kit's
|
|
28
|
+
`undo`/`redo` actions resolve a `history` dep and a consumer had nothing to give
|
|
29
|
+
them, so `<SceneCanvas>` cast the Scene itself through `unknown`. It is a façade
|
|
30
|
+
rather than the private engine: mutating members route through the scene's own
|
|
31
|
+
wrappers, so an action-driven undo bumps the version and notifies subscribers.
|
|
32
|
+
|
|
33
|
+
**`resolveOverlays` is the overlay half of the in-flight gesture channel.**
|
|
34
|
+
`resolvePreviews` already answered for the ghosts a gesture displaces; this
|
|
35
|
+
answers for the chrome it draws that is no node at all — a marquee rect, a lasso
|
|
36
|
+
trail, an insert outline — in world geometry, with every degenerate case
|
|
37
|
+
dropped. `insertPreviewExtent` is exported alongside it.
|
|
38
|
+
|
|
39
|
+
**Every overlay variant is now geometry, and the layer owns the paint.**
|
|
40
|
+
`OngoingOverlay`'s `'commands'` variant — arbitrary `DrawCommand[]`, which only
|
|
41
|
+
core's own 2D renderer could execute — **is gone**, along with the `opaque` flag
|
|
42
|
+
on the resolved form and the `action.commands` chrome id. Its two producers
|
|
43
|
+
publish the new `'polyline'` variant instead: a run of world points plus a
|
|
44
|
+
one-word `OverlayRole` (`'cut'` for `slice`, `'connector'` for
|
|
45
|
+
`@weasel-js/diagram`'s `connect`) that a painter maps to a stroke, falling back
|
|
46
|
+
to plain chrome for a role it does not know. `useDispatcherOverlayLayer` draws
|
|
47
|
+
both exactly as they were drawn before, and
|
|
48
|
+
`DispatcherOverlayStyle.roles` is where a consumer restyles one.
|
|
49
|
+
**`ConnectActionOptions.stroke` is removed** — an action no longer names a
|
|
50
|
+
paint; use `roles: { connector: … }` on the layer's style.
|
|
51
|
+
**If you produced a `'commands'` overlay**, publish a `'polyline'` for a line,
|
|
52
|
+
or paint it from a render layer of your own.
|
|
53
|
+
|
|
54
|
+
**labkit stacks two surface buffers around the trial DOM.** The shared buffer
|
|
55
|
+
sat over the trials, which is right for a mark annotating an instrument and
|
|
56
|
+
wrong for an opaque renderer that buries its own pane. `useSurfaceCanvas('under')`
|
|
57
|
+
asks for the lower buffer; the default is unchanged. **`SurfaceCanvasContext`
|
|
58
|
+
now carries `{ over, under }` rather than one canvas** — a consumer providing it
|
|
59
|
+
directly must update the value.
|
|
60
|
+
|
|
61
|
+
**labkit labs get their own chrome regions.** Every region was per-trial, so a
|
|
62
|
+
lab-level control had nowhere to go and `LabPalette` existed by casting a
|
|
63
|
+
two-field object through `as unknown as TrialChromeContext`. `<Lab labChrome>`
|
|
64
|
+
takes contributions shaped exactly like a trial's, against a real lab context.
|
|
65
|
+
|
|
66
|
+
`docs/extending.md` now states the contract for mounting tools outside
|
|
67
|
+
`<SceneCanvas>`, including the half that was written down wrong: capability
|
|
68
|
+
eligibility resolves through `RuleCtx.allowedCapabilities` and `getRuleCtx`, not
|
|
69
|
+
the `activeTool` dep.
|
|
70
|
+
- a2feeb0: The default actions now work in a scene where a container's pose is a frame.
|
|
71
|
+
Each one read a stored pose as though it were world, which is right only under
|
|
72
|
+
the identity composition every consumer ships today; under any other one a
|
|
73
|
+
selection inside a rotated container aligned to the wrong edge, orbited the
|
|
74
|
+
wrong pivot, and grew along the wrong axis.
|
|
75
|
+
|
|
76
|
+
`resize`, `rotate`, `flip`, `align`, `distribute` and `clone` now compose the
|
|
77
|
+
poses they measure up to world, and rebase every pose they write — preview,
|
|
78
|
+
override and committed op alike — into the frame the node stores it in. An
|
|
79
|
+
action that reads world and writes world looks right for one gesture and drifts
|
|
80
|
+
on the next, so `scenePoseFrame` pairs the two directions and each action's
|
|
81
|
+
tests run against a rotated-container fixture where local and world differ.
|
|
82
|
+
|
|
83
|
+
`align`, `distribute` and `flip` also read through `effectivePose` now, so they
|
|
84
|
+
see a gesture's in-flight override instead of the pose underneath it.
|
|
85
|
+
|
|
86
|
+
`group` and `ungroup` re-express every member across the change of frame, so
|
|
87
|
+
neither moves anything on screen. The container takes the world envelope of its
|
|
88
|
+
members' ink (`unionAABB`, so a turned member contributes what it covers rather
|
|
89
|
+
than the box it was posed in). Its pose is derived from its members only under
|
|
90
|
+
an identity composition: anywhere else that derivation reads poses expressed in
|
|
91
|
+
the container's own frame to compute that frame, which is circular, so the
|
|
92
|
+
container keeps the authored envelope until that has its own answer.
|
|
93
|
+
|
|
94
|
+
`useAlign` and `useDistribute` take the same seam — an optional `getParent` on
|
|
95
|
+
the adapter and an optional `composition` in the options. Both default to the
|
|
96
|
+
absolute-pose behavior they have today.
|
|
97
|
+
- 3ecc1be: Dragging a path anchor now edits the anchor on a canvas with no mode registry.
|
|
98
|
+
The drag used to move the whole shape: anchors sit on the selected body, so
|
|
99
|
+
`move`'s ambient binding matched the same press as `editAnchors` and won on
|
|
100
|
+
registration order. `move` now declines a press that hit an anchor or control
|
|
101
|
+
handle, and the drag falls through to `editAnchors`. Canvases that pass
|
|
102
|
+
`getActiveMode` were unaffected, because path-edit mode already filters `move`
|
|
103
|
+
out.
|
|
104
|
+
- dd48085: A window blur now cancels every pointer held on the canvas. A drag in flight
|
|
105
|
+
ends with reason `'cancel'` rather than waiting for a release that may never
|
|
106
|
+
arrive, and a press that had not yet become a drag is dropped without a click.
|
|
107
|
+
- efaf707: A loupe can now be edited through. `createLoupe({ views: api, interactive: true })` makes the lens a view on the canvas: a press inside it selects the node it magnifies, and a drag moves that node by the pointer travel divided by the magnification. With `views` alone the lens paints the canvas's own stack but keeps its clicks as an eyedropper; with `source` alone it is the picture it was. A lens given `views` reads back from its canvas's `getSurfaceRect()`, so over a `paintInto` pane it samples that pane without a `region`.
|
|
108
|
+
|
|
109
|
+
Views gained what that needed. `SceneCanvasApi.addView` declares a `<CanvasView>` from outside React, and its `paint: false` hands the drawing to a host such as a HUD window. `<CanvasView view>` accepts a thunk for a camera derived from the canvas's, and `interactive={false}` makes a view paint-only. A registered layer painted over a view — a HUD window over a panel — now takes presses there instead of the view beneath, and a view no longer hit-tests registered layers it does not paint.
|
|
110
|
+
|
|
111
|
+
`hud.window({ interior: 'pass' })` gives the interior's input to what it shows while the frame stays chrome.
|
|
112
|
+
- 7586835: `@weasel-js/geom` is now the single definition of the geometry both packages
|
|
113
|
+
were carrying, and `@weasel-js/core` imports it.
|
|
114
|
+
|
|
115
|
+
Two of core's command-stream walks had the pen wrong after a `Z`: `boundsOfPath`
|
|
116
|
+
measured a following curve from the last point drawn rather than the subpath
|
|
117
|
+
start, and `extractPolylines` flattened one from there. Both are fixed by
|
|
118
|
+
`forEachSegment`, which now returns the pen where SVG says `Z` leaves it, and
|
|
119
|
+
which also reports the command index and stops when its visitor returns `false`
|
|
120
|
+
— the three things core's own walks needed. Ten walks, six Bernstein
|
|
121
|
+
evaluations, an even-odd ray cast and four rect-corner literals now go through
|
|
122
|
+
geom.
|
|
123
|
+
|
|
124
|
+
`pathPoseDescriptor.remapBounds` scaled a zero-extent source axis by `0`, which
|
|
125
|
+
collapsed a flat path onto the destination origin and left a transform that
|
|
126
|
+
could not be inverted. It uses geom's `boxToBox`, which translates that axis, as
|
|
127
|
+
`scalePathToBounds` already did.
|
|
128
|
+
|
|
129
|
+
Moved into geom so core no longer keeps a second copy: bezier flattening
|
|
130
|
+
(`flattenQuadratic` and both arc-length variants included) and `pathCrop`.
|
|
131
|
+
geom's boolean adapter picks up core's ring nesting, which pre-tests bounding
|
|
132
|
+
boxes and votes over three sample vertices where geom probed one — a hole
|
|
133
|
+
sharing a vertex with its container was misclassified.
|
|
134
|
+
|
|
135
|
+
`rectToContour` now emits the four corners with the closing edge implicit,
|
|
136
|
+
matching what `pointInPolygon` documents and what every call site wants. The
|
|
137
|
+
repeated first vertex it used to emit is a zero-length closing segment for
|
|
138
|
+
anything that strokes the result.
|
|
139
|
+
- 6385c68: A container's pose can now define a **frame**: a child's stored pose is
|
|
140
|
+
expressed in it, so rotating a container rotates its contents and moving one
|
|
141
|
+
carries them without touching their poses. Opt in with
|
|
142
|
+
`<SceneCanvas poseComposition={RIGID_POSE_COMPOSITION}>` or
|
|
143
|
+
`sceneToAdapter(scene, { poseComposition })`. Omit it and nothing changes —
|
|
144
|
+
poses stay absolute, exactly as before.
|
|
145
|
+
|
|
146
|
+
Before this, nesting contributed a clip chain and nothing else. `getPose` was
|
|
147
|
+
documented as returning a local pose while every render walk, both pick
|
|
148
|
+
sources, all selection chrome and the clipboard treated it as world, which
|
|
149
|
+
agreed only because no consumer ever supplied a composition.
|
|
150
|
+
|
|
151
|
+
`getPose` still returns the stored pose. `getWorldPose` on the scene adapter is
|
|
152
|
+
the composed reading, and is what picking, chrome, `getNodeAtPoint` and the
|
|
153
|
+
clipboard consume. `composeRigidPose` / `decomposeRigidPose` and the
|
|
154
|
+
`RECT_POSE_COMPOSITION` / `RIGID_POSE_COMPOSITION` strategies are exported.
|
|
155
|
+
|
|
156
|
+
`PoseComposition` gains a required `closure` field naming the transforms it
|
|
157
|
+
represents exactly. This is a breaking change to that interface for anyone
|
|
158
|
+
constructing one by hand; nothing in the repo did. `RectPose` carries no scale
|
|
159
|
+
factor, so the strategy that ships is rigid — translate and rotate — and an
|
|
160
|
+
anisotropically scaled parent is outside what a pose can hold, since it turns a
|
|
161
|
+
rotated child into a parallelogram.
|
|
162
|
+
|
|
163
|
+
`sceneToAdapter` throws when given both `poseComposition` and
|
|
164
|
+
`cascadeContainerPose`. The cascade translates every descendant when a
|
|
165
|
+
container moves, which is what absolute poses need and would move a framed
|
|
166
|
+
child twice.
|
|
167
|
+
|
|
168
|
+
`useNodeOverlayFrame` read the authored pose, so an overlay ignored gesture
|
|
169
|
+
overrides and derived poses. It reads the effective pose now.
|
|
170
|
+
|
|
171
|
+
The default actions — resize, rotate, group, clone, flip, align, distribute —
|
|
172
|
+
read the strategy as a `poseComposition` dep alongside `poseDescriptor`, and
|
|
173
|
+
`<SceneCanvas>` publishes it through the new `usePoseCompositionDepSource`.
|
|
174
|
+
A consumer wiring actions without `SceneCanvas` has to publish that dep itself;
|
|
175
|
+
without it the actions fall back to identity and write world poses into a
|
|
176
|
+
framed scene.
|
|
177
|
+
|
|
178
|
+
Design: `docs/superpowers/specs/2026-09-10-group-as-frame-design.md`.
|
|
179
|
+
- 2f1ddd0: `@weasel-js/kernel3d` is a new package: poses, an orbit camera, ray picking and screen-projected chrome geometry over core's scene graph and dispatcher. It hosts a renderer rather than owning one — a consumer brings its own and the kernel hands it poses — and it takes core as a peer, the same tier `svg`, `diagram` and `loupe` sit in.
|
|
180
|
+
|
|
181
|
+
Core took no diff for it. `Scene` is generic over its pose and holds a `Pose3` with no adapter; a 3D host passes the dispatcher an identity `clientToWorld` so `ctx.world` stays two numbers and each dep rebuilds the ray from the camera it closes over; tools transfer untouched. The two things that do not transfer are stated rather than guessed: `ViewApi` has no orientation, so the kernel declares a `camera3d` dep of its own, and `PoseDescriptor.remapBounds`/`fromBounds` throw, because a screen rectangle does not name a 3D pose without a depth.
|
|
182
|
+
|
|
183
|
+
`@weasel-js/geom` gains a `./3d` subpath — vectors, quaternions, 4x4 matrices, ray/AABB and ray/plane intersection, and `transformAabb`. Dependency-free like the rest of the package, and immutable tuples rather than classes, so a pose survives `structuredClone` with its methods intact because it never had any.
|
|
184
|
+
|
|
185
|
+
Two corrections to code promoted out of the 3D lab. `projectAabbToScreen` now clips each of the box's twelve edges against the near plane instead of dropping the corners behind it; the old behaviour reported a box too small for anything straddling the near plane, and reported almost nothing for a solid the camera sits inside. And the seam that says how big a node is now asks for its world box rather than a local one to transform: a sphere's box is the same under every rotation, and no transform of a local box reproduces that.
|
|
186
|
+
|
|
187
|
+
`sceneFromJSON`'s `options` argument is now optional. Every field in it already was, so the natural one-argument call did not compile.
|
|
188
|
+
|
|
189
|
+
Also new: a test that a quaternion pose survives `toJSON` and `sceneFromJSON` with its rotation intact. The claim that `Scene` is dimension-neutral had only ever been run against `setPose` and undo.
|
|
190
|
+
- ea285a2: `LayerRecord.locked` now does something. A locked layer still paints, but its
|
|
191
|
+
nodes are out of reach:
|
|
192
|
+
|
|
193
|
+
- A click, marquee, lasso, double-click text edit, the `nodeAtPoint` dep (drop
|
|
194
|
+
targets, the eyedropper) and Select All all pass over them. A pick query that
|
|
195
|
+
only samples paint can take them back with `includeLocked: true`.
|
|
196
|
+
- The scene's selection never holds one: `setSelection` drops them, and locking
|
|
197
|
+
a layer drops its nodes from the selection. Undoing the lock puts them back.
|
|
198
|
+
- Every node mutation on them throws: `add` onto the layer or under a locked
|
|
199
|
+
container, `setPose`, `update`, `remove`/`removeMany` (including a cascade
|
|
200
|
+
that reaches one), `setLayer` from or onto the layer, `setDependsOn`, `move`
|
|
201
|
+
and `reorder`. Kit actions commit through those, so delete, nudge, move,
|
|
202
|
+
resize, rotate, group, ungroup, reorder, cut and the paint actions refuse too.
|
|
203
|
+
- A lock covers a container's whole subtree, whatever layers its descendants
|
|
204
|
+
are tagged to. There is no per-node lock.
|
|
205
|
+
|
|
206
|
+
`scene.isLocked(id)` answers the question, and `scene.unlocked(fn)` runs a
|
|
207
|
+
programmatic edit with the guard lifted. Undo and redo never need it. The
|
|
208
|
+
layer operations — the lock toggle itself, visibility, rename, reorder and
|
|
209
|
+
`removeLayer` — are not gated.
|
|
210
|
+
|
|
211
|
+
`scene.applyBatch`, `scene.batch` and `scene.history.apply`/`applyOps` are now
|
|
212
|
+
all-or-nothing: an op that throws partway reverts everything the call already
|
|
213
|
+
applied and records no undo entry. Before, the ops ahead of the throw stayed
|
|
214
|
+
applied with no entry to undo them.
|
|
215
|
+
- c758b4d: The loupe reads the pixels it is aimed at. Two fixes:
|
|
216
|
+
|
|
217
|
+
- **Its color comes off the frame after the aim.** `loupe.color` and `onColorChange` were read at aim time, which returns the frame before the aim. They now settle on the next frame to land. `pick()` still answers immediately, and now returns `null` if no frame has landed yet. On `@weasel-js/loupe`, a `LoupeSurface` that offers `subscribeFrame` gets this deferred sampling; a surface without it is sampled at aim time, as before.
|
|
218
|
+
- **It works over a pane of a shared canvas.** `CanvasExtensionApi.getSurfaceRect()` returns the rect of `surface` the canvas paints into: the pane's rect under `paintInto`, otherwise the whole canvas. Pass it as `createLoupe`'s new `region` option. The readback then offsets the aim by the pane's origin and stays inside the pane. Before, the loupe over a `paintInto` pane magnified whatever sat at the same offset from the shared canvas's corner.
|
|
219
|
+
|
|
220
|
+
Anything that implements `CanvasExtensionApi` by hand now has to supply `getSurfaceRect`.
|
|
221
|
+
- a41a83a: `<MinimapCanvas>` takes an `animator` prop and forwards it to its `<SceneViewCanvas>`, so the minimap repaints on the animator's ticks and paints its vertex-color overrides. Pass the main canvas's animator and the minimap shows the same animated colors instead of each node's stored ones.
|
|
222
|
+
- 794b4ff: A number pref can name how its value is shown. `ToolPrefNumber.format` is
|
|
223
|
+
`'plain'` or `'compact'`, and labkit sets it with
|
|
224
|
+
`f.number(0).range(0, 2_000_000).format('compact')`. A compact readout keeps a
|
|
225
|
+
value's precision below a thousand and abbreviates above it at one decimal:
|
|
226
|
+
`950`, `40.0K`, `2.0M`.
|
|
227
|
+
|
|
228
|
+
`SliderRow` takes the same choice as `notation`, and its readout reads typed text
|
|
229
|
+
through the new `parseNumber`: thousands commas and a `k`/`m`/`b`/`t` suffix are
|
|
230
|
+
accepted, so `2.5m` commits 2,500,000. An emptied readout now reverts instead of
|
|
231
|
+
committing zero. `formatCompact` and `parseNumber` are exported beside
|
|
232
|
+
`formatNumber`.
|
|
233
|
+
|
|
234
|
+
**A slider readout is no longer narrower than its own values.** The box was a
|
|
235
|
+
fixed width, so a six-digit value lost a digit and read as a smaller number. It
|
|
236
|
+
now widens to fit the longer of its formatted `min` and `max`, and rows whose
|
|
237
|
+
values already fit keep their width. `--wzl-property-readout-w` still sets the
|
|
238
|
+
floor.
|
|
239
|
+
|
|
240
|
+
`NumberRow` and `PrefsForm` ignore the format: one edits through a native number
|
|
241
|
+
input that cannot display `2.0M`, and the other's sliders show no value.
|
|
242
|
+
- b65f4df: `RuleCtx` carries a zoom, not a `View`.
|
|
243
|
+
|
|
244
|
+
`zoomAtLeast` is the only selector that ever read the viewport, and one number
|
|
245
|
+
is all it needs. A host whose viewport is a camera had no `View` to hand over,
|
|
246
|
+
so it could not build a rule context at all — and a dispatcher with no
|
|
247
|
+
`getRuleCtx` skips every eligibility rule silently rather than failing.
|
|
248
|
+
|
|
249
|
+
`RuleCtx.view: View` is now `RuleCtx.zoom?: number`, `BuildRuleCtxArgs` the
|
|
250
|
+
same, and `zoomAtLeast` declines when no zoom is reported. `viewZoom(view)` is
|
|
251
|
+
exported from `@weasel-js/core` for the 2D callers that now pass it; the legacy
|
|
252
|
+
`ChromeCtx` shape still carries a `View` and `resolveVisibility` converts.
|
|
253
|
+
- 90f0bd8: Every 2D affine inversion now uses `@weasel-js/geom`'s `invert` and its singularity rule, which judges the determinant against the matrix's own scale.
|
|
254
|
+
|
|
255
|
+
- SVG import now keeps a transform under a uniformly tiny parent scale, such as `scale(0.0000001)`. It used to call that parent singular and bake the child's rotation into the wrong space. A parent that really is singular now drops the child's transform with a warning, and so does a large parent whose determinant is only rounding. `@weasel-js/svg` now depends on `@weasel-js/geom`.
|
|
256
|
+
- A gradient or pattern measured in `units: 'local'` or `'world'` now draws nothing when that space has no inverse, for example under a group that scales an axis to zero. It used to draw as if untransformed. `mat3.invert` returns `null` for such a matrix instead of the identity, and `PaintBindContext.spaceInverse` now returns `Mat3 | null`, so a registered paint kind should return `null` from `bind` when it gets `null`. Both are type-level breaking changes.
|
|
257
|
+
- In the custom-shader vertex prelude, `v_world` now reads the world origin when the view has no inverse, instead of a scaled mapping that looked plausible and was wrong.
|
|
258
|
+
- `useNodeOverlayFrame`'s `toLocal` now keeps the last mapping that had an inverse while a live view flattens an axis. It used to hand the overlay point back unchanged.
|
|
259
|
+
- b5b8b69: A layer one view hides is now gone from that view for input too, not only for
|
|
260
|
+
paint.
|
|
261
|
+
|
|
262
|
+
- `<SceneCanvas>` documents `layerVisibility` / `layerOrder` as its own props.
|
|
263
|
+
They already reached the painter; now a click, a marquee, a lasso, Cmd+A and
|
|
264
|
+
hover in that canvas pass over a scene layer it hides (keyed
|
|
265
|
+
`scene:<layerId>`). Another view of the same scene still paints and takes it.
|
|
266
|
+
The scene's own `LayerRecord.visible` still applies underneath and cannot be
|
|
267
|
+
overridden.
|
|
268
|
+
- `<CanvasView>` (and `SceneCanvasApi.addView`) take the same two props,
|
|
269
|
+
applied after `layers` narrows the stack, for its paint and its picking. It
|
|
270
|
+
now also honors a layer's `defaultVisible`, as the surface does.
|
|
271
|
+
- `<SceneViewCanvas>`, `<MinimapCanvas>`, `renderSceneToCanvas` and
|
|
272
|
+
`buildSceneViewCommands` take `layerVisibility` / `layerOrder` keyed the same
|
|
273
|
+
way, so the main canvas's map can be passed straight to its minimap.
|
|
274
|
+
- `useSelectTool` takes `alphaOf` and `layerIsPainted`, so a consumer with an
|
|
275
|
+
adapter and no `Scene` can make faded or unpainted nodes unclickable.
|
|
276
|
+
- `ViewApi` gains an optional `layerIsPainted`; `selectAll`, `areaSelect` and
|
|
277
|
+
`lassoSelect` now require the `view` dep and pass it to
|
|
278
|
+
`AreaSelectDep.hitTestArea` / `LassoSelectDep.hitTestArea` / `hitTestLasso`
|
|
279
|
+
as a new optional last argument. A marquee or lasso on a canvas with
|
|
280
|
+
`alphaFor` also skips nodes painted at alpha 0, as a click already did.
|
|
281
|
+
- b2f2d45: Let a `PoseDescriptor` see the node it is describing.
|
|
282
|
+
|
|
283
|
+
Every method on the descriptor took a pose and nothing else, so two shapes
|
|
284
|
+
sharing a pose type — a sphere and a box both posed by position/rotation/scale —
|
|
285
|
+
were indistinguishable to it. New optional `forNode(node)` returns a descriptor
|
|
286
|
+
specialized to one node; `poseDescriptorForNode(descriptor, node)` is the
|
|
287
|
+
accessor, and returns the descriptor unchanged when it declares no
|
|
288
|
+
specialization.
|
|
289
|
+
|
|
290
|
+
This widens API. No existing signature changed, so a descriptor that ignores the
|
|
291
|
+
node compiles and behaves exactly as before. The kit calls `forNode` from the
|
|
292
|
+
sites that already hold a node — marquee and lasso hit-testing, select-tool
|
|
293
|
+
picking, selection chrome bounds, the container-pose cascade, the minimap union,
|
|
294
|
+
`arrayAdapter`, and the move and rotate actions. Sites holding only an id keep
|
|
295
|
+
reading the unspecialized descriptor.
|
|
296
|
+
- 6f5ff46: Pose geometry is supplied once. `<SceneCanvas poseDescriptor={…}>` tells every
|
|
297
|
+
built-in action, the selection chrome, picking and area select how to read and
|
|
298
|
+
rewrite this scene's poses; it defaults to `AUTO_POSE_DESCRIPTOR` (rect and
|
|
299
|
+
`Path` poses). A pose of any other shape now works end to end — before, dragging
|
|
300
|
+
one into a container wrote `NaN` into it.
|
|
301
|
+
|
|
302
|
+
Breaking:
|
|
303
|
+
|
|
304
|
+
- `PoseProjection` is renamed `PoseDescriptor`, and gains a required
|
|
305
|
+
`fromBounds(bounds, template)` and an optional `withRotation(pose, rotation)`.
|
|
306
|
+
- `ResizePose` and `AlignBounds` are removed; use `Bounds`.
|
|
307
|
+
- `RotateGeometry`, `AlignBoundsProjection` and `RECT_ALIGN_PROJECTION` are
|
|
308
|
+
removed.
|
|
309
|
+
- Removed options, replaced by the descriptor: `selectTool.resize.geometry` and
|
|
310
|
+
`useResizePolicy({ projection })` (use `<SceneCanvas poseDescriptor>`);
|
|
311
|
+
`UseRotateOptions.geometry` and `UseMoveOptions.translatePose` (both were
|
|
312
|
+
unread); `poseBounds` on `useSelectTool`, `arrayAdapter`, `sceneToAdapter`,
|
|
313
|
+
`MinimapCanvas` and `nestedHitTester` (use their `poseDescriptor` option);
|
|
314
|
+
`arrayAdapter`'s `intersectsRect` and `translatePose`; the selection overlay's
|
|
315
|
+
`getBounds` and `fromBounds`; the alignment behaviors' `projection`.
|
|
316
|
+
- `Canvas`'s `geometry` prop is renamed `poseDescriptor`. `SceneCanvas`'s own
|
|
317
|
+
`geometry` prop — the `pickEvery` / `boundsOf` hit-test overrides — is a
|
|
318
|
+
different prop and keeps its name.
|
|
319
|
+
- `computeFitView`'s fourth argument is a `PoseDescriptor`, not a bounds
|
|
320
|
+
function.
|
|
321
|
+
- `sceneToAdapter`'s `cascadeContainerPose` is a boolean; the cascade translates
|
|
322
|
+
through the descriptor.
|
|
323
|
+
- The kit's built-in painters only draw rect poses. A node with any other pose
|
|
324
|
+
needs its own painter.
|
|
325
|
+
- `Scene` has a read-only `registry`. For a custom pose kind,
|
|
326
|
+
`unionOfChildrenVia(descriptor)` builds the container-union function to
|
|
327
|
+
register under `UNION_OF_CHILDREN`.
|
|
328
|
+
- edd5b39: `createPoseFeed(scene)` is the channel a renderer weasel does not own uses to
|
|
329
|
+
keep its objects in step with a `Scene`. It publishes `added` / `removed` /
|
|
330
|
+
`changed` with effective poses, so a retained renderer mutates only what moved
|
|
331
|
+
instead of rebuilding every node's draw record on any change.
|
|
332
|
+
|
|
333
|
+
It reads the scene's two clocks separately. Committed edits bump
|
|
334
|
+
`Scene.getVersion()` and cost one `O(n)` walk of three reference comparisons per
|
|
335
|
+
node — the scene mutates node objects in place and swaps their `pose` and `data`
|
|
336
|
+
references, so it is those the feed snapshots. A drag lives in `Scene.overrides`,
|
|
337
|
+
which names the ids it touched and costs `O(changed)`, so the walk never runs on
|
|
338
|
+
the hot path. A `FeedNode` carries the effective pose beside the node's committed
|
|
339
|
+
one, so a host can draw both without a second channel.
|
|
340
|
+
|
|
341
|
+
The delta carries no order: a host that draws in order re-reads
|
|
342
|
+
`renderOrderNodes()`, which is cached until a structural edit. The feed does not
|
|
343
|
+
coalesce notifications either — that is the host scheduler's job.
|
|
344
|
+
|
|
345
|
+
The 3D lab is the first host.
|
|
346
|
+
- 2e2041b: `resolvePreviews(sources, scene)` reads what an in-flight gesture is proposing,
|
|
347
|
+
with no renderer in it.
|
|
348
|
+
|
|
349
|
+
An ongoing action publishes interim poses on its handle rather than writing them
|
|
350
|
+
to the scene, and until now `<SceneCanvas>`'s ghost layer was the only thing that
|
|
351
|
+
knew how to read them: which ids are in flight, whose preview wins when two
|
|
352
|
+
sources name the same id, which previewed nodes are roots and which are their
|
|
353
|
+
previewed children, and which are merely displaced rather than dragged. None of
|
|
354
|
+
that is about drawing. A consumer with its own renderer needed all of it and had
|
|
355
|
+
to rebuild it from `Dispatcher.getInFlightHandles()`.
|
|
356
|
+
|
|
357
|
+
It returns the previewing subtrees as roots, each carrying the committed node
|
|
358
|
+
beside the interim pose and data; `flattenPreviews` walks them parents-first.
|
|
359
|
+
`usePreviewGhostLayer` now draws from it, and the 3D lab under
|
|
360
|
+
`packages/labkit/examples/3d-lab` reads its drag ghosts through it instead of
|
|
361
|
+
its own copy.
|
|
362
|
+
|
|
363
|
+
The overlay channel — marquee, lasso, insert preview — is not covered: those
|
|
364
|
+
arrive as `DrawCommand[]`, which is core's own 2D renderer vocabulary.
|
|
365
|
+
- 65806bc: Fix eleven latent routing faults surfaced by the extraction's correctness pass.
|
|
366
|
+
All predate the move into `@weasel-js/routing`.
|
|
367
|
+
|
|
368
|
+
- Dispatcher and dep-registry ownership are stacks rather than single slots, so
|
|
369
|
+
with two canvases under one `<ActionsProvider>` the one still on screen keeps
|
|
370
|
+
its wiring when the other unmounts. `begin()` no longer returns `null`
|
|
371
|
+
permanently after that.
|
|
372
|
+
- An offhand hotkey hold now releases its own tool instead of whatever is on
|
|
373
|
+
top of the hold stack, so overlapping holds released out of order disengage
|
|
374
|
+
the right tool.
|
|
375
|
+
- `reportDeadClaim` no longer reads `process.env` bare. A consumer loading the
|
|
376
|
+
published ESM in a runtime with no `process` got a `ReferenceError` out of the
|
|
377
|
+
pointerdown listener on any unmatched exclusive claim.
|
|
378
|
+
- `ctx.drag.points` accumulates every pointermove vertex for actions that
|
|
379
|
+
declare no `onMove`. Such an action previously saw only the press point,
|
|
380
|
+
committing a one-vertex path.
|
|
381
|
+
- `ContributionsApi.entries` tracks the entry list rather than the entry list as
|
|
382
|
+
it stood when the focused tool last changed.
|
|
383
|
+
- The dev-only route-conflict reporter compares ambient entries against each
|
|
384
|
+
other (it never did), keeps two collisions on different predicates apart, and
|
|
385
|
+
checks a lone action against itself.
|
|
386
|
+
- `inFlightCursor` reports the most recently started gesture's cursor, agreeing
|
|
387
|
+
with `getActiveAction`; the dispatcher breaks same-specificity hotkey ties in
|
|
388
|
+
favor of the newest hold, agreeing with `ToolsApi.hotkeyEngaged`.
|
|
389
|
+
- **`Eligibility.capabilities` now gates.** It never did: `liveScope`
|
|
390
|
+
short-circuits when no `allows` predicate is supplied, and none was. A tool
|
|
391
|
+
declaring `capabilities` whose action carries no `eligible` rule kept routing
|
|
392
|
+
input in a mode that forbids those tags. The dispatcher builds the predicate
|
|
393
|
+
from the `RuleCtx` it already holds — so this changes behavior only for
|
|
394
|
+
consumers that wired the modes system, which is where the declaration was
|
|
395
|
+
meant to take effect.
|
|
396
|
+
- A multitouch handle is ended when the finger count changes rather than left in
|
|
397
|
+
flight, so a third finger landing mid-pinch no longer commits two gestures on
|
|
398
|
+
the final lift.
|
|
399
|
+
- The route-conflict reporter buckets each key alternative separately, so
|
|
400
|
+
`key: ['h','H']` is reported as colliding with `key: 'H'`. `RegistryEntry`
|
|
401
|
+
gains an optional `argAlternatives` carrying them.
|
|
402
|
+
- a614be4: Extract binding-to-action routing into `@weasel-js/routing`.
|
|
403
|
+
|
|
404
|
+
The gesture dispatcher, the action registry and invoker, tool and contribution
|
|
405
|
+
declaration, the route grammar's reflection surface, and the eligibility rule
|
|
406
|
+
algebra now live in their own package beside `@weasel-js/gestures` and
|
|
407
|
+
`@weasel-js/history`. It ships two entry points: the pure dispatcher on the main
|
|
408
|
+
entry — no React, no DOM — and the React seam that pumps browser events into it
|
|
409
|
+
behind `@weasel-js/routing/react`, with React an optional peer. A kernel that
|
|
410
|
+
drives routing itself can take the first without the second.
|
|
411
|
+
|
|
412
|
+
`@weasel-js/core` depends on the new package and re-exports every symbol that
|
|
413
|
+
moved, so **no existing import changes**, including `@weasel-js/core/routing`.
|
|
414
|
+
A consumer that adds its own dependency still writes
|
|
415
|
+
`declare module '@weasel-js/core'`; the merge carries through core's re-export.
|
|
416
|
+
|
|
417
|
+
`createPaintedCursorState` and its types move to `@weasel-js/cursor`, where the
|
|
418
|
+
cursor they hold is declared. `@weasel-js/core` re-exports them unchanged.
|
|
419
|
+
- ef60ff6: The routing layer's types now state where the dispatch boundary runs, instead of leaving it implicit inside two large interfaces.
|
|
420
|
+
|
|
421
|
+
`Action` splits into `ActionDispatch` — bindings, deps, invoker, scope, gates, cursors — and `ActionPresentation`: label, icon, group, shortcut. `Contribution` splits the same way into `ContributionRouting` and `ContributionChrome`. Both composed types keep every field they had, so nothing that authors an action or a contribution changes.
|
|
422
|
+
|
|
423
|
+
New alongside them: `BindingSource`, the id-plus-`defaultBinding` shape `actionBindings` reads; and `ActionSource`, the single `list()` method the gesture dispatcher consults a registry through. `actionBindings`, `BoundGesture`, `evaluateEnabled`, `ActionEnabledResult`, `SliceDep`, `ClipboardDep` and `TextEditDep` are re-exported from the same barrel entries as before, from new homes. `ClaimableGesture` now lives in `@weasel-js/gestures` beside `GestureName`, and is still exported from `@weasel-js/core`.
|
|
424
|
+
|
|
425
|
+
`_resetEnabledWarnsForTests` is removed. It had no callers and was never on the public barrel.
|
|
426
|
+
|
|
427
|
+
Under this, core's import graph loses a 15-file strongly connected component spanning contributions, tools, actions, the dep schema and the dispatcher. Nothing in routing, tools or contributions is in an import cycle now.
|
|
428
|
+
|
|
429
|
+
The consumer smoke test gained a check that a consumer's own `declare module '@weasel-js/core'` dep merges into `DepSchema` against the published declarations, and stopped reading a subpath import such as `@weasel-js/geom/booleans` as an undeclared package.
|
|
430
|
+
- 269d432: Animated vertex colors now reach nodes the scene paints itself. `<SceneCanvas animator>` paints `animator.colorOverrides` — what `tweenVertexColors`, `springVertexColors`, `cycleVertexColors` and `staggerVertexColors` write — onto the scene's nodes: the built-in path, shape and derived-path painters apply them, and a custom `drawOne` receives them as the new `NodePaintCtx.vertexColors`. Before, only `createPathLayer` read the registry, so a default-painted node ignored every color animation.
|
|
431
|
+
|
|
432
|
+
The same colors reach detached renders: `<SceneViewCanvas>` takes an `animator` prop, and `renderSceneToCanvas`, `renderSceneToPixels` and `buildSceneViewCommands` take the registry as `colorOverrides`. `ColorOverrideRegistry` gains `has(id)` and `resolve(id, channel, base, tMs)`, the one resolution `createPathLayer` and the scene walks now share.
|
|
433
|
+
|
|
434
|
+
Path nodes also paint fill vertex colors from `data.vertexColors`, the same field `PathDrawCommand` uses.
|
|
435
|
+
- 486f631: `staggerVertexColors` no longer jumps straight to its end colors. It published a function-form override that read its timestamp as time since the stagger began, but `createPathLayer` calls a function-form override with `performance.now()`, so on screen every anchor finished the moment the stagger started. The helper now publishes a plain color array on each tick, the same way `tweenVertexColors` does.
|
|
436
|
+
- 0f374d8: `kit:text` nodes with `align: 'center'` or `'right'` now align within
|
|
437
|
+
`pose.width`. They were anchored on `pose.x`, so centered text hung half outside
|
|
438
|
+
the left edge of its box and right-aligned text ended at that edge.
|
|
439
|
+
|
|
440
|
+
Alignment has its own width, separate from the wrap width:
|
|
441
|
+
`LayoutRunsOpts.alignWidth` and `TextDrawCommand.width`, both defaulting to
|
|
442
|
+
`maxWidth`, and a trailing `width` argument on `textCommand` /
|
|
443
|
+
`textCommandFromRuns`. The painter passes its pose width there and still does
|
|
444
|
+
not wrap. `textLineBoxes` and `caretIndexAt` align within `pose.width` even at
|
|
445
|
+
`maxWidth: Infinity`, so the silhouette and the caret follow the paint. A
|
|
446
|
+
`layoutRuns` call or text command that sets no alignment width lays out exactly
|
|
447
|
+
as before.
|
|
448
|
+
- d25a09d: `useSceneTextEdit`'s editing overlay now follows the canvas's pan and zoom
|
|
449
|
+
without being handed a `view`. It reads the camera from the weasel canvas
|
|
450
|
+
mounted inside the `container` it is given — the canvas a double-click landed
|
|
451
|
+
on, when there are several — so the overlay's text sits on the glyphs it
|
|
452
|
+
replaces at any zoom. Passing `view` still works and still wins.
|
|
453
|
+
|
|
454
|
+
The overlay is also clipped to that canvas's box, the same place the canvas
|
|
455
|
+
clips its glyphs. A zoomed text node used to paint its editor across the page
|
|
456
|
+
beside the canvas. Clipping the container with `overflow: hidden` was no fix:
|
|
457
|
+
the browser scrolls a `hidden` box to keep the caret in view, so typing past
|
|
458
|
+
the edge dragged the canvas sideways. The clip cannot scroll.
|
|
459
|
+
|
|
460
|
+
`useTextEdit` gains the underlying option, `getClipRect`: a box in container
|
|
461
|
+
pixels to clip the overlay to, re-read every frame. The overlay now mounts
|
|
462
|
+
inside a clip box of its own, one level below `container`, rather than as the
|
|
463
|
+
container's direct child. With no clip rect, nothing is clipped.
|
|
464
|
+
- deb9e79: Text wraps only where its style says so, and everything that lays a text node
|
|
465
|
+
out now agrees. `TextStyle.wrap` (default `false`) breaks lines between words
|
|
466
|
+
at the pose width; without it a line runs as long as its text and the width
|
|
467
|
+
only resolves `align`.
|
|
468
|
+
|
|
469
|
+
Before this, `kit:text` never wrapped while `createTextLayer`, `textLineBoxes`,
|
|
470
|
+
`caretIndexAt`, `fitTextPose` and the edit overlay all wrapped at the pose
|
|
471
|
+
width. Opening an edit on a `kit:text` line longer than its box reflowed it,
|
|
472
|
+
and a double-click could put the caret on a line the canvas never drew.
|
|
473
|
+
|
|
474
|
+
**Breaking:**
|
|
475
|
+
|
|
476
|
+
- `createTextLayer` and `fitTextPose` (`axis: 'height'`) no longer wrap unless
|
|
477
|
+
the style sets `wrap: true`. Add it to text that should keep wrapping.
|
|
478
|
+
- `TextLineBoxesOpts.maxWidth` and `caretIndexAt`'s `opts` argument are gone,
|
|
479
|
+
along with the `CaretIndexAtOpts` type. Both read `style.wrap`.
|
|
480
|
+
- The edit overlay is `white-space: pre` for unwrapped text, sized to its
|
|
481
|
+
content, and never breaks inside a word in either mode.
|
|
482
|
+
|
|
483
|
+
New: `layoutTextPose` and `textPoseLayoutInput` in `@weasel-js/text`, and
|
|
484
|
+
`textCommandFromPose` in `@weasel-js/core`, which `kit:text` and
|
|
485
|
+
`createTextLayer` both emit. `textLineBoxes` and `caretIndexAt` now resolve
|
|
486
|
+
`align: 'start' | 'end'` against `direction` as the painters do, and
|
|
487
|
+
`useSceneTextEdit` maps a double-click through the node's `verticalAlign`
|
|
488
|
+
(`getVerticalAlign` for custom data), which it used to ignore. SVG export
|
|
489
|
+
writes `data-weasel-wrap="true"` and import reads it back.
|
|
490
|
+
- 830cf7e: A timeline can now book its events ahead of the frame against an outside clock,
|
|
491
|
+
so a sound lands at its true sub-frame time rather than on whichever frame
|
|
492
|
+
noticed the crossing. Pass `booking: { clock }` to `animator.timeline()` and give
|
|
493
|
+
an event a `book(when)` handler; `clock` is anything with `now()` in ms, and an
|
|
494
|
+
`AudioEngine` from `@weasel-js/audio` is one:
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
animator.timeline({
|
|
498
|
+
booking: { clock: engine },
|
|
499
|
+
tracks: [{ kind: 'event', events: [{ t: 500, book: (when) => engine.play(hit, { when }) }] }],
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Each event is booked once per crossing, up to `lookahead` clock ms before its
|
|
504
|
+
edge (default 100). Returning a handle with `stop()` — a `VoiceHandle` is one —
|
|
505
|
+
lets a pause, seek, time-scale change, loop change, `edit` or cancel retract the
|
|
506
|
+
booking while the clock has not reached it; playback books it again wherever it
|
|
507
|
+
next reaches the event. A seek never books the span it skips. An event first
|
|
508
|
+
reached after its edge books at `clock.now()`, or is skipped once it is later
|
|
509
|
+
than `maxLate`. The frame clock's mapping onto the booking clock is smoothed per
|
|
510
|
+
frame, so per-frame read jitter does not reach `when`, and resynced when the two
|
|
511
|
+
jump apart.
|
|
512
|
+
|
|
513
|
+
`TimelineEvent.fire` is now optional, since an event may only book. Reading
|
|
514
|
+
`event.fire` directly needs a check for `undefined`.
|
|
515
|
+
- 50d2881: A number pref with a display unit now reads a unit typed into it: `0.25turn` or
|
|
516
|
+
`30°` in the rotation field stores π/2 or π/6, and `12mm` in a field showing
|
|
517
|
+
centimeters stores what 1.2cm is.
|
|
518
|
+
|
|
519
|
+
**`prefUnit(system, displayUnit, { precision?, suffix? })`** builds a
|
|
520
|
+
`ToolPrefNumberUnit` from a `UnitSystem`, so a leaf no longer hand-writes its
|
|
521
|
+
conversion. `ToolPrefNumberUnit` gains `accepts`, the units a person may type
|
|
522
|
+
and the factor each scales by. `ANGLE_RADIANS` joins the unit tables, and
|
|
523
|
+
`rotationDegreesUnit` is built from it.
|
|
524
|
+
|
|
525
|
+
**`parseNumber(text, units?)`** reads a trailing unit, longest name first, and
|
|
526
|
+
a unit beats a magnitude suffix: with meters accepted, `2m` is two meters.
|
|
527
|
+
|
|
528
|
+
**`UnitField`** is a text field for a number that can carry a unit. Both
|
|
529
|
+
`SelectionPanel` and `PrefsForm` edit a unit leaf through it; a leaf with no
|
|
530
|
+
unit keeps `NumberField`.
|
|
531
|
+
- a5f738a: Resolve a screen rectangle at a pose's own depth, and stop deriving a group's
|
|
532
|
+
bounds from poses the kit cannot read.
|
|
533
|
+
|
|
534
|
+
`kernel3d`'s `PoseDescriptor.remapBounds` and `fromBounds` threw: a rectangle on
|
|
535
|
+
screen names a pose only once something says how far away it is. Both now
|
|
536
|
+
resolve it on the plane through the pose they were handed, facing the camera, so
|
|
537
|
+
neither changes depth. A resize scales uniformly — two screen extents cannot
|
|
538
|
+
name three — and `fromBounds` returns a world-axis-aligned box whose third
|
|
539
|
+
extent is the mean of the two the rectangle gives it.
|
|
540
|
+
|
|
541
|
+
`core`'s `unionOfChildren`, which every scene carries under
|
|
542
|
+
`kit:unionOfChildren`, read its members as rects with no check and produced a
|
|
543
|
+
box of `NaN` in a scene posed otherwise. It now declines, and the container
|
|
544
|
+
keeps its authored pose; `unionOfChildrenVia(descriptor)` remains the way to
|
|
545
|
+
make such a container track its members.
|
|
546
|
+
- ab90aa7: Views now clamp zoom to a positive floor. A view's zoom is always finite and at
|
|
547
|
+
least `ZOOM_FLOOR` (1e-9); a zoom of 0, a negative one, `NaN` or `Infinity`
|
|
548
|
+
becomes the floor, and a non-finite position becomes 0. Dev builds warn once
|
|
549
|
+
when that happens. A negative `View.scale` axis is still a flipped (y-up) axis
|
|
550
|
+
and keeps its sign.
|
|
551
|
+
|
|
552
|
+
The rule lives in `normalizeZoom`, with `normalizeView` applying it to a `View`,
|
|
553
|
+
and every place a view enters the kit goes through it: `<Canvas>` and
|
|
554
|
+
`<SceneCanvas>` (the `view` and `defaultView` props, `setView`, the `view` dep),
|
|
555
|
+
`<CanvasView>` (including a thunked `view`), `<SceneViewCanvas>`,
|
|
556
|
+
`<MinimapCanvas>`, `createViewportLayer`, camera animation targets, `zoomAt`,
|
|
557
|
+
`fitViewToBounds` and `fitZoom`. In labkit, `CanvasStack`, `Stage`, `usePanZoom`,
|
|
558
|
+
`zoomAt`, `centerOn`, `ZoomControl`, a trial's zoom chrome and `as2DView` do the
|
|
559
|
+
same through the new `normalize2DView` and `withZoom`. A loupe's magnification
|
|
560
|
+
follows the same rule.
|
|
561
|
+
|
|
562
|
+
So `screenToWorld`, `canvasCoords` and affordance hit-testing stay finite
|
|
563
|
+
without handling a zero zoom themselves. `pxExtent` no longer guards a zero
|
|
564
|
+
axis, which a view can no longer have, and labkit's `zoomAt` now treats a
|
|
565
|
+
non-finite opening zoom as the floor rather than as 1.
|
|
566
|
+
- Updated dependencies [7586835]
|
|
567
|
+
- Updated dependencies [2f1ddd0]
|
|
568
|
+
- Updated dependencies [b65f4df]
|
|
569
|
+
- Updated dependencies [aa45d32]
|
|
570
|
+
- Updated dependencies [65806bc]
|
|
571
|
+
- Updated dependencies [a614be4]
|
|
572
|
+
- Updated dependencies [ef60ff6]
|
|
573
|
+
- Updated dependencies [0f374d8]
|
|
574
|
+
- Updated dependencies [deb9e79]
|
|
575
|
+
- @weasel-js/geom@1.5.0
|
|
576
|
+
- @weasel-js/routing@1.5.0
|
|
577
|
+
- @weasel-js/cursor@1.5.0
|
|
578
|
+
- @weasel-js/gestures@1.5.0
|
|
579
|
+
- @weasel-js/text@1.5.0
|
|
580
|
+
- @weasel-js/font@1.5.0
|
|
581
|
+
- @weasel-js/history@1.5.0
|
|
582
|
+
- @weasel-js/paint@1.5.0
|
|
583
|
+
|
|
3
584
|
## 1.4.4
|
|
4
585
|
|
|
5
586
|
### Patch Changes
|
|
@@ -1279,1951 +1860,158 @@
|
|
|
1279
1860
|
- @weasel-js/modes@1.4.0
|
|
1280
1861
|
- @weasel-js/paint@1.4.0
|
|
1281
1862
|
|
|
1282
|
-
## 1.
|
|
1863
|
+
## 1.3.0
|
|
1283
1864
|
|
|
1284
|
-
###
|
|
1865
|
+
### Minor Changes
|
|
1285
1866
|
|
|
1286
|
-
-
|
|
1287
|
-
|
|
1288
|
-
A cursor glyph is SVG path `d` strings tagged with a paint role plus a hotspot
|
|
1289
|
-
in glyph units — the one geometry form both a data-URI baker and a `Path2D`
|
|
1290
|
-
painter consume without translating. `bakeCursor` renders one to a
|
|
1291
|
-
`url(data:image/svg+xml,…)` string with the hotspot scaled to integer CSS px,
|
|
1292
|
-
and `cursorFor` memoizes that per name and size. The pencil, pen and eyedropper
|
|
1293
|
-
tools now show their own glyph instead of a shared `crosshair`.
|
|
1294
|
-
|
|
1295
|
-
Cursors ship as SVG with no bitmap fallback: Chrome rasterizes an SVG data-URI
|
|
1296
|
-
cursor at device scale, so it is already crisp on a retina display. `bakeCursor`
|
|
1297
|
-
throws above 128 CSS px rather than emitting a cursor the browser would drop
|
|
1298
|
-
silently — that size is where a later painted tier will take over.
|
|
1867
|
+
- bca99e3: Extract the typography layer into `@weasel-js/text`, and the paint vocabulary
|
|
1868
|
+
into `@weasel-js/paint` — two new Tier A leaves.
|
|
1299
1869
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1870
|
+
`@weasel-js/text` owns the run model, style resolution, `layoutRuns`, wrap and
|
|
1871
|
+
measurement. It depends on `@weasel-js/font`, `@weasel-js/geom` and
|
|
1872
|
+
`@weasel-js/paint`, and on nothing else: a consumer with its own renderer can
|
|
1873
|
+
lay out text without taking the scene graph or a React peer dependency.
|
|
1874
|
+
`layoutRuns` is now public — it was previously reachable only from inside core.
|
|
1303
1875
|
|
|
1304
|
-
|
|
1305
|
-
`
|
|
1306
|
-
|
|
1307
|
-
- Updated dependencies [36b6ee7]
|
|
1308
|
-
- @weasel-js/cursor@1.4.0-pre.1
|
|
1309
|
-
- @weasel-js/font@1.4.0-pre.1
|
|
1310
|
-
- @weasel-js/geom@1.4.0-pre.1
|
|
1311
|
-
- @weasel-js/gestures@1.4.0-pre.1
|
|
1312
|
-
- @weasel-js/history@1.4.0-pre.1
|
|
1313
|
-
- @weasel-js/modes@1.4.0-pre.1
|
|
1314
|
-
- @weasel-js/paint@1.4.0-pre.1
|
|
1315
|
-
- @weasel-js/text@1.4.0-pre.1
|
|
1316
|
-
|
|
1317
|
-
## 1.4.0-pre.0
|
|
1318
|
-
|
|
1319
|
-
### Minor Changes
|
|
1320
|
-
|
|
1321
|
-
- 1214ff5: Split a canvas's paint target from its input target.
|
|
1876
|
+
`@weasel-js/paint` holds `FillStyle`, `Stroke`, gradients, dashes and
|
|
1877
|
+
`TextureHandle`. It was the blocker named in the 2026-07-28 font split: the
|
|
1878
|
+
layout could not move while its fill type lived in the renderer's graph.
|
|
1322
1879
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
canvases share one GL context and one buffer. Each needs its own
|
|
1326
|
-
`<WeaselProvider isolate>`.
|
|
1880
|
+
`@weasel-js/core` re-exports both surfaces, so its own API is unchanged.
|
|
1881
|
+
`Rect` moves to `@weasel-js/geom`, beside `Box`.
|
|
1327
1882
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
`surface` is where pixels land. Attached, they are the same `<canvas>` and
|
|
1331
|
-
`element` keeps working as before. The HUDs render when detached too, anchored
|
|
1332
|
-
to the input box rather than to the shared surface every pane sits in.
|
|
1883
|
+
Breaking for anyone importing these through core's internal paths rather than
|
|
1884
|
+
its public entry (`core/paint-types`, `features/text/*`); those paths are gone.
|
|
1333
1885
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
`
|
|
1337
|
-
`surface` for pixels. And `clientToWorld`'s first parameter widens to
|
|
1338
|
-
`HTMLElement`, which stops compiling for a consumer who annotated that parameter
|
|
1339
|
-
as `HTMLCanvasElement`; one who let it infer is unaffected.
|
|
1886
|
+
Advances and kerning still come from a baked MSDF atlas — laying out from font
|
|
1887
|
+
bytes alone needs the metrics seam in
|
|
1888
|
+
`docs/superpowers/specs/2026-08-28-text-package-extraction-design.md`.
|
|
1340
1889
|
|
|
1341
|
-
<!-- bump-approved: minor: Mike —
|
|
1890
|
+
<!-- bump-approved: minor: Mike — two new published packages (@weasel-js/text, @weasel-js/paint) and layoutRuns promoted to public API, on top of ~50 patch changesets carrying new public surface across core, ui and labkit; called explicitly in conversation on 2026-08-29: "tag a minor release and push" -->
|
|
1342
1891
|
|
|
1343
1892
|
### Patch Changes
|
|
1344
1893
|
|
|
1345
|
-
-
|
|
1894
|
+
- 52c7b2a: Depend on `font` and `core` as exact peers
|
|
1346
1895
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
the
|
|
1352
|
-
caller as marks change.
|
|
1896
|
+
`@weasel-js/font` and `@weasel-js/core` keep registries that consumer code
|
|
1897
|
+
writes into — registered faces and glyph-ready subscribers in one, content
|
|
1898
|
+
handlers and paint kinds and shape painters in the other. Two physical copies
|
|
1899
|
+
in a tree are two registries, so a face registered into one while layout
|
|
1900
|
+
resolves against the other lays out nothing and the canvas is blank.
|
|
1353
1901
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
`
|
|
1902
|
+
Exact sibling pins are what produced the duplicate: a consumer mixing two
|
|
1903
|
+
weasel releases left npm no choice but to nest a second copy, silently. As
|
|
1904
|
+
peers, the same mix is an `ERESOLVE` at install time. `font` is now a peer of
|
|
1905
|
+
`core`, `hud` and `text`; `core` is now a peer of `svg`, joining `d3`, `hud`
|
|
1906
|
+
and `ui`, whose `>=` ranges tighten to exact so no version mix resolves by
|
|
1907
|
+
accident.
|
|
1358
1908
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
therefore `<target>/<node>`, and `createAnnotationStore` takes `targets` alone
|
|
1363
|
-
plus an optional `restore`; `SerializedAnnotations` carries `scenes`, keyed by
|
|
1364
|
-
target. Marks still do not survive a reload — the storage slot is the next arc.
|
|
1909
|
+
**This can break an install that currently succeeds.** Anyone resolving a
|
|
1910
|
+
mixed set of weasel versions by luck now gets an install error instead of a
|
|
1911
|
+
blank canvas. That is the point, but it is a break.
|
|
1365
1912
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1913
|
+
`labkit` deliberately keeps `core` as an ordinary dependency: its build aliases
|
|
1914
|
+
every core entry point to core's built files and inlines them, so it never
|
|
1915
|
+
resolves core at the consumer and has nothing to peer. The flip side is that
|
|
1916
|
+
labkit ships its own copy of core's registries, so a consumer using both still
|
|
1917
|
+
has two — this change does not address that.
|
|
1918
|
+
- 3386d64: Align, distribute and flip use visual bounds
|
|
1368
1919
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
The symptom was a canvas that stopped responding, naming neither canvas nor the
|
|
1374
|
-
registry they shared.
|
|
1920
|
+
These folded each member's unrotated pose box, so "Align Left" on a selection
|
|
1921
|
+
containing a rotated shape lined up the boxes and left the rotated shape's ink
|
|
1922
|
+
sticking out past the others. They now work on the visual bounding box, as
|
|
1923
|
+
Figma and Illustrator do.
|
|
1375
1924
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1925
|
+
Both ends moved together — expanding only the union would have made alignment
|
|
1926
|
+
worse, since the delta runs from an edge of the union to the same edge of each
|
|
1927
|
+
member's box. The new exported `visualBoundsViaDescriptor(pose, geometry)`
|
|
1928
|
+
reads a pose's bounds, recovers its rotation and expands via
|
|
1929
|
+
`axisAlignedBounds`; the union folds those with `unionAABB`. The delta is still
|
|
1930
|
+
applied as a translation of the stored pose through
|
|
1931
|
+
`translatePoseViaDescriptor`, so a shape moves rather than being re-posed.
|
|
1382
1932
|
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
surviving one. A second dispatcher claiming an occupied registry warns once,
|
|
1386
|
-
naming `isolate` as the fix.
|
|
1933
|
+
Flip needed only its union pivot changed: mirroring maps a centre and preserves
|
|
1934
|
+
size, and an expanded box is concentric with the box it came from.
|
|
1387
1935
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
- 7a0c568: Tell an event handler how late its crossing is
|
|
1936
|
+
`alignMoveBehavior` folds the dragged selection the same way, so a drag snaps
|
|
1937
|
+
by its ink.
|
|
1938
|
+
- ffafb7d: Never let an animation's virtual clock run backwards.
|
|
1392
1939
|
|
|
1393
|
-
`
|
|
1394
|
-
|
|
1395
|
-
the
|
|
1396
|
-
|
|
1397
|
-
|
|
1940
|
+
`useAnimator` seeds each animation's `lastRealNow` from `now()` at register
|
|
1941
|
+
time, then advances its virtual clock by the difference against the timestamp
|
|
1942
|
+
the frame loop supplies. Those two share a time origin in a browser, where the
|
|
1943
|
+
rAF timestamp and `performance.now()` are both page-relative — but that is a
|
|
1944
|
+
browser guarantee, not a universal one, and jsdom starts them roughly 600ms
|
|
1945
|
+
apart. The first frame's delta then came out hugely negative and `virtualNow`
|
|
1946
|
+
spent dozens of frames climbing back toward zero before a tween advanced at
|
|
1947
|
+
all: a 40ms glide took 95 frames and over a second of wall time, growing worse
|
|
1948
|
+
the longer the process had been alive.
|
|
1398
1949
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
that makes a handler comparing against `handle.time()` read a negative
|
|
1403
|
-
lateness. A nested timeline's events report the same figure as a top-level
|
|
1404
|
-
one's; the track's offset cancels.
|
|
1950
|
+
A frame's elapsed time is never negative, so the sample is now clamped at
|
|
1951
|
+
zero. Under a shared origin this is a no-op.
|
|
1952
|
+
- ba8b139: Camera animation: `viewport.animatedZoom` now does something
|
|
1405
1953
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1954
|
+
`animatedZoom` has been declared on `SceneCanvasProps.viewport` and read by
|
|
1955
|
+
nothing; Cmd+=/-/0 was a bare `view.set`. It now routes the discrete zoom steps
|
|
1956
|
+
through the kit's `Animator`. Wheel and pinch are unchanged and never animate —
|
|
1957
|
+
their input already delivers a sample per frame.
|
|
1408
1958
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
animator's virtual clock made public, which this does not do.
|
|
1959
|
+
Camera animation is a general surface, not a zoom flag. Three ways in, one
|
|
1960
|
+
runner behind them:
|
|
1412
1961
|
|
|
1413
|
-
`
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1962
|
+
- `useViewAnimation(view, animator?)` — `animate`, `animateToBounds`, `stop`,
|
|
1963
|
+
`isAnimating`, `target`.
|
|
1964
|
+
- The `view` dep gains optional `animate` / `stopAnimation` / `animationTarget`,
|
|
1965
|
+
so any action can glide the camera.
|
|
1966
|
+
- `SceneCanvasApi` gains `animateView` / `stopViewAnimation` /
|
|
1967
|
+
`isViewAnimating` for fit-to-selection, recenter, or a scripted tour. All
|
|
1968
|
+
three are **required** members: anyone hand-implementing `SceneCanvasApi`
|
|
1969
|
+
(a test double, a wrapper) has to add them, the way `CanvasExtensionApi`
|
|
1970
|
+
grew `getPaintedVersion`.
|
|
1417
1971
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
origin resolves correctly.
|
|
1972
|
+
Scale interpolates geometrically and translation is derived from the screen
|
|
1973
|
+
point the two views agree on, so a zoom stays anchored instead of drifting and
|
|
1974
|
+
each frame changes the view by the same ratio. One animation runs at a time; any
|
|
1975
|
+
other view write cancels it, and a cancel leaves the camera where it is rather
|
|
1976
|
+
than jumping to the target. On an uncontrolled canvas the whole animation costs
|
|
1977
|
+
no React render.
|
|
1425
1978
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
`
|
|
1429
|
-
|
|
1979
|
+
**Breaking:** `useViewTween` is removed. `useViewAnimation` keeps its name and
|
|
1980
|
+
changes signature — it takes a `{ get, set }` view channel plus an optional
|
|
1981
|
+
`Animator`, and `animateTo(from, to, { duration, easing })` becomes
|
|
1982
|
+
`animate(to, { ms, easing })`. The `from` argument is gone because the runner
|
|
1983
|
+
reads the live view, which is what lets an interrupted camera resume from where
|
|
1984
|
+
it actually is instead of snapping back to a captured start. `cancel()` is now
|
|
1985
|
+
`stop()`, and `animateToBounds(bounds, currentView, dims, { duration })` is now
|
|
1986
|
+
`animateToBounds(bounds, dims, { ms })` — the `currentView` argument goes for
|
|
1987
|
+
the same reason `from` does.
|
|
1430
1988
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1989
|
+
**Breaking:** `viewport.recenter` and `ViewApi.recenter` widen to
|
|
1990
|
+
`() => View | void`. Returning the target view lets Cmd+0 animate there;
|
|
1991
|
+
returning nothing keeps the existing behavior. `animatedZoom`'s config fields
|
|
1992
|
+
are `ms` / `resetMs` rather than `duration` / `resetDuration`, matching the
|
|
1993
|
+
animator's vocabulary.
|
|
1994
|
+
- 3fb3a46: Forward `onFocus` and `onBlur` from the canvas element
|
|
1436
1995
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1996
|
+
The canvas is focusable by default (`tabIndex` 0) but exposed no way to
|
|
1997
|
+
observe focus, so consumers driving focus-dependent chrome had to attach a
|
|
1998
|
+
listener to an ancestor and infer it. Both are now props on `CanvasProps`, and
|
|
1999
|
+
so reach `SceneCanvasProps` and the canvas element unchanged.
|
|
2000
|
+
- 67bcb05: Drop four values the canvas layer memo no longer reads
|
|
1441
2001
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
`
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
from a ref-driven camera. It previously derived only from the canvas's `view`
|
|
1449
|
-
prop; a consumer keeping a 60 Hz camera out of React state pins that prop to
|
|
1450
|
-
identity and got identity back for every `pan` value — a backdrop that silently
|
|
1451
|
-
never moved.
|
|
2002
|
+
`hit-test affordances against the painted chrome state` moved the selection
|
|
2003
|
+
overlay to reading bounds off the chrome state at paint time, which left
|
|
2004
|
+
`selectedIds`, `multiActive`, `previewToolPose` and `previewToolBounds`
|
|
2005
|
+
referenced only by the `layers` memo's dependency array — nothing in the body
|
|
2006
|
+
used them. Removing them from the array made all four dead locals, so they go
|
|
2007
|
+
too.
|
|
1452
2008
|
|
|
1453
|
-
`useHandTool` no longer builds a velocity tracker and a decay loop it never
|
|
1454
|
-
uses. `inertia` and `axis` were already inert; they are now documented as such
|
|
1455
|
-
until the `viewport.dragPan` action implements them.
|
|
1456
|
-
- 503b56d: Fix two path-walker bugs that produced wrong geometry with no error.
|
|
1457
|
-
|
|
1458
|
-
`tessellate` treated `Z` as a no-op, so a command following a close flattened
|
|
1459
|
-
from the last point drawn rather than from the subpath start — SVG puts the pen
|
|
1460
|
-
back at the start. `pathDistanceToPoint` dispatched through an `if`/`else if`
|
|
1461
|
-
chain with no final `else`, so an unrecognized command code left the coordinate
|
|
1462
|
-
cursor unadvanced and silently misaligned every later read; it now throws.
|
|
1463
|
-
- ac2deea: Add `polylineFromPoints` — the open counterpart to `polygonFromPoints`.
|
|
1464
|
-
|
|
1465
|
-
Same geometry, without the closing edge. A freehand stroke or a measurement
|
|
1466
|
-
line wants this; a region wants the closed one. The pencil tool's drag preview
|
|
1467
|
-
was building its ghost with `polygonFromPoints`, so the edge from the newest
|
|
1468
|
-
sample back to the first swept across the drawing as the stroke grew and read
|
|
1469
|
-
as a marquee.
|
|
1470
|
-
- 23ffb2f: `WeaselRenderer` can draw into a rect of a buffer it does not own.
|
|
1471
|
-
`setTarget({ origin, clear })` applies a viewport and scissor inside `render()`,
|
|
1472
|
-
so N renderers can share one WebGL context and one canvas without a frame clear
|
|
1473
|
-
erasing a co-tenant. The rect's size is the renderer's own `width`/`height`, so
|
|
1474
|
-
`resize()` remains the single source of it.
|
|
1475
|
-
|
|
1476
|
-
Adds API. Two behaviour changes for existing callers: `render()` now
|
|
1477
|
-
re-establishes blend, depth, cull and clear colour every frame instead of once at
|
|
1478
|
-
construction, so a co-tenant moving that state no longer corrupts weasel's
|
|
1479
|
-
frames; and the constructor now throws when handed a WebGL2 context whose
|
|
1480
|
-
attributes report no stencil buffer, which previously rendered clips and even-odd
|
|
1481
|
-
fills wrong rather than failing. A context that cannot report its attributes is
|
|
1482
|
-
unaffected.
|
|
1483
|
-
- 016851c: Stop a stroke with no paint from blanking the whole document.
|
|
1484
|
-
|
|
1485
|
-
`SelectionPanel`'s object leaf started from `{}` when the node held no value
|
|
1486
|
-
yet, so editing any non-paint field of `data.stroke` on an unstroked node
|
|
1487
|
-
committed that field alone — a `Stroke` with no `paint`, which the type
|
|
1488
|
-
forbids. The leaf's declared `default` was dead for writes; it now seeds from
|
|
1489
|
-
it, so writing one field materializes a complete value.
|
|
1490
|
-
|
|
1491
|
-
Such a stroke threw out of `fillInPoseFrame`, and the throw escaped the painter
|
|
1492
|
-
and took the frame with it: the document page and every other node vanished,
|
|
1493
|
-
and the canvas stayed stale until something unrelated requested a redraw — so
|
|
1494
|
-
WeaselDraw opened on an empty workspace and only drew once the pointer moved.
|
|
1495
|
-
`resolveNodeStroke` now reads a paintless stroke as no stroke, and the text
|
|
1496
|
-
painter routes through it like every other painter. The frame loop no longer
|
|
1497
|
-
loses its dirty flag when a paint throws, so one bad frame is retried rather
|
|
1498
|
-
than stranding the surface.
|
|
1499
|
-
- c9dd37f: Render text decorations as a toggle row, and ship a builtin font-family control
|
|
1500
|
-
|
|
1501
|
-
`SelectionPanel` rendered every boolean leaf as a `Switch`, ignoring the leaf's
|
|
1502
|
-
`control` entirely — so the three text decorations arrived as three switch rows
|
|
1503
|
-
where every text editor puts one row of U / S / O. `ToolPrefBooleanControl` now
|
|
1504
|
-
accepts `'toggle'`, `ToolPrefBoolean` carries a `short` label for it (the pair
|
|
1505
|
-
takes the row's name, leaving the leaf only a glyph's worth of room), and the
|
|
1506
|
-
panel honors both. Core's text schema asks for it: `underline`,
|
|
1507
|
-
`strikethrough` and `overline` share a `Decoration` pair.
|
|
1508
|
-
|
|
1509
|
-
A run of adjacent leaves sharing a `pair` renders as one `ToggleBar`, not one
|
|
1510
|
-
bar per leaf — the same segmented control the `Align` row beside it already
|
|
1511
|
-
draws. Each segment still writes only its own leaf, so flipping one decoration
|
|
1512
|
-
never invents values for the other two. An unset toggle is left unselected
|
|
1513
|
-
rather than dimmed: unselected is what a toggle button's off state means, and
|
|
1514
|
-
the dimming the `Switch` path uses for the same case reads as disabled on one.
|
|
1515
|
-
A leaf a consumer claims with its own `renderers` entry drops out of the run.
|
|
1516
|
-
|
|
1517
|
-
`FontFamilySelect` moves from WeaselDraw into `@weasel-js/ui`, and
|
|
1518
|
-
`SelectionPanel` reaches for it on a `font-family` leaf. Core's own default
|
|
1519
|
-
text schema declares that kind, so a consumer passing no `renderers` — the
|
|
1520
|
-
Storybook story, any app taking the defaults — got the literal
|
|
1521
|
-
`(font-family: no renderer)` placeholder where the font picker belongs. The
|
|
1522
|
-
control offers both tiers that can actually paint and probes substitution at
|
|
1523
|
-
the node's own weight and style, so its label names the variant that will
|
|
1524
|
-
render. `@weasel-js/ui` now depends on `@weasel-js/font`.
|
|
1525
|
-
- 9a000ea: A stroked text node now gets hit reach from its stroke. `TEXT_PAINTER` declared
|
|
1526
|
-
no `ink`, so picking fell back to a zero-outset default and a heavily outlined
|
|
1527
|
-
glyph was unpickable across the width of its own outline.
|
|
1528
|
-
|
|
1529
|
-
`kit:derived` also now evaluates ahead of `kit:path` / `kit:shape` / `kit:image`.
|
|
1530
|
-
A derived node whose `data` happens to carry a `path`, `shape` or `image` field
|
|
1531
|
-
was silently painted by those painters instead of from its derived path.
|
|
1532
|
-
- 016851c: Add an editor surface for superscript, subscript and overline.
|
|
1533
|
-
|
|
1534
|
-
`StyledRun.script`, `baselineShift`, `fontScale` and `overline` reached layout,
|
|
1535
|
-
SVG and the DOM overlay but nothing could apply them. The character bar now
|
|
1536
|
-
carries an x² / x₂ pair, an overline toggle beside B / I / U / S, and the two
|
|
1537
|
-
primitives `script` presets — baseline shift and scale — as percentage fields
|
|
1538
|
-
that show what the preset supplies and override just that half when typed over.
|
|
1539
|
-
`overline` also joins the sidebar's node-level Character group. Superscript and
|
|
1540
|
-
subscript take Cmd+Shift+= and Cmd+Shift+-; the unshifted pair is browser zoom,
|
|
1541
|
-
which a page cannot cancel.
|
|
1542
|
-
|
|
1543
|
-
A styling written at a collapsed caret now arms `useTextEdit`'s new
|
|
1544
|
-
`pendingStyle` and applies to the next character typed, instead of being
|
|
1545
|
-
dropped or restyling the whole node. That is what `script` needs — it has no
|
|
1546
|
-
node-level counterpart to write to by design — and it makes the bar agree with
|
|
1547
|
-
Cmd+B, which already behaved this way. `rangeStyle` reports the styling *at* a
|
|
1548
|
-
collapsed caret rather than `{}`, and `toggleStyle` is public.
|
|
1549
|
-
|
|
1550
|
-
Three fixes fall out of putting both paths through one implementation:
|
|
1551
|
-
lowering a flag the node sets now works from the bar and from a collapsed
|
|
1552
|
-
caret, not only from the keyboard over a range; a toggle reads the node's flags
|
|
1553
|
-
as well as the runs, so Cmd+B inside a `fontWeight: 700` node clears bold
|
|
1554
|
-
instead of adding it; and focus returns to the text after a styling control is
|
|
1555
|
-
clicked, so typing continues in the document rather than reaching the app as
|
|
1556
|
-
tool shortcuts.
|
|
1557
|
-
- 8ddec11: Accept a named or cubic-bezier easing wherever a curve is taken, and let a
|
|
1558
|
-
timeline's loop policy change after it is created.
|
|
1559
|
-
|
|
1560
|
-
`easing` was a bare function everywhere, which is fine to call and impossible to
|
|
1561
|
-
name back, show in a picker, or serialize. It now also accepts the name of a
|
|
1562
|
-
built-in (`'easeOutBack'`) or control points (`{ bezier: [0.4, 0, 0.2, 1] }`),
|
|
1563
|
-
resolved by `resolveEasing` at the four places a curve is actually invoked. The
|
|
1564
|
-
union is additive, so every existing function value stays assignable. Bezier x
|
|
1565
|
-
control points are clamped to 0..1, which is what keeps the solve monotone, and
|
|
1566
|
-
the control-point tuple is `readonly` so an `as const` preset is assignable.
|
|
1567
|
-
|
|
1568
|
-
`TimelineHandle.setLoop(loop)` sets policy and nothing else. A timeline already
|
|
1569
|
-
parked at its duration does not restart — `rearm` declines to revive one — so
|
|
1570
|
-
play it again by seeking to 0 and resuming. Restoring saved transport state
|
|
1571
|
-
therefore cannot start playback as a side effect.
|
|
1572
|
-
|
|
1573
|
-
Both settings now read back. `AnimationHandle.timeScale()` returns an
|
|
1574
|
-
animation's own scale, and `Animator.timeScale()` the global one, the way
|
|
1575
|
-
`isPaused()` already pairs with `pause()`. `TimelineHandle.loop()` returns the
|
|
1576
|
-
policy as it stands — `true`, `false`, or the laps a finite loop has left, which
|
|
1577
|
-
falls as they are consumed. A transport UI can drive itself off the handle
|
|
1578
|
-
instead of mirroring what it last wrote, which drifts as soon as anything else
|
|
1579
|
-
holding the handle sets it.
|
|
1580
|
-
- 28894b9: Fix the viewport primitives on an axis with negative scale.
|
|
1581
|
-
|
|
1582
|
-
`View.scale` is documented as pixels per world unit _per axis_, so `scale.y < 0`
|
|
1583
|
-
is the ordinary way to spell a y-up camera. Two primitives did not read it that
|
|
1584
|
-
way, and both failed silently rather than erroring.
|
|
1585
|
-
|
|
1586
|
-
`zoomAt` clamped the signed scale against positive bounds
|
|
1587
|
-
(`min(max, max(min, scale * factor))`), so one wheel step on a y-up view
|
|
1588
|
-
returned `scale.y = +0.1`: the axis flipped and the zoom collapsed to the
|
|
1589
|
-
minimum. It now bounds the magnitude and restores the sign, so a clamp limits a
|
|
1590
|
-
flipped axis instead of unflipping it.
|
|
1591
|
-
|
|
1592
|
-
`clampView` computed the visible world extent as `canvas.height / scale.y`,
|
|
1593
|
-
which is negative on a flipped axis. That made the "is the view zoomed out past
|
|
1594
|
-
the bounds" test never fire, and put the scroll interval on the wrong side of
|
|
1595
|
-
the anchor — a y-up view could be panned outside its own bounds. It now takes
|
|
1596
|
-
the extent as a magnitude and anchors the interval at the rect's far edge when
|
|
1597
|
-
the axis is flipped.
|
|
1598
|
-
|
|
1599
|
-
Found while giving labkit's instrument canvas a declarable coordinate system:
|
|
1600
|
-
routing its wheel through `zoomAt` looked like the obvious way to stop
|
|
1601
|
-
reimplementing fixed-point zoom, and would have been a bug.
|
|
1602
|
-
- c4ccd0a: Zoom now has one clamp. `DEFAULT_MIN_ZOOM` / `DEFAULT_MAX_ZOOM` are exported from
|
|
1603
|
-
`@weasel-js/core` and every zoom path defaults from them — `zoomAt`, the
|
|
1604
|
-
`viewport.zoom` and pinch actions, `usePinchZoomTool`, `fitViewToBounds`,
|
|
1605
|
-
`computeWheelAction` and `useZoom`.
|
|
1606
|
-
|
|
1607
|
-
**Behavior change:** the three paths that carried the second, undocumented pair
|
|
1608
|
-
now cap at 8x rather than 10x. `fitViewToBounds` could previously land at 10x and
|
|
1609
|
-
the next pinch frame would clamp it straight back to 8x. Pass an explicit
|
|
1610
|
-
`maxScale` / `max` to keep 10x.
|
|
1611
|
-
- Updated dependencies [a7fa697]
|
|
1612
|
-
- @weasel-js/geom@1.4.0-pre.0
|
|
1613
|
-
- @weasel-js/text@1.4.0-pre.0
|
|
1614
|
-
- @weasel-js/font@1.4.0-pre.0
|
|
1615
|
-
- @weasel-js/gestures@1.4.0-pre.0
|
|
1616
|
-
- @weasel-js/history@1.4.0-pre.0
|
|
1617
|
-
- @weasel-js/modes@1.4.0-pre.0
|
|
1618
|
-
- @weasel-js/paint@1.4.0-pre.0
|
|
1619
|
-
|
|
1620
|
-
## 1.3.0
|
|
1621
|
-
|
|
1622
|
-
### Minor Changes
|
|
1623
|
-
|
|
1624
|
-
- bca99e3: Extract the typography layer into `@weasel-js/text`, and the paint vocabulary
|
|
1625
|
-
into `@weasel-js/paint` — two new Tier A leaves.
|
|
1626
|
-
|
|
1627
|
-
`@weasel-js/text` owns the run model, style resolution, `layoutRuns`, wrap and
|
|
1628
|
-
measurement. It depends on `@weasel-js/font`, `@weasel-js/geom` and
|
|
1629
|
-
`@weasel-js/paint`, and on nothing else: a consumer with its own renderer can
|
|
1630
|
-
lay out text without taking the scene graph or a React peer dependency.
|
|
1631
|
-
`layoutRuns` is now public — it was previously reachable only from inside core.
|
|
1632
|
-
|
|
1633
|
-
`@weasel-js/paint` holds `FillStyle`, `Stroke`, gradients, dashes and
|
|
1634
|
-
`TextureHandle`. It was the blocker named in the 2026-07-28 font split: the
|
|
1635
|
-
layout could not move while its fill type lived in the renderer's graph.
|
|
1636
|
-
|
|
1637
|
-
`@weasel-js/core` re-exports both surfaces, so its own API is unchanged.
|
|
1638
|
-
`Rect` moves to `@weasel-js/geom`, beside `Box`.
|
|
1639
|
-
|
|
1640
|
-
Breaking for anyone importing these through core's internal paths rather than
|
|
1641
|
-
its public entry (`core/paint-types`, `features/text/*`); those paths are gone.
|
|
1642
|
-
|
|
1643
|
-
Advances and kerning still come from a baked MSDF atlas — laying out from font
|
|
1644
|
-
bytes alone needs the metrics seam in
|
|
1645
|
-
`docs/superpowers/specs/2026-08-28-text-package-extraction-design.md`.
|
|
1646
|
-
|
|
1647
|
-
<!-- bump-approved: minor: Mike — two new published packages (@weasel-js/text, @weasel-js/paint) and layoutRuns promoted to public API, on top of ~50 patch changesets carrying new public surface across core, ui and labkit; called explicitly in conversation on 2026-08-29: "tag a minor release and push" -->
|
|
1648
|
-
|
|
1649
|
-
### Patch Changes
|
|
1650
|
-
|
|
1651
|
-
- 52c7b2a: Depend on `font` and `core` as exact peers
|
|
1652
|
-
|
|
1653
|
-
`@weasel-js/font` and `@weasel-js/core` keep registries that consumer code
|
|
1654
|
-
writes into — registered faces and glyph-ready subscribers in one, content
|
|
1655
|
-
handlers and paint kinds and shape painters in the other. Two physical copies
|
|
1656
|
-
in a tree are two registries, so a face registered into one while layout
|
|
1657
|
-
resolves against the other lays out nothing and the canvas is blank.
|
|
1658
|
-
|
|
1659
|
-
Exact sibling pins are what produced the duplicate: a consumer mixing two
|
|
1660
|
-
weasel releases left npm no choice but to nest a second copy, silently. As
|
|
1661
|
-
peers, the same mix is an `ERESOLVE` at install time. `font` is now a peer of
|
|
1662
|
-
`core`, `hud` and `text`; `core` is now a peer of `svg`, joining `d3`, `hud`
|
|
1663
|
-
and `ui`, whose `>=` ranges tighten to exact so no version mix resolves by
|
|
1664
|
-
accident.
|
|
1665
|
-
|
|
1666
|
-
**This can break an install that currently succeeds.** Anyone resolving a
|
|
1667
|
-
mixed set of weasel versions by luck now gets an install error instead of a
|
|
1668
|
-
blank canvas. That is the point, but it is a break.
|
|
1669
|
-
|
|
1670
|
-
`labkit` deliberately keeps `core` as an ordinary dependency: its build aliases
|
|
1671
|
-
every core entry point to core's built files and inlines them, so it never
|
|
1672
|
-
resolves core at the consumer and has nothing to peer. The flip side is that
|
|
1673
|
-
labkit ships its own copy of core's registries, so a consumer using both still
|
|
1674
|
-
has two — this change does not address that.
|
|
1675
|
-
- 3386d64: Align, distribute and flip use visual bounds
|
|
1676
|
-
|
|
1677
|
-
These folded each member's unrotated pose box, so "Align Left" on a selection
|
|
1678
|
-
containing a rotated shape lined up the boxes and left the rotated shape's ink
|
|
1679
|
-
sticking out past the others. They now work on the visual bounding box, as
|
|
1680
|
-
Figma and Illustrator do.
|
|
1681
|
-
|
|
1682
|
-
Both ends moved together — expanding only the union would have made alignment
|
|
1683
|
-
worse, since the delta runs from an edge of the union to the same edge of each
|
|
1684
|
-
member's box. The new exported `visualBoundsViaDescriptor(pose, geometry)`
|
|
1685
|
-
reads a pose's bounds, recovers its rotation and expands via
|
|
1686
|
-
`axisAlignedBounds`; the union folds those with `unionAABB`. The delta is still
|
|
1687
|
-
applied as a translation of the stored pose through
|
|
1688
|
-
`translatePoseViaDescriptor`, so a shape moves rather than being re-posed.
|
|
1689
|
-
|
|
1690
|
-
Flip needed only its union pivot changed: mirroring maps a centre and preserves
|
|
1691
|
-
size, and an expanded box is concentric with the box it came from.
|
|
1692
|
-
|
|
1693
|
-
`alignMoveBehavior` folds the dragged selection the same way, so a drag snaps
|
|
1694
|
-
by its ink.
|
|
1695
|
-
- ffafb7d: Never let an animation's virtual clock run backwards.
|
|
1696
|
-
|
|
1697
|
-
`useAnimator` seeds each animation's `lastRealNow` from `now()` at register
|
|
1698
|
-
time, then advances its virtual clock by the difference against the timestamp
|
|
1699
|
-
the frame loop supplies. Those two share a time origin in a browser, where the
|
|
1700
|
-
rAF timestamp and `performance.now()` are both page-relative — but that is a
|
|
1701
|
-
browser guarantee, not a universal one, and jsdom starts them roughly 600ms
|
|
1702
|
-
apart. The first frame's delta then came out hugely negative and `virtualNow`
|
|
1703
|
-
spent dozens of frames climbing back toward zero before a tween advanced at
|
|
1704
|
-
all: a 40ms glide took 95 frames and over a second of wall time, growing worse
|
|
1705
|
-
the longer the process had been alive.
|
|
1706
|
-
|
|
1707
|
-
A frame's elapsed time is never negative, so the sample is now clamped at
|
|
1708
|
-
zero. Under a shared origin this is a no-op.
|
|
1709
|
-
- ba8b139: Camera animation: `viewport.animatedZoom` now does something
|
|
1710
|
-
|
|
1711
|
-
`animatedZoom` has been declared on `SceneCanvasProps.viewport` and read by
|
|
1712
|
-
nothing; Cmd+=/-/0 was a bare `view.set`. It now routes the discrete zoom steps
|
|
1713
|
-
through the kit's `Animator`. Wheel and pinch are unchanged and never animate —
|
|
1714
|
-
their input already delivers a sample per frame.
|
|
1715
|
-
|
|
1716
|
-
Camera animation is a general surface, not a zoom flag. Three ways in, one
|
|
1717
|
-
runner behind them:
|
|
1718
|
-
|
|
1719
|
-
- `useViewAnimation(view, animator?)` — `animate`, `animateToBounds`, `stop`,
|
|
1720
|
-
`isAnimating`, `target`.
|
|
1721
|
-
- The `view` dep gains optional `animate` / `stopAnimation` / `animationTarget`,
|
|
1722
|
-
so any action can glide the camera.
|
|
1723
|
-
- `SceneCanvasApi` gains `animateView` / `stopViewAnimation` /
|
|
1724
|
-
`isViewAnimating` for fit-to-selection, recenter, or a scripted tour. All
|
|
1725
|
-
three are **required** members: anyone hand-implementing `SceneCanvasApi`
|
|
1726
|
-
(a test double, a wrapper) has to add them, the way `CanvasExtensionApi`
|
|
1727
|
-
grew `getPaintedVersion`.
|
|
1728
|
-
|
|
1729
|
-
Scale interpolates geometrically and translation is derived from the screen
|
|
1730
|
-
point the two views agree on, so a zoom stays anchored instead of drifting and
|
|
1731
|
-
each frame changes the view by the same ratio. One animation runs at a time; any
|
|
1732
|
-
other view write cancels it, and a cancel leaves the camera where it is rather
|
|
1733
|
-
than jumping to the target. On an uncontrolled canvas the whole animation costs
|
|
1734
|
-
no React render.
|
|
1735
|
-
|
|
1736
|
-
**Breaking:** `useViewTween` is removed. `useViewAnimation` keeps its name and
|
|
1737
|
-
changes signature — it takes a `{ get, set }` view channel plus an optional
|
|
1738
|
-
`Animator`, and `animateTo(from, to, { duration, easing })` becomes
|
|
1739
|
-
`animate(to, { ms, easing })`. The `from` argument is gone because the runner
|
|
1740
|
-
reads the live view, which is what lets an interrupted camera resume from where
|
|
1741
|
-
it actually is instead of snapping back to a captured start. `cancel()` is now
|
|
1742
|
-
`stop()`, and `animateToBounds(bounds, currentView, dims, { duration })` is now
|
|
1743
|
-
`animateToBounds(bounds, dims, { ms })` — the `currentView` argument goes for
|
|
1744
|
-
the same reason `from` does.
|
|
1745
|
-
|
|
1746
|
-
**Breaking:** `viewport.recenter` and `ViewApi.recenter` widen to
|
|
1747
|
-
`() => View | void`. Returning the target view lets Cmd+0 animate there;
|
|
1748
|
-
returning nothing keeps the existing behavior. `animatedZoom`'s config fields
|
|
1749
|
-
are `ms` / `resetMs` rather than `duration` / `resetDuration`, matching the
|
|
1750
|
-
animator's vocabulary.
|
|
1751
|
-
- 3fb3a46: Forward `onFocus` and `onBlur` from the canvas element
|
|
1752
|
-
|
|
1753
|
-
The canvas is focusable by default (`tabIndex` 0) but exposed no way to
|
|
1754
|
-
observe focus, so consumers driving focus-dependent chrome had to attach a
|
|
1755
|
-
listener to an ancestor and infer it. Both are now props on `CanvasProps`, and
|
|
1756
|
-
so reach `SceneCanvasProps` and the canvas element unchanged.
|
|
1757
|
-
- 67bcb05: Drop four values the canvas layer memo no longer reads
|
|
1758
|
-
|
|
1759
|
-
`hit-test affordances against the painted chrome state` moved the selection
|
|
1760
|
-
overlay to reading bounds off the chrome state at paint time, which left
|
|
1761
|
-
`selectedIds`, `multiActive`, `previewToolPose` and `previewToolBounds`
|
|
1762
|
-
referenced only by the `layers` memo's dependency array — nothing in the body
|
|
1763
|
-
used them. Removing them from the array made all four dead locals, so they go
|
|
1764
|
-
too.
|
|
1765
|
-
|
|
1766
|
-
The memo now rebuilds the layer array on layer/tool/geometry changes rather
|
|
1767
|
-
than additionally on every selection and preview-pose change. Selection chrome
|
|
1768
|
-
is unaffected: it repaints from chrome state, not from the identity of this
|
|
1769
|
-
array.
|
|
1770
|
-
- 47cbb08: A closed subpath's dash no longer seams at its start vertex
|
|
1771
|
-
|
|
1772
|
-
`splitForDash` flushed the run still open when a closed subpath's walk returned
|
|
1773
|
-
to the vertex it started from as its own open sub-polyline, so it and the run
|
|
1774
|
-
that began there rendered as two butt-capped ribbons meeting at a point — a
|
|
1775
|
-
notch on the corner of any dashed rectangle whose perimeter isn't a whole
|
|
1776
|
-
multiple of the pattern. They are joined now, and the join the stroke asked for
|
|
1777
|
-
is drawn across the seam like any other corner. A pattern whose first "on"
|
|
1778
|
-
length covers the whole perimeter emits a closed ribbon, identical to the
|
|
1779
|
-
undashed stroke.
|
|
1780
|
-
- f43e9c2: A derived edge follows the drag that moves its endpoint
|
|
1781
|
-
|
|
1782
|
-
`move`, `resize` and `rotate` kept their in-flight poses in action-local
|
|
1783
|
-
scratch and published them only as `previewIds` / `previewPose`. That surface
|
|
1784
|
-
is enough to paint a ghost and size selection chrome, but nothing that asks
|
|
1785
|
-
the *scene* where a node is can see it — and `scenePoseLookup`, which resolves
|
|
1786
|
-
a derived node's geometry, asks the scene. So dragging a box left its edge
|
|
1787
|
-
anchored to the pre-drag position until the drop, when the commit invalidated
|
|
1788
|
-
the dependents and the edge jumped.
|
|
1789
|
-
|
|
1790
|
-
The three actions now also publish each frame into the scene's ephemeral pose
|
|
1791
|
-
overrides (`syncPreviewOverrides` / `dropPreviewOverrides` in
|
|
1792
|
-
`interactions/actions/previewOverrides.ts`). Overrides bypass `executeAndLog`,
|
|
1793
|
-
so a drag still commits as exactly one undo entry — the reason the actions
|
|
1794
|
-
avoided per-frame scene writes in the first place was history, and this writes
|
|
1795
|
-
no history. Entries are set once and mutated in place, published with a single
|
|
1796
|
-
`commit()` per frame.
|
|
1797
|
-
|
|
1798
|
-
Picking follows for free: the pick source resolves a derived path through its
|
|
1799
|
-
own override-aware `poseOf`, so an edge is grabbable where it is drawn
|
|
1800
|
-
mid-gesture rather than where it used to be.
|
|
1801
|
-
|
|
1802
|
-
`clone` is deliberately untouched — its previews are the new ghosts at the
|
|
1803
|
-
drag target, and the originals never move, so nothing derives from a changed
|
|
1804
|
-
pose.
|
|
1805
|
-
|
|
1806
|
-
Also closes the matching gap in the preview-ghost layer, which built a
|
|
1807
|
-
container's clip with no derived path and so ghosted a derived container
|
|
1808
|
-
without one.
|
|
1809
|
-
|
|
1810
|
-
Note for anyone with a hand-written `Scene` stand-in: `overrides` is now read
|
|
1811
|
-
on every gesture frame. It was already required by the `Scene` contract, but a
|
|
1812
|
-
partial fake that omitted it will now throw rather than silently skip.
|
|
1813
|
-
- bb27e83: A derived node is clickable where it paints
|
|
1814
|
-
|
|
1815
|
-
A node whose geometry comes from `derivePath` had no silhouette and no `ink`:
|
|
1816
|
-
`NodeShapeEntry.silhouette` took only `(node, pose)`, and a derived path is
|
|
1817
|
-
resolved from the *dependencies'* poses, which a painter has no handle on. So
|
|
1818
|
-
`kit:derived` could not report one, `shapeCoversPoint` read the resulting null
|
|
1819
|
-
as "no opinion" and answered `true` everywhere, and picking fell back to the
|
|
1820
|
-
node's own pose — for an edge, a zero-sized placeholder at the origin. An edge
|
|
1821
|
-
was unpickable, and a derived container contributed no clip.
|
|
1822
|
-
|
|
1823
|
-
`silhouette` now takes a `NodeSilhouetteCtx` carrying `derivedPath`, on the
|
|
1824
|
-
same convention `NodePaintCtx` already uses, and `kit:derived` reports the
|
|
1825
|
-
derived path as its silhouette and its declared stroke as its `ink`.
|
|
1826
|
-
|
|
1827
|
-
Resolving that path needs the scene, so it is the *source* that answers, not
|
|
1828
|
-
the painter: `PickSource.derivedPathOf`, a matching optional argument to
|
|
1829
|
-
`buildSceneTree`, and `SceneSlotConfig.derivedPathOf` — the slot already
|
|
1830
|
-
carried the derived path a node *paints*, and now also the clip a derived
|
|
1831
|
-
container *imposes*, so the live canvas and the headless walk clip alike. The
|
|
1832
|
-
bare-adapter paths supply none of them and behave exactly as before.
|
|
1833
|
-
|
|
1834
|
-
The pre-filter had to move with it. `useSceneSelectTool` grew its region test
|
|
1835
|
-
from the node's pose, which for a derived node is the wrong box entirely, so
|
|
1836
|
-
the edge was rejected before the shape test could claim it. It now tests the
|
|
1837
|
-
derived path when there is one — `poseContains` already reads a path-like pose
|
|
1838
|
-
as a path, so this reuses it rather than adding a second reach calculation.
|
|
1839
|
-
|
|
1840
|
-
`findShapeSilhouette` skips its memo when handed a derived path. That slot is
|
|
1841
|
-
keyed on `(node, pose, data)` and cannot see the path, so it would serve one
|
|
1842
|
-
caller's silhouette to a caller that passed a different one — the same reason
|
|
1843
|
-
`kit:derived` already skips `PAINT_SLOT`.
|
|
1844
|
-
- 6a33c3f: A node's path can be derived from other nodes' poses
|
|
1845
|
-
|
|
1846
|
-
A node declares `dependsOn: NodeId[]` and a `derivePath` function resolved by key
|
|
1847
|
-
through `SceneRegistry`, and the scene walks resolve its path before painting
|
|
1848
|
-
rather than it being authored. An edge drawn between two boxes is then an
|
|
1849
|
-
ordinary scene node — selectable, styleable, exportable — whose geometry never
|
|
1850
|
-
enters undo history. The seam and its traps are in `docs/extending.md`.
|
|
1851
|
-
|
|
1852
|
-
New surface: `scene.removeMany(ids)`; `dependsOn` and `derivePath` on
|
|
1853
|
-
`NodeBase` and on `AddNodeSpec`, which is what a consumer writes;
|
|
1854
|
-
`SceneRegistry.derivePath`; `SerializedNode.dependsOn` and
|
|
1855
|
-
`SerializedNode.derivePathKey`, both additions to the serialization format;
|
|
1856
|
-
`NodePaintCtx.derivedPath`.
|
|
1857
|
-
|
|
1858
|
-
Deleting a node now deletes everything that derives from it, transitively,
|
|
1859
|
-
including those nodes' own subtrees, in one undo entry — so `scene.remove` can
|
|
1860
|
-
remove nodes anywhere in the tree that the caller never named, and `removeLayer`
|
|
1861
|
-
reaches nodes on other layers. Undo after the built-in **Delete** key does not
|
|
1862
|
-
yet restore the cascaded nodes; see "Derived geometry follow-ups" in
|
|
1863
|
-
`docs/TODO.md`.
|
|
1864
|
-
|
|
1865
|
-
**Breaking: `defaultDrawOne` takes `(node, pose, view?, ctx?)`.** The paint
|
|
1866
|
-
context moves to a fourth parameter, so a call passing a `NodePaintCtx` third is
|
|
1867
|
-
now a type error rather than a silent slide into the `view` slot. The same
|
|
1868
|
-
fourth parameter is added to the `SceneViewDrawOne` and `SceneSlotConfig.drawOne`
|
|
1869
|
-
callback types, which is not a break: an existing three-parameter implementation
|
|
1870
|
-
still satisfies them, and an existing three-argument call still compiles.
|
|
1871
|
-
|
|
1872
|
-
**Breaking: `Scene` gained a required `removeMany`.** A hand-written object
|
|
1873
|
-
typed as a `Scene` — a test double, most likely — no longer typechecks until it
|
|
1874
|
-
implements it.
|
|
1875
|
-
|
|
1876
|
-
**Breaking: `kit:remove`'s op payload changed shape.** `rootId` / `parent` /
|
|
1877
|
-
`index` became `detached: { id, parent, index }[]`, because a cascaded dependent
|
|
1878
|
-
is not a descendant of the removed node and the tree has to be told about every
|
|
1879
|
-
subtree that came out of it. A history persisted by an older build now throws
|
|
1880
|
-
mid-undo rather than degrading. The break is deliberate; kit op payloads are not
|
|
1881
|
-
versioned.
|
|
1882
|
-
- c24e7de: Detached views follow pose overrides
|
|
1883
|
-
|
|
1884
|
-
`<SceneViewCanvas>` and `<MinimapCanvas>` re-rendered off `scene.getVersion()`,
|
|
1885
|
-
which a pose override deliberately never bumps — so they kept painting document
|
|
1886
|
-
poses while `<SceneCanvas>` painted the overridden ones. A minimap beside a
|
|
1887
|
-
canvas driving a drag or a simulation silently disagreed with it.
|
|
1888
|
-
|
|
1889
|
-
`<SceneViewCanvas>` now paints through `useFrameLoop` instead of from React, and
|
|
1890
|
-
subscribes to `scene.overrides`. A render (prop change or version bump) and an
|
|
1891
|
-
override commit both just mark the surface dirty, and one animation frame
|
|
1892
|
-
coalesces them — so a 60 Hz override loop repaints these views with no React
|
|
1893
|
-
render, and a backgrounded tab stops painting them entirely. The mount paint
|
|
1894
|
-
stays synchronous, so the first frame is still the scene rather than a blank
|
|
1895
|
-
canvas. `<MinimapCanvas>` inherits all of this through it.
|
|
1896
|
-
|
|
1897
|
-
Repaints driven by a prop change are now asynchronous: they land on the next
|
|
1898
|
-
animation frame rather than in the layout effect of the render that caused them.
|
|
1899
|
-
Code that renders and then reads pixels in the same tick needs to wait a frame.
|
|
1900
|
-
|
|
1901
|
-
A minimap's *framing* still derives from document poses, so a node overridden
|
|
1902
|
-
outside the document bounds paints outside the fitted frame — recomputing the
|
|
1903
|
-
fit per frame would rescale the whole minimap throughout a settle.
|
|
1904
|
-
- ce82f4a: An enum leaf can ask for a segmented control, and `pair` works inside an object
|
|
1905
|
-
|
|
1906
|
-
`ToolPrefEnumControl` gains `'toggle'`: a three-option enum shows all three at
|
|
1907
|
-
once instead of hiding two behind a select. Options carry an optional `short`
|
|
1908
|
-
label — a capital or two — for the width a property row has; the full `label`
|
|
1909
|
-
stays the accessible name, so the abbreviation never becomes the only thing
|
|
1910
|
-
naming the option. A mixed selection selects no segment rather than picking a
|
|
1911
|
-
winner.
|
|
1912
|
-
|
|
1913
|
-
`pair` now merges fields inside an object leaf, as it already did for section
|
|
1914
|
-
rows — a hint shouldn't mean something different for being a field of a value
|
|
1915
|
-
rather than a sibling of one. It merges *adjacent* leaves in both places, so
|
|
1916
|
-
the schema orders family, size, weight: size and weight pair, and family (which
|
|
1917
|
-
sat between them) moves ahead of the pair rather than splitting it.
|
|
1918
|
-
|
|
1919
|
-
A stroke's cap, join and align share one row; property rows wrap rather than
|
|
1920
|
-
overflow when the controls in them don't fit.
|
|
1921
|
-
- be697dc: Add ephemeral pose overrides to the scene
|
|
1922
|
-
|
|
1923
|
-
`scene.overrides` holds a per-node `{ pose?, alpha? }` that the render and
|
|
1924
|
-
hit-test paths read through and that history, `toJSON()` and `getVersion()`
|
|
1925
|
-
never see. It is additive: a scene with no overrides behaves exactly as before.
|
|
1926
|
-
|
|
1927
|
-
This is where per-frame motion belongs. A 60 Hz loop previously had to write
|
|
1928
|
-
through `setPose`, which records an undo entry (one per frame at best, batched)
|
|
1929
|
-
and bumps the scene version, re-rendering every `useSyncExternalStore`
|
|
1930
|
-
subscriber. It also had to allocate a fresh pose object per moving node per
|
|
1931
|
-
frame, because the painter memo keys on pose reference. An override entry is
|
|
1932
|
-
hoisted once and mutated in place; `overrides.commit()` publishes the frame and
|
|
1933
|
-
invalidates the memo for the overridden nodes only.
|
|
1934
|
-
|
|
1935
|
-
`commit()` is required after an in-place mutation — without it the memo serves
|
|
1936
|
-
the previous frame's draw. Overrides are cleared when a node is removed, since
|
|
1937
|
-
ids are reusable. To make a frame permanent, write it once through `setPose`
|
|
1938
|
-
and clear the override; that single step is the undo entry.
|
|
1939
|
-
|
|
1940
|
-
`ForceGraphDemo` now settles with zero history entries and bakes the result as
|
|
1941
|
-
one, replacing a per-tick batch of 24 `setPose` calls.
|
|
1942
|
-
- e909a3b: `fitTextPose` sizes a box the renderer will actually fill
|
|
1943
|
-
|
|
1944
|
-
It was the fourth site measuring text its own way: `ctx.measureText` per
|
|
1945
|
-
character against system fonts, no kerning, `pose.text` only. Nothing masked
|
|
1946
|
-
it the way the WebGL context masked the caret — a consumer calling it got a
|
|
1947
|
-
box that disagreed with the paint, narrower by a kern on every pair and wrong
|
|
1948
|
-
by the whole difference between the installed family and the registered face.
|
|
1949
|
-
It goes through the shared layout now, so it sees kerning and per-run styling.
|
|
1950
|
-
|
|
1951
|
-
**Breaking:** `fitTextPose(ctx, pose, opts)` is now `fitTextPose(pose, opts)`.
|
|
1952
|
-
- 26bbdcf: Paint the canvas from its own animation frame instead of from a React render
|
|
1953
|
-
|
|
1954
|
-
`requestRedraw()` marks the surface dirty and the next frame paints, so many
|
|
1955
|
-
redraws in one tick cost one paint. The view gains an imperative path on the
|
|
1956
|
-
canvas handle — `setView` / `getView` / `subscribeView` — and `SceneCanvas` no
|
|
1957
|
-
longer holds it in React state, so a camera moving at 60 Hz costs no renders.
|
|
1958
|
-
Consumers passing a `view` prop stay controlled and are unaffected.
|
|
1959
|
-
|
|
1960
|
-
Opt-ins that come with it: `syncPaint` paints inside the commit for a consumer
|
|
1961
|
-
that wants the old whole-cloth guarantee, `useScene(…, { subscribe: false })`
|
|
1962
|
-
gives a host the scene without a render per mutation, `useSceneTextEdit`'s
|
|
1963
|
-
`view` option accepts a thunk so the overlay tracks a ref-driven camera, and a
|
|
1964
|
-
`contentVersion` prop feeds the version that `getPaintedVersion()` reports.
|
|
1965
|
-
|
|
1966
|
-
Two public signatures changed. `usePinchZoomTool` takes a view getter,
|
|
1967
|
-
`getView: () => View`, where it took a `View` — nothing re-renders to refresh a
|
|
1968
|
-
captured value any more. `CanvasExtensionApi` gained five required members —
|
|
1969
|
-
`getView`, `setView`, `subscribeView`, `subscribeFrame`, `getPaintedVersion` —
|
|
1970
|
-
so external code hand-implementing that interface stops typechecking; code that
|
|
1971
|
-
only calls through the ref is unaffected.
|
|
1972
|
-
|
|
1973
|
-
Pixels and DOM can now be a frame apart, in whichever direction the change came
|
|
1974
|
-
from. A view change leads with pixels: `setView` paints without rendering, so
|
|
1975
|
-
DOM built from the view is stale until something re-renders it — position
|
|
1976
|
-
world-anchored DOM from `subscribeView`. A scene change leads with DOM:
|
|
1977
|
-
`SceneCanvas` still subscribes to the scene, so a `batch` commits now and the
|
|
1978
|
-
pixels land next frame — compare `getPaintedVersion()` against the version you
|
|
1979
|
-
are about to render when chrome must be in lockstep. Do not render scene-derived
|
|
1980
|
-
DOM inside `startTransition`: React defers it and nothing forces it to catch up.
|
|
1981
|
-
|
|
1982
|
-
Anything reading the drawing buffer back outside a paint — the hud loupe's pixel
|
|
1983
|
-
mode is the one in-tree case — can likewise see a buffer one frame older;
|
|
1984
|
-
`subscribeFrame` runs on the frame that painted and removes the lag. Nothing
|
|
1985
|
-
paints while `document.hidden` is true, `syncPaint` included, so a readback from
|
|
1986
|
-
a background tab returns the frame from before the tab was hidden.
|
|
1987
|
-
- 546f67d: Copy typed-array arguments into `makeGLRecorder`'s call log as they are
|
|
1988
|
-
recorded. A caller is entitled to reuse the array it uploads from, so storing
|
|
1989
|
-
the reference recorded a value that later frames overwrote — a test reading
|
|
1990
|
-
two frames back saw the same numbers twice and passed. Test-only surface.
|
|
1991
|
-
- 3fb3a46: Release held keys when the window loses focus
|
|
1992
|
-
|
|
1993
|
-
A window that blurs mid-hold never delivers the keyup, so every in-flight
|
|
1994
|
-
`key-held` handle stayed engaged until that key was pressed again — holding
|
|
1995
|
-
Space and tabbing away left the hand tool on the hotkey stack indefinitely.
|
|
1996
|
-
|
|
1997
|
-
The gesture dispatcher now fires the `key-held` up phase for each held key on
|
|
1998
|
-
window blur. Consumers that hand-rolled this reset can drop it; ongoing
|
|
1999
|
-
invocations see a normal `onEnd`.
|
|
2000
|
-
- ccd51cc: Add a 43-glyph monochrome icon set to `@weasel-js/ui`.
|
|
2001
|
-
|
|
2002
|
-
One register: a 20x20 viewBox drawn in `currentColor` at stroke-width 1.5 with
|
|
2003
|
-
round caps and joins, hairline weight reserved for structure, and filled
|
|
2004
|
-
regions only where an action has a subject. Covers transport, history, view,
|
|
2005
|
-
trial lifecycle, collection, state, instrument and status vocabulary. Import a
|
|
2006
|
-
named component (`CloneIcon`), or `Icon` when the glyph is chosen at runtime.
|
|
2007
|
-
|
|
2008
|
-
`@weasel-js/ui` also re-exports the tool glyphs that live in `@weasel-js/core`,
|
|
2009
|
-
so consumers have one import site for the whole set. `ImageIcon` was reachable
|
|
2010
|
-
from core's icons folder but missing from its public barrel; it is exported
|
|
2011
|
-
now.
|
|
2012
|
-
|
|
2013
|
-
Glyph geometry is generated (`npm run gen:icons`) from `packages/ui/scripts/icons/`
|
|
2014
|
-
rather than hand-placed, because arrowheads and joins that miss their terminus
|
|
2015
|
-
are invisible at chrome size.
|
|
2016
|
-
- 3fb3a46: Compose `before` and `after` layer chains in both directions
|
|
2017
|
-
|
|
2018
|
-
`composeOrderedLayers` walked the two anchor maps separately: a chain hanging
|
|
2019
|
-
off an `after` anchor only followed further `after` links, and likewise for
|
|
2020
|
-
`before`. A custom layer anchored `before: 'scene'` carrying a second custom
|
|
2021
|
-
anchored `after` it dropped that second layer to the tail with a spurious
|
|
2022
|
-
dangling-reference warning.
|
|
2023
|
-
|
|
2024
|
-
Both walks now emit a layer's `before` chain, the layer, then its `after`
|
|
2025
|
-
chain, so the two mix freely. Cycle detection and orphan fallback are
|
|
2026
|
-
unchanged.
|
|
2027
|
-
- d9f110e: Stop every frame loop while nothing can see it
|
|
2028
|
-
|
|
2029
|
-
New public hook `useVisibleRaf` in `@weasel-js/core` owns the question of
|
|
2030
|
-
whether a frame may run: nothing runs while `document.hidden`, and a loop that
|
|
2031
|
-
names an element also stops while that element is outside the viewport. A
|
|
2032
|
-
request made while suspended is held rather than dropped and re-armed on
|
|
2033
|
-
resume, so a loop never polls visibility or needs restarting by hand.
|
|
2034
|
-
|
|
2035
|
-
Ten loops now run behind it — `useFrameLoop`, `useAnimator`, `useSimulation`,
|
|
2036
|
-
`useDecayLoop`, `useTextEdit`'s overlay follow, `CursorCoordsHud`'s FPS
|
|
2037
|
-
counter, `Badge`'s crawl, and labkit's `FpsMeter`, `useTiledSurface` and
|
|
2038
|
-
`useLayerScheduler`. Only `useFrameLoop` consulted `document.hidden` before;
|
|
2039
|
-
the rest ran on any page left open. `useLayerScheduler` looked safe and wasn't:
|
|
2040
|
-
it paints only dirty layers, but a hidden tab still commits React updates and
|
|
2041
|
-
its view/size effect marks every layer dirty.
|
|
2042
|
-
|
|
2043
|
-
Loops measuring elapsed time rebase their clock through the new `onResume`
|
|
2044
|
-
option, so an hour spent hidden does not arrive as one hour-long frame — an FPS
|
|
2045
|
-
meter reporting a rate nobody achieved, a tween jumping to its end value on
|
|
2046
|
-
return. `dangerouslyRunWhenHidden` opts a loop out for offscreen recording or
|
|
2047
|
-
export; nothing in the tree sets it.
|
|
2048
|
-
|
|
2049
|
-
`npm run check:frame-loops` fails the build on a bare `requestAnimationFrame`
|
|
2050
|
-
in kit source, and runs in CI.
|
|
2051
|
-
- 0dd35a1: Fix pinch-to-zoom: mac trackpads zoomed the page, and `viewport.pinchZoom` zoomed twice
|
|
2052
|
-
|
|
2053
|
-
A trackpad pinch reaches the page as `wheel { ctrlKey: true }`. On a mac
|
|
2054
|
-
`viewport.zoom`'s `mods: { mod: true }` binding requires metaKey and forbids
|
|
2055
|
-
ctrl, and `viewport.wheelPan` forbids ctrl too, so nothing claimed the event
|
|
2056
|
-
and the browser's own ctrl+wheel page zoom ran. `viewport.zoom` now carries a
|
|
2057
|
-
second wheel binding on bare ctrl. Off mac it duplicates the `mod` binding,
|
|
2058
|
-
where the matcher picks a single winner.
|
|
2059
|
-
|
|
2060
|
-
Nothing caught that because `IS_MAC` read `navigator.platform ?? userAgent`,
|
|
2061
|
-
and jsdom reports an empty-string platform — not nullish, so the fallback never
|
|
2062
|
-
fired and every mac binding in the kit was exercised only on the non-mac
|
|
2063
|
-
branch. It reads `||` now.
|
|
2064
|
-
|
|
2065
|
-
Separately, `viewport.pinchZoom: true` mounted `<Canvas>`'s `usePinchZoomTool`
|
|
2066
|
-
alongside the `viewport.pinchZoom` action that already handled the same
|
|
2067
|
-
gesture, applying one pinch's factor twice — the opt-in broke the path that
|
|
2068
|
-
worked without it. SceneCanvas drives pinch through the action alone, and the
|
|
2069
|
-
flag configures it: new `makePinchZoomAction({ min, max })` (exported), with
|
|
2070
|
-
the kit's 0.1–8 clamp now applied by default. `pinchZoom: false` disables pinch
|
|
2071
|
-
for real; it previously left the action running. Bare `<Canvas>` keeps the hook
|
|
2072
|
-
as its own pinch path.
|
|
2073
|
-
- 1a0bea3: `useNodeOverlayFrame`: the coordinate frame a DOM overlay pinned to a node needs
|
|
2074
|
-
|
|
2075
|
-
Nothing in the kit exported one, so consumers hand-rolled it — their own
|
|
2076
|
-
`ResizeObserver` next to the existing `useCanvasSize`, and a translate-and-scale
|
|
2077
|
-
inverse built by projecting two points. That inverse silently drops
|
|
2078
|
-
`pose.rotation`, which is why on-canvas gradient handles on a rotated node sat
|
|
2079
|
-
beside the paint instead of on it.
|
|
2080
|
-
|
|
2081
|
-
```ts
|
|
2082
|
-
useNodeOverlayFrame(scene, containerRef, nodeId, { view })
|
|
2083
|
-
// → { box, toScreen, toLocal, width, height } | null
|
|
2084
|
-
```
|
|
2085
|
-
|
|
2086
|
-
`box` is the node's composed world box, unrotated — the frame `toScreen` maps
|
|
2087
|
-
from, and the box to hand `fillInPoseFrame` / `fillToBoundsFrame`. Rotation
|
|
2088
|
-
lives in the pose→world leg, where it belongs: a node's stored geometry and its
|
|
2089
|
-
bounds-frame paint are pre-rotation by definition, so neither of those two
|
|
2090
|
-
changes.
|
|
2091
|
-
|
|
2092
|
-
`@weasel-js/ui` gains `SceneGradientHandles`, the scene-aware half of
|
|
2093
|
-
`GradientHandles`: it reads the gradient out of a node's `fill` **or** its
|
|
2094
|
-
`stroke` — `slot` is a prop — and commits each drag through `setFill` or
|
|
2095
|
-
`setStroke` as one undo entry. `GradientHandles` itself stays frame-agnostic.
|
|
2096
|
-
|
|
2097
|
-
Also: `isGradientFill` narrows a `FillStyle` to its three gradient members, and
|
|
2098
|
-
`useCanvasSize` accepts any `HTMLElement` rather than only a `div`.
|
|
2099
|
-
- 9d95836: A node's `data.stroke` takes a whole `Stroke`, not just a color
|
|
2100
|
-
|
|
2101
|
-
`NodeStroke = string | Stroke`, mirroring `NodeFill`. A string is still a
|
|
2102
|
-
color and `'none'` still skips the stroke; an object is a core `Stroke` whose
|
|
2103
|
-
`width`, `cap`, `join`, `dash`, `miterLimit` and `align` all reach the
|
|
2104
|
-
renderer, which has accepted them on `PathDrawCommand` all along. The object
|
|
2105
|
-
wins outright over `data.strokeWidth` rather than merging with it, the same
|
|
2106
|
-
rule `withLeafStroke` already applied to text. A bounds-relative stroke paint
|
|
2107
|
-
is baked onto the pose box the way a fill is, so a gradient stroke resolves
|
|
2108
|
-
against the box it was authored against.
|
|
2109
|
-
|
|
2110
|
-
`kit:shape` now honors `stroke: 'none'`, which only `kit:path` checked before.
|
|
2111
|
-
|
|
2112
|
-
`NodeInk` reports `{ filled, outset, inset }` instead of `{ filled,
|
|
2113
|
-
strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
2114
|
-
`'outer'` none inside, which one number could not say, so picking grabbed the
|
|
2115
|
-
wrong side. `ink` takes an optional context carrying the view scale, so a
|
|
2116
|
-
`{ px }` stroke width resolves to world units. A painter that still returns
|
|
2117
|
-
`{ filled, strokeWidth }` is read as a centered stroke and keeps working.
|
|
2118
|
-
|
|
2119
|
-
`setStroke` and `setStrokeOpacity` no longer stringify a node's `Stroke`: a
|
|
2120
|
-
color pick replaces its paint and keeps width, cap, join and dash, and an
|
|
2121
|
-
opacity drag sets the paint's `opacity`, which is the only form that works on
|
|
2122
|
-
a gradient stroke.
|
|
2123
|
-
|
|
2124
|
-
Editing UI for the rich form is not here yet — a schema-driven color control
|
|
2125
|
-
still writes a bare string over the object, so nodes carrying one are for
|
|
2126
|
-
programmatic authorship until `SelectionPanel` learns the union. See
|
|
2127
|
-
`docs/proposals/2026-08-26-node-stroke-union.md`.
|
|
2128
|
-
- 62a3c46: Paint a gradient or pattern stroke instead of throwing.
|
|
2129
|
-
|
|
2130
|
-
`Stroke.paint` has always been a full `FillStyle`, and SVG import puts paint
|
|
2131
|
-
servers there deliberately, but the renderer refused anything but a solid — so
|
|
2132
|
-
importing a shape with `stroke="url(#grad)"` produced a scene that threw on the
|
|
2133
|
-
next frame. Both stroke paths now paint the ribbon through the same route a
|
|
2134
|
-
fill takes, including under the inner/outer alignment stencil. A non-solid
|
|
2135
|
-
even-odd fill no longer renders black.
|
|
2136
|
-
- 5f6c28e: An object leaf's fields can be organised into groups
|
|
2137
|
-
|
|
2138
|
-
`ToolPrefObject.children` takes a `ToolPrefGroup` as well as a leaf. A group
|
|
2139
|
-
heads its fields under a label and contributes nothing to the path — the same
|
|
2140
|
-
rule group keys follow at the top level of a schema, so a field inside one is
|
|
2141
|
-
still addressed as a field of the object.
|
|
2142
|
-
|
|
2143
|
-
Without it, a value with many fields renders as one undifferentiated list. A
|
|
2144
|
-
`TextStyle` is the case that needs it: its character and paragraph fields are
|
|
2145
|
-
one value but read as two lists.
|
|
2146
|
-
- 3cd1ee8: A schema leaf can hold an object, with its fields hanging off it
|
|
2147
|
-
|
|
2148
|
-
A compound value — a stroke, a shadow, a pattern spec — could be described as
|
|
2149
|
-
sibling leaves addressing into it (`data.stroke.width`, `data.stroke.cap`).
|
|
2150
|
-
It shouldn't be: each control then writes one field of a value it can only
|
|
2151
|
-
half see, and writing a field into something that isn't an object yet corrupts
|
|
2152
|
-
it outright.
|
|
2153
|
-
|
|
2154
|
-
`ToolPrefObject` describes the value instead. Its `children` are ordinary
|
|
2155
|
-
leaves whose paths are relative to the object, and every child edit commits
|
|
2156
|
-
the parent object whole. A field that is itself a union declares the kind that
|
|
2157
|
-
edits that union — a stroke's `paint` is a `paint` leaf. `fromScalar` lifts a
|
|
2158
|
-
value still held in a scalar form before a child edit lands on it, which is
|
|
2159
|
-
how a stroke stored as a bare colour string gains a width.
|
|
2160
|
-
|
|
2161
|
-
`defaultNodeProperties` describes `data.stroke` this way, so the panel shows
|
|
2162
|
-
Color, Width, Cap, Join and Align under one Stroke block, and the separate
|
|
2163
|
-
`data.strokeWidth` leaf is gone. `SelectionPanel` now honours `block`, which
|
|
2164
|
-
`PrefsForm` already did. The one-off `stroke` pref kind added days ago is
|
|
2165
|
-
replaced by this general one.
|
|
2166
|
-
|
|
2167
|
-
`dash` has no leaf: it is a `number[]` and no kind edits one. It survives
|
|
2168
|
-
import, export and rendering untouched.
|
|
2169
|
-
- 2ea772f: Selection handles are hit-tested at the size they are painted
|
|
2170
|
-
|
|
2171
|
-
Handles painted at `HANDLE_BASE_PX * targetScale` and hit-tested at the bare
|
|
2172
|
-
constant, and neither `buildAffordanceAt` call site passed the option that
|
|
2173
|
-
would have scaled it. A coarse pointer got a bigger picture and exactly the
|
|
2174
|
-
same 8px grab zone it had on a mouse — the touch forgiveness the coarse profile
|
|
2175
|
-
exists to provide never reached the hit-test. The slops debug overlay was a
|
|
2176
|
-
third unscaled copy, so it drew hit regions where they were not.
|
|
2177
|
-
|
|
2178
|
-
`core/device/targets.ts` now holds one base table and one accessor,
|
|
2179
|
-
`targetSizesPx(targetScale)`. Paint, hit-test and the debug overlay all resolve
|
|
2180
|
-
through it. `HANDLE_BASE_PX`, `ANCHOR_HIT_BASE_PX` and
|
|
2181
|
-
`ROTATION_HANDLE_BASE_PX` keep their names and values and now read off the
|
|
2182
|
-
table; the internal `HANDLE_HIT_RADIUS` and `ANCHOR_HIT_RADIUS` are gone.
|
|
2183
|
-
|
|
2184
|
-
`buildAffordanceAt` and `createSlopsDebugLayer` take an optional `targetScale`.
|
|
2185
|
-
`selectTool.handleHitRadius` now actually reaches the hit-test — it previously
|
|
2186
|
-
reached nothing.
|
|
2187
|
-
|
|
2188
|
-
`useRotateTool`'s `handleHitRadius` option is **removed**. The rotation
|
|
2189
|
-
affordance is an annulus with a band thickness and no point radius, so the
|
|
2190
|
-
option could only ever have been a second name for `rotationHandleDistance`,
|
|
2191
|
-
which is live and now defaults from the same table.
|
|
2192
|
-
|
|
2193
|
-
Known gap: `CanvasView` is a second `buildAffordanceAt` call site that reads no
|
|
2194
|
-
device profile, so a nested view still hit-tests at the fine-pointer size.
|
|
2195
|
-
- f77bd95: `getChildren` means one thing on an adapter
|
|
2196
|
-
|
|
2197
|
-
`MoveAdapter` declared `getChildren(id)` — a node's direct children, for the
|
|
2198
|
-
drag cascade — and `OrderedAdapter` declared `getChildren(parentId | null)`,
|
|
2199
|
-
the z-ordering seam where `null` means the root. Both land on the same adapter
|
|
2200
|
-
object, so `arrayAdapter` took the first shape from its config and exposed it
|
|
2201
|
-
under the name the ops read with the second meaning. An op asking for root
|
|
2202
|
-
order got `[]`, which reads as "the root has no siblings", and the slot it
|
|
2203
|
-
captured was silently lost.
|
|
2204
|
-
|
|
2205
|
-
The two declarations are now one contract, and `arrayAdapter` answers the root
|
|
2206
|
-
from its own item array rather than delegating — a consumer callback written
|
|
2207
|
-
for node ids returns `[]` there, which cannot be told apart from a genuine
|
|
2208
|
-
empty answer. A consumer's `getChildren` config is still only ever asked about
|
|
2209
|
-
a node id.
|
|
2210
|
-
|
|
2211
|
-
`arrayAdapter` still exposes no `setChildOrder`, so it places by ordinal rather
|
|
2212
|
-
than by anchor. That is unchanged, and it is why the ordinal fallback exists.
|
|
2213
|
-
- 2ea772f: The canvas and the gradient editor now sample one gradient
|
|
2214
|
-
|
|
2215
|
-
`buildGradientRamp` carried its own interpolation beside
|
|
2216
|
-
`sampleGradientStops`, and the two disagreed three ways: the ramp had no guard
|
|
2217
|
-
at either end and extrapolated past the first and last stop, the two picked
|
|
2218
|
-
opposite sides of a coincident pair, and they parsed color differently — a stop
|
|
2219
|
-
written as a CSS named color rendered on the canvas and threw in the editor.
|
|
2220
|
-
|
|
2221
|
-
`sampleGradientStops` keeps its semantics and is now the only implementation.
|
|
2222
|
-
`resolveGradientStops` sorts and parses the list once; `sampleResolvedStops`
|
|
2223
|
-
returns the color at `t`. The ramp cache builds its texels through those, so
|
|
2224
|
-
there is no interpolation math left in the renderer.
|
|
2225
|
-
|
|
2226
|
-
Two behavior changes worth naming. `resolveColor` is the surviving parser, so
|
|
2227
|
-
gradient stops accept named and functional colors everywhere — but no longer
|
|
2228
|
-
hex without a leading `#`, which only the editor path had tolerated and the
|
|
2229
|
-
canvas never accepted. And `sampleGradientStops` returns normalized hex at the
|
|
2230
|
-
endpoints instead of echoing the raw stop string, so `'red'` comes back as
|
|
2231
|
-
`'#ff0000'`.
|
|
2232
|
-
|
|
2233
|
-
**SVG export:** a conic gradient left the exporter as a dangling `url(#…)` —
|
|
2234
|
-
the element already carried the reference, the built-in serializer returned
|
|
2235
|
-
nothing, and the registry's `toSvg` slot has no in-repo implementation, so the
|
|
2236
|
-
shape disappeared in a browser with no warning at all. Serialization now falls
|
|
2237
|
-
through to the same warning the pattern path already emits when nothing can
|
|
2238
|
-
produce a paint server. A consumer that registers a `toSvg` for
|
|
2239
|
-
`conic-gradient` still serializes and gets no warning.
|
|
2240
|
-
- aba8d91: Answer "can this node be hit" in one place
|
|
2241
|
-
|
|
2242
|
-
Four tree walks answered it separately — the generic-adapter point pick, the
|
|
2243
|
-
one `<SceneCanvas>` installs, `sceneToAdapter`'s area walk, and the live
|
|
2244
|
-
marquee/lasso — plus a fifth that shadowed the third. They agreed on every case
|
|
2245
|
-
that had a test and disagreed on the rest, three times, silently. `pickWalk`
|
|
2246
|
-
now owns every gate; a query supplies only its own shape test and the clip
|
|
2247
|
-
predicate for its region.
|
|
2248
|
-
|
|
2249
|
-
Behavior that changes as a result:
|
|
2250
|
-
|
|
2251
|
-
- **A node painted at alpha 0 is no longer clickable.** The pick path reads the
|
|
2252
|
-
same number the painter does — the view's `alphaFor` times any per-node
|
|
2253
|
-
override alpha — so a node faded out of sight stops claiming clicks. The
|
|
2254
|
-
floor is exactly zero, so a fade-in is pickable from its first nonzero frame.
|
|
2255
|
-
Alpha is per view: dimming a node in one view leaves it pickable in another.
|
|
2256
|
-
- **A layer that is not painted no longer claims pointer events.** `drawLayers`
|
|
2257
|
-
drops any layer missing from a supplied `layerOrder`, and the chrome hit path
|
|
2258
|
-
only consulted `layerVisibility`. Both gates now run through one
|
|
2259
|
-
`isLayerPainted`, which is exported.
|
|
2260
|
-
- `sceneToAdapter`'s area walk reads override poses and hidden layers, which it
|
|
2261
|
-
did not; its default `poseBounds` answers a path pose instead of `NaN`, which
|
|
2262
|
-
is what the shadow walk existed to work around.
|
|
2263
|
-
- An ancestor clip now rejects an area query that reaches into the clip where
|
|
2264
|
-
the node is not, or reaches the node where the clip is not — the two terms
|
|
2265
|
-
together, where one alone let false positives through.
|
|
2266
|
-
|
|
2267
|
-
`useSceneSelectTool` takes `alphaOf` and `layerIsPainted` for the asking view.
|
|
2268
|
-
`passesAncestorClips` and its module are gone; `pickWalk`, `scenePickSource`,
|
|
2269
|
-
`adapterPickSource` and `ownClipOf` replace them.
|
|
2270
|
-
- 2ea772f: A drag-to-insert reports the bounds it paints
|
|
2271
|
-
|
|
2272
|
-
The painter, the commit factory and `getGestureBounds()` each sized an
|
|
2273
|
-
in-flight insert differently. The reporter read the drag rect alone, so a
|
|
2274
|
-
centered Alt-drag reported a half-extent of `d` against a painted circumradius
|
|
2275
|
-
of `d√2`, a purely horizontal Alt-drag reported **height 0** for a visibly tall
|
|
2276
|
-
star, and a pencil scribble that looped back to its start reported nothing at
|
|
2277
|
-
all. The painter and the commit agreed on polygon and star but not on line or
|
|
2278
|
-
pencil: the commit posed the drag AABB for a line the painter drew endpoint to
|
|
2279
|
-
endpoint, and fell back to the drag rect for a trail under four samples.
|
|
2280
|
-
|
|
2281
|
-
One function now answers it for all three. The zero-area skip in the painter
|
|
2282
|
-
and the reporter tests the resolved extent rather than the raw drag rect, and
|
|
2283
|
-
an `InsertNodeFactory` that returns no `pose` falls back to the extent. The
|
|
2284
|
-
`bounds` argument handed to a factory is unchanged.
|
|
2285
|
-
- 3386d64: Path command opcodes derive from one table
|
|
2286
|
-
|
|
2287
|
-
`M`/`L`/`C`/`Q`/`Z` and their coordinate counts were declared five times —
|
|
2288
|
-
once in core, once in `@weasel-js/geom`, and three more as `COORD_COUNT`
|
|
2289
|
-
literals in the path transform, pose-rotation and pose-descriptor walkers. They
|
|
2290
|
-
agreed, and nothing held them to each other: a sixth opcode desynchronizes two
|
|
2291
|
-
packages' reading of the same `Uint8Array` with no exception and no type error,
|
|
2292
|
-
and every walker misparses the coordinate stream from that command on.
|
|
2293
|
-
|
|
2294
|
-
`PATH_COMMANDS` in `@weasel-js/geom` is now the table. `PATH_M`…`PATH_Z`,
|
|
2295
|
-
`PATH_CMD_LENGTHS` and the new `pathCommandCoordCount` all derive from it, and
|
|
2296
|
-
core re-exports them by name, so the opcode constants keep their names, values
|
|
2297
|
-
and literal types. The three walkers moved onto `forEachSegment` rather than
|
|
2298
|
-
onto the accessor alone — they were duplicating the coordinate-cursor advance
|
|
2299
|
-
as well as the length, and the cursor is the half that actually misreads.
|
|
2300
|
-
|
|
2301
|
-
Eight further files switch on these opcodes with inline literals. Five throw on
|
|
2302
|
-
an unknown code; three — the path boolean adapter, the anchor-editing geometry,
|
|
2303
|
-
and geom's own boolean adapter — have no `default` arm and would silently stop
|
|
2304
|
-
advancing. Left as-is; they need per-command semantics, not one walker.
|
|
2305
|
-
- 68d2651: Pref leaf kinds are declared once, and every renderer is exhaustive
|
|
2306
|
-
|
|
2307
|
-
`@weasel-js/ui` carried its own copy of the pref-leaf union under a comment
|
|
2308
|
-
saying to keep it in sync with core's field-for-field. It had drifted: ui's enum
|
|
2309
|
-
leaf had neither `encoding` nor `options[].disabled`, so a dash-array
|
|
2310
|
-
preference did not merely fail to select — choosing an option wrote the option
|
|
2311
|
-
string over the stored dash array. labkit's two renderers were missing the
|
|
2312
|
-
`paint` and `object` kinds outright.
|
|
2313
|
-
|
|
2314
|
-
ui's schema is now a rename re-export of core's declaration. The public `Pref*`
|
|
2315
|
-
names are unchanged, and there is nothing left to keep in sync.
|
|
2316
|
-
|
|
2317
|
-
More importantly, all four renderer switches ended in `default:`, so adding a
|
|
2318
|
-
built-in kind produced no error at any site and simply rendered nothing —
|
|
2319
|
-
verified by adding one and typechecking. `ToolPrefLeaf` widens `kind` to
|
|
2320
|
-
`string` so app-defined prefs can ride the same tree, which means a `never`
|
|
2321
|
-
guard cannot sit on it directly. New from core: `TOOL_PREF_KINDS`, a
|
|
2322
|
-
`Record<ToolPrefKind, true>` that a new kind fails to compile against first, and
|
|
2323
|
-
`isBuiltinToolPref(leaf)`, which narrows to the closed union so each renderer
|
|
2324
|
-
can discriminate and end in a `never`. App-defined kinds take the placeholder
|
|
2325
|
-
path as before.
|
|
2326
|
-
|
|
2327
|
-
Dash-array preferences now select and commit correctly in `PrefsForm`: the enum
|
|
2328
|
-
arm threads sibling values, routes through `encoding.read` / `encoding.write`,
|
|
2329
|
-
and honors `option.disabled`. `SelectionPanel` already did all of this — it was
|
|
2330
|
-
only the forked copy that could not express it.
|
|
2331
|
-
- 3386d64: Dragging out a text box shows a live preview
|
|
2332
|
-
|
|
2333
|
-
The set of insertable kinds and the `KitInsertShape` union sat on adjacent
|
|
2334
|
-
lines with no linkage, and seven more sites restated one list or the other. The
|
|
2335
|
-
drift was already live: the text tool binds `actionId: 'insert'` and commits
|
|
2336
|
-
through the insert dep, but the runtime set never listed `text`, so a
|
|
2337
|
-
drag-to-insert text box had no preview.
|
|
2338
|
-
|
|
2339
|
-
`SHAPE_KINDS` is now one descriptor table — a row per kind, flagged for whether
|
|
2340
|
-
it has a built-in tool and whether it takes an insert preview. Both unions,
|
|
2341
|
-
`KIT_SHAPE_KINDS`, `BUNDLE_TOOLS.exhaustive`, the known-builtin-id list and the
|
|
2342
|
-
preview gate all derive from it.
|
|
2343
|
-
|
|
2344
|
-
Two type-surface consequences. `KIT_SHAPE_KINDS` is typed
|
|
2345
|
-
`readonly BuiltinShapeToolId[]` rather than a literal tuple — same contents,
|
|
2346
|
-
same order, and `(typeof KIT_SHAPE_KINDS)[number]` is unchanged; what goes is
|
|
2347
|
-
positional and length typing, which nothing uses. And `OngoingOverlay['shape']`
|
|
2348
|
-
gains `'text'`, which is the fix itself: a consumer switching exhaustively over
|
|
2349
|
-
it gains a case, handled by the existing box arm.
|
|
2350
|
-
- c6c499d: Text layout is computed once, and the caret reads the layout that was painted
|
|
2351
|
-
|
|
2352
|
-
The paint, the pose silhouette and the click-to-edit caret each ran their own
|
|
2353
|
-
walk. The paint went through a memoized `layoutRuns`; the silhouette re-ran
|
|
2354
|
-
`layoutRuns` on every pose change, because it allocates a fresh `ResolvedRun[]`
|
|
2355
|
-
per call and the cache keyed on array identity; and the caret summed
|
|
2356
|
-
`ctx.measureText` per character, which sees no kerning, reads system fonts
|
|
2357
|
-
rather than the registered face, and ignores per-run styling entirely. The
|
|
2358
|
-
caret could therefore answer with a different line, and a different glyph, than
|
|
2359
|
-
the one under the pointer — masked in practice only because it asked a WebGL
|
|
2360
|
-
canvas for a 2D context and got `null`, degrading silently to no caret at all.
|
|
2361
|
-
|
|
2362
|
-
`cachedLayoutRuns` now lives in `@weasel-js/text` beside the function it caches,
|
|
2363
|
-
and all three go through it. It keeps the array-identity `WeakMap` as the
|
|
2364
|
-
renderer's zero-cost path and falls through to a bounded LRU keyed on the runs'
|
|
2365
|
-
structure, which is what lets a caller that cannot hold a stable array hit it —
|
|
2366
|
-
about 230× cheaper than laying out again, at roughly 4× the cost of the
|
|
2367
|
-
identity hit. `LaidOutLineBox` carries the caret stops the pen produced, so
|
|
2368
|
-
snapping is to the advance cells the glyphs were actually painted in.
|
|
2369
|
-
|
|
2370
|
-
**Breaking:** `caretIndexAt(ctx, x, y, pose)` is now
|
|
2371
|
-
`caretIndexAt(x, y, pose, opts?)` — the `CanvasRenderingContext2D` is gone, and
|
|
2372
|
-
an optional `maxWidth` mirrors `textLineBoxes` for nodes the `kit:text` painter
|
|
2373
|
-
draws unwrapped. `useSceneTextEdit` no longer acquires a 2D context, so a
|
|
2374
|
-
double-click always seeds the caret instead of falling back to editing from
|
|
2375
|
-
offset 0. `@weasel-js/text` gains a `./test-seams` entry point exporting
|
|
2376
|
-
`_resetLayoutCacheForTests`.
|
|
2377
|
-
- 4f1ef0b: Lay text out from font bytes alone — no baked atlas.
|
|
2378
|
-
|
|
2379
|
-
`registerFontOutlines` was a paint upgrade for a family that already had an
|
|
2380
|
-
MSDF atlas; a family with only font bytes could not resolve, so it rendered
|
|
2381
|
-
nothing. It is now a tier in its own right: `OutlineFace` reports `ascender`,
|
|
2382
|
-
`advanceOf` and `kernOf` in em units, `resolveFontVariant` resolves an
|
|
2383
|
-
outline-only family, and `layoutRuns` reads advances, kerning and the baseline
|
|
2384
|
-
through one source the atlas and a parsed face both satisfy. `outlineMinSize`
|
|
2385
|
-
does not gate such a family — there is no other tier to prefer.
|
|
2386
|
-
|
|
2387
|
-
This does not touch metric neutrality where it applies: a family that has an
|
|
2388
|
-
atlas still resolves to the atlas, so registering outlines cannot move text
|
|
2389
|
-
that was already rendering.
|
|
2390
|
-
|
|
2391
|
-
Also fixes the outline tier in Node. opentype.js publishes ESM under `module`
|
|
2392
|
-
and UMD under `main`; Node takes the UMD build, whose named exports it cannot
|
|
2393
|
-
detect, so `parse` was undefined and every face failed to load — silently, via
|
|
2394
|
-
the fallback to SDF. A browser bundler reading `module` never saw it.
|
|
2395
|
-
|
|
2396
|
-
Breaking for a consumer-supplied `OutlineParser`: a face must now report
|
|
2397
|
-
metrics as well as geometry.
|
|
2398
|
-
- 0114abf: Add `PaintInput`, a control that edits a whole `FillStyle`.
|
|
2399
|
-
|
|
2400
|
-
A kind bar over a per-kind body, driven by the paint-kind registry rather than
|
|
2401
|
-
a fixed list, so a consumer's registered kind appears in the bar and renders
|
|
2402
|
-
that entry's `Editor`. `SelectionPanel`'s `paint` leaf renders it in place of
|
|
2403
|
-
the chip that showed a gradient as indeterminate and wrote a solid over it on
|
|
2404
|
-
first touch — so the checkerboard now means a mixed selection and nothing else,
|
|
2405
|
-
and a gradient stroke is editable rather than merely paintable.
|
|
2406
|
-
|
|
2407
|
-
Switching kinds keeps a per-kind memory for the control's lifetime, so
|
|
2408
|
-
linear -> solid -> linear comes back with its stops instead of the ramp
|
|
2409
|
-
`withGradientKind` cannot carry.
|
|
2410
|
-
|
|
2411
|
-
`PatternPicker` moves from WeaselDraw into `@weasel-js/ui`, which now depends
|
|
2412
|
-
on `@weasel-js/svg` for its tile previews.
|
|
2413
|
-
|
|
2414
|
-
The bar offers **None**: "what kind of paint is this?" takes no-paint as an
|
|
2415
|
-
answer. `setFill` and `setStroke` accept `paint: null` to write it — a fill
|
|
2416
|
-
becomes `null`, and a stroke goes away entirely rather than keeping a width
|
|
2417
|
-
that draws no ink. `PaintKindEntry` gains an optional `icon`, and the five
|
|
2418
|
-
built-in kinds carry glyphs so six segments fit a property row.
|
|
2419
|
-
|
|
2420
|
-
`FILL` and `STROKE` are now peer sections: the `appearance` group goes headless
|
|
2421
|
-
and `data.fill` becomes a block leaf. The stroke's paint is no longer paired
|
|
2422
|
-
with its width — a whole paint editor cannot share a row with a slider.
|
|
2423
|
-
- 50bc909: `FillStyle` is open: register a sixth paint kind and it renders, converts
|
|
2424
|
-
frames and serializes.
|
|
2425
|
-
|
|
2426
|
-
`registerPaintKind(entry)` returns a disposer and `_resetPaintKindsForTests`
|
|
2427
|
-
re-seeds the five built-ins, matching the kit's other module-global
|
|
2428
|
-
registries. An entry carries the editor's slots (`label`, `seed`, `colorOf`,
|
|
2429
|
-
`Editor`), a render slot, both frame-conversion directions, and an SVG
|
|
2430
|
-
`<defs>` slot. `listPaintKinds()` enumerates them, and `asPaint` types a
|
|
2431
|
-
consumer's own paint as a `FillStyle` — the union itself stays closed, because
|
|
2432
|
-
opening its discriminant would widen every built-in member.
|
|
2433
|
-
|
|
2434
|
-
Three defects fall out of the same change, each of which a sixth kind hit
|
|
2435
|
-
immediately. The renderer's fill dispatch fell off the end of its switch into
|
|
2436
|
-
an unguarded cast to the gradient union, so an unknown kind read `stops` off a
|
|
2437
|
-
paint with none and threw mid-frame. `fillInPoseFrame` and its inverse returned
|
|
2438
|
-
an unknown kind untouched, leaving it painting in screen space on a node that
|
|
2439
|
-
moves. `<defs>` emitted nothing for a kind `gradientXml` did not know while
|
|
2440
|
-
still writing the `url(#id)` that referenced it.
|
|
2441
|
-
|
|
2442
|
-
Registering a kind now bumps the node memo generation, so a node painted
|
|
2443
|
-
before the registration repaints rather than holding the frame it resolved
|
|
2444
|
-
when the kind was unknown.
|
|
2445
|
-
- 6a06f6d: Node paint is an object: `data.fill` is a `FillStyle`, `data.stroke` a `Stroke`
|
|
2446
|
-
|
|
2447
|
-
Each concept now has exactly one shape. `data.fill` holds a `FillStyle`,
|
|
2448
|
-
`data.stroke` a whole `Stroke`, and `null` on either is an explicit "no paint"
|
|
2449
|
-
where `undefined` takes the painter's fallback. Two new authoring helpers keep
|
|
2450
|
-
hand-written node data short:
|
|
2451
|
-
|
|
2452
|
-
```ts
|
|
2453
|
-
data: { path, fill: solid('#7fb069'), stroke: strokeOf('#1c1c1c', 2) }
|
|
2454
|
-
```
|
|
2455
|
-
|
|
2456
|
-
**Breaking, with no compatibility path.** A document written against the old
|
|
2457
|
-
shapes renders wrong rather than failing, which is accepted:
|
|
2458
|
-
|
|
2459
|
-
- `NodeFill = string | FillStyle` and `NodeStroke = string | Stroke` are gone,
|
|
2460
|
-
and so are the string branches of `resolveNodeFill` / `resolveNodeStroke`.
|
|
2461
|
-
A node holding `fill: '#f00'` now paints the default grey.
|
|
2462
|
-
- `data.strokeWidth` is deleted. A stroke's width is `Stroke.width`.
|
|
2463
|
-
- `data.color` — the legacy alias `kit:path` and the rect fallback read — is
|
|
2464
|
-
deleted. The fallback painter reads `data.fill` like everything else.
|
|
2465
|
-
- `fill: 'none'` is now `fill: null`; `stroke: 'none'` is `stroke: null`.
|
|
2466
|
-
- `NodeInkResult` is gone: a painter's `ink` returns `NodeInk` and nothing
|
|
2467
|
-
else. A painter returning `{ filled, strokeWidth }` no longer type-checks
|
|
2468
|
-
and its reach is read as zero.
|
|
2469
|
-
- `@weasel-js/ui` drops `isStrokeObject`, which existed only to discriminate
|
|
2470
|
-
the union; `strokeColorOf` and `strokeWithColor` lose their string branches.
|
|
2471
|
-
- `@weasel-js/svg`'s `strokeDataFromSvg` returns `Stroke | undefined` instead
|
|
2472
|
-
of a `{ stroke, strokeWidth }` pair, and stops flattening a plain solid
|
|
2473
|
-
stroke into a color. SVG's `fill="none"` imports as `fill: null`.
|
|
2474
|
-
|
|
2475
|
-
**A paint's alpha lives in `opacity`, one slot for every paint kind.** That is
|
|
2476
|
-
the only slot a gradient or a pattern has, so it is the slot all of them use,
|
|
2477
|
-
and the renderer multiplies a hex alpha by it — the two would fight if both
|
|
2478
|
-
carried the value. `solid()` therefore moves an alpha channel out of the hex:
|
|
2479
|
-
`solid('#ff000080')` is `{ color: '#ff0000', opacity: 0.502 }`.
|
|
2480
|
-
|
|
2481
|
-
The four setter actions follow: `setFillOpacity` / `setStrokeOpacity` write
|
|
2482
|
-
`opacity` rather than splicing hex, so they now work on a gradient fill, which
|
|
2483
|
-
they used to leave untouched. `setFill` / `setStroke` given a `color` recolor
|
|
2484
|
-
the node's existing paint through the new `paintWithColor`, keeping its opacity
|
|
2485
|
-
unless the picked color states an alpha of its own — and `setStroke` keeps the
|
|
2486
|
-
stroke's width, cap, join and dash instead of replacing the whole value.
|
|
2487
|
-
|
|
2488
|
-
New exports: `solid`, `strokeOf`, `paintAlpha`, `paintWithAlpha`,
|
|
2489
|
-
`paintWithColor`, `DEFAULT_SHAPE_FILL`.
|
|
2490
|
-
|
|
2491
|
-
`defaultNodeProperties` moves `data.fill` from a `color` leaf to a `paint` one
|
|
2492
|
-
— a color control pointed at a `FillStyle` reads `undefined` off a gradient and
|
|
2493
|
-
writes a bare string over it — and the `data.stroke` object leaf drops its
|
|
2494
|
-
`fromScalar`, which had nothing left to lift.
|
|
2495
|
-
- a37ee0b: Separate a text node's content from its typography, and draw depth only where a label marks it
|
|
2496
|
-
|
|
2497
|
-
The text schema put `data.text` in a group named Text, so the section read
|
|
2498
|
-
TEXT and the row inside it read Text — one word nested in itself — and the
|
|
2499
|
-
style groups below it read as fields of the content string rather than as its
|
|
2500
|
-
siblings. Content is its own section now, with the field full-width because
|
|
2501
|
-
the section already names it.
|
|
2502
|
-
|
|
2503
|
-
A group with an empty `name` renders no heading. That already worked for
|
|
2504
|
-
sections and is now documented on `ToolPrefGroup`, since it is how a schema
|
|
2505
|
-
says "this group organises, it doesn't name": `Character` and `Paragraph`
|
|
2506
|
-
carry the labels, and a `Typography` heading over them named nothing new.
|
|
2507
|
-
It stays opt-in rather than a rule that rolls up any all-group parent —
|
|
2508
|
-
a `Border` over `Top` / `Right` / `Bottom` needs its name.
|
|
2509
|
-
|
|
2510
|
-
Rows under a suppressed heading no longer indent. Depth drawn without a
|
|
2511
|
-
visible parent put `Character` a level deeper than `Content` while being its
|
|
2512
|
-
peer, which is the panel's own tree discipline broken by its own hand.
|
|
2513
|
-
- 611b30e: Layers and deps answer for the view they are drawn for
|
|
2514
|
-
|
|
2515
|
-
Nine lookups closed over the *surface's* state at construction, so they answered
|
|
2516
|
-
for view zero in every view. `<CanvasView>` draws the surface's layer array
|
|
2517
|
-
unchanged and only the draw envelope differs, which makes a `draw: (_data, …)`
|
|
2518
|
-
a guarantee of answering for the wrong view rather than merely an unused
|
|
2519
|
-
argument. A drag in view B ghosted in view A, the marquee painted in the wrong
|
|
2520
|
-
view, chrome-caps resolved against the surface's selection, every Cmd+V centered
|
|
2521
|
-
on the wrong camera, and Escape in view B cancelled view A.
|
|
2522
|
-
|
|
2523
|
-
**New on `CanvasViewHelpers`** — `getPreviewSources()`, `getGestureOverlays()`
|
|
2524
|
-
and `getIsVisible()`. All three are **required members**: anyone hand-writing a
|
|
2525
|
-
`CanvasViewHelpers` (a test double, a wrapper) has to add them.
|
|
2526
|
-
`getIsVisible` **moves off `CanvasSurfaceHelpers`**, where it could only ever
|
|
2527
|
-
have answered for one view.
|
|
2528
|
-
|
|
2529
|
-
**New on `GestureSource`** — `previewSources()` and `overlays()`, also required,
|
|
2530
|
-
alongside the newly exported `GesturePreviewSource`. `toolPreviewSources(tools)`
|
|
2531
|
-
is the tool half.
|
|
2532
|
-
|
|
2533
|
-
**Layer options changed.** `createPathEditingOverlayLayer` and
|
|
2534
|
-
`createSlopsDebugLayer` take `getPose(id, previews)` and have lost their
|
|
2535
|
-
`isVisible` / `selectionRef` / `boundsOf` options — those come off the envelope
|
|
2536
|
-
now. `usePreviewGhostLayer` has lost `tools`. Both it and
|
|
2537
|
-
`useDispatcherOverlayLayer` keep `dispatcher` **only** to subscribe for repaint.
|
|
2538
|
-
|
|
2539
|
-
**Picking takes a camera.** `pickEvery`, `pickBest` and `makeGetNodeAtPoint`'s
|
|
2540
|
-
result accept an optional trailing `PickCamera`. A world point does not carry
|
|
2541
|
-
the scale it was produced under and picking has no draw envelope, so the caller
|
|
2542
|
-
that produced the point supplies it; omitting it keeps the surface camera.
|
|
2543
|
-
|
|
2544
|
-
`useHoverTracking` took a `clientToWorld` thunk beside a world-space
|
|
2545
|
-
`getNodeAtPoint` — the first resolved the view and the second did not, so hover
|
|
2546
|
-
picked at the surface's scale inside a panel. It takes one
|
|
2547
|
-
`nodeAtClientPoint(clientX, clientY)` now.
|
|
2548
|
-
|
|
2549
|
-
Anchor-editing target state stays surface-wide; only the preview resolution on
|
|
2550
|
-
that path is per-view.
|
|
2551
|
-
- 9ad8cb2: Picking answers for what was painted
|
|
2552
|
-
|
|
2553
|
-
Three defects in `<SceneCanvas>`'s hit paths, all one shape — a pick answering
|
|
2554
|
-
from something other than what the renderer drew.
|
|
2555
|
-
|
|
2556
|
-
**Pose overrides were painted through and picked around.** `PoseOverride.pose`
|
|
2557
|
-
is documented as replacing the document pose *everywhere the render and
|
|
2558
|
-
hit-test paths read one*, and `sceneAdapter.getPose` honored it. But
|
|
2559
|
-
`<SceneCanvas>` supplies its own `pickEvery`, which read `node.pose` raw — as
|
|
2560
|
-
did the bounds resolver feeding selection chrome and the affordance
|
|
2561
|
-
`ChromeState`, and the marquee/lasso scan. A consumer animating nodes through
|
|
2562
|
-
overrides painted them at one place and picked them at another. `effectivePose`
|
|
2563
|
-
is now the single rule and every one of those reads through it.
|
|
2564
|
-
|
|
2565
|
-
**A clipped-away child was still clickable.** A container clips its subtree and
|
|
2566
|
-
the renderer honors it, so a child outside the clip is not painted.
|
|
2567
|
-
`useSelectTool`'s own walk has rejected those since clipping shipped; the walk
|
|
2568
|
-
`<SceneCanvas>` installs instead had no clip term at all. The new
|
|
2569
|
-
`passesAncestorClips` walks the parent chain per surviving candidate, so a flat
|
|
2570
|
-
render-order scan can apply the same test.
|
|
2571
|
-
|
|
2572
|
-
**The marquee's fast-reject used the unrotated pose box.** A 100×20 rect turned
|
|
2573
|
-
45° puts a corner 32 units above that box; a rubber-band over that corner was
|
|
2574
|
-
rejected before the rotation-correct silhouette test ran, while a click on the
|
|
2575
|
-
same pixel selected the shape.
|
|
2576
|
-
- c1b8511: Repaint the scene-graph side-scroller demo's world from `data.fill`. Its
|
|
2577
|
-
tiles, coins, enemies and flagpole still declared `data.color`, the alias
|
|
2578
|
-
removed when node paint became an object, so every one of them rendered in
|
|
2579
|
-
the default gray — the demo whose whole point is being the visual twin of the
|
|
2580
|
-
immediate-mode load test.
|
|
2581
|
-
- d793d3c: Flip negates rotation; alignment guides and `gaps` distribute measure ink
|
|
2582
|
-
|
|
2583
|
-
Three paths read a pose's stored, unrotated box where the rotated extent was
|
|
2584
|
-
wanted.
|
|
2585
|
-
|
|
2586
|
-
`flipPoseAboutBounds` carried rotation through untouched, so a mirrored shape
|
|
2587
|
-
came back turned the same way — invisible on a rectangle, whose AABB is
|
|
2588
|
-
symmetric under a sign flip, and plainly wrong on an asymmetric one, which
|
|
2589
|
-
translated instead of mirroring. It now negates the pose's rotation.
|
|
2590
|
-
|
|
2591
|
-
`deriveAlignmentGuides` advertised a stationary rotated sibling's lines at its
|
|
2592
|
-
stored edges, while the dragged selection matched against them by its ink.
|
|
2593
|
-
`RECT_ALIGN_PROJECTION.boundsOf` now returns the rotated AABB and
|
|
2594
|
-
`deriveAlignmentGuides` reads its targets through the same projection — a new
|
|
2595
|
-
`projection` option defaulting to the rect one, so existing callers get the fix
|
|
2596
|
-
without a change.
|
|
2597
|
-
|
|
2598
|
-
`useDistribute`'s `gaps` mode divided the leftover span by stored widths, so a
|
|
2599
|
-
rotated member ended up with a gap short by the difference; `centers` shared the
|
|
2600
|
-
line and the blind spot. Both now measure with `visualBoundsViaDescriptor`.
|
|
2601
|
-
`distributeHorizontalAction` / `distributeVerticalAction` also take
|
|
2602
|
-
`params.mode`, so `gaps` is reachable from a binding rather than only from the
|
|
2603
|
-
hook.
|
|
2604
|
-
|
|
2605
|
-
Flip and distribute return different poses than before for rotated shapes.
|
|
2606
|
-
That is the fix, but it is a behavior change for anything depending on the
|
|
2607
|
-
old output.
|
|
2608
|
-
- 3386d64: `@weasel-js/core/routing` exports the route-string projection
|
|
2609
|
-
|
|
2610
|
-
Anything rendering a `GestureSpec` as a route string had to re-implement the
|
|
2611
|
-
projection, and the copy in WeaselDraw's registry inspector had drifted three
|
|
2612
|
-
ways: it answered `drop` and `paste` with no gesture name, so every binding of
|
|
2613
|
-
either vanished from the route list; its argument lookup missed a spec field;
|
|
2614
|
-
and it gated targets on a hand-listed set of kinds, dropping them for
|
|
2615
|
-
`pointerDown`, `longPress` and `wheel`.
|
|
2616
|
-
|
|
2617
|
-
New from the routing subpath: `routesForSpec(spec)` — every route string one
|
|
2618
|
-
spec declares — plus `routeGestureForSpecKind(kind)` over the single spec-kind
|
|
2619
|
-
map, and `PREDICATE_TARGET`, which `registry.ts` already exported but the
|
|
2620
|
-
subpath index did not, so consumers reading `RegistryEntry.target` had no way
|
|
2621
|
-
to compare against the sentinel its own docs name.
|
|
2622
|
-
- ce2b5c7: Make the inline run grammar a parameter instead of a hardcoded branch.
|
|
2623
|
-
|
|
2624
|
-
`runsToMarkdown` and `markdownToRuns` each had the markdown subset spelled out
|
|
2625
|
-
in their control flow — `***`/`**`/`*` and a two-character escape set — so
|
|
2626
|
-
reading or writing any other spelling meant forking both. They now take a
|
|
2627
|
-
`RunGrammar`: a table of markers pairing a repeated delimiter with the run
|
|
2628
|
-
flags it toggles, defaulting to `MARKDOWN_RUN_GRAMMAR`, which is exactly
|
|
2629
|
-
today's behavior. Escaping follows the grammar's own delimiters.
|
|
2630
|
-
|
|
2631
|
-
Nothing changes for a caller that passes no grammar. `underline` and
|
|
2632
|
-
`strikethrough` still have no markdown spelling and are still dropped by
|
|
2633
|
-
`runsToMarkdown` — a grammar that wants `~~struck~~` now adds one marker
|
|
2634
|
-
rather than editing the parser.
|
|
2635
|
-
- 2ea772f: `createSelectionOutlineLayer` and `createSelectionHandlesLayer` now do what the overlay layer does
|
|
2636
|
-
|
|
2637
|
-
`createSelectionOverlayLayer` documents itself as equivalent to stacking the
|
|
2638
|
-
other two, and it was not. It reads `ChromeState` off the draw envelope,
|
|
2639
|
-
resolves the synthetic multi-resize id to the union AABB, honors chrome-caps
|
|
2640
|
-
visibility and suppressed ids, and takes selection and poses from the envelope
|
|
2641
|
-
when they are omitted. The two primitives did none of that: they ignored the
|
|
2642
|
-
draw envelope entirely, required a construction-time `getPose` cascade, and
|
|
2643
|
-
knew nothing about the multi-selection union — so a consumer who stacked them,
|
|
2644
|
-
on the wrapper's own promise, got chrome in the wrong place with no way to
|
|
2645
|
-
tell.
|
|
2646
|
-
|
|
2647
|
-
All three now run one body and differ only in which passes they enable, so the
|
|
2648
|
-
promise holds by construction. `SelectionOutlineLayerOpts` and
|
|
2649
|
-
`SelectionHandlesLayerOpts` become the overlay's option set minus the visuals
|
|
2650
|
-
that don't apply, which makes `getSelection` and `getPose` optional on both and
|
|
2651
|
-
adds `getOutlineIds` and `getSuppressedIds`. Handle visuals are now the named
|
|
2652
|
-
`SelectionHandleStyle`.
|
|
2653
|
-
- 3fb3a46: Key `usePublishSelection` on the publish callback, not the context value
|
|
2654
|
-
|
|
2655
|
-
The effect depended on the whole selection-context value, and the provider
|
|
2656
|
-
mints a new value object on every publish. So one publisher publishing refired
|
|
2657
|
-
the effect for every other publisher in scope, each of which republished its
|
|
2658
|
-
own ids — a newer selection got stomped back to an older one, and two
|
|
2659
|
-
publishers holding different ids under one provider never settled at all.
|
|
2660
|
-
|
|
2661
|
-
`publishSelection` is already a stable `useCallback`, so the effect now depends
|
|
2662
|
-
on it directly. No provider change and no API change.
|
|
2663
|
-
- 84db1f6: Close four gaps that produced wrong answers with no error
|
|
2664
|
-
|
|
2665
|
-
Three path walkers — `pathToMultiPolygon` in core and in `@weasel-js/geom`, and
|
|
2666
|
-
`enumerateAnchors` behind the bezier-edit overlay — handled M/L/C/Q/Z with no
|
|
2667
|
-
`default:` arm, so a command code they did not know fell out of the switch
|
|
2668
|
-
without advancing the coordinate cursor and every segment after it read the
|
|
2669
|
-
wrong floats. They now throw, matching the six sibling walkers. This is a
|
|
2670
|
-
behavior change for anyone feeding these a path built with an opcode outside
|
|
2671
|
-
`PATH_COMMANDS`: what used to come back subtly wrong now raises.
|
|
2672
|
-
|
|
2673
|
-
A `<CanvasView>` built its affordance hit-test without a device profile, so a
|
|
2674
|
-
nested view resolved fine-pointer radii even under a coarse pointer — 8px grab
|
|
2675
|
-
zones against the 14px chrome the surface paints. It reads the profile
|
|
2676
|
-
`<SceneCanvas>` publishes.
|
|
2677
|
-
|
|
2678
|
-
`moveGestureAdapter`'s `insertNode` took no `index`, and the adapter carried
|
|
2679
|
-
neither `getChildren` nor `setChildOrder`, so the sibling slot a delete op
|
|
2680
|
-
records had nowhere to land: undoing a delete through the move pipeline
|
|
2681
|
-
appended the node to the end of its parent instead of putting it back where it
|
|
2682
|
-
was. All three are there now.
|
|
2683
|
-
|
|
2684
|
-
The dev inspector's gesture panel formatted bindings with a private formatter
|
|
2685
|
-
that reported only modifiers set to `true`. The `ingest` action marks every
|
|
2686
|
-
modifier `'optional'`, so its drop and paste bindings rendered blank and the
|
|
2687
|
-
action was invisible on both gestures. Both of the panel's plain-text
|
|
2688
|
-
formatters now go through the kit's `routesForSpec`.
|
|
2689
|
-
- 3386d64: Undoing a multi-node delete or group restores document order
|
|
2690
|
-
|
|
2691
|
-
Restoring by stored index cannot survive replay: history runs a batch's
|
|
2692
|
-
inverses in reverse, while indices captured before the mutation are only
|
|
2693
|
-
correct in ascending order. Deleting `b, c, d` from `[a, b, c, d, e]` and
|
|
2694
|
-
undoing gave `a, b, e, c, d`; Cmd+G on the same three did the same.
|
|
2695
|
-
|
|
2696
|
-
Ops now record a `Slot` — an ordinal plus the id of the following sibling at
|
|
2697
|
-
capture. The anchor is the source of truth whenever it resolves, and it
|
|
2698
|
-
resolves whatever else the batch has already restored. The ordinal remains as
|
|
2699
|
-
the fallback for an adapter that can place by index but cannot enumerate
|
|
2700
|
-
children. `before: null` means "last" and needs no sibling list; an absent
|
|
2701
|
-
`before` means "unobserved", and the two survive `History.serialize` because
|
|
2702
|
-
`undefined` drops out of JSON and `null` does not.
|
|
2703
|
-
|
|
2704
|
-
The ops observe their own slot during `apply()` rather than taking one from the
|
|
2705
|
-
caller, so every existing emitter gets this without a call-site change.
|
|
2706
|
-
`createDeleteOp`'s `index` argument is now a seed that `apply` supersedes; its
|
|
2707
|
-
docstring said it was sufficient on its own, which it never was.
|
|
2708
|
-
|
|
2709
|
-
Adapters without an ordering seam still append, as they did before:
|
|
2710
|
-
`arrayAdapter` has no `setChildOrder`, and the move gesture's adapter has
|
|
2711
|
-
neither that nor an `index` parameter on `insertNode`.
|
|
2712
|
-
- 7a746df: A stroke's dash is edited as a style, not as an array
|
|
2713
|
-
|
|
2714
|
-
`Stroke.dash` already rendered, imported and exported; it had no control,
|
|
2715
|
-
because a `number[]` has no leaf kind. It doesn't need one — the thing a person
|
|
2716
|
-
chooses is a style, and the array is how it is stored. The stroke block gains a
|
|
2717
|
-
Solid / Dashed / Dotted / Custom bar under cap, join and align.
|
|
2718
|
-
|
|
2719
|
-
`ToolPrefEnum` gains `encoding`: `read`/`write` between the stored value and
|
|
2720
|
-
the option string, the counterpart of the `unit` a number leaf already has for
|
|
2721
|
-
a value stored in a canonical unit. Both directions are handed the object the
|
|
2722
|
-
leaf is a field of, because a dash pattern is meaningless without the width it
|
|
2723
|
-
scales by — SVG dash lengths are absolute, so a fixed `[6, 3]` is dots on a
|
|
2724
|
-
hairline and a railroad on a 20px stroke. `dashForStrokeStyle` /
|
|
2725
|
-
`strokeDashStyleOf` are the mapping, exported: **dashed is 3× the width on and
|
|
2726
|
-
2× off, dotted 1× on and 2× off**. An array matching neither reads as `custom`,
|
|
2727
|
-
a new `disabled` option — one a control reports but refuses to author, since
|
|
2728
|
-
there is no array behind it. `solid` is stored as no dash at all, and an object
|
|
2729
|
-
leaf's field written as `undefined` is now removed rather than left holding it.
|
|
2730
|
-
- 4f19274: Cap, join and align are chosen by glyph, and the stroke block drops its labels
|
|
2731
|
-
|
|
2732
|
-
Nine option glyphs and four category glyphs join the icon set. The option
|
|
2733
|
-
glyphs are filled silhouettes — the glyph is the ink, so a choice reads as a
|
|
2734
|
-
shape rather than as a diagram of one. `align` is a circle zoomed until the
|
|
2735
|
-
ink band's far edge leaves the box: `inner` closes into a disc, `outer` into
|
|
2736
|
-
the box's complement of it, and `center` is the annulus straddling the path,
|
|
2737
|
-
so the three are one band at three offsets. The categories are the bare path
|
|
2738
|
-
each row treats, drawn in the outlined register.
|
|
2739
|
-
|
|
2740
|
-
A schema carries a glyph *id*, not a component: `ToolPrefEnum`'s options gain
|
|
2741
|
-
`icon`, and every leaf gains one for rows whose own label is spent on a
|
|
2742
|
-
`pair`. Core ships no icon set and cannot depend on one, so the field is a
|
|
2743
|
-
plain string; weasel-ui resolves it against `ICON_PATHS` and falls back to
|
|
2744
|
-
`short` where it names no glyph.
|
|
2745
|
-
|
|
2746
|
-
`SelectionPanel` now honours `block` inside an object leaf, not only at the
|
|
2747
|
-
section level. A row whose fields are all `block` drops the 64px label column
|
|
2748
|
-
and spans the block. The default stroke schema uses both: paint and width
|
|
2749
|
-
share one label-less row, and cap/join/align share the next.
|
|
2750
|
-
|
|
2751
|
-
`align`'s options run inner, center, outer — the order the ink moves outward.
|
|
2752
|
-
- 94f2446: Add stroke markers — arrowheads and other line terminators as stroke style.
|
|
2753
|
-
|
|
2754
|
-
`markerStart` / `markerMid` / `markerEnd` on `Stroke` take a key resolved
|
|
2755
|
-
through a new registry (`registerMarker`), shipping eight built-in shapes.
|
|
2756
|
-
Unlike SVG, the stroke stops short of a filled head rather than running under
|
|
2757
|
-
it to the tip; the distance is declared per marker, so an open V still reaches
|
|
2758
|
-
the vertex. Round-trips through `@weasel-js/svg` as `marker-*` attributes plus
|
|
2759
|
-
`<marker>` defs.
|
|
2760
|
-
- 07fd2de: `setStroke` takes a whole paint, so a gradient or pattern stroke is writable.
|
|
2761
|
-
|
|
2762
|
-
It accepted `{ color }` only, and merged through `paintWithColor`, which
|
|
2763
|
-
supersedes a non-solid paint with a solid one — a gradient stroke was
|
|
2764
|
-
unreachable even though `setStrokeOpacity` could already reach its alpha.
|
|
2765
|
-
`paint` now wins over `color`, a color arriving later in the gesture supersedes
|
|
2766
|
-
an earlier paint, and the stroke's width, cap, join, dash and align survive
|
|
2767
|
-
either. New `strokeWith(paint, width?)` is `strokeOf`'s sibling for a paint
|
|
2768
|
-
that has no color to pass.
|
|
2769
|
-
|
|
2770
|
-
Two fixes alongside it: `setFill` started with no `color` and no `paint` seeded
|
|
2771
|
-
from `DEFAULT_STROKE_COLOR`, painting the selection black where
|
|
2772
|
-
`setFillOpacity` seeds the same slot from `DEFAULT_FILL_COLOR`; and
|
|
2773
|
-
`gradientForBounds`'s doc comment claimed a corner-to-corner linear gradient
|
|
2774
|
-
where the body builds a left-edge-to-right-edge one.
|
|
2775
|
-
|
|
2776
|
-
`@weasel-js/ui` no longer exports `strokeWithColor`. It shared a name with
|
|
2777
|
-
core's and disagreed with it — core's keeps the paint's opacity, ui's dropped
|
|
2778
|
-
it — and nothing imported it.
|
|
2779
|
-
- 81213fc: Edit a node's stroke as the union it is
|
|
2780
|
-
|
|
2781
|
-
`data.stroke` holds `string | Stroke`, and the schema described it with a
|
|
2782
|
-
`color` leaf — which reads `undefined` off the object form, shows its own
|
|
2783
|
-
default, and writes a bare hex back over the stroke's width, cap, join and
|
|
2784
|
-
dash on the first edit. The same trap `ToolPrefPaint` was introduced to avoid
|
|
2785
|
-
for `FillStyle`.
|
|
2786
|
-
|
|
2787
|
-
A `stroke` pref kind now describes it, and `defaultNodeProperties` uses it.
|
|
2788
|
-
Its control shows whichever color the value has — the string itself, or a
|
|
2789
|
-
solid paint's color — gives a gradient stroke the indeterminate chip rather
|
|
2790
|
-
than claiming a color it doesn't have, and preserves the form on write.
|
|
2791
|
-
|
|
2792
|
-
`PrefsForm` gained the `stroke` case and the `paint` case it never had; a
|
|
2793
|
-
`paint` leaf used to render as the literal text `(paint: no renderer)`.
|
|
2794
|
-
`solidColorOf`, `strokeColorOf`, `strokeWithColor` and `isStrokeObject` are
|
|
2795
|
-
exported from `@weasel-js/ui` for consumers writing their own property
|
|
2796
|
-
renderers against either union.
|
|
2797
|
-
|
|
2798
|
-
Cap, join and dash are not editable from a panel yet, and `data.strokeWidth`
|
|
2799
|
-
remains its own leaf — see `docs/proposals/2026-08-26-node-stroke-union.md`
|
|
2800
|
-
for why that waits on the SVG mapping.
|
|
2801
|
-
- 2f225d7: A thick stroke is clickable across its whole width
|
|
2802
|
-
|
|
2803
|
-
`shapeCoversPoint` grants a grab out to a stroke's outward reach — a full
|
|
2804
|
-
stroke width for an `outer` align — but the AABB pre-filter that runs before it
|
|
2805
|
-
grew only by the pointer slop. So half a thick outer stroke's ink was
|
|
2806
|
-
unclickable: the point was rejected before the refinement that would have
|
|
2807
|
-
claimed it ever ran. `poseContains` carried a comment claiming the pre-filter
|
|
2808
|
-
was at least as generous as the refinement, which it cannot be on its own,
|
|
2809
|
-
since it never sees the stroke. That budget is the caller's, and the comment
|
|
2810
|
-
says so now.
|
|
2811
|
-
|
|
2812
|
-
`ShapeCoversPointOptions.scale` was never passed either, so a stroke width
|
|
2813
|
-
declared in `px` resolved as world units and the reach was wrong at every zoom
|
|
2814
|
-
but 1 — while the caller computed `meanScale(view.scale)` one line above.
|
|
2815
|
-
- 68069dc: Right-to-left text lays out in visual order
|
|
2816
|
-
|
|
2817
|
-
`LayoutRunsOpts` takes an optional `bidi` engine. Given one, `layoutRuns`
|
|
2818
|
-
analyses the paragraph, reorders each line after the wrap, and mirrors brackets
|
|
2819
|
-
in right-to-left runs. Given none, nothing changes: text lays out logically,
|
|
2820
|
-
exactly as before.
|
|
2821
|
-
|
|
2822
|
-
`@weasel-js/text` declares the `BidiResolver` interface and does not depend on
|
|
2823
|
-
`@weasel-js/bidi` — the dependency runs the other way from the usual, so a
|
|
2824
|
-
consumer who renders no right-to-left text never installs the Unicode tables,
|
|
2825
|
-
and a different implementation can be substituted. `@weasel-js/bidi` is a
|
|
2826
|
-
devDependency here only, for a test that drives real Hebrew through the real
|
|
2827
|
-
engine; types lining up is not evidence the semantics do.
|
|
2828
|
-
|
|
2829
|
-
`LaidOutCell` gains `advance` and `level`, and **`x` is no longer monotonic
|
|
2830
|
-
across `cells`**. Cells stay in logical order — slot `i` is still character `i`
|
|
2831
|
-
— while their x values follow the reordering. Sort on `x` for visual order, and
|
|
2832
|
-
read a cell's extent as `[x, x + advance)` rather than reaching for the next
|
|
2833
|
-
cell's `x`. Hit-testing was doing exactly that and now sweeps in visual order
|
|
2834
|
-
against each cell's own extent, taking a right-to-left cell's visually-leading
|
|
2835
|
-
half as the character's logical end.
|
|
2836
|
-
|
|
2837
|
-
Kerning is a gap between two adjacent characters, and the wrap measures it
|
|
2838
|
-
logically. Reordering can put a different pair side by side, so the gap taken
|
|
2839
|
-
is the one belonging to whichever of the two is logically second, and none at
|
|
2840
|
-
all across a direction boundary — where the pair never touched in the source.
|
|
2841
|
-
|
|
2842
|
-
Laying out right-to-left text with no engine now warns once, naming the import.
|
|
2843
|
-
The alternative is glyphs silently appearing reversed, which is the one real
|
|
2844
|
-
hazard of making this opt-in.
|
|
2845
|
-
- 5d0ff9c: Every code point on a line gets a cell
|
|
2846
|
-
|
|
2847
|
-
`LaidOutLineBox` replaces its `caretXs` / `caretIndices` pair with
|
|
2848
|
-
`cells: LaidOutCell[]` plus a `srcEnd` closing offset. A cell carries
|
|
2849
|
-
`srcIndex`, `srcEnd`, `cp`, `x` and `drawsInk`, so slot `i` is `cells[i]` and
|
|
2850
|
-
a consumer indexing per character no longer has to reconcile a sparse array
|
|
2851
|
-
against the source string.
|
|
2852
|
-
|
|
2853
|
-
The old arrays were documented as non-contiguous, and two causes were real:
|
|
2854
|
-
|
|
2855
|
-
- A code point no tier could serve was dropped outright, taking its caret stop
|
|
2856
|
-
with it. It now occupies a zero-advance cell. This is reachable whenever the
|
|
2857
|
-
dynamic canvas fallback is off — which is the normal configuration for a
|
|
2858
|
-
consumer registering its own outlines, where the outline tier has no rung
|
|
2859
|
-
below it.
|
|
2860
|
-
- A space opening a line — at the start of the text, or after a newline — was
|
|
2861
|
-
discarded. It now keeps its cell and still consumes no width, so a line is
|
|
2862
|
-
addressable per character without gaining an indent. A space that opens a
|
|
2863
|
-
*wrapped* line was never affected: the wrap leaves it as a trailing cell on
|
|
2864
|
-
the line before.
|
|
2865
|
-
|
|
2866
|
-
Neither changes any geometry: both cells carry zero advance, zero tracking and
|
|
2867
|
-
no kerning, so bounds, line widths and glyph positions are unchanged.
|
|
2868
|
-
|
|
2869
|
-
A newline still has no cell, since it separates cells rather than being one.
|
|
2870
|
-
`srcEnd` is what a blank line carries in its place.
|
|
2871
|
-
|
|
2872
|
-
`drawsInk` is a property of the code point and the face, not of the call that
|
|
2873
|
-
produced it: it does not flip when a dynamic bake lands or the outline
|
|
2874
|
-
threshold is crossed, so the same text reports the same slots every time. A
|
|
2875
|
-
zero-advance combining mark is `true` — it inks without advancing.
|
|
2876
|
-
- c1b8511: **Breaking:** paint leaves `TextStyle`. A text node's color and outline are
|
|
2877
|
-
`data.fill` and `data.stroke` — the same two leaves every other node kind
|
|
2878
|
-
paints from — and `TextStyle` holds typography only. `TextStyle.fill` and
|
|
2879
|
-
`TextStyle.stroke` are gone, with no compatibility read: a document that put
|
|
2880
|
-
its color in `style.fill` now renders in the default black rather than
|
|
2881
|
-
erroring, so check documents that predate this.
|
|
2882
|
-
|
|
2883
|
-
This fixes a real asymmetry rather than only moving fields. `data.stroke`
|
|
2884
|
-
already reached text through a fold in the painter, but `data.fill` did not:
|
|
2885
|
-
picking a fill color with a text node selected wrote a field nothing read, so
|
|
2886
|
-
the canvas did not change. `setFill`, `setFillOpacity`, the opacity scrub and
|
|
2887
|
-
the Appearance leaf now all mean the same thing on text as on a rect. The
|
|
2888
|
-
duplicate `data.style.fill` control is gone from the text schema with them.
|
|
2889
|
-
|
|
2890
|
-
`resolveTextStyle(style, paint)` takes the node's paint as a second argument
|
|
2891
|
-
and is what derives the caret and selection colors, so the edit overlay
|
|
2892
|
-
matches the glyphs it sits on; `useTextEdit` gained a `getPaint` option for
|
|
2893
|
-
the same reason, defaulted by `useSceneTextEdit` from `data.fill` /
|
|
2894
|
-
`data.stroke`. `TextPose` gained `fill` / `stroke`, so text drawn through
|
|
2895
|
-
`createTextLayer` is painted rather than black. `SvgTextNode` gained the same
|
|
2896
|
-
two, and SVG import and export carry text paint there instead of inside the
|
|
2897
|
-
style. `StyledRun.fill` and `.stroke` are unchanged and still override the
|
|
2898
|
-
node's per range — which is also where a caller with no node at all, a HUD
|
|
2899
|
-
widget or a debug overlay, now states its color.
|
|
2900
|
-
|
|
2901
|
-
`textCommandFromRuns` is exported from the package root.
|
|
2902
|
-
- 546f67d: Draw text from a ring of reused vertex buffers instead of minting a vertex
|
|
2903
|
-
array and two buffers per draw. `drawTextGroup` and `drawTextDecorations` were
|
|
2904
|
-
the last paths still doing what `drawImage` stopped doing; text now costs
|
|
2905
|
-
**3.3 us/command, down from 6.65** at 512 commands a frame on an M2 Max via
|
|
2906
|
-
ANGLE (`tests/perf/transition-matrix.spec.ts`), which puts it level with an
|
|
2907
|
-
image draw. No other command kind moved.
|
|
2908
|
-
|
|
2909
|
-
A text group is as many quads as it has glyphs, so unlike the image ring a
|
|
2910
|
-
slot's buffer grows to the largest run it has seen rather than being fixed at
|
|
2911
|
-
four vertices. The quad index pattern is a pure function of the quad count —
|
|
2912
|
-
the pattern for N quads is a prefix of the pattern for any larger N — so one
|
|
2913
|
-
index buffer serves every slot, grown the same way and written only when it
|
|
2914
|
-
grows.
|
|
2915
|
-
- c2ffa49: Alignment can resolve against reading direction
|
|
2916
|
-
|
|
2917
|
-
`align` gains `start` and `end` alongside `left` / `center` / `right`, and
|
|
2918
|
-
`TextStyle` gains `direction: 'ltr' | 'rtl'`. The split is CSS `text-align`'s:
|
|
2919
|
-
the relative pair resolves against the direction, the absolute pair ignores it.
|
|
2920
|
-
`resolveAlign(align, direction)` collapses one to the other and is exported for
|
|
2921
|
-
consumers that need an edge rather than an intent.
|
|
2922
|
-
|
|
2923
|
-
Direction is an input, not something this package discovers. `@weasel-js/text`
|
|
2924
|
-
has no DOM, so a consumer that reads `getComputedStyle(box).direction` passes
|
|
2925
|
-
what it found; nothing here sniffs an environment.
|
|
2926
|
-
|
|
2927
|
-
Defaults are unchanged — `align: 'left'`, `direction: 'ltr'` — so no existing
|
|
2928
|
-
layout moves. Making `start` the default alignment is a separate call.
|
|
2929
|
-
|
|
2930
|
-
`@weasel-js/svg` carries the direction through: `direction` joins the
|
|
2931
|
-
inheritable presentation properties, and `text-anchor` is now written and read
|
|
2932
|
-
against it. Two things were wrong before and are worth naming, because both
|
|
2933
|
-
rendered plausible output:
|
|
2934
|
-
|
|
2935
|
-
- `align: 'start'` serialized to `text-anchor="end"` — the opposite edge — via
|
|
2936
|
-
a mapping that assumed three values and read the fourth as its `else`.
|
|
2937
|
-
- SVG's initial `text-anchor` is `start`, which under `direction="rtl"` is the
|
|
2938
|
-
right edge, while this model's default `align` is `left`. They agree under
|
|
2939
|
-
`ltr` and only there, so an RTL document with no explicit anchor imported as
|
|
2940
|
-
left-aligned.
|
|
2941
|
-
|
|
2942
|
-
This is alignment and round-tripping only. Layout still walks code points in
|
|
2943
|
-
logical order with the pen always increasing: there is no bidi reordering and
|
|
2944
|
-
no shaping, so a Hebrew or Arabic string aligns to the correct edge and still
|
|
2945
|
-
renders in logical order, and Arabic still renders unjoined.
|
|
2946
|
-
- 4c097ef: Sit every run on a line on one baseline
|
|
2947
|
-
|
|
2948
|
-
Mixed-size text hung each run off the *line top* at its own ascent instead of
|
|
2949
|
-
off a shared baseline, so a 16-unit run beside a 40-unit run floated up level
|
|
2950
|
-
with the big run's cap rather than standing on the line with it. Two faces with
|
|
2951
|
-
different ascents at the same size diverged the same way. Baseline alignment is
|
|
2952
|
-
what inline text does everywhere else, and the module header already claimed
|
|
2953
|
-
this behavior — the walk just never implemented it.
|
|
2954
|
-
|
|
2955
|
-
A line now sinks one baseline far enough to clear its tallest run's ascent and
|
|
2956
|
-
places every glyph against it. Glyph quads derive their top from that baseline
|
|
2957
|
-
rather than from the pen's line top, which is the whole of the change:
|
|
2958
|
-
`qy0 = baselineY + (yoffset - metrics.base) * scale`.
|
|
2959
|
-
|
|
2960
|
-
Uniform-size text — nearly all text — is unchanged, since the maximum over one
|
|
2961
|
-
value is that value. Only lines that actually mix sizes or faces move, and they
|
|
2962
|
-
move to where they always should have been.
|
|
2963
|
-
|
|
2964
|
-
The test named "mixed-size runs share a baseline on the same line" asserted only
|
|
2965
|
-
a quad count and passed throughout; it now asserts the baselines.
|
|
2966
|
-
- 2b86e00: A text node's style is one value, not ten sibling paths
|
|
2967
|
-
|
|
2968
|
-
`data.style.fontSize`, `.fontWeight`, `.align` and the rest addressed into one
|
|
2969
|
-
`TextStyle` from ten independent leaves, each control writing a field of a
|
|
2970
|
-
value it could only half see. `data.style` is an object leaf now, with
|
|
2971
|
-
Character and Paragraph as groups inside it — groups head their fields and
|
|
2972
|
-
contribute nothing to the path, so a field is still a field of the style and
|
|
2973
|
-
one commit writes the whole thing.
|
|
2974
|
-
|
|
2975
|
-
An object leaf whose fields are entirely grouped no longer prints its own
|
|
2976
|
-
heading, which would stack straight onto the first group's, and a group's
|
|
2977
|
-
fields sit under a rule so the nesting reads. WeaselDraw's inspector descends
|
|
2978
|
-
into an object leaf when listing what a kind exposes — the fields are the
|
|
2979
|
-
editable surface; the leaf is the container.
|
|
2980
|
-
|
|
2981
|
-
`SelectionPanel` has a story now, which is how the two layout defects above
|
|
2982
|
-
were found.
|
|
2983
|
-
- d933a89: Superscript, subscript and overline for styled runs
|
|
2984
|
-
|
|
2985
|
-
`StyledRun` gains `script: 'super' | 'sub'` — a raised or lowered baseline and
|
|
2986
|
-
a smaller size together, the pair `<sup>` and `<sub>` imply. It is a preset
|
|
2987
|
-
over two new primitives rather than a mechanism of its own:
|
|
2988
|
-
|
|
2989
|
-
- `baselineShift` — raise (positive) or lower (negative) a run off the line's
|
|
2990
|
-
shared baseline, in ems of the inherited font size.
|
|
2991
|
-
- `fontScale` — a multiplier on the inherited font size, the relative
|
|
2992
|
-
counterpart to `fontSize`. An absolute `fontSize` still wins over it.
|
|
2993
|
-
|
|
2994
|
-
Naming either directly overrides that half of `script` and leaves the other
|
|
2995
|
-
alone. The preset's numbers are exported as `SCRIPT_METRICS` (58.3% size,
|
|
2996
|
-
±33.3% position — Adobe's defaults, so a character panel can show percentages
|
|
2997
|
-
its users already recognize) and are derived, not read from the font: `OS/2`
|
|
2998
|
-
carries real `ySuperscript*` metrics but the baked atlas tier has no slot for
|
|
2999
|
-
them, and metrics that applied on one glyph tier and not the other would
|
|
3000
|
-
reflow text as it crossed the size threshold.
|
|
3001
|
-
|
|
3002
|
-
`resolveRuns` folds all of it into one world-unit `baselineShift` and a final
|
|
3003
|
-
`fontSize`, so layout never learns superscripts exist — it places a run against
|
|
3004
|
-
a baseline and an offset. The shift moves a run's glyphs, its outline geometry
|
|
3005
|
-
and its own decoration rules together, and deliberately does not feed back into
|
|
3006
|
-
the line's baseline or height: a superscript rides the line rather than
|
|
3007
|
-
reflowing it.
|
|
3008
|
-
|
|
3009
|
-
`overline` joins `underline` and `strikethrough` on both `TextStyle` and
|
|
3010
|
-
`StyledRun`, additive over the node style like the other two, and is now
|
|
3011
|
-
available to a custom `RunGrammar` as a `RunFlag`. The default markdown grammar
|
|
3012
|
-
is unchanged — it stays silent on the decorations, as it always has been.
|
|
3013
|
-
- 5923c8b: `Animator.tween` no longer fires `onDone` for a tween that was cancelled during
|
|
3014
|
-
its own final `onTick`. The last tick emitted the value and completed in one
|
|
3015
|
-
pass, so a write made from that tick — cancelling the tween — still got the
|
|
3016
|
-
completion callback, against the documented "not called on cancel" contract.
|
|
3017
|
-
- 2ea772f: Undo of a delete restores the subtree; undo of a group restores the slot
|
|
3018
|
-
|
|
3019
|
-
Two ops inverted to something narrower than what they applied, so undo
|
|
3020
|
-
silently lost data.
|
|
3021
|
-
|
|
3022
|
-
`createDeleteOp.invert()` re-inserted a single node while `apply()` called
|
|
3023
|
-
`removeNode`, which cascades the whole subtree. Delete a container with two
|
|
3024
|
-
children, undo, and the container came back with `children: []` while both
|
|
3025
|
-
children were gone. The op now snapshots its descendants preorder through the
|
|
3026
|
-
adapter's optional `getNode` / `getChildren` — the snapshot is written back
|
|
3027
|
-
into `args`, so an op rebuilt from a serialized entry still inverts — and
|
|
3028
|
-
re-inserts each descendant at its captured slot. A flat adapter's `removeNode`
|
|
3029
|
-
does not cascade, so the inverse skips any descendant the adapter still reports
|
|
3030
|
-
as live rather than duplicating it.
|
|
3031
|
-
|
|
3032
|
-
`createReparentOp` carried only the parent ids, so undoing a Cmd+G appended
|
|
3033
|
-
instead of restoring the sibling slot and paint order changed. `ReparentArgs`
|
|
3034
|
-
now carries `fromIndex` / `toIndex` and places through the existing
|
|
3035
|
-
`getChildren` / `setChildOrder` seam that `createReorderOp` already uses —
|
|
3036
|
-
`setParent`'s signature is unchanged. Adapters without that seam no-op as
|
|
3037
|
-
before. `groupAction` captures each member's index before mutating; `move` and
|
|
3038
|
-
`snapToContainer` pass none and are byte-identical.
|
|
3039
|
-
|
|
3040
|
-
`ops/delete.test.ts` stubbed `removeNode` as a one-id delete that did not
|
|
3041
|
-
cascade, which is why nothing caught the first bug. It now runs against a
|
|
3042
|
-
tree-backed fake.
|
|
3043
|
-
- 2ea772f: Selection chrome, gesture bounds and SVG export fold rotated ink, not pose boxes
|
|
3044
|
-
|
|
3045
|
-
Every union a user looks at or clicks folded each member's *unrotated* box.
|
|
3046
|
-
Select two shapes, rotate one, and the multi-selection frame and its handles
|
|
3047
|
-
sat inside the rotated shape's ink — affordances hand `ChromeState.unionBounds`
|
|
3048
|
-
out as the target bounds for paint *and* hit-test, so the handles were both
|
|
3049
|
-
drawn and grabbable in the wrong place, while `getGestureBounds()` reported the
|
|
3050
|
-
correct larger box.
|
|
3051
|
-
|
|
3052
|
-
`unionAABB` expands each rotated member via `axisAlignedBounds` before folding
|
|
3053
|
-
and is now the one implementation. It lives in `core/geometry/unionBounds.ts`
|
|
3054
|
-
beside the rotation-free `unionBounds`, which stays correct for commit-time
|
|
3055
|
-
actions that write poses back in the unrotated frame; the module says which to
|
|
3056
|
-
reach for. `unionGestureBounds` is **removed** — it was `unionAABB` under
|
|
3057
|
-
another name. Both new functions are exported from the package root.
|
|
3058
|
-
|
|
3059
|
-
Moved onto it: `ChromeState.unionBounds`, the selection overlay's
|
|
3060
|
-
container-to-leaves resolver, the multi-rotate pivot (which put the pivot in
|
|
3061
|
-
the wrong place whenever a member was rotated), and WeaselDraw's export
|
|
3062
|
-
viewBox, which clipped rotated shapes out of the copied SVG.
|
|
3063
|
-
- 3fb3a46: Warn in dev when `useAction` finds no `ActionsProvider`
|
|
3064
|
-
|
|
3065
|
-
`useAction` returned early on a null registry, so an action registered above
|
|
3066
|
-
the provider — or with no provider mounted — silently never fired its
|
|
3067
|
-
bindings. It now warns in dev, naming the action id. Runtime behavior in
|
|
3068
|
-
production builds is unchanged.
|
|
3069
|
-
- Updated dependencies [5c8e9e6]
|
|
3070
|
-
- Updated dependencies [2621cbf]
|
|
3071
|
-
- Updated dependencies [0f936da]
|
|
3072
|
-
- Updated dependencies [4180095]
|
|
3073
|
-
- Updated dependencies [9977908]
|
|
3074
|
-
- Updated dependencies [52c7b2a]
|
|
3075
|
-
- Updated dependencies [3386d64]
|
|
3076
|
-
- Updated dependencies [c6c499d]
|
|
3077
|
-
- Updated dependencies [20097e6]
|
|
3078
|
-
- Updated dependencies [84db1f6]
|
|
3079
|
-
- Updated dependencies [94f2446]
|
|
3080
|
-
- Updated dependencies [68069dc]
|
|
3081
|
-
- Updated dependencies [5d0ff9c]
|
|
3082
|
-
- Updated dependencies [0bb27a5]
|
|
3083
|
-
- Updated dependencies [c2ffa49]
|
|
3084
|
-
- Updated dependencies [4c097ef]
|
|
3085
|
-
- Updated dependencies [d933a89]
|
|
3086
|
-
- @weasel-js/text@1.3.0
|
|
3087
|
-
- @weasel-js/geom@1.3.0
|
|
3088
|
-
- @weasel-js/font@1.3.0
|
|
3089
|
-
- @weasel-js/gestures@1.3.0
|
|
3090
|
-
- @weasel-js/history@1.3.0
|
|
3091
|
-
- @weasel-js/modes@1.3.0
|
|
3092
|
-
- @weasel-js/paint@1.3.0
|
|
3093
|
-
|
|
3094
|
-
## 2.0.0-pre.0
|
|
3095
|
-
|
|
3096
|
-
### Minor Changes
|
|
3097
|
-
|
|
3098
|
-
- bca99e3: Extract the typography layer into `@weasel-js/text`, and the paint vocabulary
|
|
3099
|
-
into `@weasel-js/paint` — two new Tier A leaves.
|
|
3100
|
-
|
|
3101
|
-
`@weasel-js/text` owns the run model, style resolution, `layoutRuns`, wrap and
|
|
3102
|
-
measurement. It depends on `@weasel-js/font`, `@weasel-js/geom` and
|
|
3103
|
-
`@weasel-js/paint`, and on nothing else: a consumer with its own renderer can
|
|
3104
|
-
lay out text without taking the scene graph or a React peer dependency.
|
|
3105
|
-
`layoutRuns` is now public — it was previously reachable only from inside core.
|
|
3106
|
-
|
|
3107
|
-
`@weasel-js/paint` holds `FillStyle`, `Stroke`, gradients, dashes and
|
|
3108
|
-
`TextureHandle`. It was the blocker named in the 2026-07-28 font split: the
|
|
3109
|
-
layout could not move while its fill type lived in the renderer's graph.
|
|
3110
|
-
|
|
3111
|
-
`@weasel-js/core` re-exports both surfaces, so its own API is unchanged.
|
|
3112
|
-
`Rect` moves to `@weasel-js/geom`, beside `Box`.
|
|
3113
|
-
|
|
3114
|
-
Breaking for anyone importing these through core's internal paths rather than
|
|
3115
|
-
its public entry (`core/paint-types`, `features/text/*`); those paths are gone.
|
|
3116
|
-
|
|
3117
|
-
Advances and kerning still come from a baked MSDF atlas — laying out from font
|
|
3118
|
-
bytes alone needs the metrics seam in
|
|
3119
|
-
`docs/superpowers/specs/2026-08-28-text-package-extraction-design.md`.
|
|
3120
|
-
|
|
3121
|
-
<!-- bump-approved: minor: Mike — two new published packages (@weasel-js/text, @weasel-js/paint) and layoutRuns promoted to public API, on top of ~50 patch changesets carrying new public surface across core, ui and labkit; called explicitly in conversation on 2026-08-29: "tag a minor release and push" -->
|
|
3122
|
-
|
|
3123
|
-
### Patch Changes
|
|
3124
|
-
|
|
3125
|
-
- 3386d64: Align, distribute and flip use visual bounds
|
|
3126
|
-
|
|
3127
|
-
These folded each member's unrotated pose box, so "Align Left" on a selection
|
|
3128
|
-
containing a rotated shape lined up the boxes and left the rotated shape's ink
|
|
3129
|
-
sticking out past the others. They now work on the visual bounding box, as
|
|
3130
|
-
Figma and Illustrator do.
|
|
3131
|
-
|
|
3132
|
-
Both ends moved together — expanding only the union would have made alignment
|
|
3133
|
-
worse, since the delta runs from an edge of the union to the same edge of each
|
|
3134
|
-
member's box. The new exported `visualBoundsViaDescriptor(pose, geometry)`
|
|
3135
|
-
reads a pose's bounds, recovers its rotation and expands via
|
|
3136
|
-
`axisAlignedBounds`; the union folds those with `unionAABB`. The delta is still
|
|
3137
|
-
applied as a translation of the stored pose through
|
|
3138
|
-
`translatePoseViaDescriptor`, so a shape moves rather than being re-posed.
|
|
3139
|
-
|
|
3140
|
-
Flip needed only its union pivot changed: mirroring maps a centre and preserves
|
|
3141
|
-
size, and an expanded box is concentric with the box it came from.
|
|
3142
|
-
|
|
3143
|
-
`alignMoveBehavior` folds the dragged selection the same way, so a drag snaps
|
|
3144
|
-
by its ink.
|
|
3145
|
-
|
|
3146
|
-
- ffafb7d: Never let an animation's virtual clock run backwards.
|
|
3147
|
-
|
|
3148
|
-
`useAnimator` seeds each animation's `lastRealNow` from `now()` at register
|
|
3149
|
-
time, then advances its virtual clock by the difference against the timestamp
|
|
3150
|
-
the frame loop supplies. Those two share a time origin in a browser, where the
|
|
3151
|
-
rAF timestamp and `performance.now()` are both page-relative — but that is a
|
|
3152
|
-
browser guarantee, not a universal one, and jsdom starts them roughly 600ms
|
|
3153
|
-
apart. The first frame's delta then came out hugely negative and `virtualNow`
|
|
3154
|
-
spent dozens of frames climbing back toward zero before a tween advanced at
|
|
3155
|
-
all: a 40ms glide took 95 frames and over a second of wall time, growing worse
|
|
3156
|
-
the longer the process had been alive.
|
|
3157
|
-
|
|
3158
|
-
A frame's elapsed time is never negative, so the sample is now clamped at
|
|
3159
|
-
zero. Under a shared origin this is a no-op.
|
|
3160
|
-
|
|
3161
|
-
- ba8b139: Camera animation: `viewport.animatedZoom` now does something
|
|
3162
|
-
|
|
3163
|
-
`animatedZoom` has been declared on `SceneCanvasProps.viewport` and read by
|
|
3164
|
-
nothing; Cmd+=/-/0 was a bare `view.set`. It now routes the discrete zoom steps
|
|
3165
|
-
through the kit's `Animator`. Wheel and pinch are unchanged and never animate —
|
|
3166
|
-
their input already delivers a sample per frame.
|
|
3167
|
-
|
|
3168
|
-
Camera animation is a general surface, not a zoom flag. Three ways in, one
|
|
3169
|
-
runner behind them:
|
|
3170
|
-
|
|
3171
|
-
- `useViewAnimation(view, animator?)` — `animate`, `animateToBounds`, `stop`,
|
|
3172
|
-
`isAnimating`, `target`.
|
|
3173
|
-
- The `view` dep gains optional `animate` / `stopAnimation` / `animationTarget`,
|
|
3174
|
-
so any action can glide the camera.
|
|
3175
|
-
- `SceneCanvasApi` gains `animateView` / `stopViewAnimation` /
|
|
3176
|
-
`isViewAnimating` for fit-to-selection, recenter, or a scripted tour. All
|
|
3177
|
-
three are **required** members: anyone hand-implementing `SceneCanvasApi`
|
|
3178
|
-
(a test double, a wrapper) has to add them, the way `CanvasExtensionApi`
|
|
3179
|
-
grew `getPaintedVersion`.
|
|
3180
|
-
|
|
3181
|
-
Scale interpolates geometrically and translation is derived from the screen
|
|
3182
|
-
point the two views agree on, so a zoom stays anchored instead of drifting and
|
|
3183
|
-
each frame changes the view by the same ratio. One animation runs at a time; any
|
|
3184
|
-
other view write cancels it, and a cancel leaves the camera where it is rather
|
|
3185
|
-
than jumping to the target. On an uncontrolled canvas the whole animation costs
|
|
3186
|
-
no React render.
|
|
3187
|
-
|
|
3188
|
-
**Breaking:** `useViewTween` is removed. `useViewAnimation` keeps its name and
|
|
3189
|
-
changes signature — it takes a `{ get, set }` view channel plus an optional
|
|
3190
|
-
`Animator`, and `animateTo(from, to, { duration, easing })` becomes
|
|
3191
|
-
`animate(to, { ms, easing })`. The `from` argument is gone because the runner
|
|
3192
|
-
reads the live view, which is what lets an interrupted camera resume from where
|
|
3193
|
-
it actually is instead of snapping back to a captured start. `cancel()` is now
|
|
3194
|
-
`stop()`, and `animateToBounds(bounds, currentView, dims, { duration })` is now
|
|
3195
|
-
`animateToBounds(bounds, dims, { ms })` — the `currentView` argument goes for
|
|
3196
|
-
the same reason `from` does.
|
|
3197
|
-
|
|
3198
|
-
**Breaking:** `viewport.recenter` and `ViewApi.recenter` widen to
|
|
3199
|
-
`() => View | void`. Returning the target view lets Cmd+0 animate there;
|
|
3200
|
-
returning nothing keeps the existing behavior. `animatedZoom`'s config fields
|
|
3201
|
-
are `ms` / `resetMs` rather than `duration` / `resetDuration`, matching the
|
|
3202
|
-
animator's vocabulary.
|
|
3203
|
-
|
|
3204
|
-
- 3fb3a46: Forward `onFocus` and `onBlur` from the canvas element
|
|
3205
|
-
|
|
3206
|
-
The canvas is focusable by default (`tabIndex` 0) but exposed no way to
|
|
3207
|
-
observe focus, so consumers driving focus-dependent chrome had to attach a
|
|
3208
|
-
listener to an ancestor and infer it. Both are now props on `CanvasProps`, and
|
|
3209
|
-
so reach `SceneCanvasProps` and the canvas element unchanged.
|
|
3210
|
-
|
|
3211
|
-
- 67bcb05: Drop four values the canvas layer memo no longer reads
|
|
3212
|
-
|
|
3213
|
-
`hit-test affordances against the painted chrome state` moved the selection
|
|
3214
|
-
overlay to reading bounds off the chrome state at paint time, which left
|
|
3215
|
-
`selectedIds`, `multiActive`, `previewToolPose` and `previewToolBounds`
|
|
3216
|
-
referenced only by the `layers` memo's dependency array — nothing in the body
|
|
3217
|
-
used them. Removing them from the array made all four dead locals, so they go
|
|
3218
|
-
too.
|
|
3219
|
-
|
|
3220
2009
|
The memo now rebuilds the layer array on layer/tool/geometry changes rather
|
|
3221
2010
|
than additionally on every selection and preview-pose change. Selection chrome
|
|
3222
2011
|
is unaffected: it repaints from chrome state, not from the identity of this
|
|
3223
2012
|
array.
|
|
3224
|
-
|
|
3225
2013
|
- 47cbb08: A closed subpath's dash no longer seams at its start vertex
|
|
3226
|
-
|
|
2014
|
+
|
|
3227
2015
|
`splitForDash` flushed the run still open when a closed subpath's walk returned
|
|
3228
2016
|
to the vertex it started from as its own open sub-polyline, so it and the run
|
|
3229
2017
|
that began there rendered as two butt-capped ribbons meeting at a point — a
|
|
@@ -3232,17 +2020,16 @@
|
|
|
3232
2020
|
is drawn across the seam like any other corner. A pattern whose first "on"
|
|
3233
2021
|
length covers the whole perimeter emits a closed ribbon, identical to the
|
|
3234
2022
|
undashed stroke.
|
|
3235
|
-
|
|
3236
2023
|
- f43e9c2: A derived edge follows the drag that moves its endpoint
|
|
3237
|
-
|
|
2024
|
+
|
|
3238
2025
|
`move`, `resize` and `rotate` kept their in-flight poses in action-local
|
|
3239
2026
|
scratch and published them only as `previewIds` / `previewPose`. That surface
|
|
3240
2027
|
is enough to paint a ghost and size selection chrome, but nothing that asks
|
|
3241
|
-
the
|
|
2028
|
+
the *scene* where a node is can see it — and `scenePoseLookup`, which resolves
|
|
3242
2029
|
a derived node's geometry, asks the scene. So dragging a box left its edge
|
|
3243
2030
|
anchored to the pre-drag position until the drop, when the commit invalidated
|
|
3244
2031
|
the dependents and the edge jumped.
|
|
3245
|
-
|
|
2032
|
+
|
|
3246
2033
|
The three actions now also publish each frame into the scene's ephemeral pose
|
|
3247
2034
|
overrides (`syncPreviewOverrides` / `dropPreviewOverrides` in
|
|
3248
2035
|
`interactions/actions/previewOverrides.ts`). Overrides bypass `executeAndLog`,
|
|
@@ -3250,101 +2037,98 @@
|
|
|
3250
2037
|
avoided per-frame scene writes in the first place was history, and this writes
|
|
3251
2038
|
no history. Entries are set once and mutated in place, published with a single
|
|
3252
2039
|
`commit()` per frame.
|
|
3253
|
-
|
|
2040
|
+
|
|
3254
2041
|
Picking follows for free: the pick source resolves a derived path through its
|
|
3255
2042
|
own override-aware `poseOf`, so an edge is grabbable where it is drawn
|
|
3256
2043
|
mid-gesture rather than where it used to be.
|
|
3257
|
-
|
|
2044
|
+
|
|
3258
2045
|
`clone` is deliberately untouched — its previews are the new ghosts at the
|
|
3259
2046
|
drag target, and the originals never move, so nothing derives from a changed
|
|
3260
2047
|
pose.
|
|
3261
|
-
|
|
2048
|
+
|
|
3262
2049
|
Also closes the matching gap in the preview-ghost layer, which built a
|
|
3263
2050
|
container's clip with no derived path and so ghosted a derived container
|
|
3264
2051
|
without one.
|
|
3265
|
-
|
|
2052
|
+
|
|
3266
2053
|
Note for anyone with a hand-written `Scene` stand-in: `overrides` is now read
|
|
3267
2054
|
on every gesture frame. It was already required by the `Scene` contract, but a
|
|
3268
2055
|
partial fake that omitted it will now throw rather than silently skip.
|
|
3269
|
-
|
|
3270
2056
|
- bb27e83: A derived node is clickable where it paints
|
|
3271
|
-
|
|
2057
|
+
|
|
3272
2058
|
A node whose geometry comes from `derivePath` had no silhouette and no `ink`:
|
|
3273
2059
|
`NodeShapeEntry.silhouette` took only `(node, pose)`, and a derived path is
|
|
3274
|
-
resolved from the
|
|
2060
|
+
resolved from the *dependencies'* poses, which a painter has no handle on. So
|
|
3275
2061
|
`kit:derived` could not report one, `shapeCoversPoint` read the resulting null
|
|
3276
2062
|
as "no opinion" and answered `true` everywhere, and picking fell back to the
|
|
3277
2063
|
node's own pose — for an edge, a zero-sized placeholder at the origin. An edge
|
|
3278
2064
|
was unpickable, and a derived container contributed no clip.
|
|
3279
|
-
|
|
2065
|
+
|
|
3280
2066
|
`silhouette` now takes a `NodeSilhouetteCtx` carrying `derivedPath`, on the
|
|
3281
2067
|
same convention `NodePaintCtx` already uses, and `kit:derived` reports the
|
|
3282
2068
|
derived path as its silhouette and its declared stroke as its `ink`.
|
|
3283
|
-
|
|
3284
|
-
Resolving that path needs the scene, so it is the
|
|
2069
|
+
|
|
2070
|
+
Resolving that path needs the scene, so it is the *source* that answers, not
|
|
3285
2071
|
the painter: `PickSource.derivedPathOf`, a matching optional argument to
|
|
3286
2072
|
`buildSceneTree`, and `SceneSlotConfig.derivedPathOf` — the slot already
|
|
3287
|
-
carried the derived path a node
|
|
3288
|
-
container
|
|
2073
|
+
carried the derived path a node *paints*, and now also the clip a derived
|
|
2074
|
+
container *imposes*, so the live canvas and the headless walk clip alike. The
|
|
3289
2075
|
bare-adapter paths supply none of them and behave exactly as before.
|
|
3290
|
-
|
|
2076
|
+
|
|
3291
2077
|
The pre-filter had to move with it. `useSceneSelectTool` grew its region test
|
|
3292
2078
|
from the node's pose, which for a derived node is the wrong box entirely, so
|
|
3293
2079
|
the edge was rejected before the shape test could claim it. It now tests the
|
|
3294
2080
|
derived path when there is one — `poseContains` already reads a path-like pose
|
|
3295
2081
|
as a path, so this reuses it rather than adding a second reach calculation.
|
|
3296
|
-
|
|
2082
|
+
|
|
3297
2083
|
`findShapeSilhouette` skips its memo when handed a derived path. That slot is
|
|
3298
2084
|
keyed on `(node, pose, data)` and cannot see the path, so it would serve one
|
|
3299
2085
|
caller's silhouette to a caller that passed a different one — the same reason
|
|
3300
2086
|
`kit:derived` already skips `PAINT_SLOT`.
|
|
3301
|
-
|
|
3302
2087
|
- 6a33c3f: A node's path can be derived from other nodes' poses
|
|
3303
|
-
|
|
2088
|
+
|
|
3304
2089
|
A node declares `dependsOn: NodeId[]` and a `derivePath` function resolved by key
|
|
3305
2090
|
through `SceneRegistry`, and the scene walks resolve its path before painting
|
|
3306
2091
|
rather than it being authored. An edge drawn between two boxes is then an
|
|
3307
2092
|
ordinary scene node — selectable, styleable, exportable — whose geometry never
|
|
3308
2093
|
enters undo history. The seam and its traps are in `docs/extending.md`.
|
|
3309
|
-
|
|
2094
|
+
|
|
3310
2095
|
New surface: `scene.removeMany(ids)`; `dependsOn` and `derivePath` on
|
|
3311
2096
|
`NodeBase` and on `AddNodeSpec`, which is what a consumer writes;
|
|
3312
2097
|
`SceneRegistry.derivePath`; `SerializedNode.dependsOn` and
|
|
3313
2098
|
`SerializedNode.derivePathKey`, both additions to the serialization format;
|
|
3314
2099
|
`NodePaintCtx.derivedPath`.
|
|
3315
|
-
|
|
2100
|
+
|
|
3316
2101
|
Deleting a node now deletes everything that derives from it, transitively,
|
|
3317
2102
|
including those nodes' own subtrees, in one undo entry — so `scene.remove` can
|
|
3318
2103
|
remove nodes anywhere in the tree that the caller never named, and `removeLayer`
|
|
3319
2104
|
reaches nodes on other layers. Undo after the built-in **Delete** key does not
|
|
3320
2105
|
yet restore the cascaded nodes; see "Derived geometry follow-ups" in
|
|
3321
2106
|
`docs/TODO.md`.
|
|
3322
|
-
|
|
2107
|
+
|
|
3323
2108
|
**Breaking: `defaultDrawOne` takes `(node, pose, view?, ctx?)`.** The paint
|
|
3324
2109
|
context moves to a fourth parameter, so a call passing a `NodePaintCtx` third is
|
|
3325
2110
|
now a type error rather than a silent slide into the `view` slot. The same
|
|
3326
2111
|
fourth parameter is added to the `SceneViewDrawOne` and `SceneSlotConfig.drawOne`
|
|
3327
2112
|
callback types, which is not a break: an existing three-parameter implementation
|
|
3328
2113
|
still satisfies them, and an existing three-argument call still compiles.
|
|
3329
|
-
|
|
2114
|
+
|
|
3330
2115
|
**Breaking: `Scene` gained a required `removeMany`.** A hand-written object
|
|
3331
2116
|
typed as a `Scene` — a test double, most likely — no longer typechecks until it
|
|
3332
2117
|
implements it.
|
|
3333
|
-
|
|
2118
|
+
|
|
3334
2119
|
**Breaking: `kit:remove`'s op payload changed shape.** `rootId` / `parent` /
|
|
3335
2120
|
`index` became `detached: { id, parent, index }[]`, because a cascaded dependent
|
|
3336
2121
|
is not a descendant of the removed node and the tree has to be told about every
|
|
3337
2122
|
subtree that came out of it. A history persisted by an older build now throws
|
|
3338
2123
|
mid-undo rather than degrading. The break is deliberate; kit op payloads are not
|
|
3339
2124
|
versioned.
|
|
3340
|
-
|
|
3341
2125
|
- c24e7de: Detached views follow pose overrides
|
|
3342
|
-
|
|
2126
|
+
|
|
3343
2127
|
`<SceneViewCanvas>` and `<MinimapCanvas>` re-rendered off `scene.getVersion()`,
|
|
3344
2128
|
which a pose override deliberately never bumps — so they kept painting document
|
|
3345
2129
|
poses while `<SceneCanvas>` painted the overridden ones. A minimap beside a
|
|
3346
2130
|
canvas driving a drag or a simulation silently disagreed with it.
|
|
3347
|
-
|
|
2131
|
+
|
|
3348
2132
|
`<SceneViewCanvas>` now paints through `useFrameLoop` instead of from React, and
|
|
3349
2133
|
subscribes to `scene.overrides`. A render (prop change or version bump) and an
|
|
3350
2134
|
override commit both just mark the surface dirty, and one animation frame
|
|
@@ -3352,39 +2136,37 @@
|
|
|
3352
2136
|
render, and a backgrounded tab stops painting them entirely. The mount paint
|
|
3353
2137
|
stays synchronous, so the first frame is still the scene rather than a blank
|
|
3354
2138
|
canvas. `<MinimapCanvas>` inherits all of this through it.
|
|
3355
|
-
|
|
2139
|
+
|
|
3356
2140
|
Repaints driven by a prop change are now asynchronous: they land on the next
|
|
3357
2141
|
animation frame rather than in the layout effect of the render that caused them.
|
|
3358
2142
|
Code that renders and then reads pixels in the same tick needs to wait a frame.
|
|
3359
|
-
|
|
3360
|
-
A minimap's
|
|
2143
|
+
|
|
2144
|
+
A minimap's *framing* still derives from document poses, so a node overridden
|
|
3361
2145
|
outside the document bounds paints outside the fitted frame — recomputing the
|
|
3362
2146
|
fit per frame would rescale the whole minimap throughout a settle.
|
|
3363
|
-
|
|
3364
2147
|
- ce82f4a: An enum leaf can ask for a segmented control, and `pair` works inside an object
|
|
3365
|
-
|
|
2148
|
+
|
|
3366
2149
|
`ToolPrefEnumControl` gains `'toggle'`: a three-option enum shows all three at
|
|
3367
2150
|
once instead of hiding two behind a select. Options carry an optional `short`
|
|
3368
2151
|
label — a capital or two — for the width a property row has; the full `label`
|
|
3369
2152
|
stays the accessible name, so the abbreviation never becomes the only thing
|
|
3370
2153
|
naming the option. A mixed selection selects no segment rather than picking a
|
|
3371
2154
|
winner.
|
|
3372
|
-
|
|
2155
|
+
|
|
3373
2156
|
`pair` now merges fields inside an object leaf, as it already did for section
|
|
3374
2157
|
rows — a hint shouldn't mean something different for being a field of a value
|
|
3375
|
-
rather than a sibling of one. It merges
|
|
2158
|
+
rather than a sibling of one. It merges *adjacent* leaves in both places, so
|
|
3376
2159
|
the schema orders family, size, weight: size and weight pair, and family (which
|
|
3377
2160
|
sat between them) moves ahead of the pair rather than splitting it.
|
|
3378
|
-
|
|
2161
|
+
|
|
3379
2162
|
A stroke's cap, join and align share one row; property rows wrap rather than
|
|
3380
2163
|
overflow when the controls in them don't fit.
|
|
3381
|
-
|
|
3382
2164
|
- be697dc: Add ephemeral pose overrides to the scene
|
|
3383
|
-
|
|
2165
|
+
|
|
3384
2166
|
`scene.overrides` holds a per-node `{ pose?, alpha? }` that the render and
|
|
3385
2167
|
hit-test paths read through and that history, `toJSON()` and `getVersion()`
|
|
3386
2168
|
never see. It is additive: a scene with no overrides behaves exactly as before.
|
|
3387
|
-
|
|
2169
|
+
|
|
3388
2170
|
This is where per-frame motion belongs. A 60 Hz loop previously had to write
|
|
3389
2171
|
through `setPose`, which records an undo entry (one per frame at best, batched)
|
|
3390
2172
|
and bumps the scene version, re-rendering every `useSyncExternalStore`
|
|
@@ -3392,47 +2174,45 @@
|
|
|
3392
2174
|
frame, because the painter memo keys on pose reference. An override entry is
|
|
3393
2175
|
hoisted once and mutated in place; `overrides.commit()` publishes the frame and
|
|
3394
2176
|
invalidates the memo for the overridden nodes only.
|
|
3395
|
-
|
|
2177
|
+
|
|
3396
2178
|
`commit()` is required after an in-place mutation — without it the memo serves
|
|
3397
2179
|
the previous frame's draw. Overrides are cleared when a node is removed, since
|
|
3398
2180
|
ids are reusable. To make a frame permanent, write it once through `setPose`
|
|
3399
2181
|
and clear the override; that single step is the undo entry.
|
|
3400
|
-
|
|
2182
|
+
|
|
3401
2183
|
`ForceGraphDemo` now settles with zero history entries and bakes the result as
|
|
3402
2184
|
one, replacing a per-tick batch of 24 `setPose` calls.
|
|
3403
|
-
|
|
3404
2185
|
- e909a3b: `fitTextPose` sizes a box the renderer will actually fill
|
|
3405
|
-
|
|
2186
|
+
|
|
3406
2187
|
It was the fourth site measuring text its own way: `ctx.measureText` per
|
|
3407
2188
|
character against system fonts, no kerning, `pose.text` only. Nothing masked
|
|
3408
2189
|
it the way the WebGL context masked the caret — a consumer calling it got a
|
|
3409
2190
|
box that disagreed with the paint, narrower by a kern on every pair and wrong
|
|
3410
2191
|
by the whole difference between the installed family and the registered face.
|
|
3411
2192
|
It goes through the shared layout now, so it sees kerning and per-run styling.
|
|
3412
|
-
|
|
2193
|
+
|
|
3413
2194
|
**Breaking:** `fitTextPose(ctx, pose, opts)` is now `fitTextPose(pose, opts)`.
|
|
3414
|
-
|
|
3415
2195
|
- 26bbdcf: Paint the canvas from its own animation frame instead of from a React render
|
|
3416
|
-
|
|
2196
|
+
|
|
3417
2197
|
`requestRedraw()` marks the surface dirty and the next frame paints, so many
|
|
3418
2198
|
redraws in one tick cost one paint. The view gains an imperative path on the
|
|
3419
2199
|
canvas handle — `setView` / `getView` / `subscribeView` — and `SceneCanvas` no
|
|
3420
2200
|
longer holds it in React state, so a camera moving at 60 Hz costs no renders.
|
|
3421
2201
|
Consumers passing a `view` prop stay controlled and are unaffected.
|
|
3422
|
-
|
|
2202
|
+
|
|
3423
2203
|
Opt-ins that come with it: `syncPaint` paints inside the commit for a consumer
|
|
3424
2204
|
that wants the old whole-cloth guarantee, `useScene(…, { subscribe: false })`
|
|
3425
2205
|
gives a host the scene without a render per mutation, `useSceneTextEdit`'s
|
|
3426
2206
|
`view` option accepts a thunk so the overlay tracks a ref-driven camera, and a
|
|
3427
2207
|
`contentVersion` prop feeds the version that `getPaintedVersion()` reports.
|
|
3428
|
-
|
|
2208
|
+
|
|
3429
2209
|
Two public signatures changed. `usePinchZoomTool` takes a view getter,
|
|
3430
2210
|
`getView: () => View`, where it took a `View` — nothing re-renders to refresh a
|
|
3431
2211
|
captured value any more. `CanvasExtensionApi` gained five required members —
|
|
3432
2212
|
`getView`, `setView`, `subscribeView`, `subscribeFrame`, `getPaintedVersion` —
|
|
3433
2213
|
so external code hand-implementing that interface stops typechecking; code that
|
|
3434
2214
|
only calls through the ref is unaffected.
|
|
3435
|
-
|
|
2215
|
+
|
|
3436
2216
|
Pixels and DOM can now be a frame apart, in whichever direction the change came
|
|
3437
2217
|
from. A view change leads with pixels: `setView` paints without rendering, so
|
|
3438
2218
|
DOM built from the view is stale until something re-renders it — position
|
|
@@ -3441,64 +2221,60 @@
|
|
|
3441
2221
|
pixels land next frame — compare `getPaintedVersion()` against the version you
|
|
3442
2222
|
are about to render when chrome must be in lockstep. Do not render scene-derived
|
|
3443
2223
|
DOM inside `startTransition`: React defers it and nothing forces it to catch up.
|
|
3444
|
-
|
|
2224
|
+
|
|
3445
2225
|
Anything reading the drawing buffer back outside a paint — the hud loupe's pixel
|
|
3446
2226
|
mode is the one in-tree case — can likewise see a buffer one frame older;
|
|
3447
2227
|
`subscribeFrame` runs on the frame that painted and removes the lag. Nothing
|
|
3448
2228
|
paints while `document.hidden` is true, `syncPaint` included, so a readback from
|
|
3449
2229
|
a background tab returns the frame from before the tab was hidden.
|
|
3450
|
-
|
|
3451
2230
|
- 546f67d: Copy typed-array arguments into `makeGLRecorder`'s call log as they are
|
|
3452
2231
|
recorded. A caller is entitled to reuse the array it uploads from, so storing
|
|
3453
2232
|
the reference recorded a value that later frames overwrote — a test reading
|
|
3454
2233
|
two frames back saw the same numbers twice and passed. Test-only surface.
|
|
3455
2234
|
- 3fb3a46: Release held keys when the window loses focus
|
|
3456
|
-
|
|
2235
|
+
|
|
3457
2236
|
A window that blurs mid-hold never delivers the keyup, so every in-flight
|
|
3458
2237
|
`key-held` handle stayed engaged until that key was pressed again — holding
|
|
3459
2238
|
Space and tabbing away left the hand tool on the hotkey stack indefinitely.
|
|
3460
|
-
|
|
2239
|
+
|
|
3461
2240
|
The gesture dispatcher now fires the `key-held` up phase for each held key on
|
|
3462
2241
|
window blur. Consumers that hand-rolled this reset can drop it; ongoing
|
|
3463
2242
|
invocations see a normal `onEnd`.
|
|
3464
|
-
|
|
3465
2243
|
- ccd51cc: Add a 43-glyph monochrome icon set to `@weasel-js/ui`.
|
|
3466
|
-
|
|
2244
|
+
|
|
3467
2245
|
One register: a 20x20 viewBox drawn in `currentColor` at stroke-width 1.5 with
|
|
3468
2246
|
round caps and joins, hairline weight reserved for structure, and filled
|
|
3469
2247
|
regions only where an action has a subject. Covers transport, history, view,
|
|
3470
2248
|
trial lifecycle, collection, state, instrument and status vocabulary. Import a
|
|
3471
2249
|
named component (`CloneIcon`), or `Icon` when the glyph is chosen at runtime.
|
|
3472
|
-
|
|
2250
|
+
|
|
3473
2251
|
`@weasel-js/ui` also re-exports the tool glyphs that live in `@weasel-js/core`,
|
|
3474
2252
|
so consumers have one import site for the whole set. `ImageIcon` was reachable
|
|
3475
2253
|
from core's icons folder but missing from its public barrel; it is exported
|
|
3476
2254
|
now.
|
|
3477
|
-
|
|
2255
|
+
|
|
3478
2256
|
Glyph geometry is generated (`npm run gen:icons`) from `packages/ui/scripts/icons/`
|
|
3479
2257
|
rather than hand-placed, because arrowheads and joins that miss their terminus
|
|
3480
2258
|
are invisible at chrome size.
|
|
3481
|
-
|
|
3482
2259
|
- 3fb3a46: Compose `before` and `after` layer chains in both directions
|
|
3483
|
-
|
|
2260
|
+
|
|
3484
2261
|
`composeOrderedLayers` walked the two anchor maps separately: a chain hanging
|
|
3485
2262
|
off an `after` anchor only followed further `after` links, and likewise for
|
|
3486
2263
|
`before`. A custom layer anchored `before: 'scene'` carrying a second custom
|
|
3487
2264
|
anchored `after` it dropped that second layer to the tail with a spurious
|
|
3488
2265
|
dangling-reference warning.
|
|
3489
|
-
|
|
2266
|
+
|
|
3490
2267
|
Both walks now emit a layer's `before` chain, the layer, then its `after`
|
|
3491
2268
|
chain, so the two mix freely. Cycle detection and orphan fallback are
|
|
3492
2269
|
unchanged.
|
|
3493
|
-
|
|
3494
2270
|
- d9f110e: Stop every frame loop while nothing can see it
|
|
3495
|
-
|
|
2271
|
+
|
|
3496
2272
|
New public hook `useVisibleRaf` in `@weasel-js/core` owns the question of
|
|
3497
2273
|
whether a frame may run: nothing runs while `document.hidden`, and a loop that
|
|
3498
2274
|
names an element also stops while that element is outside the viewport. A
|
|
3499
2275
|
request made while suspended is held rather than dropped and re-armed on
|
|
3500
2276
|
resume, so a loop never polls visibility or needs restarting by hand.
|
|
3501
|
-
|
|
2277
|
+
|
|
3502
2278
|
Ten loops now run behind it — `useFrameLoop`, `useAnimator`, `useSimulation`,
|
|
3503
2279
|
`useDecayLoop`, `useTextEdit`'s overlay follow, `CursorCoordsHud`'s FPS
|
|
3504
2280
|
counter, `Badge`'s crawl, and labkit's `FpsMeter`, `useTiledSurface` and
|
|
@@ -3506,30 +2282,29 @@
|
|
|
3506
2282
|
the rest ran on any page left open. `useLayerScheduler` looked safe and wasn't:
|
|
3507
2283
|
it paints only dirty layers, but a hidden tab still commits React updates and
|
|
3508
2284
|
its view/size effect marks every layer dirty.
|
|
3509
|
-
|
|
2285
|
+
|
|
3510
2286
|
Loops measuring elapsed time rebase their clock through the new `onResume`
|
|
3511
2287
|
option, so an hour spent hidden does not arrive as one hour-long frame — an FPS
|
|
3512
2288
|
meter reporting a rate nobody achieved, a tween jumping to its end value on
|
|
3513
2289
|
return. `dangerouslyRunWhenHidden` opts a loop out for offscreen recording or
|
|
3514
2290
|
export; nothing in the tree sets it.
|
|
3515
|
-
|
|
2291
|
+
|
|
3516
2292
|
`npm run check:frame-loops` fails the build on a bare `requestAnimationFrame`
|
|
3517
2293
|
in kit source, and runs in CI.
|
|
3518
|
-
|
|
3519
2294
|
- 0dd35a1: Fix pinch-to-zoom: mac trackpads zoomed the page, and `viewport.pinchZoom` zoomed twice
|
|
3520
|
-
|
|
2295
|
+
|
|
3521
2296
|
A trackpad pinch reaches the page as `wheel { ctrlKey: true }`. On a mac
|
|
3522
2297
|
`viewport.zoom`'s `mods: { mod: true }` binding requires metaKey and forbids
|
|
3523
2298
|
ctrl, and `viewport.wheelPan` forbids ctrl too, so nothing claimed the event
|
|
3524
2299
|
and the browser's own ctrl+wheel page zoom ran. `viewport.zoom` now carries a
|
|
3525
2300
|
second wheel binding on bare ctrl. Off mac it duplicates the `mod` binding,
|
|
3526
2301
|
where the matcher picks a single winner.
|
|
3527
|
-
|
|
2302
|
+
|
|
3528
2303
|
Nothing caught that because `IS_MAC` read `navigator.platform ?? userAgent`,
|
|
3529
2304
|
and jsdom reports an empty-string platform — not nullish, so the fallback never
|
|
3530
2305
|
fired and every mac binding in the kit was exercised only on the non-mac
|
|
3531
2306
|
branch. It reads `||` now.
|
|
3532
|
-
|
|
2307
|
+
|
|
3533
2308
|
Separately, `viewport.pinchZoom: true` mounted `<Canvas>`'s `usePinchZoomTool`
|
|
3534
2309
|
alongside the `viewport.pinchZoom` action that already handled the same
|
|
3535
2310
|
gesture, applying one pinch's factor twice — the opt-in broke the path that
|
|
@@ -3538,36 +2313,34 @@
|
|
|
3538
2313
|
the kit's 0.1–8 clamp now applied by default. `pinchZoom: false` disables pinch
|
|
3539
2314
|
for real; it previously left the action running. Bare `<Canvas>` keeps the hook
|
|
3540
2315
|
as its own pinch path.
|
|
3541
|
-
|
|
3542
2316
|
- 1a0bea3: `useNodeOverlayFrame`: the coordinate frame a DOM overlay pinned to a node needs
|
|
3543
|
-
|
|
2317
|
+
|
|
3544
2318
|
Nothing in the kit exported one, so consumers hand-rolled it — their own
|
|
3545
2319
|
`ResizeObserver` next to the existing `useCanvasSize`, and a translate-and-scale
|
|
3546
2320
|
inverse built by projecting two points. That inverse silently drops
|
|
3547
2321
|
`pose.rotation`, which is why on-canvas gradient handles on a rotated node sat
|
|
3548
2322
|
beside the paint instead of on it.
|
|
3549
|
-
|
|
2323
|
+
|
|
3550
2324
|
```ts
|
|
3551
|
-
useNodeOverlayFrame(scene, containerRef, nodeId, { view })
|
|
2325
|
+
useNodeOverlayFrame(scene, containerRef, nodeId, { view })
|
|
3552
2326
|
// → { box, toScreen, toLocal, width, height } | null
|
|
3553
2327
|
```
|
|
3554
|
-
|
|
2328
|
+
|
|
3555
2329
|
`box` is the node's composed world box, unrotated — the frame `toScreen` maps
|
|
3556
2330
|
from, and the box to hand `fillInPoseFrame` / `fillToBoundsFrame`. Rotation
|
|
3557
2331
|
lives in the pose→world leg, where it belongs: a node's stored geometry and its
|
|
3558
2332
|
bounds-frame paint are pre-rotation by definition, so neither of those two
|
|
3559
2333
|
changes.
|
|
3560
|
-
|
|
2334
|
+
|
|
3561
2335
|
`@weasel-js/ui` gains `SceneGradientHandles`, the scene-aware half of
|
|
3562
2336
|
`GradientHandles`: it reads the gradient out of a node's `fill` **or** its
|
|
3563
2337
|
`stroke` — `slot` is a prop — and commits each drag through `setFill` or
|
|
3564
2338
|
`setStroke` as one undo entry. `GradientHandles` itself stays frame-agnostic.
|
|
3565
|
-
|
|
2339
|
+
|
|
3566
2340
|
Also: `isGradientFill` narrows a `FillStyle` to its three gradient members, and
|
|
3567
2341
|
`useCanvasSize` accepts any `HTMLElement` rather than only a `div`.
|
|
3568
|
-
|
|
3569
2342
|
- 9d95836: A node's `data.stroke` takes a whole `Stroke`, not just a color
|
|
3570
|
-
|
|
2343
|
+
|
|
3571
2344
|
`NodeStroke = string | Stroke`, mirroring `NodeFill`. A string is still a
|
|
3572
2345
|
color and `'none'` still skips the stroke; an object is a core `Stroke` whose
|
|
3573
2346
|
`width`, `cap`, `join`, `dash`, `miterLimit` and `align` all reach the
|
|
@@ -3576,99 +2349,94 @@
|
|
|
3576
2349
|
rule `withLeafStroke` already applied to text. A bounds-relative stroke paint
|
|
3577
2350
|
is baked onto the pose box the way a fill is, so a gradient stroke resolves
|
|
3578
2351
|
against the box it was authored against.
|
|
3579
|
-
|
|
2352
|
+
|
|
3580
2353
|
`kit:shape` now honors `stroke: 'none'`, which only `kit:path` checked before.
|
|
3581
|
-
|
|
2354
|
+
|
|
3582
2355
|
`NodeInk` reports `{ filled, outset, inset }` instead of `{ filled,
|
|
3583
|
-
strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
2356
|
+
strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
3584
2357
|
`'outer'` none inside, which one number could not say, so picking grabbed the
|
|
3585
2358
|
wrong side. `ink` takes an optional context carrying the view scale, so a
|
|
3586
2359
|
`{ px }` stroke width resolves to world units. A painter that still returns
|
|
3587
2360
|
`{ filled, strokeWidth }` is read as a centered stroke and keeps working.
|
|
3588
|
-
|
|
2361
|
+
|
|
3589
2362
|
`setStroke` and `setStrokeOpacity` no longer stringify a node's `Stroke`: a
|
|
3590
2363
|
color pick replaces its paint and keeps width, cap, join and dash, and an
|
|
3591
2364
|
opacity drag sets the paint's `opacity`, which is the only form that works on
|
|
3592
2365
|
a gradient stroke.
|
|
3593
|
-
|
|
2366
|
+
|
|
3594
2367
|
Editing UI for the rich form is not here yet — a schema-driven color control
|
|
3595
2368
|
still writes a bare string over the object, so nodes carrying one are for
|
|
3596
2369
|
programmatic authorship until `SelectionPanel` learns the union. See
|
|
3597
2370
|
`docs/proposals/2026-08-26-node-stroke-union.md`.
|
|
3598
|
-
|
|
3599
2371
|
- 62a3c46: Paint a gradient or pattern stroke instead of throwing.
|
|
3600
|
-
|
|
2372
|
+
|
|
3601
2373
|
`Stroke.paint` has always been a full `FillStyle`, and SVG import puts paint
|
|
3602
2374
|
servers there deliberately, but the renderer refused anything but a solid — so
|
|
3603
2375
|
importing a shape with `stroke="url(#grad)"` produced a scene that threw on the
|
|
3604
2376
|
next frame. Both stroke paths now paint the ribbon through the same route a
|
|
3605
2377
|
fill takes, including under the inner/outer alignment stencil. A non-solid
|
|
3606
2378
|
even-odd fill no longer renders black.
|
|
3607
|
-
|
|
3608
2379
|
- 5f6c28e: An object leaf's fields can be organised into groups
|
|
3609
|
-
|
|
2380
|
+
|
|
3610
2381
|
`ToolPrefObject.children` takes a `ToolPrefGroup` as well as a leaf. A group
|
|
3611
2382
|
heads its fields under a label and contributes nothing to the path — the same
|
|
3612
2383
|
rule group keys follow at the top level of a schema, so a field inside one is
|
|
3613
2384
|
still addressed as a field of the object.
|
|
3614
|
-
|
|
2385
|
+
|
|
3615
2386
|
Without it, a value with many fields renders as one undifferentiated list. A
|
|
3616
2387
|
`TextStyle` is the case that needs it: its character and paragraph fields are
|
|
3617
2388
|
one value but read as two lists.
|
|
3618
|
-
|
|
3619
2389
|
- 3cd1ee8: A schema leaf can hold an object, with its fields hanging off it
|
|
3620
|
-
|
|
2390
|
+
|
|
3621
2391
|
A compound value — a stroke, a shadow, a pattern spec — could be described as
|
|
3622
2392
|
sibling leaves addressing into it (`data.stroke.width`, `data.stroke.cap`).
|
|
3623
2393
|
It shouldn't be: each control then writes one field of a value it can only
|
|
3624
2394
|
half see, and writing a field into something that isn't an object yet corrupts
|
|
3625
2395
|
it outright.
|
|
3626
|
-
|
|
2396
|
+
|
|
3627
2397
|
`ToolPrefObject` describes the value instead. Its `children` are ordinary
|
|
3628
2398
|
leaves whose paths are relative to the object, and every child edit commits
|
|
3629
2399
|
the parent object whole. A field that is itself a union declares the kind that
|
|
3630
2400
|
edits that union — a stroke's `paint` is a `paint` leaf. `fromScalar` lifts a
|
|
3631
2401
|
value still held in a scalar form before a child edit lands on it, which is
|
|
3632
2402
|
how a stroke stored as a bare colour string gains a width.
|
|
3633
|
-
|
|
2403
|
+
|
|
3634
2404
|
`defaultNodeProperties` describes `data.stroke` this way, so the panel shows
|
|
3635
2405
|
Color, Width, Cap, Join and Align under one Stroke block, and the separate
|
|
3636
2406
|
`data.strokeWidth` leaf is gone. `SelectionPanel` now honours `block`, which
|
|
3637
2407
|
`PrefsForm` already did. The one-off `stroke` pref kind added days ago is
|
|
3638
2408
|
replaced by this general one.
|
|
3639
|
-
|
|
2409
|
+
|
|
3640
2410
|
`dash` has no leaf: it is a `number[]` and no kind edits one. It survives
|
|
3641
2411
|
import, export and rendering untouched.
|
|
3642
|
-
|
|
3643
2412
|
- 2ea772f: Selection handles are hit-tested at the size they are painted
|
|
3644
|
-
|
|
2413
|
+
|
|
3645
2414
|
Handles painted at `HANDLE_BASE_PX * targetScale` and hit-tested at the bare
|
|
3646
2415
|
constant, and neither `buildAffordanceAt` call site passed the option that
|
|
3647
2416
|
would have scaled it. A coarse pointer got a bigger picture and exactly the
|
|
3648
2417
|
same 8px grab zone it had on a mouse — the touch forgiveness the coarse profile
|
|
3649
2418
|
exists to provide never reached the hit-test. The slops debug overlay was a
|
|
3650
2419
|
third unscaled copy, so it drew hit regions where they were not.
|
|
3651
|
-
|
|
2420
|
+
|
|
3652
2421
|
`core/device/targets.ts` now holds one base table and one accessor,
|
|
3653
2422
|
`targetSizesPx(targetScale)`. Paint, hit-test and the debug overlay all resolve
|
|
3654
2423
|
through it. `HANDLE_BASE_PX`, `ANCHOR_HIT_BASE_PX` and
|
|
3655
2424
|
`ROTATION_HANDLE_BASE_PX` keep their names and values and now read off the
|
|
3656
2425
|
table; the internal `HANDLE_HIT_RADIUS` and `ANCHOR_HIT_RADIUS` are gone.
|
|
3657
|
-
|
|
2426
|
+
|
|
3658
2427
|
`buildAffordanceAt` and `createSlopsDebugLayer` take an optional `targetScale`.
|
|
3659
2428
|
`selectTool.handleHitRadius` now actually reaches the hit-test — it previously
|
|
3660
2429
|
reached nothing.
|
|
3661
|
-
|
|
2430
|
+
|
|
3662
2431
|
`useRotateTool`'s `handleHitRadius` option is **removed**. The rotation
|
|
3663
2432
|
affordance is an annulus with a band thickness and no point radius, so the
|
|
3664
2433
|
option could only ever have been a second name for `rotationHandleDistance`,
|
|
3665
2434
|
which is live and now defaults from the same table.
|
|
3666
|
-
|
|
2435
|
+
|
|
3667
2436
|
Known gap: `CanvasView` is a second `buildAffordanceAt` call site that reads no
|
|
3668
2437
|
device profile, so a nested view still hit-tests at the fine-pointer size.
|
|
3669
|
-
|
|
3670
2438
|
- f77bd95: `getChildren` means one thing on an adapter
|
|
3671
|
-
|
|
2439
|
+
|
|
3672
2440
|
`MoveAdapter` declared `getChildren(id)` — a node's direct children, for the
|
|
3673
2441
|
drag cascade — and `OrderedAdapter` declared `getChildren(parentId | null)`,
|
|
3674
2442
|
the z-ordering seam where `null` means the root. Both land on the same adapter
|
|
@@ -3676,36 +2444,35 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3676
2444
|
under the name the ops read with the second meaning. An op asking for root
|
|
3677
2445
|
order got `[]`, which reads as "the root has no siblings", and the slot it
|
|
3678
2446
|
captured was silently lost.
|
|
3679
|
-
|
|
2447
|
+
|
|
3680
2448
|
The two declarations are now one contract, and `arrayAdapter` answers the root
|
|
3681
2449
|
from its own item array rather than delegating — a consumer callback written
|
|
3682
2450
|
for node ids returns `[]` there, which cannot be told apart from a genuine
|
|
3683
2451
|
empty answer. A consumer's `getChildren` config is still only ever asked about
|
|
3684
2452
|
a node id.
|
|
3685
|
-
|
|
2453
|
+
|
|
3686
2454
|
`arrayAdapter` still exposes no `setChildOrder`, so it places by ordinal rather
|
|
3687
2455
|
than by anchor. That is unchanged, and it is why the ordinal fallback exists.
|
|
3688
|
-
|
|
3689
2456
|
- 2ea772f: The canvas and the gradient editor now sample one gradient
|
|
3690
|
-
|
|
2457
|
+
|
|
3691
2458
|
`buildGradientRamp` carried its own interpolation beside
|
|
3692
2459
|
`sampleGradientStops`, and the two disagreed three ways: the ramp had no guard
|
|
3693
2460
|
at either end and extrapolated past the first and last stop, the two picked
|
|
3694
2461
|
opposite sides of a coincident pair, and they parsed color differently — a stop
|
|
3695
2462
|
written as a CSS named color rendered on the canvas and threw in the editor.
|
|
3696
|
-
|
|
2463
|
+
|
|
3697
2464
|
`sampleGradientStops` keeps its semantics and is now the only implementation.
|
|
3698
2465
|
`resolveGradientStops` sorts and parses the list once; `sampleResolvedStops`
|
|
3699
2466
|
returns the color at `t`. The ramp cache builds its texels through those, so
|
|
3700
2467
|
there is no interpolation math left in the renderer.
|
|
3701
|
-
|
|
2468
|
+
|
|
3702
2469
|
Two behavior changes worth naming. `resolveColor` is the surviving parser, so
|
|
3703
2470
|
gradient stops accept named and functional colors everywhere — but no longer
|
|
3704
2471
|
hex without a leading `#`, which only the editor path had tolerated and the
|
|
3705
2472
|
canvas never accepted. And `sampleGradientStops` returns normalized hex at the
|
|
3706
2473
|
endpoints instead of echoing the raw stop string, so `'red'` comes back as
|
|
3707
2474
|
`'#ff0000'`.
|
|
3708
|
-
|
|
2475
|
+
|
|
3709
2476
|
**SVG export:** a conic gradient left the exporter as a dangling `url(#…)` —
|
|
3710
2477
|
the element already carried the reference, the built-in serializer returned
|
|
3711
2478
|
nothing, and the registry's `toSvg` slot has no in-repo implementation, so the
|
|
@@ -3713,18 +2480,17 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3713
2480
|
through to the same warning the pattern path already emits when nothing can
|
|
3714
2481
|
produce a paint server. A consumer that registers a `toSvg` for
|
|
3715
2482
|
`conic-gradient` still serializes and gets no warning.
|
|
3716
|
-
|
|
3717
2483
|
- aba8d91: Answer "can this node be hit" in one place
|
|
3718
|
-
|
|
2484
|
+
|
|
3719
2485
|
Four tree walks answered it separately — the generic-adapter point pick, the
|
|
3720
2486
|
one `<SceneCanvas>` installs, `sceneToAdapter`'s area walk, and the live
|
|
3721
2487
|
marquee/lasso — plus a fifth that shadowed the third. They agreed on every case
|
|
3722
2488
|
that had a test and disagreed on the rest, three times, silently. `pickWalk`
|
|
3723
2489
|
now owns every gate; a query supplies only its own shape test and the clip
|
|
3724
2490
|
predicate for its region.
|
|
3725
|
-
|
|
2491
|
+
|
|
3726
2492
|
Behavior that changes as a result:
|
|
3727
|
-
|
|
2493
|
+
|
|
3728
2494
|
- **A node painted at alpha 0 is no longer clickable.** The pick path reads the
|
|
3729
2495
|
same number the painter does — the view's `alphaFor` times any per-node
|
|
3730
2496
|
override alpha — so a node faded out of sight stops claiming clicks. The
|
|
@@ -3740,13 +2506,12 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3740
2506
|
- An ancestor clip now rejects an area query that reaches into the clip where
|
|
3741
2507
|
the node is not, or reaches the node where the clip is not — the two terms
|
|
3742
2508
|
together, where one alone let false positives through.
|
|
3743
|
-
|
|
2509
|
+
|
|
3744
2510
|
`useSceneSelectTool` takes `alphaOf` and `layerIsPainted` for the asking view.
|
|
3745
2511
|
`passesAncestorClips` and its module are gone; `pickWalk`, `scenePickSource`,
|
|
3746
2512
|
`adapterPickSource` and `ownClipOf` replace them.
|
|
3747
|
-
|
|
3748
2513
|
- 2ea772f: A drag-to-insert reports the bounds it paints
|
|
3749
|
-
|
|
2514
|
+
|
|
3750
2515
|
The painter, the commit factory and `getGestureBounds()` each sized an
|
|
3751
2516
|
in-flight insert differently. The reporter read the drag rect alone, so a
|
|
3752
2517
|
centered Alt-drag reported a half-extent of `d` against a painted circumradius
|
|
@@ -3755,45 +2520,43 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3755
2520
|
all. The painter and the commit agreed on polygon and star but not on line or
|
|
3756
2521
|
pencil: the commit posed the drag AABB for a line the painter drew endpoint to
|
|
3757
2522
|
endpoint, and fell back to the drag rect for a trail under four samples.
|
|
3758
|
-
|
|
2523
|
+
|
|
3759
2524
|
One function now answers it for all three. The zero-area skip in the painter
|
|
3760
2525
|
and the reporter tests the resolved extent rather than the raw drag rect, and
|
|
3761
2526
|
an `InsertNodeFactory` that returns no `pose` falls back to the extent. The
|
|
3762
2527
|
`bounds` argument handed to a factory is unchanged.
|
|
3763
|
-
|
|
3764
2528
|
- 3386d64: Path command opcodes derive from one table
|
|
3765
|
-
|
|
2529
|
+
|
|
3766
2530
|
`M`/`L`/`C`/`Q`/`Z` and their coordinate counts were declared five times —
|
|
3767
2531
|
once in core, once in `@weasel-js/geom`, and three more as `COORD_COUNT`
|
|
3768
2532
|
literals in the path transform, pose-rotation and pose-descriptor walkers. They
|
|
3769
2533
|
agreed, and nothing held them to each other: a sixth opcode desynchronizes two
|
|
3770
2534
|
packages' reading of the same `Uint8Array` with no exception and no type error,
|
|
3771
2535
|
and every walker misparses the coordinate stream from that command on.
|
|
3772
|
-
|
|
2536
|
+
|
|
3773
2537
|
`PATH_COMMANDS` in `@weasel-js/geom` is now the table. `PATH_M`…`PATH_Z`,
|
|
3774
2538
|
`PATH_CMD_LENGTHS` and the new `pathCommandCoordCount` all derive from it, and
|
|
3775
2539
|
core re-exports them by name, so the opcode constants keep their names, values
|
|
3776
2540
|
and literal types. The three walkers moved onto `forEachSegment` rather than
|
|
3777
2541
|
onto the accessor alone — they were duplicating the coordinate-cursor advance
|
|
3778
2542
|
as well as the length, and the cursor is the half that actually misreads.
|
|
3779
|
-
|
|
2543
|
+
|
|
3780
2544
|
Eight further files switch on these opcodes with inline literals. Five throw on
|
|
3781
2545
|
an unknown code; three — the path boolean adapter, the anchor-editing geometry,
|
|
3782
2546
|
and geom's own boolean adapter — have no `default` arm and would silently stop
|
|
3783
2547
|
advancing. Left as-is; they need per-command semantics, not one walker.
|
|
3784
|
-
|
|
3785
2548
|
- 68d2651: Pref leaf kinds are declared once, and every renderer is exhaustive
|
|
3786
|
-
|
|
2549
|
+
|
|
3787
2550
|
`@weasel-js/ui` carried its own copy of the pref-leaf union under a comment
|
|
3788
2551
|
saying to keep it in sync with core's field-for-field. It had drifted: ui's enum
|
|
3789
2552
|
leaf had neither `encoding` nor `options[].disabled`, so a dash-array
|
|
3790
2553
|
preference did not merely fail to select — choosing an option wrote the option
|
|
3791
2554
|
string over the stored dash array. labkit's two renderers were missing the
|
|
3792
2555
|
`paint` and `object` kinds outright.
|
|
3793
|
-
|
|
2556
|
+
|
|
3794
2557
|
ui's schema is now a rename re-export of core's declaration. The public `Pref*`
|
|
3795
2558
|
names are unchanged, and there is nothing left to keep in sync.
|
|
3796
|
-
|
|
2559
|
+
|
|
3797
2560
|
More importantly, all four renderer switches ended in `default:`, so adding a
|
|
3798
2561
|
built-in kind produced no error at any site and simply rendered nothing —
|
|
3799
2562
|
verified by adding one and typechecking. `ToolPrefLeaf` widens `kind` to
|
|
@@ -3803,34 +2566,32 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3803
2566
|
`isBuiltinToolPref(leaf)`, which narrows to the closed union so each renderer
|
|
3804
2567
|
can discriminate and end in a `never`. App-defined kinds take the placeholder
|
|
3805
2568
|
path as before.
|
|
3806
|
-
|
|
2569
|
+
|
|
3807
2570
|
Dash-array preferences now select and commit correctly in `PrefsForm`: the enum
|
|
3808
2571
|
arm threads sibling values, routes through `encoding.read` / `encoding.write`,
|
|
3809
2572
|
and honors `option.disabled`. `SelectionPanel` already did all of this — it was
|
|
3810
2573
|
only the forked copy that could not express it.
|
|
3811
|
-
|
|
3812
2574
|
- 3386d64: Dragging out a text box shows a live preview
|
|
3813
|
-
|
|
2575
|
+
|
|
3814
2576
|
The set of insertable kinds and the `KitInsertShape` union sat on adjacent
|
|
3815
2577
|
lines with no linkage, and seven more sites restated one list or the other. The
|
|
3816
2578
|
drift was already live: the text tool binds `actionId: 'insert'` and commits
|
|
3817
2579
|
through the insert dep, but the runtime set never listed `text`, so a
|
|
3818
2580
|
drag-to-insert text box had no preview.
|
|
3819
|
-
|
|
2581
|
+
|
|
3820
2582
|
`SHAPE_KINDS` is now one descriptor table — a row per kind, flagged for whether
|
|
3821
2583
|
it has a built-in tool and whether it takes an insert preview. Both unions,
|
|
3822
2584
|
`KIT_SHAPE_KINDS`, `BUNDLE_TOOLS.exhaustive`, the known-builtin-id list and the
|
|
3823
2585
|
preview gate all derive from it.
|
|
3824
|
-
|
|
2586
|
+
|
|
3825
2587
|
Two type-surface consequences. `KIT_SHAPE_KINDS` is typed
|
|
3826
2588
|
`readonly BuiltinShapeToolId[]` rather than a literal tuple — same contents,
|
|
3827
2589
|
same order, and `(typeof KIT_SHAPE_KINDS)[number]` is unchanged; what goes is
|
|
3828
2590
|
positional and length typing, which nothing uses. And `OngoingOverlay['shape']`
|
|
3829
2591
|
gains `'text'`, which is the fix itself: a consumer switching exhaustively over
|
|
3830
2592
|
it gains a case, handled by the existing box arm.
|
|
3831
|
-
|
|
3832
2593
|
- c6c499d: Text layout is computed once, and the caret reads the layout that was painted
|
|
3833
|
-
|
|
2594
|
+
|
|
3834
2595
|
The paint, the pose silhouette and the click-to-edit caret each ran their own
|
|
3835
2596
|
walk. The paint went through a memoized `layoutRuns`; the silhouette re-ran
|
|
3836
2597
|
`layoutRuns` on every pose change, because it allocates a fresh `ResolvedRun[]`
|
|
@@ -3840,7 +2601,7 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3840
2601
|
caret could therefore answer with a different line, and a different glyph, than
|
|
3841
2602
|
the one under the pointer — masked in practice only because it asked a WebGL
|
|
3842
2603
|
canvas for a 2D context and got `null`, degrading silently to no caret at all.
|
|
3843
|
-
|
|
2604
|
+
|
|
3844
2605
|
`cachedLayoutRuns` now lives in `@weasel-js/text` beside the function it caches,
|
|
3845
2606
|
and all three go through it. It keeps the array-identity `WeakMap` as the
|
|
3846
2607
|
renderer's zero-cost path and falls through to a bounded LRU keyed on the runs'
|
|
@@ -3848,7 +2609,7 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3848
2609
|
about 230× cheaper than laying out again, at roughly 4× the cost of the
|
|
3849
2610
|
identity hit. `LaidOutLineBox` carries the caret stops the pen produced, so
|
|
3850
2611
|
snapping is to the advance cells the glyphs were actually painted in.
|
|
3851
|
-
|
|
2612
|
+
|
|
3852
2613
|
**Breaking:** `caretIndexAt(ctx, x, y, pose)` is now
|
|
3853
2614
|
`caretIndexAt(x, y, pose, opts?)` — the `CanvasRenderingContext2D` is gone, and
|
|
3854
2615
|
an optional `maxWidth` mirrors `textLineBoxes` for nodes the `kit:text` painter
|
|
@@ -3856,9 +2617,8 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3856
2617
|
double-click always seeds the caret instead of falling back to editing from
|
|
3857
2618
|
offset 0. `@weasel-js/text` gains a `./test-seams` entry point exporting
|
|
3858
2619
|
`_resetLayoutCacheForTests`.
|
|
3859
|
-
|
|
3860
2620
|
- 4f1ef0b: Lay text out from font bytes alone — no baked atlas.
|
|
3861
|
-
|
|
2621
|
+
|
|
3862
2622
|
`registerFontOutlines` was a paint upgrade for a family that already had an
|
|
3863
2623
|
MSDF atlas; a family with only font bytes could not resolve, so it rendered
|
|
3864
2624
|
nothing. It is now a tier in its own right: `OutlineFace` reports `ascender`,
|
|
@@ -3866,48 +2626,46 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3866
2626
|
outline-only family, and `layoutRuns` reads advances, kerning and the baseline
|
|
3867
2627
|
through one source the atlas and a parsed face both satisfy. `outlineMinSize`
|
|
3868
2628
|
does not gate such a family — there is no other tier to prefer.
|
|
3869
|
-
|
|
2629
|
+
|
|
3870
2630
|
This does not touch metric neutrality where it applies: a family that has an
|
|
3871
2631
|
atlas still resolves to the atlas, so registering outlines cannot move text
|
|
3872
2632
|
that was already rendering.
|
|
3873
|
-
|
|
2633
|
+
|
|
3874
2634
|
Also fixes the outline tier in Node. opentype.js publishes ESM under `module`
|
|
3875
2635
|
and UMD under `main`; Node takes the UMD build, whose named exports it cannot
|
|
3876
2636
|
detect, so `parse` was undefined and every face failed to load — silently, via
|
|
3877
2637
|
the fallback to SDF. A browser bundler reading `module` never saw it.
|
|
3878
|
-
|
|
2638
|
+
|
|
3879
2639
|
Breaking for a consumer-supplied `OutlineParser`: a face must now report
|
|
3880
2640
|
metrics as well as geometry.
|
|
3881
|
-
|
|
3882
2641
|
- 0114abf: Add `PaintInput`, a control that edits a whole `FillStyle`.
|
|
3883
|
-
|
|
2642
|
+
|
|
3884
2643
|
A kind bar over a per-kind body, driven by the paint-kind registry rather than
|
|
3885
2644
|
a fixed list, so a consumer's registered kind appears in the bar and renders
|
|
3886
2645
|
that entry's `Editor`. `SelectionPanel`'s `paint` leaf renders it in place of
|
|
3887
2646
|
the chip that showed a gradient as indeterminate and wrote a solid over it on
|
|
3888
2647
|
first touch — so the checkerboard now means a mixed selection and nothing else,
|
|
3889
2648
|
and a gradient stroke is editable rather than merely paintable.
|
|
3890
|
-
|
|
2649
|
+
|
|
3891
2650
|
Switching kinds keeps a per-kind memory for the control's lifetime, so
|
|
3892
2651
|
linear -> solid -> linear comes back with its stops instead of the ramp
|
|
3893
2652
|
`withGradientKind` cannot carry.
|
|
3894
|
-
|
|
2653
|
+
|
|
3895
2654
|
`PatternPicker` moves from WeaselDraw into `@weasel-js/ui`, which now depends
|
|
3896
2655
|
on `@weasel-js/svg` for its tile previews.
|
|
3897
|
-
|
|
2656
|
+
|
|
3898
2657
|
The bar offers **None**: "what kind of paint is this?" takes no-paint as an
|
|
3899
2658
|
answer. `setFill` and `setStroke` accept `paint: null` to write it — a fill
|
|
3900
2659
|
becomes `null`, and a stroke goes away entirely rather than keeping a width
|
|
3901
2660
|
that draws no ink. `PaintKindEntry` gains an optional `icon`, and the five
|
|
3902
2661
|
built-in kinds carry glyphs so six segments fit a property row.
|
|
3903
|
-
|
|
2662
|
+
|
|
3904
2663
|
`FILL` and `STROKE` are now peer sections: the `appearance` group goes headless
|
|
3905
2664
|
and `data.fill` becomes a block leaf. The stroke's paint is no longer paired
|
|
3906
2665
|
with its width — a whole paint editor cannot share a row with a slider.
|
|
3907
|
-
|
|
3908
2666
|
- 50bc909: `FillStyle` is open: register a sixth paint kind and it renders, converts
|
|
3909
2667
|
frames and serializes.
|
|
3910
|
-
|
|
2668
|
+
|
|
3911
2669
|
`registerPaintKind(entry)` returns a disposer and `_resetPaintKindsForTests`
|
|
3912
2670
|
re-seeds the five built-ins, matching the kit's other module-global
|
|
3913
2671
|
registries. An entry carries the editor's slots (`label`, `seed`, `colorOf`,
|
|
@@ -3915,7 +2673,7 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3915
2673
|
`<defs>` slot. `listPaintKinds()` enumerates them, and `asPaint` types a
|
|
3916
2674
|
consumer's own paint as a `FillStyle` — the union itself stays closed, because
|
|
3917
2675
|
opening its discriminant would widen every built-in member.
|
|
3918
|
-
|
|
2676
|
+
|
|
3919
2677
|
Three defects fall out of the same change, each of which a sixth kind hit
|
|
3920
2678
|
immediately. The renderer's fill dispatch fell off the end of its switch into
|
|
3921
2679
|
an unguarded cast to the gradient union, so an unknown kind read `stops` off a
|
|
@@ -3923,25 +2681,24 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3923
2681
|
an unknown kind untouched, leaving it painting in screen space on a node that
|
|
3924
2682
|
moves. `<defs>` emitted nothing for a kind `gradientXml` did not know while
|
|
3925
2683
|
still writing the `url(#id)` that referenced it.
|
|
3926
|
-
|
|
2684
|
+
|
|
3927
2685
|
Registering a kind now bumps the node memo generation, so a node painted
|
|
3928
2686
|
before the registration repaints rather than holding the frame it resolved
|
|
3929
2687
|
when the kind was unknown.
|
|
3930
|
-
|
|
3931
2688
|
- 6a06f6d: Node paint is an object: `data.fill` is a `FillStyle`, `data.stroke` a `Stroke`
|
|
3932
|
-
|
|
2689
|
+
|
|
3933
2690
|
Each concept now has exactly one shape. `data.fill` holds a `FillStyle`,
|
|
3934
2691
|
`data.stroke` a whole `Stroke`, and `null` on either is an explicit "no paint"
|
|
3935
2692
|
where `undefined` takes the painter's fallback. Two new authoring helpers keep
|
|
3936
2693
|
hand-written node data short:
|
|
3937
|
-
|
|
2694
|
+
|
|
3938
2695
|
```ts
|
|
3939
2696
|
data: { path, fill: solid('#7fb069'), stroke: strokeOf('#1c1c1c', 2) }
|
|
3940
2697
|
```
|
|
3941
|
-
|
|
2698
|
+
|
|
3942
2699
|
**Breaking, with no compatibility path.** A document written against the old
|
|
3943
2700
|
shapes renders wrong rather than failing, which is accepted:
|
|
3944
|
-
|
|
2701
|
+
|
|
3945
2702
|
- `NodeFill = string | FillStyle` and `NodeStroke = string | Stroke` are gone,
|
|
3946
2703
|
and so are the string branches of `resolveNodeFill` / `resolveNodeStroke`.
|
|
3947
2704
|
A node holding `fill: '#f00'` now paints the default grey.
|
|
@@ -3957,176 +2714,169 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3957
2714
|
- `@weasel-js/svg`'s `strokeDataFromSvg` returns `Stroke | undefined` instead
|
|
3958
2715
|
of a `{ stroke, strokeWidth }` pair, and stops flattening a plain solid
|
|
3959
2716
|
stroke into a color. SVG's `fill="none"` imports as `fill: null`.
|
|
3960
|
-
|
|
2717
|
+
|
|
3961
2718
|
**A paint's alpha lives in `opacity`, one slot for every paint kind.** That is
|
|
3962
2719
|
the only slot a gradient or a pattern has, so it is the slot all of them use,
|
|
3963
2720
|
and the renderer multiplies a hex alpha by it — the two would fight if both
|
|
3964
2721
|
carried the value. `solid()` therefore moves an alpha channel out of the hex:
|
|
3965
2722
|
`solid('#ff000080')` is `{ color: '#ff0000', opacity: 0.502 }`.
|
|
3966
|
-
|
|
2723
|
+
|
|
3967
2724
|
The four setter actions follow: `setFillOpacity` / `setStrokeOpacity` write
|
|
3968
2725
|
`opacity` rather than splicing hex, so they now work on a gradient fill, which
|
|
3969
2726
|
they used to leave untouched. `setFill` / `setStroke` given a `color` recolor
|
|
3970
2727
|
the node's existing paint through the new `paintWithColor`, keeping its opacity
|
|
3971
2728
|
unless the picked color states an alpha of its own — and `setStroke` keeps the
|
|
3972
2729
|
stroke's width, cap, join and dash instead of replacing the whole value.
|
|
3973
|
-
|
|
2730
|
+
|
|
3974
2731
|
New exports: `solid`, `strokeOf`, `paintAlpha`, `paintWithAlpha`,
|
|
3975
2732
|
`paintWithColor`, `DEFAULT_SHAPE_FILL`.
|
|
3976
|
-
|
|
2733
|
+
|
|
3977
2734
|
`defaultNodeProperties` moves `data.fill` from a `color` leaf to a `paint` one
|
|
3978
2735
|
— a color control pointed at a `FillStyle` reads `undefined` off a gradient and
|
|
3979
2736
|
writes a bare string over it — and the `data.stroke` object leaf drops its
|
|
3980
2737
|
`fromScalar`, which had nothing left to lift.
|
|
3981
|
-
|
|
3982
2738
|
- a37ee0b: Separate a text node's content from its typography, and draw depth only where a label marks it
|
|
3983
|
-
|
|
2739
|
+
|
|
3984
2740
|
The text schema put `data.text` in a group named Text, so the section read
|
|
3985
2741
|
TEXT and the row inside it read Text — one word nested in itself — and the
|
|
3986
2742
|
style groups below it read as fields of the content string rather than as its
|
|
3987
2743
|
siblings. Content is its own section now, with the field full-width because
|
|
3988
2744
|
the section already names it.
|
|
3989
|
-
|
|
2745
|
+
|
|
3990
2746
|
A group with an empty `name` renders no heading. That already worked for
|
|
3991
2747
|
sections and is now documented on `ToolPrefGroup`, since it is how a schema
|
|
3992
2748
|
says "this group organises, it doesn't name": `Character` and `Paragraph`
|
|
3993
2749
|
carry the labels, and a `Typography` heading over them named nothing new.
|
|
3994
2750
|
It stays opt-in rather than a rule that rolls up any all-group parent —
|
|
3995
2751
|
a `Border` over `Top` / `Right` / `Bottom` needs its name.
|
|
3996
|
-
|
|
2752
|
+
|
|
3997
2753
|
Rows under a suppressed heading no longer indent. Depth drawn without a
|
|
3998
2754
|
visible parent put `Character` a level deeper than `Content` while being its
|
|
3999
2755
|
peer, which is the panel's own tree discipline broken by its own hand.
|
|
4000
|
-
|
|
4001
2756
|
- 611b30e: Layers and deps answer for the view they are drawn for
|
|
4002
|
-
|
|
4003
|
-
Nine lookups closed over the
|
|
2757
|
+
|
|
2758
|
+
Nine lookups closed over the *surface's* state at construction, so they answered
|
|
4004
2759
|
for view zero in every view. `<CanvasView>` draws the surface's layer array
|
|
4005
2760
|
unchanged and only the draw envelope differs, which makes a `draw: (_data, …)`
|
|
4006
2761
|
a guarantee of answering for the wrong view rather than merely an unused
|
|
4007
2762
|
argument. A drag in view B ghosted in view A, the marquee painted in the wrong
|
|
4008
2763
|
view, chrome-caps resolved against the surface's selection, every Cmd+V centered
|
|
4009
2764
|
on the wrong camera, and Escape in view B cancelled view A.
|
|
4010
|
-
|
|
2765
|
+
|
|
4011
2766
|
**New on `CanvasViewHelpers`** — `getPreviewSources()`, `getGestureOverlays()`
|
|
4012
2767
|
and `getIsVisible()`. All three are **required members**: anyone hand-writing a
|
|
4013
2768
|
`CanvasViewHelpers` (a test double, a wrapper) has to add them.
|
|
4014
2769
|
`getIsVisible` **moves off `CanvasSurfaceHelpers`**, where it could only ever
|
|
4015
2770
|
have answered for one view.
|
|
4016
|
-
|
|
2771
|
+
|
|
4017
2772
|
**New on `GestureSource`** — `previewSources()` and `overlays()`, also required,
|
|
4018
2773
|
alongside the newly exported `GesturePreviewSource`. `toolPreviewSources(tools)`
|
|
4019
2774
|
is the tool half.
|
|
4020
|
-
|
|
2775
|
+
|
|
4021
2776
|
**Layer options changed.** `createPathEditingOverlayLayer` and
|
|
4022
2777
|
`createSlopsDebugLayer` take `getPose(id, previews)` and have lost their
|
|
4023
2778
|
`isVisible` / `selectionRef` / `boundsOf` options — those come off the envelope
|
|
4024
2779
|
now. `usePreviewGhostLayer` has lost `tools`. Both it and
|
|
4025
2780
|
`useDispatcherOverlayLayer` keep `dispatcher` **only** to subscribe for repaint.
|
|
4026
|
-
|
|
2781
|
+
|
|
4027
2782
|
**Picking takes a camera.** `pickEvery`, `pickBest` and `makeGetNodeAtPoint`'s
|
|
4028
2783
|
result accept an optional trailing `PickCamera`. A world point does not carry
|
|
4029
2784
|
the scale it was produced under and picking has no draw envelope, so the caller
|
|
4030
2785
|
that produced the point supplies it; omitting it keeps the surface camera.
|
|
4031
|
-
|
|
2786
|
+
|
|
4032
2787
|
`useHoverTracking` took a `clientToWorld` thunk beside a world-space
|
|
4033
2788
|
`getNodeAtPoint` — the first resolved the view and the second did not, so hover
|
|
4034
2789
|
picked at the surface's scale inside a panel. It takes one
|
|
4035
2790
|
`nodeAtClientPoint(clientX, clientY)` now.
|
|
4036
|
-
|
|
2791
|
+
|
|
4037
2792
|
Anchor-editing target state stays surface-wide; only the preview resolution on
|
|
4038
2793
|
that path is per-view.
|
|
4039
|
-
|
|
4040
2794
|
- 9ad8cb2: Picking answers for what was painted
|
|
4041
|
-
|
|
2795
|
+
|
|
4042
2796
|
Three defects in `<SceneCanvas>`'s hit paths, all one shape — a pick answering
|
|
4043
2797
|
from something other than what the renderer drew.
|
|
4044
|
-
|
|
2798
|
+
|
|
4045
2799
|
**Pose overrides were painted through and picked around.** `PoseOverride.pose`
|
|
4046
|
-
is documented as replacing the document pose
|
|
4047
|
-
hit-test paths read
|
|
2800
|
+
is documented as replacing the document pose *everywhere the render and
|
|
2801
|
+
hit-test paths read one*, and `sceneAdapter.getPose` honored it. But
|
|
4048
2802
|
`<SceneCanvas>` supplies its own `pickEvery`, which read `node.pose` raw — as
|
|
4049
2803
|
did the bounds resolver feeding selection chrome and the affordance
|
|
4050
2804
|
`ChromeState`, and the marquee/lasso scan. A consumer animating nodes through
|
|
4051
2805
|
overrides painted them at one place and picked them at another. `effectivePose`
|
|
4052
2806
|
is now the single rule and every one of those reads through it.
|
|
4053
|
-
|
|
2807
|
+
|
|
4054
2808
|
**A clipped-away child was still clickable.** A container clips its subtree and
|
|
4055
2809
|
the renderer honors it, so a child outside the clip is not painted.
|
|
4056
2810
|
`useSelectTool`'s own walk has rejected those since clipping shipped; the walk
|
|
4057
2811
|
`<SceneCanvas>` installs instead had no clip term at all. The new
|
|
4058
2812
|
`passesAncestorClips` walks the parent chain per surviving candidate, so a flat
|
|
4059
2813
|
render-order scan can apply the same test.
|
|
4060
|
-
|
|
2814
|
+
|
|
4061
2815
|
**The marquee's fast-reject used the unrotated pose box.** A 100×20 rect turned
|
|
4062
2816
|
45° puts a corner 32 units above that box; a rubber-band over that corner was
|
|
4063
2817
|
rejected before the rotation-correct silhouette test ran, while a click on the
|
|
4064
2818
|
same pixel selected the shape.
|
|
4065
|
-
|
|
4066
2819
|
- c1b8511: Repaint the scene-graph side-scroller demo's world from `data.fill`. Its
|
|
4067
2820
|
tiles, coins, enemies and flagpole still declared `data.color`, the alias
|
|
4068
2821
|
removed when node paint became an object, so every one of them rendered in
|
|
4069
2822
|
the default gray — the demo whose whole point is being the visual twin of the
|
|
4070
2823
|
immediate-mode load test.
|
|
4071
2824
|
- d793d3c: Flip negates rotation; alignment guides and `gaps` distribute measure ink
|
|
4072
|
-
|
|
2825
|
+
|
|
4073
2826
|
Three paths read a pose's stored, unrotated box where the rotated extent was
|
|
4074
2827
|
wanted.
|
|
4075
|
-
|
|
2828
|
+
|
|
4076
2829
|
`flipPoseAboutBounds` carried rotation through untouched, so a mirrored shape
|
|
4077
2830
|
came back turned the same way — invisible on a rectangle, whose AABB is
|
|
4078
2831
|
symmetric under a sign flip, and plainly wrong on an asymmetric one, which
|
|
4079
2832
|
translated instead of mirroring. It now negates the pose's rotation.
|
|
4080
|
-
|
|
2833
|
+
|
|
4081
2834
|
`deriveAlignmentGuides` advertised a stationary rotated sibling's lines at its
|
|
4082
2835
|
stored edges, while the dragged selection matched against them by its ink.
|
|
4083
2836
|
`RECT_ALIGN_PROJECTION.boundsOf` now returns the rotated AABB and
|
|
4084
2837
|
`deriveAlignmentGuides` reads its targets through the same projection — a new
|
|
4085
2838
|
`projection` option defaulting to the rect one, so existing callers get the fix
|
|
4086
2839
|
without a change.
|
|
4087
|
-
|
|
2840
|
+
|
|
4088
2841
|
`useDistribute`'s `gaps` mode divided the leftover span by stored widths, so a
|
|
4089
2842
|
rotated member ended up with a gap short by the difference; `centers` shared the
|
|
4090
2843
|
line and the blind spot. Both now measure with `visualBoundsViaDescriptor`.
|
|
4091
2844
|
`distributeHorizontalAction` / `distributeVerticalAction` also take
|
|
4092
2845
|
`params.mode`, so `gaps` is reachable from a binding rather than only from the
|
|
4093
2846
|
hook.
|
|
4094
|
-
|
|
2847
|
+
|
|
4095
2848
|
Flip and distribute return different poses than before for rotated shapes.
|
|
4096
2849
|
That is the fix, but it is a behavior change for anything depending on the
|
|
4097
2850
|
old output.
|
|
4098
|
-
|
|
4099
2851
|
- 3386d64: `@weasel-js/core/routing` exports the route-string projection
|
|
4100
|
-
|
|
2852
|
+
|
|
4101
2853
|
Anything rendering a `GestureSpec` as a route string had to re-implement the
|
|
4102
2854
|
projection, and the copy in WeaselDraw's registry inspector had drifted three
|
|
4103
2855
|
ways: it answered `drop` and `paste` with no gesture name, so every binding of
|
|
4104
2856
|
either vanished from the route list; its argument lookup missed a spec field;
|
|
4105
2857
|
and it gated targets on a hand-listed set of kinds, dropping them for
|
|
4106
2858
|
`pointerDown`, `longPress` and `wheel`.
|
|
4107
|
-
|
|
2859
|
+
|
|
4108
2860
|
New from the routing subpath: `routesForSpec(spec)` — every route string one
|
|
4109
2861
|
spec declares — plus `routeGestureForSpecKind(kind)` over the single spec-kind
|
|
4110
2862
|
map, and `PREDICATE_TARGET`, which `registry.ts` already exported but the
|
|
4111
2863
|
subpath index did not, so consumers reading `RegistryEntry.target` had no way
|
|
4112
2864
|
to compare against the sentinel its own docs name.
|
|
4113
|
-
|
|
4114
2865
|
- ce2b5c7: Make the inline run grammar a parameter instead of a hardcoded branch.
|
|
4115
|
-
|
|
2866
|
+
|
|
4116
2867
|
`runsToMarkdown` and `markdownToRuns` each had the markdown subset spelled out
|
|
4117
2868
|
in their control flow — `***`/`**`/`*` and a two-character escape set — so
|
|
4118
2869
|
reading or writing any other spelling meant forking both. They now take a
|
|
4119
2870
|
`RunGrammar`: a table of markers pairing a repeated delimiter with the run
|
|
4120
2871
|
flags it toggles, defaulting to `MARKDOWN_RUN_GRAMMAR`, which is exactly
|
|
4121
2872
|
today's behavior. Escaping follows the grammar's own delimiters.
|
|
4122
|
-
|
|
2873
|
+
|
|
4123
2874
|
Nothing changes for a caller that passes no grammar. `underline` and
|
|
4124
2875
|
`strikethrough` still have no markdown spelling and are still dropped by
|
|
4125
2876
|
`runsToMarkdown` — a grammar that wants `~~struck~~` now adds one marker
|
|
4126
2877
|
rather than editing the parser.
|
|
4127
|
-
|
|
4128
2878
|
- 2ea772f: `createSelectionOutlineLayer` and `createSelectionHandlesLayer` now do what the overlay layer does
|
|
4129
|
-
|
|
2879
|
+
|
|
4130
2880
|
`createSelectionOverlayLayer` documents itself as equivalent to stacking the
|
|
4131
2881
|
other two, and it was not. It reads `ChromeState` off the draw envelope,
|
|
4132
2882
|
resolves the synthetic multi-resize id to the union AABB, honors chrome-caps
|
|
@@ -4136,27 +2886,25 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4136
2886
|
knew nothing about the multi-selection union — so a consumer who stacked them,
|
|
4137
2887
|
on the wrapper's own promise, got chrome in the wrong place with no way to
|
|
4138
2888
|
tell.
|
|
4139
|
-
|
|
2889
|
+
|
|
4140
2890
|
All three now run one body and differ only in which passes they enable, so the
|
|
4141
2891
|
promise holds by construction. `SelectionOutlineLayerOpts` and
|
|
4142
2892
|
`SelectionHandlesLayerOpts` become the overlay's option set minus the visuals
|
|
4143
2893
|
that don't apply, which makes `getSelection` and `getPose` optional on both and
|
|
4144
2894
|
adds `getOutlineIds` and `getSuppressedIds`. Handle visuals are now the named
|
|
4145
2895
|
`SelectionHandleStyle`.
|
|
4146
|
-
|
|
4147
2896
|
- 3fb3a46: Key `usePublishSelection` on the publish callback, not the context value
|
|
4148
|
-
|
|
2897
|
+
|
|
4149
2898
|
The effect depended on the whole selection-context value, and the provider
|
|
4150
2899
|
mints a new value object on every publish. So one publisher publishing refired
|
|
4151
2900
|
the effect for every other publisher in scope, each of which republished its
|
|
4152
2901
|
own ids — a newer selection got stomped back to an older one, and two
|
|
4153
2902
|
publishers holding different ids under one provider never settled at all.
|
|
4154
|
-
|
|
2903
|
+
|
|
4155
2904
|
`publishSelection` is already a stable `useCallback`, so the effect now depends
|
|
4156
2905
|
on it directly. No provider change and no API change.
|
|
4157
|
-
|
|
4158
2906
|
- 84db1f6: Close four gaps that produced wrong answers with no error
|
|
4159
|
-
|
|
2907
|
+
|
|
4160
2908
|
Three path walkers — `pathToMultiPolygon` in core and in `@weasel-js/geom`, and
|
|
4161
2909
|
`enumerateAnchors` behind the bezier-edit overlay — handled M/L/C/Q/Z with no
|
|
4162
2910
|
`default:` arm, so a command code they did not know fell out of the switch
|
|
@@ -4164,31 +2912,30 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4164
2912
|
wrong floats. They now throw, matching the six sibling walkers. This is a
|
|
4165
2913
|
behavior change for anyone feeding these a path built with an opcode outside
|
|
4166
2914
|
`PATH_COMMANDS`: what used to come back subtly wrong now raises.
|
|
4167
|
-
|
|
2915
|
+
|
|
4168
2916
|
A `<CanvasView>` built its affordance hit-test without a device profile, so a
|
|
4169
2917
|
nested view resolved fine-pointer radii even under a coarse pointer — 8px grab
|
|
4170
2918
|
zones against the 14px chrome the surface paints. It reads the profile
|
|
4171
2919
|
`<SceneCanvas>` publishes.
|
|
4172
|
-
|
|
2920
|
+
|
|
4173
2921
|
`moveGestureAdapter`'s `insertNode` took no `index`, and the adapter carried
|
|
4174
2922
|
neither `getChildren` nor `setChildOrder`, so the sibling slot a delete op
|
|
4175
2923
|
records had nowhere to land: undoing a delete through the move pipeline
|
|
4176
2924
|
appended the node to the end of its parent instead of putting it back where it
|
|
4177
2925
|
was. All three are there now.
|
|
4178
|
-
|
|
2926
|
+
|
|
4179
2927
|
The dev inspector's gesture panel formatted bindings with a private formatter
|
|
4180
2928
|
that reported only modifiers set to `true`. The `ingest` action marks every
|
|
4181
2929
|
modifier `'optional'`, so its drop and paste bindings rendered blank and the
|
|
4182
2930
|
action was invisible on both gestures. Both of the panel's plain-text
|
|
4183
2931
|
formatters now go through the kit's `routesForSpec`.
|
|
4184
|
-
|
|
4185
2932
|
- 3386d64: Undoing a multi-node delete or group restores document order
|
|
4186
|
-
|
|
2933
|
+
|
|
4187
2934
|
Restoring by stored index cannot survive replay: history runs a batch's
|
|
4188
2935
|
inverses in reverse, while indices captured before the mutation are only
|
|
4189
2936
|
correct in ascending order. Deleting `b, c, d` from `[a, b, c, d, e]` and
|
|
4190
2937
|
undoing gave `a, b, e, c, d`; Cmd+G on the same three did the same.
|
|
4191
|
-
|
|
2938
|
+
|
|
4192
2939
|
Ops now record a `Slot` — an ordinal plus the id of the following sibling at
|
|
4193
2940
|
capture. The anchor is the source of truth whenever it resolves, and it
|
|
4194
2941
|
resolves whatever else the batch has already restored. The ordinal remains as
|
|
@@ -4196,23 +2943,22 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4196
2943
|
children. `before: null` means "last" and needs no sibling list; an absent
|
|
4197
2944
|
`before` means "unobserved", and the two survive `History.serialize` because
|
|
4198
2945
|
`undefined` drops out of JSON and `null` does not.
|
|
4199
|
-
|
|
2946
|
+
|
|
4200
2947
|
The ops observe their own slot during `apply()` rather than taking one from the
|
|
4201
2948
|
caller, so every existing emitter gets this without a call-site change.
|
|
4202
2949
|
`createDeleteOp`'s `index` argument is now a seed that `apply` supersedes; its
|
|
4203
2950
|
docstring said it was sufficient on its own, which it never was.
|
|
4204
|
-
|
|
2951
|
+
|
|
4205
2952
|
Adapters without an ordering seam still append, as they did before:
|
|
4206
2953
|
`arrayAdapter` has no `setChildOrder`, and the move gesture's adapter has
|
|
4207
2954
|
neither that nor an `index` parameter on `insertNode`.
|
|
4208
|
-
|
|
4209
2955
|
- 7a746df: A stroke's dash is edited as a style, not as an array
|
|
4210
|
-
|
|
2956
|
+
|
|
4211
2957
|
`Stroke.dash` already rendered, imported and exported; it had no control,
|
|
4212
2958
|
because a `number[]` has no leaf kind. It doesn't need one — the thing a person
|
|
4213
2959
|
chooses is a style, and the array is how it is stored. The stroke block gains a
|
|
4214
2960
|
Solid / Dashed / Dotted / Custom bar under cap, join and align.
|
|
4215
|
-
|
|
2961
|
+
|
|
4216
2962
|
`ToolPrefEnum` gains `encoding`: `read`/`write` between the stored value and
|
|
4217
2963
|
the option string, the counterpart of the `unit` a number leaf already has for
|
|
4218
2964
|
a value stored in a canonical unit. Both directions are handed the object the
|
|
@@ -4224,9 +2970,8 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4224
2970
|
a new `disabled` option — one a control reports but refuses to author, since
|
|
4225
2971
|
there is no array behind it. `solid` is stored as no dash at all, and an object
|
|
4226
2972
|
leaf's field written as `undefined` is now removed rather than left holding it.
|
|
4227
|
-
|
|
4228
2973
|
- 4f19274: Cap, join and align are chosen by glyph, and the stroke block drops its labels
|
|
4229
|
-
|
|
2974
|
+
|
|
4230
2975
|
Nine option glyphs and four category glyphs join the icon set. The option
|
|
4231
2976
|
glyphs are filled silhouettes — the glyph is the ink, so a choice reads as a
|
|
4232
2977
|
shape rather than as a diagram of one. `align` is a circle zoomed until the
|
|
@@ -4234,31 +2979,29 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4234
2979
|
the box's complement of it, and `center` is the annulus straddling the path,
|
|
4235
2980
|
so the three are one band at three offsets. The categories are the bare path
|
|
4236
2981
|
each row treats, drawn in the outlined register.
|
|
4237
|
-
|
|
4238
|
-
A schema carries a glyph
|
|
2982
|
+
|
|
2983
|
+
A schema carries a glyph *id*, not a component: `ToolPrefEnum`'s options gain
|
|
4239
2984
|
`icon`, and every leaf gains one for rows whose own label is spent on a
|
|
4240
2985
|
`pair`. Core ships no icon set and cannot depend on one, so the field is a
|
|
4241
2986
|
plain string; weasel-ui resolves it against `ICON_PATHS` and falls back to
|
|
4242
2987
|
`short` where it names no glyph.
|
|
4243
|
-
|
|
2988
|
+
|
|
4244
2989
|
`SelectionPanel` now honours `block` inside an object leaf, not only at the
|
|
4245
2990
|
section level. A row whose fields are all `block` drops the 64px label column
|
|
4246
2991
|
and spans the block. The default stroke schema uses both: paint and width
|
|
4247
2992
|
share one label-less row, and cap/join/align share the next.
|
|
4248
|
-
|
|
2993
|
+
|
|
4249
2994
|
`align`'s options run inner, center, outer — the order the ink moves outward.
|
|
4250
|
-
|
|
4251
2995
|
- 94f2446: Add stroke markers — arrowheads and other line terminators as stroke style.
|
|
4252
|
-
|
|
2996
|
+
|
|
4253
2997
|
`markerStart` / `markerMid` / `markerEnd` on `Stroke` take a key resolved
|
|
4254
2998
|
through a new registry (`registerMarker`), shipping eight built-in shapes.
|
|
4255
2999
|
Unlike SVG, the stroke stops short of a filled head rather than running under
|
|
4256
3000
|
it to the tip; the distance is declared per marker, so an open V still reaches
|
|
4257
3001
|
the vertex. Round-trips through `@weasel-js/svg` as `marker-*` attributes plus
|
|
4258
3002
|
`<marker>` defs.
|
|
4259
|
-
|
|
4260
3003
|
- 07fd2de: `setStroke` takes a whole paint, so a gradient or pattern stroke is writable.
|
|
4261
|
-
|
|
3004
|
+
|
|
4262
3005
|
It accepted `{ color }` only, and merged through `paintWithColor`, which
|
|
4263
3006
|
supersedes a non-solid paint with a solid one — a gradient stroke was
|
|
4264
3007
|
unreachable even though `setStrokeOpacity` could already reach its alpha.
|
|
@@ -4266,42 +3009,40 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4266
3009
|
an earlier paint, and the stroke's width, cap, join, dash and align survive
|
|
4267
3010
|
either. New `strokeWith(paint, width?)` is `strokeOf`'s sibling for a paint
|
|
4268
3011
|
that has no color to pass.
|
|
4269
|
-
|
|
3012
|
+
|
|
4270
3013
|
Two fixes alongside it: `setFill` started with no `color` and no `paint` seeded
|
|
4271
3014
|
from `DEFAULT_STROKE_COLOR`, painting the selection black where
|
|
4272
3015
|
`setFillOpacity` seeds the same slot from `DEFAULT_FILL_COLOR`; and
|
|
4273
3016
|
`gradientForBounds`'s doc comment claimed a corner-to-corner linear gradient
|
|
4274
3017
|
where the body builds a left-edge-to-right-edge one.
|
|
4275
|
-
|
|
3018
|
+
|
|
4276
3019
|
`@weasel-js/ui` no longer exports `strokeWithColor`. It shared a name with
|
|
4277
3020
|
core's and disagreed with it — core's keeps the paint's opacity, ui's dropped
|
|
4278
3021
|
it — and nothing imported it.
|
|
4279
|
-
|
|
4280
3022
|
- 81213fc: Edit a node's stroke as the union it is
|
|
4281
|
-
|
|
3023
|
+
|
|
4282
3024
|
`data.stroke` holds `string | Stroke`, and the schema described it with a
|
|
4283
3025
|
`color` leaf — which reads `undefined` off the object form, shows its own
|
|
4284
3026
|
default, and writes a bare hex back over the stroke's width, cap, join and
|
|
4285
3027
|
dash on the first edit. The same trap `ToolPrefPaint` was introduced to avoid
|
|
4286
3028
|
for `FillStyle`.
|
|
4287
|
-
|
|
3029
|
+
|
|
4288
3030
|
A `stroke` pref kind now describes it, and `defaultNodeProperties` uses it.
|
|
4289
3031
|
Its control shows whichever color the value has — the string itself, or a
|
|
4290
3032
|
solid paint's color — gives a gradient stroke the indeterminate chip rather
|
|
4291
3033
|
than claiming a color it doesn't have, and preserves the form on write.
|
|
4292
|
-
|
|
3034
|
+
|
|
4293
3035
|
`PrefsForm` gained the `stroke` case and the `paint` case it never had; a
|
|
4294
3036
|
`paint` leaf used to render as the literal text `(paint: no renderer)`.
|
|
4295
3037
|
`solidColorOf`, `strokeColorOf`, `strokeWithColor` and `isStrokeObject` are
|
|
4296
3038
|
exported from `@weasel-js/ui` for consumers writing their own property
|
|
4297
3039
|
renderers against either union.
|
|
4298
|
-
|
|
3040
|
+
|
|
4299
3041
|
Cap, join and dash are not editable from a panel yet, and `data.strokeWidth`
|
|
4300
3042
|
remains its own leaf — see `docs/proposals/2026-08-26-node-stroke-union.md`
|
|
4301
3043
|
for why that waits on the SVG mapping.
|
|
4302
|
-
|
|
4303
3044
|
- 2f225d7: A thick stroke is clickable across its whole width
|
|
4304
|
-
|
|
3045
|
+
|
|
4305
3046
|
`shapeCoversPoint` grants a grab out to a stroke's outward reach — a full
|
|
4306
3047
|
stroke width for an `outer` align — but the AABB pre-filter that runs before it
|
|
4307
3048
|
grew only by the pointer slop. So half a thick outer stroke's ink was
|
|
@@ -4310,25 +3051,24 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4310
3051
|
was at least as generous as the refinement, which it cannot be on its own,
|
|
4311
3052
|
since it never sees the stroke. That budget is the caller's, and the comment
|
|
4312
3053
|
says so now.
|
|
4313
|
-
|
|
3054
|
+
|
|
4314
3055
|
`ShapeCoversPointOptions.scale` was never passed either, so a stroke width
|
|
4315
3056
|
declared in `px` resolved as world units and the reach was wrong at every zoom
|
|
4316
3057
|
but 1 — while the caller computed `meanScale(view.scale)` one line above.
|
|
4317
|
-
|
|
4318
3058
|
- 68069dc: Right-to-left text lays out in visual order
|
|
4319
|
-
|
|
3059
|
+
|
|
4320
3060
|
`LayoutRunsOpts` takes an optional `bidi` engine. Given one, `layoutRuns`
|
|
4321
3061
|
analyses the paragraph, reorders each line after the wrap, and mirrors brackets
|
|
4322
3062
|
in right-to-left runs. Given none, nothing changes: text lays out logically,
|
|
4323
3063
|
exactly as before.
|
|
4324
|
-
|
|
3064
|
+
|
|
4325
3065
|
`@weasel-js/text` declares the `BidiResolver` interface and does not depend on
|
|
4326
3066
|
`@weasel-js/bidi` — the dependency runs the other way from the usual, so a
|
|
4327
3067
|
consumer who renders no right-to-left text never installs the Unicode tables,
|
|
4328
3068
|
and a different implementation can be substituted. `@weasel-js/bidi` is a
|
|
4329
3069
|
devDependency here only, for a test that drives real Hebrew through the real
|
|
4330
3070
|
engine; types lining up is not evidence the semantics do.
|
|
4331
|
-
|
|
3071
|
+
|
|
4332
3072
|
`LaidOutCell` gains `advance` and `level`, and **`x` is no longer monotonic
|
|
4333
3073
|
across `cells`**. Cells stay in logical order — slot `i` is still character `i`
|
|
4334
3074
|
— while their x values follow the reordering. Sort on `x` for visual order, and
|
|
@@ -4336,26 +3076,25 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4336
3076
|
cell's `x`. Hit-testing was doing exactly that and now sweeps in visual order
|
|
4337
3077
|
against each cell's own extent, taking a right-to-left cell's visually-leading
|
|
4338
3078
|
half as the character's logical end.
|
|
4339
|
-
|
|
3079
|
+
|
|
4340
3080
|
Kerning is a gap between two adjacent characters, and the wrap measures it
|
|
4341
3081
|
logically. Reordering can put a different pair side by side, so the gap taken
|
|
4342
3082
|
is the one belonging to whichever of the two is logically second, and none at
|
|
4343
3083
|
all across a direction boundary — where the pair never touched in the source.
|
|
4344
|
-
|
|
3084
|
+
|
|
4345
3085
|
Laying out right-to-left text with no engine now warns once, naming the import.
|
|
4346
3086
|
The alternative is glyphs silently appearing reversed, which is the one real
|
|
4347
3087
|
hazard of making this opt-in.
|
|
4348
|
-
|
|
4349
3088
|
- 5d0ff9c: Every code point on a line gets a cell
|
|
4350
|
-
|
|
3089
|
+
|
|
4351
3090
|
`LaidOutLineBox` replaces its `caretXs` / `caretIndices` pair with
|
|
4352
3091
|
`cells: LaidOutCell[]` plus a `srcEnd` closing offset. A cell carries
|
|
4353
3092
|
`srcIndex`, `srcEnd`, `cp`, `x` and `drawsInk`, so slot `i` is `cells[i]` and
|
|
4354
3093
|
a consumer indexing per character no longer has to reconcile a sparse array
|
|
4355
3094
|
against the source string.
|
|
4356
|
-
|
|
3095
|
+
|
|
4357
3096
|
The old arrays were documented as non-contiguous, and two causes were real:
|
|
4358
|
-
|
|
3097
|
+
|
|
4359
3098
|
- A code point no tier could serve was dropped outright, taking its caret stop
|
|
4360
3099
|
with it. It now occupies a zero-advance cell. This is reachable whenever the
|
|
4361
3100
|
dynamic canvas fallback is off — which is the normal configuration for a
|
|
@@ -4364,34 +3103,33 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4364
3103
|
- A space opening a line — at the start of the text, or after a newline — was
|
|
4365
3104
|
discarded. It now keeps its cell and still consumes no width, so a line is
|
|
4366
3105
|
addressable per character without gaining an indent. A space that opens a
|
|
4367
|
-
|
|
3106
|
+
*wrapped* line was never affected: the wrap leaves it as a trailing cell on
|
|
4368
3107
|
the line before.
|
|
4369
|
-
|
|
3108
|
+
|
|
4370
3109
|
Neither changes any geometry: both cells carry zero advance, zero tracking and
|
|
4371
3110
|
no kerning, so bounds, line widths and glyph positions are unchanged.
|
|
4372
|
-
|
|
3111
|
+
|
|
4373
3112
|
A newline still has no cell, since it separates cells rather than being one.
|
|
4374
3113
|
`srcEnd` is what a blank line carries in its place.
|
|
4375
|
-
|
|
3114
|
+
|
|
4376
3115
|
`drawsInk` is a property of the code point and the face, not of the call that
|
|
4377
3116
|
produced it: it does not flip when a dynamic bake lands or the outline
|
|
4378
3117
|
threshold is crossed, so the same text reports the same slots every time. A
|
|
4379
3118
|
zero-advance combining mark is `true` — it inks without advancing.
|
|
4380
|
-
|
|
4381
3119
|
- c1b8511: **Breaking:** paint leaves `TextStyle`. A text node's color and outline are
|
|
4382
3120
|
`data.fill` and `data.stroke` — the same two leaves every other node kind
|
|
4383
3121
|
paints from — and `TextStyle` holds typography only. `TextStyle.fill` and
|
|
4384
3122
|
`TextStyle.stroke` are gone, with no compatibility read: a document that put
|
|
4385
3123
|
its color in `style.fill` now renders in the default black rather than
|
|
4386
3124
|
erroring, so check documents that predate this.
|
|
4387
|
-
|
|
3125
|
+
|
|
4388
3126
|
This fixes a real asymmetry rather than only moving fields. `data.stroke`
|
|
4389
3127
|
already reached text through a fold in the painter, but `data.fill` did not:
|
|
4390
3128
|
picking a fill color with a text node selected wrote a field nothing read, so
|
|
4391
3129
|
the canvas did not change. `setFill`, `setFillOpacity`, the opacity scrub and
|
|
4392
3130
|
the Appearance leaf now all mean the same thing on text as on a rect. The
|
|
4393
3131
|
duplicate `data.style.fill` control is gone from the text schema with them.
|
|
4394
|
-
|
|
3132
|
+
|
|
4395
3133
|
`resolveTextStyle(style, paint)` takes the node's paint as a second argument
|
|
4396
3134
|
and is what derives the caret and selection colors, so the edit overlay
|
|
4397
3135
|
matches the glyphs it sits on; `useTextEdit` gained a `getPaint` option for
|
|
@@ -4402,105 +3140,100 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4402
3140
|
style. `StyledRun.fill` and `.stroke` are unchanged and still override the
|
|
4403
3141
|
node's per range — which is also where a caller with no node at all, a HUD
|
|
4404
3142
|
widget or a debug overlay, now states its color.
|
|
4405
|
-
|
|
3143
|
+
|
|
4406
3144
|
`textCommandFromRuns` is exported from the package root.
|
|
4407
|
-
|
|
4408
3145
|
- 546f67d: Draw text from a ring of reused vertex buffers instead of minting a vertex
|
|
4409
3146
|
array and two buffers per draw. `drawTextGroup` and `drawTextDecorations` were
|
|
4410
3147
|
the last paths still doing what `drawImage` stopped doing; text now costs
|
|
4411
3148
|
**3.3 us/command, down from 6.65** at 512 commands a frame on an M2 Max via
|
|
4412
3149
|
ANGLE (`tests/perf/transition-matrix.spec.ts`), which puts it level with an
|
|
4413
3150
|
image draw. No other command kind moved.
|
|
4414
|
-
|
|
3151
|
+
|
|
4415
3152
|
A text group is as many quads as it has glyphs, so unlike the image ring a
|
|
4416
3153
|
slot's buffer grows to the largest run it has seen rather than being fixed at
|
|
4417
3154
|
four vertices. The quad index pattern is a pure function of the quad count —
|
|
4418
3155
|
the pattern for N quads is a prefix of the pattern for any larger N — so one
|
|
4419
3156
|
index buffer serves every slot, grown the same way and written only when it
|
|
4420
3157
|
grows.
|
|
4421
|
-
|
|
4422
3158
|
- c2ffa49: Alignment can resolve against reading direction
|
|
4423
|
-
|
|
3159
|
+
|
|
4424
3160
|
`align` gains `start` and `end` alongside `left` / `center` / `right`, and
|
|
4425
3161
|
`TextStyle` gains `direction: 'ltr' | 'rtl'`. The split is CSS `text-align`'s:
|
|
4426
3162
|
the relative pair resolves against the direction, the absolute pair ignores it.
|
|
4427
3163
|
`resolveAlign(align, direction)` collapses one to the other and is exported for
|
|
4428
3164
|
consumers that need an edge rather than an intent.
|
|
4429
|
-
|
|
3165
|
+
|
|
4430
3166
|
Direction is an input, not something this package discovers. `@weasel-js/text`
|
|
4431
3167
|
has no DOM, so a consumer that reads `getComputedStyle(box).direction` passes
|
|
4432
3168
|
what it found; nothing here sniffs an environment.
|
|
4433
|
-
|
|
3169
|
+
|
|
4434
3170
|
Defaults are unchanged — `align: 'left'`, `direction: 'ltr'` — so no existing
|
|
4435
3171
|
layout moves. Making `start` the default alignment is a separate call.
|
|
4436
|
-
|
|
3172
|
+
|
|
4437
3173
|
`@weasel-js/svg` carries the direction through: `direction` joins the
|
|
4438
3174
|
inheritable presentation properties, and `text-anchor` is now written and read
|
|
4439
3175
|
against it. Two things were wrong before and are worth naming, because both
|
|
4440
3176
|
rendered plausible output:
|
|
4441
|
-
|
|
3177
|
+
|
|
4442
3178
|
- `align: 'start'` serialized to `text-anchor="end"` — the opposite edge — via
|
|
4443
3179
|
a mapping that assumed three values and read the fourth as its `else`.
|
|
4444
3180
|
- SVG's initial `text-anchor` is `start`, which under `direction="rtl"` is the
|
|
4445
3181
|
right edge, while this model's default `align` is `left`. They agree under
|
|
4446
3182
|
`ltr` and only there, so an RTL document with no explicit anchor imported as
|
|
4447
3183
|
left-aligned.
|
|
4448
|
-
|
|
3184
|
+
|
|
4449
3185
|
This is alignment and round-tripping only. Layout still walks code points in
|
|
4450
3186
|
logical order with the pen always increasing: there is no bidi reordering and
|
|
4451
3187
|
no shaping, so a Hebrew or Arabic string aligns to the correct edge and still
|
|
4452
3188
|
renders in logical order, and Arabic still renders unjoined.
|
|
4453
|
-
|
|
4454
3189
|
- 4c097ef: Sit every run on a line on one baseline
|
|
4455
|
-
|
|
4456
|
-
Mixed-size text hung each run off the
|
|
3190
|
+
|
|
3191
|
+
Mixed-size text hung each run off the *line top* at its own ascent instead of
|
|
4457
3192
|
off a shared baseline, so a 16-unit run beside a 40-unit run floated up level
|
|
4458
3193
|
with the big run's cap rather than standing on the line with it. Two faces with
|
|
4459
3194
|
different ascents at the same size diverged the same way. Baseline alignment is
|
|
4460
3195
|
what inline text does everywhere else, and the module header already claimed
|
|
4461
3196
|
this behavior — the walk just never implemented it.
|
|
4462
|
-
|
|
3197
|
+
|
|
4463
3198
|
A line now sinks one baseline far enough to clear its tallest run's ascent and
|
|
4464
3199
|
places every glyph against it. Glyph quads derive their top from that baseline
|
|
4465
3200
|
rather than from the pen's line top, which is the whole of the change:
|
|
4466
3201
|
`qy0 = baselineY + (yoffset - metrics.base) * scale`.
|
|
4467
|
-
|
|
3202
|
+
|
|
4468
3203
|
Uniform-size text — nearly all text — is unchanged, since the maximum over one
|
|
4469
3204
|
value is that value. Only lines that actually mix sizes or faces move, and they
|
|
4470
3205
|
move to where they always should have been.
|
|
4471
|
-
|
|
3206
|
+
|
|
4472
3207
|
The test named "mixed-size runs share a baseline on the same line" asserted only
|
|
4473
3208
|
a quad count and passed throughout; it now asserts the baselines.
|
|
4474
|
-
|
|
4475
3209
|
- 2b86e00: A text node's style is one value, not ten sibling paths
|
|
4476
|
-
|
|
3210
|
+
|
|
4477
3211
|
`data.style.fontSize`, `.fontWeight`, `.align` and the rest addressed into one
|
|
4478
3212
|
`TextStyle` from ten independent leaves, each control writing a field of a
|
|
4479
3213
|
value it could only half see. `data.style` is an object leaf now, with
|
|
4480
3214
|
Character and Paragraph as groups inside it — groups head their fields and
|
|
4481
3215
|
contribute nothing to the path, so a field is still a field of the style and
|
|
4482
3216
|
one commit writes the whole thing.
|
|
4483
|
-
|
|
3217
|
+
|
|
4484
3218
|
An object leaf whose fields are entirely grouped no longer prints its own
|
|
4485
3219
|
heading, which would stack straight onto the first group's, and a group's
|
|
4486
3220
|
fields sit under a rule so the nesting reads. WeaselDraw's inspector descends
|
|
4487
3221
|
into an object leaf when listing what a kind exposes — the fields are the
|
|
4488
3222
|
editable surface; the leaf is the container.
|
|
4489
|
-
|
|
3223
|
+
|
|
4490
3224
|
`SelectionPanel` has a story now, which is how the two layout defects above
|
|
4491
3225
|
were found.
|
|
4492
|
-
|
|
4493
3226
|
- d933a89: Superscript, subscript and overline for styled runs
|
|
4494
|
-
|
|
3227
|
+
|
|
4495
3228
|
`StyledRun` gains `script: 'super' | 'sub'` — a raised or lowered baseline and
|
|
4496
3229
|
a smaller size together, the pair `<sup>` and `<sub>` imply. It is a preset
|
|
4497
3230
|
over two new primitives rather than a mechanism of its own:
|
|
4498
|
-
|
|
3231
|
+
|
|
4499
3232
|
- `baselineShift` — raise (positive) or lower (negative) a run off the line's
|
|
4500
3233
|
shared baseline, in ems of the inherited font size.
|
|
4501
3234
|
- `fontScale` — a multiplier on the inherited font size, the relative
|
|
4502
3235
|
counterpart to `fontSize`. An absolute `fontSize` still wins over it.
|
|
4503
|
-
|
|
3236
|
+
|
|
4504
3237
|
Naming either directly overrides that half of `script` and leaves the other
|
|
4505
3238
|
alone. The preset's numbers are exported as `SCRIPT_METRICS` (58.3% size,
|
|
4506
3239
|
±33.3% position — Adobe's defaults, so a character panel can show percentages
|
|
@@ -4508,28 +3241,27 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4508
3241
|
carries real `ySuperscript*` metrics but the baked atlas tier has no slot for
|
|
4509
3242
|
them, and metrics that applied on one glyph tier and not the other would
|
|
4510
3243
|
reflow text as it crossed the size threshold.
|
|
4511
|
-
|
|
3244
|
+
|
|
4512
3245
|
`resolveRuns` folds all of it into one world-unit `baselineShift` and a final
|
|
4513
3246
|
`fontSize`, so layout never learns superscripts exist — it places a run against
|
|
4514
3247
|
a baseline and an offset. The shift moves a run's glyphs, its outline geometry
|
|
4515
3248
|
and its own decoration rules together, and deliberately does not feed back into
|
|
4516
3249
|
the line's baseline or height: a superscript rides the line rather than
|
|
4517
3250
|
reflowing it.
|
|
4518
|
-
|
|
3251
|
+
|
|
4519
3252
|
`overline` joins `underline` and `strikethrough` on both `TextStyle` and
|
|
4520
3253
|
`StyledRun`, additive over the node style like the other two, and is now
|
|
4521
3254
|
available to a custom `RunGrammar` as a `RunFlag`. The default markdown grammar
|
|
4522
3255
|
is unchanged — it stays silent on the decorations, as it always has been.
|
|
4523
|
-
|
|
4524
3256
|
- 5923c8b: `Animator.tween` no longer fires `onDone` for a tween that was cancelled during
|
|
4525
3257
|
its own final `onTick`. The last tick emitted the value and completed in one
|
|
4526
3258
|
pass, so a write made from that tick — cancelling the tween — still got the
|
|
4527
3259
|
completion callback, against the documented "not called on cancel" contract.
|
|
4528
3260
|
- 2ea772f: Undo of a delete restores the subtree; undo of a group restores the slot
|
|
4529
|
-
|
|
3261
|
+
|
|
4530
3262
|
Two ops inverted to something narrower than what they applied, so undo
|
|
4531
3263
|
silently lost data.
|
|
4532
|
-
|
|
3264
|
+
|
|
4533
3265
|
`createDeleteOp.invert()` re-inserted a single node while `apply()` called
|
|
4534
3266
|
`removeNode`, which cascades the whole subtree. Delete a container with two
|
|
4535
3267
|
children, undo, and the container came back with `children: []` while both
|
|
@@ -4539,7 +3271,7 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4539
3271
|
re-inserts each descendant at its captured slot. A flat adapter's `removeNode`
|
|
4540
3272
|
does not cascade, so the inverse skips any descendant the adapter still reports
|
|
4541
3273
|
as live rather than duplicating it.
|
|
4542
|
-
|
|
3274
|
+
|
|
4543
3275
|
`createReparentOp` carried only the parent ids, so undoing a Cmd+G appended
|
|
4544
3276
|
instead of restoring the sibling slot and paint order changed. `ReparentArgs`
|
|
4545
3277
|
now carries `fromIndex` / `toIndex` and places through the existing
|
|
@@ -4547,39 +3279,42 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4547
3279
|
`setParent`'s signature is unchanged. Adapters without that seam no-op as
|
|
4548
3280
|
before. `groupAction` captures each member's index before mutating; `move` and
|
|
4549
3281
|
`snapToContainer` pass none and are byte-identical.
|
|
4550
|
-
|
|
3282
|
+
|
|
4551
3283
|
`ops/delete.test.ts` stubbed `removeNode` as a one-id delete that did not
|
|
4552
3284
|
cascade, which is why nothing caught the first bug. It now runs against a
|
|
4553
3285
|
tree-backed fake.
|
|
4554
|
-
|
|
4555
3286
|
- 2ea772f: Selection chrome, gesture bounds and SVG export fold rotated ink, not pose boxes
|
|
4556
|
-
|
|
4557
|
-
Every union a user looks at or clicks folded each member's
|
|
3287
|
+
|
|
3288
|
+
Every union a user looks at or clicks folded each member's *unrotated* box.
|
|
4558
3289
|
Select two shapes, rotate one, and the multi-selection frame and its handles
|
|
4559
3290
|
sat inside the rotated shape's ink — affordances hand `ChromeState.unionBounds`
|
|
4560
|
-
out as the target bounds for paint
|
|
3291
|
+
out as the target bounds for paint *and* hit-test, so the handles were both
|
|
4561
3292
|
drawn and grabbable in the wrong place, while `getGestureBounds()` reported the
|
|
4562
3293
|
correct larger box.
|
|
4563
|
-
|
|
3294
|
+
|
|
4564
3295
|
`unionAABB` expands each rotated member via `axisAlignedBounds` before folding
|
|
4565
3296
|
and is now the one implementation. It lives in `core/geometry/unionBounds.ts`
|
|
4566
3297
|
beside the rotation-free `unionBounds`, which stays correct for commit-time
|
|
4567
3298
|
actions that write poses back in the unrotated frame; the module says which to
|
|
4568
3299
|
reach for. `unionGestureBounds` is **removed** — it was `unionAABB` under
|
|
4569
3300
|
another name. Both new functions are exported from the package root.
|
|
4570
|
-
|
|
3301
|
+
|
|
4571
3302
|
Moved onto it: `ChromeState.unionBounds`, the selection overlay's
|
|
4572
3303
|
container-to-leaves resolver, the multi-rotate pivot (which put the pivot in
|
|
4573
3304
|
the wrong place whenever a member was rotated), and WeaselDraw's export
|
|
4574
3305
|
viewBox, which clipped rotated shapes out of the copied SVG.
|
|
4575
|
-
|
|
4576
3306
|
- 3fb3a46: Warn in dev when `useAction` finds no `ActionsProvider`
|
|
4577
|
-
|
|
3307
|
+
|
|
4578
3308
|
`useAction` returned early on a null registry, so an action registered above
|
|
4579
3309
|
the provider — or with no provider mounted — silently never fired its
|
|
4580
3310
|
bindings. It now warns in dev, naming the action id. Runtime behavior in
|
|
4581
3311
|
production builds is unchanged.
|
|
4582
|
-
|
|
3312
|
+
- Updated dependencies [5c8e9e6]
|
|
3313
|
+
- Updated dependencies [2621cbf]
|
|
3314
|
+
- Updated dependencies [0f936da]
|
|
3315
|
+
- Updated dependencies [4180095]
|
|
3316
|
+
- Updated dependencies [9977908]
|
|
3317
|
+
- Updated dependencies [52c7b2a]
|
|
4583
3318
|
- Updated dependencies [3386d64]
|
|
4584
3319
|
- Updated dependencies [c6c499d]
|
|
4585
3320
|
- Updated dependencies [20097e6]
|
|
@@ -4591,13 +3326,13 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4591
3326
|
- Updated dependencies [c2ffa49]
|
|
4592
3327
|
- Updated dependencies [4c097ef]
|
|
4593
3328
|
- Updated dependencies [d933a89]
|
|
4594
|
-
- @weasel-js/
|
|
4595
|
-
- @weasel-js/
|
|
4596
|
-
- @weasel-js/
|
|
4597
|
-
- @weasel-js/
|
|
4598
|
-
- @weasel-js/
|
|
4599
|
-
- @weasel-js/
|
|
4600
|
-
- @weasel-js/
|
|
3329
|
+
- @weasel-js/text@1.3.0
|
|
3330
|
+
- @weasel-js/geom@1.3.0
|
|
3331
|
+
- @weasel-js/font@1.3.0
|
|
3332
|
+
- @weasel-js/gestures@1.3.0
|
|
3333
|
+
- @weasel-js/history@1.3.0
|
|
3334
|
+
- @weasel-js/modes@1.3.0
|
|
3335
|
+
- @weasel-js/paint@1.3.0
|
|
4601
3336
|
|
|
4602
3337
|
## 1.2.0
|
|
4603
3338
|
|