@weasel-js/core 1.4.3 → 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 +1299 -2197
- package/README.md +118 -75
- package/dist/{pointSnapToGrid-D1bRdq-j.d.ts → autoPoseDescriptor-CvjflWJK.d.ts} +35 -17
- package/dist/{chunk-ORZNKVXM.js → chunk-BDWAA634.js} +4 -22
- package/dist/chunk-BDWAA634.js.map +1 -0
- package/dist/{chunk-WYPSGIYS.js → chunk-MG7OXCAI.js} +4149 -5316
- package/dist/chunk-MG7OXCAI.js.map +1 -0
- package/dist/{chunk-BLJNRMKB.js → chunk-MQI4PIX3.js} +3 -3
- package/dist/chunk-MQI4PIX3.js.map +1 -0
- package/dist/{chunk-SBFC6J3C.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-Bw8cSde-.d.ts → grid-BrIa38gG.d.ts} +7 -23
- package/dist/index.d.ts +2134 -1210
- 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-DYfUlZxk.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 -11
- 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-BJqsTlXl.d.ts → types-DMyo7dnM.d.ts} +12 -41
- package/dist/{types-CoVTbo_y.d.ts → types-DtjCJA5r.d.ts} +10 -9
- package/package.json +13 -10
- package/dist/DrawCommand-CD-ug3d9.d.ts +0 -332
- package/dist/builtins-CtGORLCG.d.ts +0 -740
- package/dist/chunk-BLJNRMKB.js.map +0 -1
- package/dist/chunk-LEURURX3.js +0 -561
- package/dist/chunk-LEURURX3.js.map +0 -1
- package/dist/chunk-ORZNKVXM.js.map +0 -1
- package/dist/chunk-SBFC6J3C.js.map +0 -1
- package/dist/chunk-WYPSGIYS.js.map +0 -1
- package/dist/geometry-CHR36Ub_.d.ts +0 -114
- package/dist/path-JEV2c5If.d.ts +0 -48
- package/dist/registry-DnoTWGED.d.ts +0 -4003
- package/dist/types-BHK2dkMu.d.ts +0 -172
- package/dist/types-H7o6MaPo.d.ts +0 -512
- package/dist/view-DSQgxBJB.d.ts +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,953 @@
|
|
|
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
|
+
|
|
584
|
+
## 1.4.4
|
|
585
|
+
|
|
586
|
+
### Patch Changes
|
|
587
|
+
|
|
588
|
+
- 9ce6f00: Fix solid fills coming out multiplied by whatever bitmap shared their batch.
|
|
589
|
+
A solid vertex carried the UV of a 1x1 white texel, but the flush bound the
|
|
590
|
+
run's *image*, so any solid staged alongside an image quad sampled that bitmap
|
|
591
|
+
at its middle texel instead. A wall of thumbnails is a ground rect under an
|
|
592
|
+
atlas quad per cell — the shape the merged batch exists for — so its grounds
|
|
593
|
+
came out tinted by the atlas: white drew olive. Reported from outside the repo
|
|
594
|
+
against a canvas2d reference; no visual baseline caught it, because in every
|
|
595
|
+
demo the quad covers its ground.
|
|
596
|
+
|
|
597
|
+
Every batch vertex now names the texture slot it samples. Slot 0 is always the
|
|
598
|
+
white texel, so a solid's `texture() * a_vertexColor` is the vertex color
|
|
599
|
+
exactly whatever else joins its run, and bitmaps take the slots above it.
|
|
600
|
+
|
|
601
|
+
That also lets one run hold up to seven distinct bitmaps. A document with a
|
|
602
|
+
handful of loose images used to break its run on every change of bitmap; now it
|
|
603
|
+
breaks only when the slots run out, or when one bitmap is drawn at two
|
|
604
|
+
MAG_FILTERs, which is state on the texture object and cannot be had both ways
|
|
605
|
+
in one draw.
|
|
606
|
+
|
|
607
|
+
`tests/visual/batch-pixels.spec.ts` reads the framebuffer channel by channel
|
|
608
|
+
rather than diffing a screenshot, which is what it takes to see a run's
|
|
609
|
+
composition at all.
|
|
610
|
+
- 6f876a7: Two canvases under one `DepRegistryProvider` no longer take each other's deps
|
|
611
|
+
down.
|
|
612
|
+
|
|
613
|
+
`register` set one source per name and its release did a bare `delete`. Mount a
|
|
614
|
+
second canvas under a shared provider and it displaced the first's `view` /
|
|
615
|
+
`scene` / `selection`; unmount either one and the name went dark for the canvas
|
|
616
|
+
still on screen.
|
|
617
|
+
|
|
618
|
+
Sources now stack per name, newest live, and a release removes its own entry
|
|
619
|
+
wherever it sits — so a displaced source comes back when the one above it
|
|
620
|
+
leaves, and a displaced source leaving disturbs nothing. Same shape as the
|
|
621
|
+
`ActionsProvider` registrant stack, one layer down.
|
|
622
|
+
- ed400a3: A derivation now receives its dependencies as `{ node, pose }` rather than as
|
|
623
|
+
bare poses. Breaking for anything with a `derivePath` or `derivePose`: read
|
|
624
|
+
`deps[0]?.pose` where it read `deps[0]`.
|
|
625
|
+
|
|
626
|
+
A connector legitimately reads more than a box — one that thickens with its
|
|
627
|
+
endpoint's weight, or routes only to nodes on a given layer, is answering off
|
|
628
|
+
`data` and `layer`. The scene has invalidated dependents on `kit:setData` and
|
|
629
|
+
`kit:setLayer` since those landed, and `scene.ts` said in a comment that a
|
|
630
|
+
derivation "is handed its dependencies' nodes, not only their poses". It was
|
|
631
|
+
not: the invalidation was paying for a read the signature could not perform.
|
|
632
|
+
|
|
633
|
+
`scenePoseLookup` is now `sceneDepLookup` and answers `DerivedDep` for the same
|
|
634
|
+
reason. `DerivedDep` is exported.
|
|
635
|
+
- fc00dae: Hand a derivation its dependencies' paths, and label an edge with one.
|
|
636
|
+
|
|
637
|
+
`DerivedDep` is now `{ node, pose, path }`. The path resolves on first read and
|
|
638
|
+
memoizes, so a route costs the same whether one node reads it or five, and a
|
|
639
|
+
dependency nobody asks about costs nothing. `resolveDerivedPath` moves beside
|
|
640
|
+
`derivedPose` in `core/scene` — a pose can now derive from a dependency's path —
|
|
641
|
+
and picks up the cycle guard the pose side already had.
|
|
642
|
+
|
|
643
|
+
`pointAlongPath(path, t)` is the new geometry primitive underneath: where a path
|
|
644
|
+
is at a fraction of its length, and which way it heads there, measured along the
|
|
645
|
+
flattened arc.
|
|
646
|
+
|
|
647
|
+
In `@weasel-js/diagram`, an edge label is an ordinary leaf node with
|
|
648
|
+
`dependsOn: [edge]` and `LABEL_DERIVE_POSE`. Its trait says where it sits —
|
|
649
|
+
`at: 'start' | 'mid' | 'end'` or a fraction, plus an `offset` perpendicular to
|
|
650
|
+
the route — and it reads the edge's resolved path rather than routing again, so
|
|
651
|
+
a label and its arrowhead can never disagree about where the edge went.
|
|
652
|
+
- 730da55: A derived node now recomputes when a dependency's `data` or `layer` changes.
|
|
653
|
+
|
|
654
|
+
`derivePath` is handed its dependencies' nodes, not only their poses — so a
|
|
655
|
+
connector that thickens with a node's weight, or draws only for nodes on a given
|
|
656
|
+
layer, is answering off `data` and `layer`. `kit:setPose` and `kit:move`
|
|
657
|
+
invalidated the dependent's memo; `kit:setData` and `kit:setLayer` did not, so
|
|
658
|
+
the derived geometry kept the old answer with nothing on screen to show it was
|
|
659
|
+
stale. Undo and redo were wrong the same way.
|
|
660
|
+
|
|
661
|
+
Both ops now invalidate dependents on `apply` and on `revert`, matching the two
|
|
662
|
+
that already did.
|
|
663
|
+
- 60ba9d9: A stroke marker on a derived path is drawn.
|
|
664
|
+
|
|
665
|
+
`markerStart` / `markerMid` / `markerEnd` reached the `kit:derived` painter
|
|
666
|
+
intact and were then dropped: the painter emitted its stroke command and
|
|
667
|
+
returned, where `kit:path` follows with a marker pass. So an arrowhead on a
|
|
668
|
+
diagram edge — the whole reason markers and derived geometry landed in the same
|
|
669
|
+
release — silently drew nothing. Its own `ink` had been reserving the hit-test
|
|
670
|
+
reach for the marker all along, which is the shape of the bug: the pointer could
|
|
671
|
+
already grab past the end of a line with no head on it.
|
|
672
|
+
|
|
673
|
+
A connect-authored edge now carries `markerEnd: 'arrow'` by default. An edge
|
|
674
|
+
runs *from* one node *to* another and a plain line does not say so;
|
|
675
|
+
`DEFAULT_EDGE_STROKE` is exported for a consumer overriding `commit` who wants
|
|
676
|
+
the rest of it.
|
|
677
|
+
- 5732951: The minimap's scene fit and the scene text editor read a derived pose rather
|
|
678
|
+
than the placeholder a derived node authors. `computeFitView`'s `'scene'` fit
|
|
679
|
+
frames on `documentPose`, so a derived node is framed where it actually is
|
|
680
|
+
while a drag still leaves the framing alone; `useSceneTextEdit` resolves both
|
|
681
|
+
its double-click hit test and the overlay's own projection through
|
|
682
|
+
`effectivePose`, so double-clicking a derived label opens the editor on it and
|
|
683
|
+
the box lands on the text.
|
|
684
|
+
|
|
685
|
+
Removes `UseMoveOptions.cascadeWorldPose`. Nothing has read it since the move
|
|
686
|
+
action started walking `scene.childrenOf` for its own cascade — it was a
|
|
687
|
+
documented option that silently did nothing, and two doc comments described the
|
|
688
|
+
behavior it used to drive.
|
|
689
|
+
- 2ff4824: A scene node can now derive its **pose** from its dependencies, the way it
|
|
690
|
+
already derived its path. New API; a group's bounds change behavior.
|
|
691
|
+
|
|
692
|
+
`Node.derivePose` takes the same `dependsOn` list as `derivePath`, serializes
|
|
693
|
+
through `SceneRegistry.derivePose` by key, and rides the same push
|
|
694
|
+
invalidation. Where a derived path is resolved at paint time and reaches only
|
|
695
|
+
the painter, a derived pose is what the node *is* at — it feeds bounds,
|
|
696
|
+
hit-testing, selection chrome, snapping and layout.
|
|
697
|
+
|
|
698
|
+
`dependsOn` gains a second form, `'children'`: "my own children, in child
|
|
699
|
+
order", which a fixed id list cannot express because reparenting would have to
|
|
700
|
+
maintain it. The two forms differ in lifetime as well as membership — deleting
|
|
701
|
+
a node still deletes everything that names it in `dependsOn`, but a container
|
|
702
|
+
outlives the children it derives from, because an emptied group is still a
|
|
703
|
+
group.
|
|
704
|
+
|
|
705
|
+
`groupAction` uses it, which fixes the group-bounds defect: a container's union
|
|
706
|
+
AABB was computed once at creation and never re-derived, so moving a member
|
|
707
|
+
left the group's bounds, selection chrome and hit area behind. The kit
|
|
708
|
+
registers the union function under `kit:unionOfChildren` and merges its own
|
|
709
|
+
registry entries under the consumer's, so a grouped document round-trips
|
|
710
|
+
through `toJSON` in any scene.
|
|
711
|
+
|
|
712
|
+
`effectivePose(scene, node)` is the one rule — override, else derived, else
|
|
713
|
+
authored — and now takes the scene rather than the override table alone. The
|
|
714
|
+
three render walks, the pick walk, and the scene, commit and gesture adapters
|
|
715
|
+
all resolve through it or through `documentPose`, the same answer minus the
|
|
716
|
+
override step for a reader that must not see an in-flight gesture. Both are
|
|
717
|
+
exported.
|
|
718
|
+
|
|
719
|
+
`clipFromPose`, `derivePath` and `derivePose` now share one table-driven
|
|
720
|
+
serialization path (`core/scene/nodeFnFields.ts`) instead of a copy per field.
|
|
721
|
+
- 3d89141: Layout: `layered`, `tree` and `force`, and the action that runs one.
|
|
722
|
+
|
|
723
|
+
A layout is a plain function of the graph — no scene, no ops, no history. It
|
|
724
|
+
hands back the new top-left for every node that **moves**, and a node already
|
|
725
|
+
standing where the layout wants it is absent from the answer, so re-running a
|
|
726
|
+
layout on an arrangement it produced writes nothing and pushes no undo entry.
|
|
727
|
+
|
|
728
|
+
Three rules keep a re-layout from scrambling a diagram someone has arranged.
|
|
729
|
+
There is no RNG anywhere in the path, so the same graph always lays out the same
|
|
730
|
+
way. Within-rank order is seeded from where the nodes already sit on the cross
|
|
731
|
+
axis rather than from crossing-minimization, so two branches an author dragged
|
|
732
|
+
into an order come back in it. And a node carrying `pinned: true` never moves,
|
|
733
|
+
with the rest of the layout translated to sit around it — with no pin, the
|
|
734
|
+
layout lands on the diagram's own bounding box rather than at the origin.
|
|
735
|
+
|
|
736
|
+
`layered` ranks by longest path, breaking cycles with a depth-first walk in node
|
|
737
|
+
order so a loop draws as an edge running back up the page. `tree` centers a
|
|
738
|
+
parent over its children's block; a graph that is not a tree still lays out,
|
|
739
|
+
since roots are the nodes nothing points at and anything the walk cannot reach
|
|
740
|
+
becomes a root of its own. `force` is an iterative relaxation seeded from the
|
|
741
|
+
current positions — **the one layout that is not idempotent**, since re-running
|
|
742
|
+
it keeps relaxing.
|
|
743
|
+
|
|
744
|
+
`buildGraph` reads the adjacency index from the same participant source the port
|
|
745
|
+
affordance takes, per invocation rather than maintaining one. `createLayoutAction`
|
|
746
|
+
rebuilds it on each press and writes the whole move as a single `scene.batch`,
|
|
747
|
+
carrying a container's whole subtree — `setPose` does not cascade, and a built
|
|
748
|
+
body would otherwise walk out from under its own label rows.
|
|
749
|
+
|
|
750
|
+
In core, `createSimulation` is the velocity-Verlet integrator with no clock
|
|
751
|
+
attached: `tick()` is the only thing that moves a node, so a pure function can
|
|
752
|
+
run a whole relaxation and read the result. `useSimulation` is now that
|
|
753
|
+
integrator on a frame loop and is otherwise unchanged. `SimulationCore` and
|
|
754
|
+
`SimulationOptions` name the halves, and forces can be handed a seeded `random`
|
|
755
|
+
in place of `Math.random`.
|
|
756
|
+
- 4a128c4: New package `@weasel-js/diagram` — the skeleton of weasel's node-link
|
|
757
|
+
diagramming: flowcharts, pipelines, code-flow diagrams, simple visual
|
|
758
|
+
programming. Arc 3 of the diagram design; edges, routing, the connect gesture
|
|
759
|
+
and layout are still to come.
|
|
760
|
+
|
|
761
|
+
**`DiagramNode` is a trait on an existing scene node, not a node type the
|
|
762
|
+
package mints.** A text block, an image, a path, a group or a plain rect all
|
|
763
|
+
become participants by carrying it, and nothing has to be authored through this
|
|
764
|
+
package to take part. Two ways to attach it, both answered by the default
|
|
765
|
+
reader: on the node's own `data.diagram`, or by kind — `createDiagramNodes`
|
|
766
|
+
takes predicates over `data` the way `createNodeRouting` does, so a class of
|
|
767
|
+
node takes part without being stamped one at a time.
|
|
768
|
+
|
|
769
|
+
**Ports default to the perimeter, so a node needs to say nothing to be
|
|
770
|
+
connectable.** `portsOf(node, pose)` resolves every port to a world point and
|
|
771
|
+
an outward normal, reading bounds through the pose descriptor and carrying the
|
|
772
|
+
node's rotation. Anchors are normalized against the bounds — `{ u, v }` from
|
|
773
|
+
the top-left — so a port stays where it was put when the node is resized.
|
|
774
|
+
|
|
775
|
+
**The body builder is optional**, for nodes that should look like a flowchart
|
|
776
|
+
box: `rect`, `diamond`, `stadium` and `parallelogram` outlines plus a `Row[]`
|
|
777
|
+
body of labels, fields, port rows and slots. Rows measure a floor, and the
|
|
778
|
+
authored pose is maxed against it rather than set to it — adding a row can grow
|
|
779
|
+
a node, nothing shrinks one back — which is what keeps resize, align,
|
|
780
|
+
distribute, guides, snapping and undo free of a special case. Text measurement
|
|
781
|
+
is a seam rather than an import; `canvasMeasure` adapts a 2D context.
|
|
782
|
+
|
|
783
|
+
Rows lay out in the shape's **content box**, not its bounding box.
|
|
784
|
+
`contentBox` reports the largest axis-aligned box inside an outline — a
|
|
785
|
+
diamond's inscribed rect, a parallelogram minus its lean, the flat span between
|
|
786
|
+
a stadium's ends — and `boxForContent` inverts it so the floor grows to suit.
|
|
787
|
+
Without it a diamond's label is placed against the bounding box, lands outside
|
|
788
|
+
the diamond, and the silhouette clip removes it: the label simply vanishes.
|
|
789
|
+
|
|
790
|
+
`registerDiagramShape` paints a node whose trait names an outline, and reports
|
|
791
|
+
the outline as its silhouette so picking and clipping follow the diamond rather
|
|
792
|
+
than its box. Rows are not painted there — a built body's rows are ordinary
|
|
793
|
+
scene nodes, so the kit's own text painter draws them and text editing,
|
|
794
|
+
selection and styling work on them unchanged.
|
|
795
|
+
|
|
796
|
+
Core exports `AUTO_POSE_DESCRIPTOR` and `isPathLike`, which were already
|
|
797
|
+
general-purpose but reachable only from inside the package. A peer package
|
|
798
|
+
computing a node's bounds needs the kit's own default descriptor rather than a
|
|
799
|
+
second copy of it.
|
|
800
|
+
- aee9d92: `createPoseOverrides` is public.
|
|
801
|
+
|
|
802
|
+
`Scene` is public and its `overrides: PoseOverrides<TPose>` is mandatory — and
|
|
803
|
+
load-bearing, since every ongoing gesture writes a frame to it. The factory that
|
|
804
|
+
builds one was internal, so a consumer assembling a scene-like object by hand
|
|
805
|
+
had to reimplement the table from its type. It is now exported alongside
|
|
806
|
+
`createScene`.
|
|
807
|
+
- c067221: Put every baked gradient ramp in one texture, a row each, rather than a texture
|
|
808
|
+
each. `GradientRampAtlas` replaces `GradientRampCache`: a stop list is baked
|
|
809
|
+
once into a 256-texel strip and written to a row, and the fragment shader picks
|
|
810
|
+
its row with `u_rampV`. Every gradient in a frame now samples the same texture
|
|
811
|
+
unit, which is what a gradient needs before it can take a batch texture slot
|
|
812
|
+
the way a bitmap or a font atlas already does.
|
|
813
|
+
|
|
814
|
+
The atlas doubles from 16 rows and stops at 1024, recycling the least recently
|
|
815
|
+
used row past that. An animating gradient mints a new stop list every frame, so
|
|
816
|
+
the old cache grew a GL texture per frame and freed none of them; the cap is
|
|
817
|
+
what bounds that.
|
|
818
|
+
|
|
819
|
+
`PaintBindContext.bindRamp` now returns the `v` its ramp sits at. A registered
|
|
820
|
+
paint kind that samples the ramp at a constant `v` reads whatever gradient
|
|
821
|
+
happens to own that row, so it must sample at the returned value.
|
|
822
|
+
- 26d40bf: Fold gradients into the shared batch. A gradient fill used to bind its own
|
|
823
|
+
program and break the run of solid geometry, image quads and glyphs around it;
|
|
824
|
+
now it stages alongside them, so a page of gradient-filled shapes is one draw
|
|
825
|
+
rather than one per shape.
|
|
826
|
+
|
|
827
|
+
All three kinds go, and for the same reason — not that the ramp position is
|
|
828
|
+
affine in position, which is true only of a linear gradient, but that the
|
|
829
|
+
*coordinate* the ramp position is computed from is affine in all three. So a
|
|
830
|
+
vertex carries that and the rasterizer's interpolation across a triangle is
|
|
831
|
+
exact. A linear gradient's coordinate is the ramp position itself, which is why
|
|
832
|
+
it needs no paint mode of its own; a radial or conic one carries a
|
|
833
|
+
gradient-space point, with its atlas row where a plain vertex keeps its alpha,
|
|
834
|
+
and the shader takes a `length` or an `atan` of it behind a branch on the flat
|
|
835
|
+
paint mode. Fill opacity and group alpha ride the vertices the way a solid's
|
|
836
|
+
already did, and fills, stroke ribbons and glyph-outline meshes all take the
|
|
837
|
+
route.
|
|
838
|
+
|
|
839
|
+
Per-vertex-colored and even-odd fills still take their own draw, as do patterns
|
|
840
|
+
and shaders.
|
|
841
|
+
|
|
842
|
+
The ramp atlas is what makes the slot arithmetic work: every gradient in a run
|
|
843
|
+
shares one texture slot, so a document full of them costs the same one slot a
|
|
844
|
+
single gradient does. Growing the atlas moves every row, so a bake that would
|
|
845
|
+
grow it — or recycle a row — flushes the run first.
|
|
846
|
+
|
|
847
|
+
Gradient fills now apply the group's color matrix. `gradFill` was the only
|
|
848
|
+
paint program that did not, and since the batch program applies it to
|
|
849
|
+
everything in a run, leaving it out would have made a linear gradient and a
|
|
850
|
+
radial one under the same group paint differently.
|
|
851
|
+
- b8d2940: Layout you can watch, and push against.
|
|
852
|
+
|
|
853
|
+
`usePoseRun` is the transport: each frame it asks a producer for poses,
|
|
854
|
+
publishes them to the scene's ephemeral override channel — the one a drag
|
|
855
|
+
already writes to, which `effectivePose`, derived geometry and the pick source
|
|
856
|
+
read — and commits the lot as one batch when the producer says it is done or
|
|
857
|
+
the consumer stops it. Cancel drops the frames and the document is untouched.
|
|
858
|
+
It runs behind `useVisibleRaf`, and it knows nothing about layout.
|
|
859
|
+
|
|
860
|
+
A node carrying an override the run did not publish belongs to another gesture:
|
|
861
|
+
the run never writes it, never commits it, and reports it to the producer as
|
|
862
|
+
pinned. Dragging a box mid-run is therefore the consumer's ordinary move tool,
|
|
863
|
+
with no gesture contributed by the diagram package.
|
|
864
|
+
|
|
865
|
+
`useLiveLayout` in `@weasel-js/diagram` drives it. `force` relaxes one tick a
|
|
866
|
+
frame off the same force list the one-shot `force` runs, holding a pinned node
|
|
867
|
+
with `fx`/`fy` while its neighbors answer; `layered` and `tree` ease into a
|
|
868
|
+
target computed once. A node or edge appearing or disappearing re-heats the run.
|
|
869
|
+
|
|
870
|
+
`SceneNode.pickable: false` makes a node transparent to the hit-test walk, so a
|
|
871
|
+
press lands on what is behind it. Without it the innermost hit wins and dragging
|
|
872
|
+
a labeled box pulls the label out of the box.
|
|
873
|
+
- b5e2cd9: Solid geometry and image quads now share one batch, so a wall of thumbnails —
|
|
874
|
+
a ground rect under an atlas quad, per cell — draws in one call rather than
|
|
875
|
+
one per command. The two batches used to be exclusive: staging a solid drained
|
|
876
|
+
the image run and staging an image drained the solid one, so a shape that
|
|
877
|
+
batches perfectly in either half alone paid a flush per command. Solid vertices
|
|
878
|
+
carry the UV of a 1x1 white texel, which makes `texture() * a_vertexColor` the
|
|
879
|
+
vertex color exactly, so the merge is pixel-identical rather than close.
|
|
880
|
+
|
|
881
|
+
Measured over a viewport-filling grid of those cells on an M2 Max via ANGLE
|
|
882
|
+
(`tests/perf/atlas-wall.spec.ts`): 600 draw commands 2.83 -> 0.10 ms, 1,650
|
|
883
|
+
11.37 -> 0.20, 5,400 40.50 -> 0.58, 15,000 126.15 -> 1.50. A run still breaks
|
|
884
|
+
on what a run cannot carry — a second bitmap, a different MAG_FILTER, a clip
|
|
885
|
+
depth, a color matrix.
|
|
886
|
+
- 89276ee: `usePoseRun` and `useSimulation` hand their injected clock straight to
|
|
887
|
+
`useVisibleRaf` instead of defaulting it themselves. The gate already falls
|
|
888
|
+
back to `requestAnimationFrame`, so both were defaulting it twice — and the
|
|
889
|
+
copy in `usePoseRun` was a bare `requestAnimationFrame` in kit source, which
|
|
890
|
+
`check:frame-loops` fails the build on. The allowlist is back down to the gate
|
|
891
|
+
itself.
|
|
892
|
+
- 36950d8: `scene.setDependsOn(id, dependsOn)` retargets a node's dependencies as one
|
|
893
|
+
undoable step, so dragging an existing edge's end onto a different node no
|
|
894
|
+
longer means removing the edge and adding another one. Switching a container
|
|
895
|
+
between an id list and `'children'` goes through the same call.
|
|
896
|
+
|
|
897
|
+
Both indices move with it — the reverse dependents index that drives cascade
|
|
898
|
+
delete, and the `'children'` set that drives the ancestor walk — and the
|
|
899
|
+
retargeted node's own memo is dropped alongside its dependents', since its
|
|
900
|
+
derivation now reads different sources. Order is significant, because a
|
|
901
|
+
derivation reads its dependencies positionally; declaring what a node already
|
|
902
|
+
declares records no history entry.
|
|
903
|
+
- 4f8c6b2: Text no longer breaks a batched run. Glyphs, the rules under underlined words,
|
|
904
|
+
and tessellated glyph outlines all stage into the same draw as the solid
|
|
905
|
+
geometry and image quads around them, so a wall of captioned thumbnails is one
|
|
906
|
+
draw where every label used to cost two.
|
|
907
|
+
|
|
908
|
+
The batch shader carries the glyph math behind a paint mode, which packs into
|
|
909
|
+
the texture-slot attribute the vertex already had, so the vertex does not grow
|
|
910
|
+
and a wall of thumbnails costs what it did before. It runs that math on every
|
|
911
|
+
fragment, glyph or not, because `fwidth` in non-uniform control flow is
|
|
912
|
+
undefined and the derivative has to be taken before anything selects on the
|
|
913
|
+
mode — priced at about 1.4% of a fragment that is not a glyph. A synthetic
|
|
914
|
+
oblique now shears on the CPU as the batch places its corners, rather than in a
|
|
915
|
+
vertex shader that read the baseline from a vertex attribute.
|
|
916
|
+
|
|
917
|
+
Three things a run used to break on are gone: a second text color in the same
|
|
918
|
+
paragraph, a decoration whose fill differs from the glyphs it sits under, and
|
|
919
|
+
the difference between a baked MSDF atlas and a runtime canvas bake. What still
|
|
920
|
+
breaks a run is a change of synthetic-bold threshold, which is a uniform — that
|
|
921
|
+
one is a fallback path, since a registered bold face never sets it.
|
|
922
|
+
|
|
923
|
+
**Breaking for anyone importing the text shader sources.** `TEXT_VERT_SRC`,
|
|
924
|
+
`TEXT_FRAG_SRC`, `TEXT_FRAG_R8_SRC`, `TEXT_SDF_UNIFORMS` and
|
|
925
|
+
`TEXT_SDF_ATTRIBUTES` are removed from `@weasel-js/font`: text has no program of
|
|
926
|
+
its own any more. What replaces them is `GLYPH_COVERAGE_GLSL`, the snippet a
|
|
927
|
+
program pastes in to turn an atlas sample into coverage, alongside
|
|
928
|
+
`GLYPH_MODE_MSDF` and `GLYPH_MODE_R8` naming the two channel layouts.
|
|
929
|
+
- 1240956: `kit:text` nodes take a `verticalAlign`, and picking follows it.
|
|
930
|
+
|
|
931
|
+
Centering a glyph in its box meant nudging `pose.y` by hand and re-deriving the
|
|
932
|
+
nudge whenever the font size changed. The painter forwarded the pose's height
|
|
933
|
+
but never an alignment, so the box the renderer aligned within was always
|
|
934
|
+
resolved as `'top'`.
|
|
935
|
+
|
|
936
|
+
`data.verticalAlign` — `'top' | 'center' | 'bottom'`, the same spelling the draw
|
|
937
|
+
command already took — now reaches both halves: the paint command and the
|
|
938
|
+
silhouette `textLineBoxes` builds, so a centered block is grabbable where it
|
|
939
|
+
draws rather than where a top-aligned one would have. A node that names none
|
|
940
|
+
paints exactly where it did before.
|
|
941
|
+
- Updated dependencies [4f8c6b2]
|
|
942
|
+
- @weasel-js/font@1.4.4
|
|
943
|
+
- @weasel-js/text@1.4.4
|
|
944
|
+
- @weasel-js/cursor@1.4.4
|
|
945
|
+
- @weasel-js/geom@1.4.4
|
|
946
|
+
- @weasel-js/gestures@1.4.4
|
|
947
|
+
- @weasel-js/history@1.4.4
|
|
948
|
+
- @weasel-js/modes@1.4.4
|
|
949
|
+
- @weasel-js/paint@1.4.4
|
|
950
|
+
|
|
3
951
|
## 1.4.3
|
|
4
952
|
|
|
5
953
|
### Patch Changes
|
|
@@ -912,1951 +1860,158 @@
|
|
|
912
1860
|
- @weasel-js/modes@1.4.0
|
|
913
1861
|
- @weasel-js/paint@1.4.0
|
|
914
1862
|
|
|
915
|
-
## 1.
|
|
1863
|
+
## 1.3.0
|
|
916
1864
|
|
|
917
|
-
###
|
|
1865
|
+
### Minor Changes
|
|
918
1866
|
|
|
919
|
-
-
|
|
920
|
-
|
|
921
|
-
A cursor glyph is SVG path `d` strings tagged with a paint role plus a hotspot
|
|
922
|
-
in glyph units — the one geometry form both a data-URI baker and a `Path2D`
|
|
923
|
-
painter consume without translating. `bakeCursor` renders one to a
|
|
924
|
-
`url(data:image/svg+xml,…)` string with the hotspot scaled to integer CSS px,
|
|
925
|
-
and `cursorFor` memoizes that per name and size. The pencil, pen and eyedropper
|
|
926
|
-
tools now show their own glyph instead of a shared `crosshair`.
|
|
927
|
-
|
|
928
|
-
Cursors ship as SVG with no bitmap fallback: Chrome rasterizes an SVG data-URI
|
|
929
|
-
cursor at device scale, so it is already crisp on a retina display. `bakeCursor`
|
|
930
|
-
throws above 128 CSS px rather than emitting a cursor the browser would drop
|
|
931
|
-
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.
|
|
932
1869
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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.
|
|
936
1875
|
|
|
937
|
-
|
|
938
|
-
`
|
|
939
|
-
|
|
940
|
-
- Updated dependencies [36b6ee7]
|
|
941
|
-
- @weasel-js/cursor@1.4.0-pre.1
|
|
942
|
-
- @weasel-js/font@1.4.0-pre.1
|
|
943
|
-
- @weasel-js/geom@1.4.0-pre.1
|
|
944
|
-
- @weasel-js/gestures@1.4.0-pre.1
|
|
945
|
-
- @weasel-js/history@1.4.0-pre.1
|
|
946
|
-
- @weasel-js/modes@1.4.0-pre.1
|
|
947
|
-
- @weasel-js/paint@1.4.0-pre.1
|
|
948
|
-
- @weasel-js/text@1.4.0-pre.1
|
|
949
|
-
|
|
950
|
-
## 1.4.0-pre.0
|
|
951
|
-
|
|
952
|
-
### Minor Changes
|
|
953
|
-
|
|
954
|
-
- 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.
|
|
955
1879
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
canvases share one GL context and one buffer. Each needs its own
|
|
959
|
-
`<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`.
|
|
960
1882
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
`surface` is where pixels land. Attached, they are the same `<canvas>` and
|
|
964
|
-
`element` keeps working as before. The HUDs render when detached too, anchored
|
|
965
|
-
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.
|
|
966
1885
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
`
|
|
970
|
-
`surface` for pixels. And `clientToWorld`'s first parameter widens to
|
|
971
|
-
`HTMLElement`, which stops compiling for a consumer who annotated that parameter
|
|
972
|
-
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`.
|
|
973
1889
|
|
|
974
|
-
<!-- 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" -->
|
|
975
1891
|
|
|
976
1892
|
### Patch Changes
|
|
977
1893
|
|
|
978
|
-
-
|
|
979
|
-
|
|
980
|
-
An instrument that declares `annotations` now gets a drawing surface on every
|
|
981
|
-
target it names — weasel tools, weasel selection, marks that pan and zoom with
|
|
982
|
-
what they mark — plus a palette (select, freehand, line, arrow, rectangle,
|
|
983
|
-
ellipse, text) and its own tool slot. `useAnnotations()` reaches the store from
|
|
984
|
-
the instrument's render or from a chrome contribution, and re-renders its
|
|
985
|
-
caller as marks change.
|
|
1894
|
+
- 52c7b2a: Depend on `font` and `core` as exact peers
|
|
986
1895
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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.
|
|
991
1901
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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.
|
|
998
1908
|
|
|
999
|
-
|
|
1000
|
-
|
|
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.
|
|
1001
1912
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
|
1008
1919
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
/ `DepRegistryProvider` to shadow the ambient scope, and both now say `isolate`
|
|
1014
|
-
instead.
|
|
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.
|
|
1015
1924
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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.
|
|
1020
1932
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
not in this change.
|
|
1024
|
-
- 7a0c568: Tell an event handler how late its crossing is
|
|
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.
|
|
1025
1935
|
|
|
1026
|
-
`
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
resolution against an audio engine built for sample resolution: a measured peak
|
|
1030
|
-
spread of 33–47 ms on the looping run cycle.
|
|
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.
|
|
1031
1939
|
|
|
1032
|
-
`
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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.
|
|
1038
1949
|
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1041
1953
|
|
|
1042
|
-
`
|
|
1043
|
-
|
|
1044
|
-
|
|
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.
|
|
1045
1958
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
latency rather than audible spread.
|
|
1049
|
-
- a7fa697: Add an anchored-placement solver and keep HUD windows on their host.
|
|
1959
|
+
Camera animation is a general surface, not a zoom flag. Three ways in, one
|
|
1960
|
+
runner behind them:
|
|
1050
1961
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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`.
|
|
1058
1971
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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.
|
|
1063
1978
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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.
|
|
1069
1988
|
|
|
1070
|
-
`
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
|
1074
1995
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
- 2272682: `createParallaxLayer` takes an optional `getOuterView`, so a plane can derive
|
|
1081
|
-
from a ref-driven camera. It previously derived only from the canvas's `view`
|
|
1082
|
-
prop; a consumer keeping a 60 Hz camera out of React state pins that prop to
|
|
1083
|
-
identity and got identity back for every `pan` value — a backdrop that silently
|
|
1084
|
-
never moved.
|
|
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
|
|
1085
2001
|
|
|
1086
|
-
`useHandTool` no longer builds a velocity tracker and a decay loop it never
|
|
1087
|
-
uses. `inertia` and `axis` were already inert; they are now documented as such
|
|
1088
|
-
until the `viewport.dragPan` action implements them.
|
|
1089
|
-
- 503b56d: Fix two path-walker bugs that produced wrong geometry with no error.
|
|
1090
|
-
|
|
1091
|
-
`tessellate` treated `Z` as a no-op, so a command following a close flattened
|
|
1092
|
-
from the last point drawn rather than from the subpath start — SVG puts the pen
|
|
1093
|
-
back at the start. `pathDistanceToPoint` dispatched through an `if`/`else if`
|
|
1094
|
-
chain with no final `else`, so an unrecognized command code left the coordinate
|
|
1095
|
-
cursor unadvanced and silently misaligned every later read; it now throws.
|
|
1096
|
-
- ac2deea: Add `polylineFromPoints` — the open counterpart to `polygonFromPoints`.
|
|
1097
|
-
|
|
1098
|
-
Same geometry, without the closing edge. A freehand stroke or a measurement
|
|
1099
|
-
line wants this; a region wants the closed one. The pencil tool's drag preview
|
|
1100
|
-
was building its ghost with `polygonFromPoints`, so the edge from the newest
|
|
1101
|
-
sample back to the first swept across the drawing as the stroke grew and read
|
|
1102
|
-
as a marquee.
|
|
1103
|
-
- 23ffb2f: `WeaselRenderer` can draw into a rect of a buffer it does not own.
|
|
1104
|
-
`setTarget({ origin, clear })` applies a viewport and scissor inside `render()`,
|
|
1105
|
-
so N renderers can share one WebGL context and one canvas without a frame clear
|
|
1106
|
-
erasing a co-tenant. The rect's size is the renderer's own `width`/`height`, so
|
|
1107
|
-
`resize()` remains the single source of it.
|
|
1108
|
-
|
|
1109
|
-
Adds API. Two behaviour changes for existing callers: `render()` now
|
|
1110
|
-
re-establishes blend, depth, cull and clear colour every frame instead of once at
|
|
1111
|
-
construction, so a co-tenant moving that state no longer corrupts weasel's
|
|
1112
|
-
frames; and the constructor now throws when handed a WebGL2 context whose
|
|
1113
|
-
attributes report no stencil buffer, which previously rendered clips and even-odd
|
|
1114
|
-
fills wrong rather than failing. A context that cannot report its attributes is
|
|
1115
|
-
unaffected.
|
|
1116
|
-
- 016851c: Stop a stroke with no paint from blanking the whole document.
|
|
1117
|
-
|
|
1118
|
-
`SelectionPanel`'s object leaf started from `{}` when the node held no value
|
|
1119
|
-
yet, so editing any non-paint field of `data.stroke` on an unstroked node
|
|
1120
|
-
committed that field alone — a `Stroke` with no `paint`, which the type
|
|
1121
|
-
forbids. The leaf's declared `default` was dead for writes; it now seeds from
|
|
1122
|
-
it, so writing one field materializes a complete value.
|
|
1123
|
-
|
|
1124
|
-
Such a stroke threw out of `fillInPoseFrame`, and the throw escaped the painter
|
|
1125
|
-
and took the frame with it: the document page and every other node vanished,
|
|
1126
|
-
and the canvas stayed stale until something unrelated requested a redraw — so
|
|
1127
|
-
WeaselDraw opened on an empty workspace and only drew once the pointer moved.
|
|
1128
|
-
`resolveNodeStroke` now reads a paintless stroke as no stroke, and the text
|
|
1129
|
-
painter routes through it like every other painter. The frame loop no longer
|
|
1130
|
-
loses its dirty flag when a paint throws, so one bad frame is retried rather
|
|
1131
|
-
than stranding the surface.
|
|
1132
|
-
- c9dd37f: Render text decorations as a toggle row, and ship a builtin font-family control
|
|
1133
|
-
|
|
1134
|
-
`SelectionPanel` rendered every boolean leaf as a `Switch`, ignoring the leaf's
|
|
1135
|
-
`control` entirely — so the three text decorations arrived as three switch rows
|
|
1136
|
-
where every text editor puts one row of U / S / O. `ToolPrefBooleanControl` now
|
|
1137
|
-
accepts `'toggle'`, `ToolPrefBoolean` carries a `short` label for it (the pair
|
|
1138
|
-
takes the row's name, leaving the leaf only a glyph's worth of room), and the
|
|
1139
|
-
panel honors both. Core's text schema asks for it: `underline`,
|
|
1140
|
-
`strikethrough` and `overline` share a `Decoration` pair.
|
|
1141
|
-
|
|
1142
|
-
A run of adjacent leaves sharing a `pair` renders as one `ToggleBar`, not one
|
|
1143
|
-
bar per leaf — the same segmented control the `Align` row beside it already
|
|
1144
|
-
draws. Each segment still writes only its own leaf, so flipping one decoration
|
|
1145
|
-
never invents values for the other two. An unset toggle is left unselected
|
|
1146
|
-
rather than dimmed: unselected is what a toggle button's off state means, and
|
|
1147
|
-
the dimming the `Switch` path uses for the same case reads as disabled on one.
|
|
1148
|
-
A leaf a consumer claims with its own `renderers` entry drops out of the run.
|
|
1149
|
-
|
|
1150
|
-
`FontFamilySelect` moves from WeaselDraw into `@weasel-js/ui`, and
|
|
1151
|
-
`SelectionPanel` reaches for it on a `font-family` leaf. Core's own default
|
|
1152
|
-
text schema declares that kind, so a consumer passing no `renderers` — the
|
|
1153
|
-
Storybook story, any app taking the defaults — got the literal
|
|
1154
|
-
`(font-family: no renderer)` placeholder where the font picker belongs. The
|
|
1155
|
-
control offers both tiers that can actually paint and probes substitution at
|
|
1156
|
-
the node's own weight and style, so its label names the variant that will
|
|
1157
|
-
render. `@weasel-js/ui` now depends on `@weasel-js/font`.
|
|
1158
|
-
- 9a000ea: A stroked text node now gets hit reach from its stroke. `TEXT_PAINTER` declared
|
|
1159
|
-
no `ink`, so picking fell back to a zero-outset default and a heavily outlined
|
|
1160
|
-
glyph was unpickable across the width of its own outline.
|
|
1161
|
-
|
|
1162
|
-
`kit:derived` also now evaluates ahead of `kit:path` / `kit:shape` / `kit:image`.
|
|
1163
|
-
A derived node whose `data` happens to carry a `path`, `shape` or `image` field
|
|
1164
|
-
was silently painted by those painters instead of from its derived path.
|
|
1165
|
-
- 016851c: Add an editor surface for superscript, subscript and overline.
|
|
1166
|
-
|
|
1167
|
-
`StyledRun.script`, `baselineShift`, `fontScale` and `overline` reached layout,
|
|
1168
|
-
SVG and the DOM overlay but nothing could apply them. The character bar now
|
|
1169
|
-
carries an x² / x₂ pair, an overline toggle beside B / I / U / S, and the two
|
|
1170
|
-
primitives `script` presets — baseline shift and scale — as percentage fields
|
|
1171
|
-
that show what the preset supplies and override just that half when typed over.
|
|
1172
|
-
`overline` also joins the sidebar's node-level Character group. Superscript and
|
|
1173
|
-
subscript take Cmd+Shift+= and Cmd+Shift+-; the unshifted pair is browser zoom,
|
|
1174
|
-
which a page cannot cancel.
|
|
1175
|
-
|
|
1176
|
-
A styling written at a collapsed caret now arms `useTextEdit`'s new
|
|
1177
|
-
`pendingStyle` and applies to the next character typed, instead of being
|
|
1178
|
-
dropped or restyling the whole node. That is what `script` needs — it has no
|
|
1179
|
-
node-level counterpart to write to by design — and it makes the bar agree with
|
|
1180
|
-
Cmd+B, which already behaved this way. `rangeStyle` reports the styling *at* a
|
|
1181
|
-
collapsed caret rather than `{}`, and `toggleStyle` is public.
|
|
1182
|
-
|
|
1183
|
-
Three fixes fall out of putting both paths through one implementation:
|
|
1184
|
-
lowering a flag the node sets now works from the bar and from a collapsed
|
|
1185
|
-
caret, not only from the keyboard over a range; a toggle reads the node's flags
|
|
1186
|
-
as well as the runs, so Cmd+B inside a `fontWeight: 700` node clears bold
|
|
1187
|
-
instead of adding it; and focus returns to the text after a styling control is
|
|
1188
|
-
clicked, so typing continues in the document rather than reaching the app as
|
|
1189
|
-
tool shortcuts.
|
|
1190
|
-
- 8ddec11: Accept a named or cubic-bezier easing wherever a curve is taken, and let a
|
|
1191
|
-
timeline's loop policy change after it is created.
|
|
1192
|
-
|
|
1193
|
-
`easing` was a bare function everywhere, which is fine to call and impossible to
|
|
1194
|
-
name back, show in a picker, or serialize. It now also accepts the name of a
|
|
1195
|
-
built-in (`'easeOutBack'`) or control points (`{ bezier: [0.4, 0, 0.2, 1] }`),
|
|
1196
|
-
resolved by `resolveEasing` at the four places a curve is actually invoked. The
|
|
1197
|
-
union is additive, so every existing function value stays assignable. Bezier x
|
|
1198
|
-
control points are clamped to 0..1, which is what keeps the solve monotone, and
|
|
1199
|
-
the control-point tuple is `readonly` so an `as const` preset is assignable.
|
|
1200
|
-
|
|
1201
|
-
`TimelineHandle.setLoop(loop)` sets policy and nothing else. A timeline already
|
|
1202
|
-
parked at its duration does not restart — `rearm` declines to revive one — so
|
|
1203
|
-
play it again by seeking to 0 and resuming. Restoring saved transport state
|
|
1204
|
-
therefore cannot start playback as a side effect.
|
|
1205
|
-
|
|
1206
|
-
Both settings now read back. `AnimationHandle.timeScale()` returns an
|
|
1207
|
-
animation's own scale, and `Animator.timeScale()` the global one, the way
|
|
1208
|
-
`isPaused()` already pairs with `pause()`. `TimelineHandle.loop()` returns the
|
|
1209
|
-
policy as it stands — `true`, `false`, or the laps a finite loop has left, which
|
|
1210
|
-
falls as they are consumed. A transport UI can drive itself off the handle
|
|
1211
|
-
instead of mirroring what it last wrote, which drifts as soon as anything else
|
|
1212
|
-
holding the handle sets it.
|
|
1213
|
-
- 28894b9: Fix the viewport primitives on an axis with negative scale.
|
|
1214
|
-
|
|
1215
|
-
`View.scale` is documented as pixels per world unit _per axis_, so `scale.y < 0`
|
|
1216
|
-
is the ordinary way to spell a y-up camera. Two primitives did not read it that
|
|
1217
|
-
way, and both failed silently rather than erroring.
|
|
1218
|
-
|
|
1219
|
-
`zoomAt` clamped the signed scale against positive bounds
|
|
1220
|
-
(`min(max, max(min, scale * factor))`), so one wheel step on a y-up view
|
|
1221
|
-
returned `scale.y = +0.1`: the axis flipped and the zoom collapsed to the
|
|
1222
|
-
minimum. It now bounds the magnitude and restores the sign, so a clamp limits a
|
|
1223
|
-
flipped axis instead of unflipping it.
|
|
1224
|
-
|
|
1225
|
-
`clampView` computed the visible world extent as `canvas.height / scale.y`,
|
|
1226
|
-
which is negative on a flipped axis. That made the "is the view zoomed out past
|
|
1227
|
-
the bounds" test never fire, and put the scroll interval on the wrong side of
|
|
1228
|
-
the anchor — a y-up view could be panned outside its own bounds. It now takes
|
|
1229
|
-
the extent as a magnitude and anchors the interval at the rect's far edge when
|
|
1230
|
-
the axis is flipped.
|
|
1231
|
-
|
|
1232
|
-
Found while giving labkit's instrument canvas a declarable coordinate system:
|
|
1233
|
-
routing its wheel through `zoomAt` looked like the obvious way to stop
|
|
1234
|
-
reimplementing fixed-point zoom, and would have been a bug.
|
|
1235
|
-
- c4ccd0a: Zoom now has one clamp. `DEFAULT_MIN_ZOOM` / `DEFAULT_MAX_ZOOM` are exported from
|
|
1236
|
-
`@weasel-js/core` and every zoom path defaults from them — `zoomAt`, the
|
|
1237
|
-
`viewport.zoom` and pinch actions, `usePinchZoomTool`, `fitViewToBounds`,
|
|
1238
|
-
`computeWheelAction` and `useZoom`.
|
|
1239
|
-
|
|
1240
|
-
**Behavior change:** the three paths that carried the second, undocumented pair
|
|
1241
|
-
now cap at 8x rather than 10x. `fitViewToBounds` could previously land at 10x and
|
|
1242
|
-
the next pinch frame would clamp it straight back to 8x. Pass an explicit
|
|
1243
|
-
`maxScale` / `max` to keep 10x.
|
|
1244
|
-
- Updated dependencies [a7fa697]
|
|
1245
|
-
- @weasel-js/geom@1.4.0-pre.0
|
|
1246
|
-
- @weasel-js/text@1.4.0-pre.0
|
|
1247
|
-
- @weasel-js/font@1.4.0-pre.0
|
|
1248
|
-
- @weasel-js/gestures@1.4.0-pre.0
|
|
1249
|
-
- @weasel-js/history@1.4.0-pre.0
|
|
1250
|
-
- @weasel-js/modes@1.4.0-pre.0
|
|
1251
|
-
- @weasel-js/paint@1.4.0-pre.0
|
|
1252
|
-
|
|
1253
|
-
## 1.3.0
|
|
1254
|
-
|
|
1255
|
-
### Minor Changes
|
|
1256
|
-
|
|
1257
|
-
- bca99e3: Extract the typography layer into `@weasel-js/text`, and the paint vocabulary
|
|
1258
|
-
into `@weasel-js/paint` — two new Tier A leaves.
|
|
1259
|
-
|
|
1260
|
-
`@weasel-js/text` owns the run model, style resolution, `layoutRuns`, wrap and
|
|
1261
|
-
measurement. It depends on `@weasel-js/font`, `@weasel-js/geom` and
|
|
1262
|
-
`@weasel-js/paint`, and on nothing else: a consumer with its own renderer can
|
|
1263
|
-
lay out text without taking the scene graph or a React peer dependency.
|
|
1264
|
-
`layoutRuns` is now public — it was previously reachable only from inside core.
|
|
1265
|
-
|
|
1266
|
-
`@weasel-js/paint` holds `FillStyle`, `Stroke`, gradients, dashes and
|
|
1267
|
-
`TextureHandle`. It was the blocker named in the 2026-07-28 font split: the
|
|
1268
|
-
layout could not move while its fill type lived in the renderer's graph.
|
|
1269
|
-
|
|
1270
|
-
`@weasel-js/core` re-exports both surfaces, so its own API is unchanged.
|
|
1271
|
-
`Rect` moves to `@weasel-js/geom`, beside `Box`.
|
|
1272
|
-
|
|
1273
|
-
Breaking for anyone importing these through core's internal paths rather than
|
|
1274
|
-
its public entry (`core/paint-types`, `features/text/*`); those paths are gone.
|
|
1275
|
-
|
|
1276
|
-
Advances and kerning still come from a baked MSDF atlas — laying out from font
|
|
1277
|
-
bytes alone needs the metrics seam in
|
|
1278
|
-
`docs/superpowers/specs/2026-08-28-text-package-extraction-design.md`.
|
|
1279
|
-
|
|
1280
|
-
<!-- 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" -->
|
|
1281
|
-
|
|
1282
|
-
### Patch Changes
|
|
1283
|
-
|
|
1284
|
-
- 52c7b2a: Depend on `font` and `core` as exact peers
|
|
1285
|
-
|
|
1286
|
-
`@weasel-js/font` and `@weasel-js/core` keep registries that consumer code
|
|
1287
|
-
writes into — registered faces and glyph-ready subscribers in one, content
|
|
1288
|
-
handlers and paint kinds and shape painters in the other. Two physical copies
|
|
1289
|
-
in a tree are two registries, so a face registered into one while layout
|
|
1290
|
-
resolves against the other lays out nothing and the canvas is blank.
|
|
1291
|
-
|
|
1292
|
-
Exact sibling pins are what produced the duplicate: a consumer mixing two
|
|
1293
|
-
weasel releases left npm no choice but to nest a second copy, silently. As
|
|
1294
|
-
peers, the same mix is an `ERESOLVE` at install time. `font` is now a peer of
|
|
1295
|
-
`core`, `hud` and `text`; `core` is now a peer of `svg`, joining `d3`, `hud`
|
|
1296
|
-
and `ui`, whose `>=` ranges tighten to exact so no version mix resolves by
|
|
1297
|
-
accident.
|
|
1298
|
-
|
|
1299
|
-
**This can break an install that currently succeeds.** Anyone resolving a
|
|
1300
|
-
mixed set of weasel versions by luck now gets an install error instead of a
|
|
1301
|
-
blank canvas. That is the point, but it is a break.
|
|
1302
|
-
|
|
1303
|
-
`labkit` deliberately keeps `core` as an ordinary dependency: its build aliases
|
|
1304
|
-
every core entry point to core's built files and inlines them, so it never
|
|
1305
|
-
resolves core at the consumer and has nothing to peer. The flip side is that
|
|
1306
|
-
labkit ships its own copy of core's registries, so a consumer using both still
|
|
1307
|
-
has two — this change does not address that.
|
|
1308
|
-
- 3386d64: Align, distribute and flip use visual bounds
|
|
1309
|
-
|
|
1310
|
-
These folded each member's unrotated pose box, so "Align Left" on a selection
|
|
1311
|
-
containing a rotated shape lined up the boxes and left the rotated shape's ink
|
|
1312
|
-
sticking out past the others. They now work on the visual bounding box, as
|
|
1313
|
-
Figma and Illustrator do.
|
|
1314
|
-
|
|
1315
|
-
Both ends moved together — expanding only the union would have made alignment
|
|
1316
|
-
worse, since the delta runs from an edge of the union to the same edge of each
|
|
1317
|
-
member's box. The new exported `visualBoundsViaDescriptor(pose, geometry)`
|
|
1318
|
-
reads a pose's bounds, recovers its rotation and expands via
|
|
1319
|
-
`axisAlignedBounds`; the union folds those with `unionAABB`. The delta is still
|
|
1320
|
-
applied as a translation of the stored pose through
|
|
1321
|
-
`translatePoseViaDescriptor`, so a shape moves rather than being re-posed.
|
|
1322
|
-
|
|
1323
|
-
Flip needed only its union pivot changed: mirroring maps a centre and preserves
|
|
1324
|
-
size, and an expanded box is concentric with the box it came from.
|
|
1325
|
-
|
|
1326
|
-
`alignMoveBehavior` folds the dragged selection the same way, so a drag snaps
|
|
1327
|
-
by its ink.
|
|
1328
|
-
- ffafb7d: Never let an animation's virtual clock run backwards.
|
|
1329
|
-
|
|
1330
|
-
`useAnimator` seeds each animation's `lastRealNow` from `now()` at register
|
|
1331
|
-
time, then advances its virtual clock by the difference against the timestamp
|
|
1332
|
-
the frame loop supplies. Those two share a time origin in a browser, where the
|
|
1333
|
-
rAF timestamp and `performance.now()` are both page-relative — but that is a
|
|
1334
|
-
browser guarantee, not a universal one, and jsdom starts them roughly 600ms
|
|
1335
|
-
apart. The first frame's delta then came out hugely negative and `virtualNow`
|
|
1336
|
-
spent dozens of frames climbing back toward zero before a tween advanced at
|
|
1337
|
-
all: a 40ms glide took 95 frames and over a second of wall time, growing worse
|
|
1338
|
-
the longer the process had been alive.
|
|
1339
|
-
|
|
1340
|
-
A frame's elapsed time is never negative, so the sample is now clamped at
|
|
1341
|
-
zero. Under a shared origin this is a no-op.
|
|
1342
|
-
- ba8b139: Camera animation: `viewport.animatedZoom` now does something
|
|
1343
|
-
|
|
1344
|
-
`animatedZoom` has been declared on `SceneCanvasProps.viewport` and read by
|
|
1345
|
-
nothing; Cmd+=/-/0 was a bare `view.set`. It now routes the discrete zoom steps
|
|
1346
|
-
through the kit's `Animator`. Wheel and pinch are unchanged and never animate —
|
|
1347
|
-
their input already delivers a sample per frame.
|
|
1348
|
-
|
|
1349
|
-
Camera animation is a general surface, not a zoom flag. Three ways in, one
|
|
1350
|
-
runner behind them:
|
|
1351
|
-
|
|
1352
|
-
- `useViewAnimation(view, animator?)` — `animate`, `animateToBounds`, `stop`,
|
|
1353
|
-
`isAnimating`, `target`.
|
|
1354
|
-
- The `view` dep gains optional `animate` / `stopAnimation` / `animationTarget`,
|
|
1355
|
-
so any action can glide the camera.
|
|
1356
|
-
- `SceneCanvasApi` gains `animateView` / `stopViewAnimation` /
|
|
1357
|
-
`isViewAnimating` for fit-to-selection, recenter, or a scripted tour. All
|
|
1358
|
-
three are **required** members: anyone hand-implementing `SceneCanvasApi`
|
|
1359
|
-
(a test double, a wrapper) has to add them, the way `CanvasExtensionApi`
|
|
1360
|
-
grew `getPaintedVersion`.
|
|
1361
|
-
|
|
1362
|
-
Scale interpolates geometrically and translation is derived from the screen
|
|
1363
|
-
point the two views agree on, so a zoom stays anchored instead of drifting and
|
|
1364
|
-
each frame changes the view by the same ratio. One animation runs at a time; any
|
|
1365
|
-
other view write cancels it, and a cancel leaves the camera where it is rather
|
|
1366
|
-
than jumping to the target. On an uncontrolled canvas the whole animation costs
|
|
1367
|
-
no React render.
|
|
1368
|
-
|
|
1369
|
-
**Breaking:** `useViewTween` is removed. `useViewAnimation` keeps its name and
|
|
1370
|
-
changes signature — it takes a `{ get, set }` view channel plus an optional
|
|
1371
|
-
`Animator`, and `animateTo(from, to, { duration, easing })` becomes
|
|
1372
|
-
`animate(to, { ms, easing })`. The `from` argument is gone because the runner
|
|
1373
|
-
reads the live view, which is what lets an interrupted camera resume from where
|
|
1374
|
-
it actually is instead of snapping back to a captured start. `cancel()` is now
|
|
1375
|
-
`stop()`, and `animateToBounds(bounds, currentView, dims, { duration })` is now
|
|
1376
|
-
`animateToBounds(bounds, dims, { ms })` — the `currentView` argument goes for
|
|
1377
|
-
the same reason `from` does.
|
|
1378
|
-
|
|
1379
|
-
**Breaking:** `viewport.recenter` and `ViewApi.recenter` widen to
|
|
1380
|
-
`() => View | void`. Returning the target view lets Cmd+0 animate there;
|
|
1381
|
-
returning nothing keeps the existing behavior. `animatedZoom`'s config fields
|
|
1382
|
-
are `ms` / `resetMs` rather than `duration` / `resetDuration`, matching the
|
|
1383
|
-
animator's vocabulary.
|
|
1384
|
-
- 3fb3a46: Forward `onFocus` and `onBlur` from the canvas element
|
|
1385
|
-
|
|
1386
|
-
The canvas is focusable by default (`tabIndex` 0) but exposed no way to
|
|
1387
|
-
observe focus, so consumers driving focus-dependent chrome had to attach a
|
|
1388
|
-
listener to an ancestor and infer it. Both are now props on `CanvasProps`, and
|
|
1389
|
-
so reach `SceneCanvasProps` and the canvas element unchanged.
|
|
1390
|
-
- 67bcb05: Drop four values the canvas layer memo no longer reads
|
|
1391
|
-
|
|
1392
|
-
`hit-test affordances against the painted chrome state` moved the selection
|
|
1393
|
-
overlay to reading bounds off the chrome state at paint time, which left
|
|
1394
|
-
`selectedIds`, `multiActive`, `previewToolPose` and `previewToolBounds`
|
|
1395
|
-
referenced only by the `layers` memo's dependency array — nothing in the body
|
|
1396
|
-
used them. Removing them from the array made all four dead locals, so they go
|
|
1397
|
-
too.
|
|
1398
|
-
|
|
1399
|
-
The memo now rebuilds the layer array on layer/tool/geometry changes rather
|
|
1400
|
-
than additionally on every selection and preview-pose change. Selection chrome
|
|
1401
|
-
is unaffected: it repaints from chrome state, not from the identity of this
|
|
1402
|
-
array.
|
|
1403
|
-
- 47cbb08: A closed subpath's dash no longer seams at its start vertex
|
|
1404
|
-
|
|
1405
|
-
`splitForDash` flushed the run still open when a closed subpath's walk returned
|
|
1406
|
-
to the vertex it started from as its own open sub-polyline, so it and the run
|
|
1407
|
-
that began there rendered as two butt-capped ribbons meeting at a point — a
|
|
1408
|
-
notch on the corner of any dashed rectangle whose perimeter isn't a whole
|
|
1409
|
-
multiple of the pattern. They are joined now, and the join the stroke asked for
|
|
1410
|
-
is drawn across the seam like any other corner. A pattern whose first "on"
|
|
1411
|
-
length covers the whole perimeter emits a closed ribbon, identical to the
|
|
1412
|
-
undashed stroke.
|
|
1413
|
-
- f43e9c2: A derived edge follows the drag that moves its endpoint
|
|
1414
|
-
|
|
1415
|
-
`move`, `resize` and `rotate` kept their in-flight poses in action-local
|
|
1416
|
-
scratch and published them only as `previewIds` / `previewPose`. That surface
|
|
1417
|
-
is enough to paint a ghost and size selection chrome, but nothing that asks
|
|
1418
|
-
the *scene* where a node is can see it — and `scenePoseLookup`, which resolves
|
|
1419
|
-
a derived node's geometry, asks the scene. So dragging a box left its edge
|
|
1420
|
-
anchored to the pre-drag position until the drop, when the commit invalidated
|
|
1421
|
-
the dependents and the edge jumped.
|
|
1422
|
-
|
|
1423
|
-
The three actions now also publish each frame into the scene's ephemeral pose
|
|
1424
|
-
overrides (`syncPreviewOverrides` / `dropPreviewOverrides` in
|
|
1425
|
-
`interactions/actions/previewOverrides.ts`). Overrides bypass `executeAndLog`,
|
|
1426
|
-
so a drag still commits as exactly one undo entry — the reason the actions
|
|
1427
|
-
avoided per-frame scene writes in the first place was history, and this writes
|
|
1428
|
-
no history. Entries are set once and mutated in place, published with a single
|
|
1429
|
-
`commit()` per frame.
|
|
1430
|
-
|
|
1431
|
-
Picking follows for free: the pick source resolves a derived path through its
|
|
1432
|
-
own override-aware `poseOf`, so an edge is grabbable where it is drawn
|
|
1433
|
-
mid-gesture rather than where it used to be.
|
|
1434
|
-
|
|
1435
|
-
`clone` is deliberately untouched — its previews are the new ghosts at the
|
|
1436
|
-
drag target, and the originals never move, so nothing derives from a changed
|
|
1437
|
-
pose.
|
|
1438
|
-
|
|
1439
|
-
Also closes the matching gap in the preview-ghost layer, which built a
|
|
1440
|
-
container's clip with no derived path and so ghosted a derived container
|
|
1441
|
-
without one.
|
|
1442
|
-
|
|
1443
|
-
Note for anyone with a hand-written `Scene` stand-in: `overrides` is now read
|
|
1444
|
-
on every gesture frame. It was already required by the `Scene` contract, but a
|
|
1445
|
-
partial fake that omitted it will now throw rather than silently skip.
|
|
1446
|
-
- bb27e83: A derived node is clickable where it paints
|
|
1447
|
-
|
|
1448
|
-
A node whose geometry comes from `derivePath` had no silhouette and no `ink`:
|
|
1449
|
-
`NodeShapeEntry.silhouette` took only `(node, pose)`, and a derived path is
|
|
1450
|
-
resolved from the *dependencies'* poses, which a painter has no handle on. So
|
|
1451
|
-
`kit:derived` could not report one, `shapeCoversPoint` read the resulting null
|
|
1452
|
-
as "no opinion" and answered `true` everywhere, and picking fell back to the
|
|
1453
|
-
node's own pose — for an edge, a zero-sized placeholder at the origin. An edge
|
|
1454
|
-
was unpickable, and a derived container contributed no clip.
|
|
1455
|
-
|
|
1456
|
-
`silhouette` now takes a `NodeSilhouetteCtx` carrying `derivedPath`, on the
|
|
1457
|
-
same convention `NodePaintCtx` already uses, and `kit:derived` reports the
|
|
1458
|
-
derived path as its silhouette and its declared stroke as its `ink`.
|
|
1459
|
-
|
|
1460
|
-
Resolving that path needs the scene, so it is the *source* that answers, not
|
|
1461
|
-
the painter: `PickSource.derivedPathOf`, a matching optional argument to
|
|
1462
|
-
`buildSceneTree`, and `SceneSlotConfig.derivedPathOf` — the slot already
|
|
1463
|
-
carried the derived path a node *paints*, and now also the clip a derived
|
|
1464
|
-
container *imposes*, so the live canvas and the headless walk clip alike. The
|
|
1465
|
-
bare-adapter paths supply none of them and behave exactly as before.
|
|
1466
|
-
|
|
1467
|
-
The pre-filter had to move with it. `useSceneSelectTool` grew its region test
|
|
1468
|
-
from the node's pose, which for a derived node is the wrong box entirely, so
|
|
1469
|
-
the edge was rejected before the shape test could claim it. It now tests the
|
|
1470
|
-
derived path when there is one — `poseContains` already reads a path-like pose
|
|
1471
|
-
as a path, so this reuses it rather than adding a second reach calculation.
|
|
1472
|
-
|
|
1473
|
-
`findShapeSilhouette` skips its memo when handed a derived path. That slot is
|
|
1474
|
-
keyed on `(node, pose, data)` and cannot see the path, so it would serve one
|
|
1475
|
-
caller's silhouette to a caller that passed a different one — the same reason
|
|
1476
|
-
`kit:derived` already skips `PAINT_SLOT`.
|
|
1477
|
-
- 6a33c3f: A node's path can be derived from other nodes' poses
|
|
1478
|
-
|
|
1479
|
-
A node declares `dependsOn: NodeId[]` and a `derivePath` function resolved by key
|
|
1480
|
-
through `SceneRegistry`, and the scene walks resolve its path before painting
|
|
1481
|
-
rather than it being authored. An edge drawn between two boxes is then an
|
|
1482
|
-
ordinary scene node — selectable, styleable, exportable — whose geometry never
|
|
1483
|
-
enters undo history. The seam and its traps are in `docs/extending.md`.
|
|
1484
|
-
|
|
1485
|
-
New surface: `scene.removeMany(ids)`; `dependsOn` and `derivePath` on
|
|
1486
|
-
`NodeBase` and on `AddNodeSpec`, which is what a consumer writes;
|
|
1487
|
-
`SceneRegistry.derivePath`; `SerializedNode.dependsOn` and
|
|
1488
|
-
`SerializedNode.derivePathKey`, both additions to the serialization format;
|
|
1489
|
-
`NodePaintCtx.derivedPath`.
|
|
1490
|
-
|
|
1491
|
-
Deleting a node now deletes everything that derives from it, transitively,
|
|
1492
|
-
including those nodes' own subtrees, in one undo entry — so `scene.remove` can
|
|
1493
|
-
remove nodes anywhere in the tree that the caller never named, and `removeLayer`
|
|
1494
|
-
reaches nodes on other layers. Undo after the built-in **Delete** key does not
|
|
1495
|
-
yet restore the cascaded nodes; see "Derived geometry follow-ups" in
|
|
1496
|
-
`docs/TODO.md`.
|
|
1497
|
-
|
|
1498
|
-
**Breaking: `defaultDrawOne` takes `(node, pose, view?, ctx?)`.** The paint
|
|
1499
|
-
context moves to a fourth parameter, so a call passing a `NodePaintCtx` third is
|
|
1500
|
-
now a type error rather than a silent slide into the `view` slot. The same
|
|
1501
|
-
fourth parameter is added to the `SceneViewDrawOne` and `SceneSlotConfig.drawOne`
|
|
1502
|
-
callback types, which is not a break: an existing three-parameter implementation
|
|
1503
|
-
still satisfies them, and an existing three-argument call still compiles.
|
|
1504
|
-
|
|
1505
|
-
**Breaking: `Scene` gained a required `removeMany`.** A hand-written object
|
|
1506
|
-
typed as a `Scene` — a test double, most likely — no longer typechecks until it
|
|
1507
|
-
implements it.
|
|
1508
|
-
|
|
1509
|
-
**Breaking: `kit:remove`'s op payload changed shape.** `rootId` / `parent` /
|
|
1510
|
-
`index` became `detached: { id, parent, index }[]`, because a cascaded dependent
|
|
1511
|
-
is not a descendant of the removed node and the tree has to be told about every
|
|
1512
|
-
subtree that came out of it. A history persisted by an older build now throws
|
|
1513
|
-
mid-undo rather than degrading. The break is deliberate; kit op payloads are not
|
|
1514
|
-
versioned.
|
|
1515
|
-
- c24e7de: Detached views follow pose overrides
|
|
1516
|
-
|
|
1517
|
-
`<SceneViewCanvas>` and `<MinimapCanvas>` re-rendered off `scene.getVersion()`,
|
|
1518
|
-
which a pose override deliberately never bumps — so they kept painting document
|
|
1519
|
-
poses while `<SceneCanvas>` painted the overridden ones. A minimap beside a
|
|
1520
|
-
canvas driving a drag or a simulation silently disagreed with it.
|
|
1521
|
-
|
|
1522
|
-
`<SceneViewCanvas>` now paints through `useFrameLoop` instead of from React, and
|
|
1523
|
-
subscribes to `scene.overrides`. A render (prop change or version bump) and an
|
|
1524
|
-
override commit both just mark the surface dirty, and one animation frame
|
|
1525
|
-
coalesces them — so a 60 Hz override loop repaints these views with no React
|
|
1526
|
-
render, and a backgrounded tab stops painting them entirely. The mount paint
|
|
1527
|
-
stays synchronous, so the first frame is still the scene rather than a blank
|
|
1528
|
-
canvas. `<MinimapCanvas>` inherits all of this through it.
|
|
1529
|
-
|
|
1530
|
-
Repaints driven by a prop change are now asynchronous: they land on the next
|
|
1531
|
-
animation frame rather than in the layout effect of the render that caused them.
|
|
1532
|
-
Code that renders and then reads pixels in the same tick needs to wait a frame.
|
|
1533
|
-
|
|
1534
|
-
A minimap's *framing* still derives from document poses, so a node overridden
|
|
1535
|
-
outside the document bounds paints outside the fitted frame — recomputing the
|
|
1536
|
-
fit per frame would rescale the whole minimap throughout a settle.
|
|
1537
|
-
- ce82f4a: An enum leaf can ask for a segmented control, and `pair` works inside an object
|
|
1538
|
-
|
|
1539
|
-
`ToolPrefEnumControl` gains `'toggle'`: a three-option enum shows all three at
|
|
1540
|
-
once instead of hiding two behind a select. Options carry an optional `short`
|
|
1541
|
-
label — a capital or two — for the width a property row has; the full `label`
|
|
1542
|
-
stays the accessible name, so the abbreviation never becomes the only thing
|
|
1543
|
-
naming the option. A mixed selection selects no segment rather than picking a
|
|
1544
|
-
winner.
|
|
1545
|
-
|
|
1546
|
-
`pair` now merges fields inside an object leaf, as it already did for section
|
|
1547
|
-
rows — a hint shouldn't mean something different for being a field of a value
|
|
1548
|
-
rather than a sibling of one. It merges *adjacent* leaves in both places, so
|
|
1549
|
-
the schema orders family, size, weight: size and weight pair, and family (which
|
|
1550
|
-
sat between them) moves ahead of the pair rather than splitting it.
|
|
1551
|
-
|
|
1552
|
-
A stroke's cap, join and align share one row; property rows wrap rather than
|
|
1553
|
-
overflow when the controls in them don't fit.
|
|
1554
|
-
- be697dc: Add ephemeral pose overrides to the scene
|
|
1555
|
-
|
|
1556
|
-
`scene.overrides` holds a per-node `{ pose?, alpha? }` that the render and
|
|
1557
|
-
hit-test paths read through and that history, `toJSON()` and `getVersion()`
|
|
1558
|
-
never see. It is additive: a scene with no overrides behaves exactly as before.
|
|
1559
|
-
|
|
1560
|
-
This is where per-frame motion belongs. A 60 Hz loop previously had to write
|
|
1561
|
-
through `setPose`, which records an undo entry (one per frame at best, batched)
|
|
1562
|
-
and bumps the scene version, re-rendering every `useSyncExternalStore`
|
|
1563
|
-
subscriber. It also had to allocate a fresh pose object per moving node per
|
|
1564
|
-
frame, because the painter memo keys on pose reference. An override entry is
|
|
1565
|
-
hoisted once and mutated in place; `overrides.commit()` publishes the frame and
|
|
1566
|
-
invalidates the memo for the overridden nodes only.
|
|
1567
|
-
|
|
1568
|
-
`commit()` is required after an in-place mutation — without it the memo serves
|
|
1569
|
-
the previous frame's draw. Overrides are cleared when a node is removed, since
|
|
1570
|
-
ids are reusable. To make a frame permanent, write it once through `setPose`
|
|
1571
|
-
and clear the override; that single step is the undo entry.
|
|
1572
|
-
|
|
1573
|
-
`ForceGraphDemo` now settles with zero history entries and bakes the result as
|
|
1574
|
-
one, replacing a per-tick batch of 24 `setPose` calls.
|
|
1575
|
-
- e909a3b: `fitTextPose` sizes a box the renderer will actually fill
|
|
1576
|
-
|
|
1577
|
-
It was the fourth site measuring text its own way: `ctx.measureText` per
|
|
1578
|
-
character against system fonts, no kerning, `pose.text` only. Nothing masked
|
|
1579
|
-
it the way the WebGL context masked the caret — a consumer calling it got a
|
|
1580
|
-
box that disagreed with the paint, narrower by a kern on every pair and wrong
|
|
1581
|
-
by the whole difference between the installed family and the registered face.
|
|
1582
|
-
It goes through the shared layout now, so it sees kerning and per-run styling.
|
|
1583
|
-
|
|
1584
|
-
**Breaking:** `fitTextPose(ctx, pose, opts)` is now `fitTextPose(pose, opts)`.
|
|
1585
|
-
- 26bbdcf: Paint the canvas from its own animation frame instead of from a React render
|
|
1586
|
-
|
|
1587
|
-
`requestRedraw()` marks the surface dirty and the next frame paints, so many
|
|
1588
|
-
redraws in one tick cost one paint. The view gains an imperative path on the
|
|
1589
|
-
canvas handle — `setView` / `getView` / `subscribeView` — and `SceneCanvas` no
|
|
1590
|
-
longer holds it in React state, so a camera moving at 60 Hz costs no renders.
|
|
1591
|
-
Consumers passing a `view` prop stay controlled and are unaffected.
|
|
1592
|
-
|
|
1593
|
-
Opt-ins that come with it: `syncPaint` paints inside the commit for a consumer
|
|
1594
|
-
that wants the old whole-cloth guarantee, `useScene(…, { subscribe: false })`
|
|
1595
|
-
gives a host the scene without a render per mutation, `useSceneTextEdit`'s
|
|
1596
|
-
`view` option accepts a thunk so the overlay tracks a ref-driven camera, and a
|
|
1597
|
-
`contentVersion` prop feeds the version that `getPaintedVersion()` reports.
|
|
1598
|
-
|
|
1599
|
-
Two public signatures changed. `usePinchZoomTool` takes a view getter,
|
|
1600
|
-
`getView: () => View`, where it took a `View` — nothing re-renders to refresh a
|
|
1601
|
-
captured value any more. `CanvasExtensionApi` gained five required members —
|
|
1602
|
-
`getView`, `setView`, `subscribeView`, `subscribeFrame`, `getPaintedVersion` —
|
|
1603
|
-
so external code hand-implementing that interface stops typechecking; code that
|
|
1604
|
-
only calls through the ref is unaffected.
|
|
1605
|
-
|
|
1606
|
-
Pixels and DOM can now be a frame apart, in whichever direction the change came
|
|
1607
|
-
from. A view change leads with pixels: `setView` paints without rendering, so
|
|
1608
|
-
DOM built from the view is stale until something re-renders it — position
|
|
1609
|
-
world-anchored DOM from `subscribeView`. A scene change leads with DOM:
|
|
1610
|
-
`SceneCanvas` still subscribes to the scene, so a `batch` commits now and the
|
|
1611
|
-
pixels land next frame — compare `getPaintedVersion()` against the version you
|
|
1612
|
-
are about to render when chrome must be in lockstep. Do not render scene-derived
|
|
1613
|
-
DOM inside `startTransition`: React defers it and nothing forces it to catch up.
|
|
1614
|
-
|
|
1615
|
-
Anything reading the drawing buffer back outside a paint — the hud loupe's pixel
|
|
1616
|
-
mode is the one in-tree case — can likewise see a buffer one frame older;
|
|
1617
|
-
`subscribeFrame` runs on the frame that painted and removes the lag. Nothing
|
|
1618
|
-
paints while `document.hidden` is true, `syncPaint` included, so a readback from
|
|
1619
|
-
a background tab returns the frame from before the tab was hidden.
|
|
1620
|
-
- 546f67d: Copy typed-array arguments into `makeGLRecorder`'s call log as they are
|
|
1621
|
-
recorded. A caller is entitled to reuse the array it uploads from, so storing
|
|
1622
|
-
the reference recorded a value that later frames overwrote — a test reading
|
|
1623
|
-
two frames back saw the same numbers twice and passed. Test-only surface.
|
|
1624
|
-
- 3fb3a46: Release held keys when the window loses focus
|
|
1625
|
-
|
|
1626
|
-
A window that blurs mid-hold never delivers the keyup, so every in-flight
|
|
1627
|
-
`key-held` handle stayed engaged until that key was pressed again — holding
|
|
1628
|
-
Space and tabbing away left the hand tool on the hotkey stack indefinitely.
|
|
1629
|
-
|
|
1630
|
-
The gesture dispatcher now fires the `key-held` up phase for each held key on
|
|
1631
|
-
window blur. Consumers that hand-rolled this reset can drop it; ongoing
|
|
1632
|
-
invocations see a normal `onEnd`.
|
|
1633
|
-
- ccd51cc: Add a 43-glyph monochrome icon set to `@weasel-js/ui`.
|
|
1634
|
-
|
|
1635
|
-
One register: a 20x20 viewBox drawn in `currentColor` at stroke-width 1.5 with
|
|
1636
|
-
round caps and joins, hairline weight reserved for structure, and filled
|
|
1637
|
-
regions only where an action has a subject. Covers transport, history, view,
|
|
1638
|
-
trial lifecycle, collection, state, instrument and status vocabulary. Import a
|
|
1639
|
-
named component (`CloneIcon`), or `Icon` when the glyph is chosen at runtime.
|
|
1640
|
-
|
|
1641
|
-
`@weasel-js/ui` also re-exports the tool glyphs that live in `@weasel-js/core`,
|
|
1642
|
-
so consumers have one import site for the whole set. `ImageIcon` was reachable
|
|
1643
|
-
from core's icons folder but missing from its public barrel; it is exported
|
|
1644
|
-
now.
|
|
1645
|
-
|
|
1646
|
-
Glyph geometry is generated (`npm run gen:icons`) from `packages/ui/scripts/icons/`
|
|
1647
|
-
rather than hand-placed, because arrowheads and joins that miss their terminus
|
|
1648
|
-
are invisible at chrome size.
|
|
1649
|
-
- 3fb3a46: Compose `before` and `after` layer chains in both directions
|
|
1650
|
-
|
|
1651
|
-
`composeOrderedLayers` walked the two anchor maps separately: a chain hanging
|
|
1652
|
-
off an `after` anchor only followed further `after` links, and likewise for
|
|
1653
|
-
`before`. A custom layer anchored `before: 'scene'` carrying a second custom
|
|
1654
|
-
anchored `after` it dropped that second layer to the tail with a spurious
|
|
1655
|
-
dangling-reference warning.
|
|
1656
|
-
|
|
1657
|
-
Both walks now emit a layer's `before` chain, the layer, then its `after`
|
|
1658
|
-
chain, so the two mix freely. Cycle detection and orphan fallback are
|
|
1659
|
-
unchanged.
|
|
1660
|
-
- d9f110e: Stop every frame loop while nothing can see it
|
|
1661
|
-
|
|
1662
|
-
New public hook `useVisibleRaf` in `@weasel-js/core` owns the question of
|
|
1663
|
-
whether a frame may run: nothing runs while `document.hidden`, and a loop that
|
|
1664
|
-
names an element also stops while that element is outside the viewport. A
|
|
1665
|
-
request made while suspended is held rather than dropped and re-armed on
|
|
1666
|
-
resume, so a loop never polls visibility or needs restarting by hand.
|
|
1667
|
-
|
|
1668
|
-
Ten loops now run behind it — `useFrameLoop`, `useAnimator`, `useSimulation`,
|
|
1669
|
-
`useDecayLoop`, `useTextEdit`'s overlay follow, `CursorCoordsHud`'s FPS
|
|
1670
|
-
counter, `Badge`'s crawl, and labkit's `FpsMeter`, `useTiledSurface` and
|
|
1671
|
-
`useLayerScheduler`. Only `useFrameLoop` consulted `document.hidden` before;
|
|
1672
|
-
the rest ran on any page left open. `useLayerScheduler` looked safe and wasn't:
|
|
1673
|
-
it paints only dirty layers, but a hidden tab still commits React updates and
|
|
1674
|
-
its view/size effect marks every layer dirty.
|
|
1675
|
-
|
|
1676
|
-
Loops measuring elapsed time rebase their clock through the new `onResume`
|
|
1677
|
-
option, so an hour spent hidden does not arrive as one hour-long frame — an FPS
|
|
1678
|
-
meter reporting a rate nobody achieved, a tween jumping to its end value on
|
|
1679
|
-
return. `dangerouslyRunWhenHidden` opts a loop out for offscreen recording or
|
|
1680
|
-
export; nothing in the tree sets it.
|
|
1681
|
-
|
|
1682
|
-
`npm run check:frame-loops` fails the build on a bare `requestAnimationFrame`
|
|
1683
|
-
in kit source, and runs in CI.
|
|
1684
|
-
- 0dd35a1: Fix pinch-to-zoom: mac trackpads zoomed the page, and `viewport.pinchZoom` zoomed twice
|
|
1685
|
-
|
|
1686
|
-
A trackpad pinch reaches the page as `wheel { ctrlKey: true }`. On a mac
|
|
1687
|
-
`viewport.zoom`'s `mods: { mod: true }` binding requires metaKey and forbids
|
|
1688
|
-
ctrl, and `viewport.wheelPan` forbids ctrl too, so nothing claimed the event
|
|
1689
|
-
and the browser's own ctrl+wheel page zoom ran. `viewport.zoom` now carries a
|
|
1690
|
-
second wheel binding on bare ctrl. Off mac it duplicates the `mod` binding,
|
|
1691
|
-
where the matcher picks a single winner.
|
|
1692
|
-
|
|
1693
|
-
Nothing caught that because `IS_MAC` read `navigator.platform ?? userAgent`,
|
|
1694
|
-
and jsdom reports an empty-string platform — not nullish, so the fallback never
|
|
1695
|
-
fired and every mac binding in the kit was exercised only on the non-mac
|
|
1696
|
-
branch. It reads `||` now.
|
|
1697
|
-
|
|
1698
|
-
Separately, `viewport.pinchZoom: true` mounted `<Canvas>`'s `usePinchZoomTool`
|
|
1699
|
-
alongside the `viewport.pinchZoom` action that already handled the same
|
|
1700
|
-
gesture, applying one pinch's factor twice — the opt-in broke the path that
|
|
1701
|
-
worked without it. SceneCanvas drives pinch through the action alone, and the
|
|
1702
|
-
flag configures it: new `makePinchZoomAction({ min, max })` (exported), with
|
|
1703
|
-
the kit's 0.1–8 clamp now applied by default. `pinchZoom: false` disables pinch
|
|
1704
|
-
for real; it previously left the action running. Bare `<Canvas>` keeps the hook
|
|
1705
|
-
as its own pinch path.
|
|
1706
|
-
- 1a0bea3: `useNodeOverlayFrame`: the coordinate frame a DOM overlay pinned to a node needs
|
|
1707
|
-
|
|
1708
|
-
Nothing in the kit exported one, so consumers hand-rolled it — their own
|
|
1709
|
-
`ResizeObserver` next to the existing `useCanvasSize`, and a translate-and-scale
|
|
1710
|
-
inverse built by projecting two points. That inverse silently drops
|
|
1711
|
-
`pose.rotation`, which is why on-canvas gradient handles on a rotated node sat
|
|
1712
|
-
beside the paint instead of on it.
|
|
1713
|
-
|
|
1714
|
-
```ts
|
|
1715
|
-
useNodeOverlayFrame(scene, containerRef, nodeId, { view })
|
|
1716
|
-
// → { box, toScreen, toLocal, width, height } | null
|
|
1717
|
-
```
|
|
1718
|
-
|
|
1719
|
-
`box` is the node's composed world box, unrotated — the frame `toScreen` maps
|
|
1720
|
-
from, and the box to hand `fillInPoseFrame` / `fillToBoundsFrame`. Rotation
|
|
1721
|
-
lives in the pose→world leg, where it belongs: a node's stored geometry and its
|
|
1722
|
-
bounds-frame paint are pre-rotation by definition, so neither of those two
|
|
1723
|
-
changes.
|
|
1724
|
-
|
|
1725
|
-
`@weasel-js/ui` gains `SceneGradientHandles`, the scene-aware half of
|
|
1726
|
-
`GradientHandles`: it reads the gradient out of a node's `fill` **or** its
|
|
1727
|
-
`stroke` — `slot` is a prop — and commits each drag through `setFill` or
|
|
1728
|
-
`setStroke` as one undo entry. `GradientHandles` itself stays frame-agnostic.
|
|
1729
|
-
|
|
1730
|
-
Also: `isGradientFill` narrows a `FillStyle` to its three gradient members, and
|
|
1731
|
-
`useCanvasSize` accepts any `HTMLElement` rather than only a `div`.
|
|
1732
|
-
- 9d95836: A node's `data.stroke` takes a whole `Stroke`, not just a color
|
|
1733
|
-
|
|
1734
|
-
`NodeStroke = string | Stroke`, mirroring `NodeFill`. A string is still a
|
|
1735
|
-
color and `'none'` still skips the stroke; an object is a core `Stroke` whose
|
|
1736
|
-
`width`, `cap`, `join`, `dash`, `miterLimit` and `align` all reach the
|
|
1737
|
-
renderer, which has accepted them on `PathDrawCommand` all along. The object
|
|
1738
|
-
wins outright over `data.strokeWidth` rather than merging with it, the same
|
|
1739
|
-
rule `withLeafStroke` already applied to text. A bounds-relative stroke paint
|
|
1740
|
-
is baked onto the pose box the way a fill is, so a gradient stroke resolves
|
|
1741
|
-
against the box it was authored against.
|
|
1742
|
-
|
|
1743
|
-
`kit:shape` now honors `stroke: 'none'`, which only `kit:path` checked before.
|
|
1744
|
-
|
|
1745
|
-
`NodeInk` reports `{ filled, outset, inset }` instead of `{ filled,
|
|
1746
|
-
strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
1747
|
-
`'outer'` none inside, which one number could not say, so picking grabbed the
|
|
1748
|
-
wrong side. `ink` takes an optional context carrying the view scale, so a
|
|
1749
|
-
`{ px }` stroke width resolves to world units. A painter that still returns
|
|
1750
|
-
`{ filled, strokeWidth }` is read as a centered stroke and keeps working.
|
|
1751
|
-
|
|
1752
|
-
`setStroke` and `setStrokeOpacity` no longer stringify a node's `Stroke`: a
|
|
1753
|
-
color pick replaces its paint and keeps width, cap, join and dash, and an
|
|
1754
|
-
opacity drag sets the paint's `opacity`, which is the only form that works on
|
|
1755
|
-
a gradient stroke.
|
|
1756
|
-
|
|
1757
|
-
Editing UI for the rich form is not here yet — a schema-driven color control
|
|
1758
|
-
still writes a bare string over the object, so nodes carrying one are for
|
|
1759
|
-
programmatic authorship until `SelectionPanel` learns the union. See
|
|
1760
|
-
`docs/proposals/2026-08-26-node-stroke-union.md`.
|
|
1761
|
-
- 62a3c46: Paint a gradient or pattern stroke instead of throwing.
|
|
1762
|
-
|
|
1763
|
-
`Stroke.paint` has always been a full `FillStyle`, and SVG import puts paint
|
|
1764
|
-
servers there deliberately, but the renderer refused anything but a solid — so
|
|
1765
|
-
importing a shape with `stroke="url(#grad)"` produced a scene that threw on the
|
|
1766
|
-
next frame. Both stroke paths now paint the ribbon through the same route a
|
|
1767
|
-
fill takes, including under the inner/outer alignment stencil. A non-solid
|
|
1768
|
-
even-odd fill no longer renders black.
|
|
1769
|
-
- 5f6c28e: An object leaf's fields can be organised into groups
|
|
1770
|
-
|
|
1771
|
-
`ToolPrefObject.children` takes a `ToolPrefGroup` as well as a leaf. A group
|
|
1772
|
-
heads its fields under a label and contributes nothing to the path — the same
|
|
1773
|
-
rule group keys follow at the top level of a schema, so a field inside one is
|
|
1774
|
-
still addressed as a field of the object.
|
|
1775
|
-
|
|
1776
|
-
Without it, a value with many fields renders as one undifferentiated list. A
|
|
1777
|
-
`TextStyle` is the case that needs it: its character and paragraph fields are
|
|
1778
|
-
one value but read as two lists.
|
|
1779
|
-
- 3cd1ee8: A schema leaf can hold an object, with its fields hanging off it
|
|
1780
|
-
|
|
1781
|
-
A compound value — a stroke, a shadow, a pattern spec — could be described as
|
|
1782
|
-
sibling leaves addressing into it (`data.stroke.width`, `data.stroke.cap`).
|
|
1783
|
-
It shouldn't be: each control then writes one field of a value it can only
|
|
1784
|
-
half see, and writing a field into something that isn't an object yet corrupts
|
|
1785
|
-
it outright.
|
|
1786
|
-
|
|
1787
|
-
`ToolPrefObject` describes the value instead. Its `children` are ordinary
|
|
1788
|
-
leaves whose paths are relative to the object, and every child edit commits
|
|
1789
|
-
the parent object whole. A field that is itself a union declares the kind that
|
|
1790
|
-
edits that union — a stroke's `paint` is a `paint` leaf. `fromScalar` lifts a
|
|
1791
|
-
value still held in a scalar form before a child edit lands on it, which is
|
|
1792
|
-
how a stroke stored as a bare colour string gains a width.
|
|
1793
|
-
|
|
1794
|
-
`defaultNodeProperties` describes `data.stroke` this way, so the panel shows
|
|
1795
|
-
Color, Width, Cap, Join and Align under one Stroke block, and the separate
|
|
1796
|
-
`data.strokeWidth` leaf is gone. `SelectionPanel` now honours `block`, which
|
|
1797
|
-
`PrefsForm` already did. The one-off `stroke` pref kind added days ago is
|
|
1798
|
-
replaced by this general one.
|
|
1799
|
-
|
|
1800
|
-
`dash` has no leaf: it is a `number[]` and no kind edits one. It survives
|
|
1801
|
-
import, export and rendering untouched.
|
|
1802
|
-
- 2ea772f: Selection handles are hit-tested at the size they are painted
|
|
1803
|
-
|
|
1804
|
-
Handles painted at `HANDLE_BASE_PX * targetScale` and hit-tested at the bare
|
|
1805
|
-
constant, and neither `buildAffordanceAt` call site passed the option that
|
|
1806
|
-
would have scaled it. A coarse pointer got a bigger picture and exactly the
|
|
1807
|
-
same 8px grab zone it had on a mouse — the touch forgiveness the coarse profile
|
|
1808
|
-
exists to provide never reached the hit-test. The slops debug overlay was a
|
|
1809
|
-
third unscaled copy, so it drew hit regions where they were not.
|
|
1810
|
-
|
|
1811
|
-
`core/device/targets.ts` now holds one base table and one accessor,
|
|
1812
|
-
`targetSizesPx(targetScale)`. Paint, hit-test and the debug overlay all resolve
|
|
1813
|
-
through it. `HANDLE_BASE_PX`, `ANCHOR_HIT_BASE_PX` and
|
|
1814
|
-
`ROTATION_HANDLE_BASE_PX` keep their names and values and now read off the
|
|
1815
|
-
table; the internal `HANDLE_HIT_RADIUS` and `ANCHOR_HIT_RADIUS` are gone.
|
|
1816
|
-
|
|
1817
|
-
`buildAffordanceAt` and `createSlopsDebugLayer` take an optional `targetScale`.
|
|
1818
|
-
`selectTool.handleHitRadius` now actually reaches the hit-test — it previously
|
|
1819
|
-
reached nothing.
|
|
1820
|
-
|
|
1821
|
-
`useRotateTool`'s `handleHitRadius` option is **removed**. The rotation
|
|
1822
|
-
affordance is an annulus with a band thickness and no point radius, so the
|
|
1823
|
-
option could only ever have been a second name for `rotationHandleDistance`,
|
|
1824
|
-
which is live and now defaults from the same table.
|
|
1825
|
-
|
|
1826
|
-
Known gap: `CanvasView` is a second `buildAffordanceAt` call site that reads no
|
|
1827
|
-
device profile, so a nested view still hit-tests at the fine-pointer size.
|
|
1828
|
-
- f77bd95: `getChildren` means one thing on an adapter
|
|
1829
|
-
|
|
1830
|
-
`MoveAdapter` declared `getChildren(id)` — a node's direct children, for the
|
|
1831
|
-
drag cascade — and `OrderedAdapter` declared `getChildren(parentId | null)`,
|
|
1832
|
-
the z-ordering seam where `null` means the root. Both land on the same adapter
|
|
1833
|
-
object, so `arrayAdapter` took the first shape from its config and exposed it
|
|
1834
|
-
under the name the ops read with the second meaning. An op asking for root
|
|
1835
|
-
order got `[]`, which reads as "the root has no siblings", and the slot it
|
|
1836
|
-
captured was silently lost.
|
|
1837
|
-
|
|
1838
|
-
The two declarations are now one contract, and `arrayAdapter` answers the root
|
|
1839
|
-
from its own item array rather than delegating — a consumer callback written
|
|
1840
|
-
for node ids returns `[]` there, which cannot be told apart from a genuine
|
|
1841
|
-
empty answer. A consumer's `getChildren` config is still only ever asked about
|
|
1842
|
-
a node id.
|
|
1843
|
-
|
|
1844
|
-
`arrayAdapter` still exposes no `setChildOrder`, so it places by ordinal rather
|
|
1845
|
-
than by anchor. That is unchanged, and it is why the ordinal fallback exists.
|
|
1846
|
-
- 2ea772f: The canvas and the gradient editor now sample one gradient
|
|
1847
|
-
|
|
1848
|
-
`buildGradientRamp` carried its own interpolation beside
|
|
1849
|
-
`sampleGradientStops`, and the two disagreed three ways: the ramp had no guard
|
|
1850
|
-
at either end and extrapolated past the first and last stop, the two picked
|
|
1851
|
-
opposite sides of a coincident pair, and they parsed color differently — a stop
|
|
1852
|
-
written as a CSS named color rendered on the canvas and threw in the editor.
|
|
1853
|
-
|
|
1854
|
-
`sampleGradientStops` keeps its semantics and is now the only implementation.
|
|
1855
|
-
`resolveGradientStops` sorts and parses the list once; `sampleResolvedStops`
|
|
1856
|
-
returns the color at `t`. The ramp cache builds its texels through those, so
|
|
1857
|
-
there is no interpolation math left in the renderer.
|
|
1858
|
-
|
|
1859
|
-
Two behavior changes worth naming. `resolveColor` is the surviving parser, so
|
|
1860
|
-
gradient stops accept named and functional colors everywhere — but no longer
|
|
1861
|
-
hex without a leading `#`, which only the editor path had tolerated and the
|
|
1862
|
-
canvas never accepted. And `sampleGradientStops` returns normalized hex at the
|
|
1863
|
-
endpoints instead of echoing the raw stop string, so `'red'` comes back as
|
|
1864
|
-
`'#ff0000'`.
|
|
1865
|
-
|
|
1866
|
-
**SVG export:** a conic gradient left the exporter as a dangling `url(#…)` —
|
|
1867
|
-
the element already carried the reference, the built-in serializer returned
|
|
1868
|
-
nothing, and the registry's `toSvg` slot has no in-repo implementation, so the
|
|
1869
|
-
shape disappeared in a browser with no warning at all. Serialization now falls
|
|
1870
|
-
through to the same warning the pattern path already emits when nothing can
|
|
1871
|
-
produce a paint server. A consumer that registers a `toSvg` for
|
|
1872
|
-
`conic-gradient` still serializes and gets no warning.
|
|
1873
|
-
- aba8d91: Answer "can this node be hit" in one place
|
|
1874
|
-
|
|
1875
|
-
Four tree walks answered it separately — the generic-adapter point pick, the
|
|
1876
|
-
one `<SceneCanvas>` installs, `sceneToAdapter`'s area walk, and the live
|
|
1877
|
-
marquee/lasso — plus a fifth that shadowed the third. They agreed on every case
|
|
1878
|
-
that had a test and disagreed on the rest, three times, silently. `pickWalk`
|
|
1879
|
-
now owns every gate; a query supplies only its own shape test and the clip
|
|
1880
|
-
predicate for its region.
|
|
1881
|
-
|
|
1882
|
-
Behavior that changes as a result:
|
|
1883
|
-
|
|
1884
|
-
- **A node painted at alpha 0 is no longer clickable.** The pick path reads the
|
|
1885
|
-
same number the painter does — the view's `alphaFor` times any per-node
|
|
1886
|
-
override alpha — so a node faded out of sight stops claiming clicks. The
|
|
1887
|
-
floor is exactly zero, so a fade-in is pickable from its first nonzero frame.
|
|
1888
|
-
Alpha is per view: dimming a node in one view leaves it pickable in another.
|
|
1889
|
-
- **A layer that is not painted no longer claims pointer events.** `drawLayers`
|
|
1890
|
-
drops any layer missing from a supplied `layerOrder`, and the chrome hit path
|
|
1891
|
-
only consulted `layerVisibility`. Both gates now run through one
|
|
1892
|
-
`isLayerPainted`, which is exported.
|
|
1893
|
-
- `sceneToAdapter`'s area walk reads override poses and hidden layers, which it
|
|
1894
|
-
did not; its default `poseBounds` answers a path pose instead of `NaN`, which
|
|
1895
|
-
is what the shadow walk existed to work around.
|
|
1896
|
-
- An ancestor clip now rejects an area query that reaches into the clip where
|
|
1897
|
-
the node is not, or reaches the node where the clip is not — the two terms
|
|
1898
|
-
together, where one alone let false positives through.
|
|
1899
|
-
|
|
1900
|
-
`useSceneSelectTool` takes `alphaOf` and `layerIsPainted` for the asking view.
|
|
1901
|
-
`passesAncestorClips` and its module are gone; `pickWalk`, `scenePickSource`,
|
|
1902
|
-
`adapterPickSource` and `ownClipOf` replace them.
|
|
1903
|
-
- 2ea772f: A drag-to-insert reports the bounds it paints
|
|
1904
|
-
|
|
1905
|
-
The painter, the commit factory and `getGestureBounds()` each sized an
|
|
1906
|
-
in-flight insert differently. The reporter read the drag rect alone, so a
|
|
1907
|
-
centered Alt-drag reported a half-extent of `d` against a painted circumradius
|
|
1908
|
-
of `d√2`, a purely horizontal Alt-drag reported **height 0** for a visibly tall
|
|
1909
|
-
star, and a pencil scribble that looped back to its start reported nothing at
|
|
1910
|
-
all. The painter and the commit agreed on polygon and star but not on line or
|
|
1911
|
-
pencil: the commit posed the drag AABB for a line the painter drew endpoint to
|
|
1912
|
-
endpoint, and fell back to the drag rect for a trail under four samples.
|
|
1913
|
-
|
|
1914
|
-
One function now answers it for all three. The zero-area skip in the painter
|
|
1915
|
-
and the reporter tests the resolved extent rather than the raw drag rect, and
|
|
1916
|
-
an `InsertNodeFactory` that returns no `pose` falls back to the extent. The
|
|
1917
|
-
`bounds` argument handed to a factory is unchanged.
|
|
1918
|
-
- 3386d64: Path command opcodes derive from one table
|
|
1919
|
-
|
|
1920
|
-
`M`/`L`/`C`/`Q`/`Z` and their coordinate counts were declared five times —
|
|
1921
|
-
once in core, once in `@weasel-js/geom`, and three more as `COORD_COUNT`
|
|
1922
|
-
literals in the path transform, pose-rotation and pose-descriptor walkers. They
|
|
1923
|
-
agreed, and nothing held them to each other: a sixth opcode desynchronizes two
|
|
1924
|
-
packages' reading of the same `Uint8Array` with no exception and no type error,
|
|
1925
|
-
and every walker misparses the coordinate stream from that command on.
|
|
1926
|
-
|
|
1927
|
-
`PATH_COMMANDS` in `@weasel-js/geom` is now the table. `PATH_M`…`PATH_Z`,
|
|
1928
|
-
`PATH_CMD_LENGTHS` and the new `pathCommandCoordCount` all derive from it, and
|
|
1929
|
-
core re-exports them by name, so the opcode constants keep their names, values
|
|
1930
|
-
and literal types. The three walkers moved onto `forEachSegment` rather than
|
|
1931
|
-
onto the accessor alone — they were duplicating the coordinate-cursor advance
|
|
1932
|
-
as well as the length, and the cursor is the half that actually misreads.
|
|
1933
|
-
|
|
1934
|
-
Eight further files switch on these opcodes with inline literals. Five throw on
|
|
1935
|
-
an unknown code; three — the path boolean adapter, the anchor-editing geometry,
|
|
1936
|
-
and geom's own boolean adapter — have no `default` arm and would silently stop
|
|
1937
|
-
advancing. Left as-is; they need per-command semantics, not one walker.
|
|
1938
|
-
- 68d2651: Pref leaf kinds are declared once, and every renderer is exhaustive
|
|
1939
|
-
|
|
1940
|
-
`@weasel-js/ui` carried its own copy of the pref-leaf union under a comment
|
|
1941
|
-
saying to keep it in sync with core's field-for-field. It had drifted: ui's enum
|
|
1942
|
-
leaf had neither `encoding` nor `options[].disabled`, so a dash-array
|
|
1943
|
-
preference did not merely fail to select — choosing an option wrote the option
|
|
1944
|
-
string over the stored dash array. labkit's two renderers were missing the
|
|
1945
|
-
`paint` and `object` kinds outright.
|
|
1946
|
-
|
|
1947
|
-
ui's schema is now a rename re-export of core's declaration. The public `Pref*`
|
|
1948
|
-
names are unchanged, and there is nothing left to keep in sync.
|
|
1949
|
-
|
|
1950
|
-
More importantly, all four renderer switches ended in `default:`, so adding a
|
|
1951
|
-
built-in kind produced no error at any site and simply rendered nothing —
|
|
1952
|
-
verified by adding one and typechecking. `ToolPrefLeaf` widens `kind` to
|
|
1953
|
-
`string` so app-defined prefs can ride the same tree, which means a `never`
|
|
1954
|
-
guard cannot sit on it directly. New from core: `TOOL_PREF_KINDS`, a
|
|
1955
|
-
`Record<ToolPrefKind, true>` that a new kind fails to compile against first, and
|
|
1956
|
-
`isBuiltinToolPref(leaf)`, which narrows to the closed union so each renderer
|
|
1957
|
-
can discriminate and end in a `never`. App-defined kinds take the placeholder
|
|
1958
|
-
path as before.
|
|
1959
|
-
|
|
1960
|
-
Dash-array preferences now select and commit correctly in `PrefsForm`: the enum
|
|
1961
|
-
arm threads sibling values, routes through `encoding.read` / `encoding.write`,
|
|
1962
|
-
and honors `option.disabled`. `SelectionPanel` already did all of this — it was
|
|
1963
|
-
only the forked copy that could not express it.
|
|
1964
|
-
- 3386d64: Dragging out a text box shows a live preview
|
|
1965
|
-
|
|
1966
|
-
The set of insertable kinds and the `KitInsertShape` union sat on adjacent
|
|
1967
|
-
lines with no linkage, and seven more sites restated one list or the other. The
|
|
1968
|
-
drift was already live: the text tool binds `actionId: 'insert'` and commits
|
|
1969
|
-
through the insert dep, but the runtime set never listed `text`, so a
|
|
1970
|
-
drag-to-insert text box had no preview.
|
|
1971
|
-
|
|
1972
|
-
`SHAPE_KINDS` is now one descriptor table — a row per kind, flagged for whether
|
|
1973
|
-
it has a built-in tool and whether it takes an insert preview. Both unions,
|
|
1974
|
-
`KIT_SHAPE_KINDS`, `BUNDLE_TOOLS.exhaustive`, the known-builtin-id list and the
|
|
1975
|
-
preview gate all derive from it.
|
|
1976
|
-
|
|
1977
|
-
Two type-surface consequences. `KIT_SHAPE_KINDS` is typed
|
|
1978
|
-
`readonly BuiltinShapeToolId[]` rather than a literal tuple — same contents,
|
|
1979
|
-
same order, and `(typeof KIT_SHAPE_KINDS)[number]` is unchanged; what goes is
|
|
1980
|
-
positional and length typing, which nothing uses. And `OngoingOverlay['shape']`
|
|
1981
|
-
gains `'text'`, which is the fix itself: a consumer switching exhaustively over
|
|
1982
|
-
it gains a case, handled by the existing box arm.
|
|
1983
|
-
- c6c499d: Text layout is computed once, and the caret reads the layout that was painted
|
|
1984
|
-
|
|
1985
|
-
The paint, the pose silhouette and the click-to-edit caret each ran their own
|
|
1986
|
-
walk. The paint went through a memoized `layoutRuns`; the silhouette re-ran
|
|
1987
|
-
`layoutRuns` on every pose change, because it allocates a fresh `ResolvedRun[]`
|
|
1988
|
-
per call and the cache keyed on array identity; and the caret summed
|
|
1989
|
-
`ctx.measureText` per character, which sees no kerning, reads system fonts
|
|
1990
|
-
rather than the registered face, and ignores per-run styling entirely. The
|
|
1991
|
-
caret could therefore answer with a different line, and a different glyph, than
|
|
1992
|
-
the one under the pointer — masked in practice only because it asked a WebGL
|
|
1993
|
-
canvas for a 2D context and got `null`, degrading silently to no caret at all.
|
|
1994
|
-
|
|
1995
|
-
`cachedLayoutRuns` now lives in `@weasel-js/text` beside the function it caches,
|
|
1996
|
-
and all three go through it. It keeps the array-identity `WeakMap` as the
|
|
1997
|
-
renderer's zero-cost path and falls through to a bounded LRU keyed on the runs'
|
|
1998
|
-
structure, which is what lets a caller that cannot hold a stable array hit it —
|
|
1999
|
-
about 230× cheaper than laying out again, at roughly 4× the cost of the
|
|
2000
|
-
identity hit. `LaidOutLineBox` carries the caret stops the pen produced, so
|
|
2001
|
-
snapping is to the advance cells the glyphs were actually painted in.
|
|
2002
|
-
|
|
2003
|
-
**Breaking:** `caretIndexAt(ctx, x, y, pose)` is now
|
|
2004
|
-
`caretIndexAt(x, y, pose, opts?)` — the `CanvasRenderingContext2D` is gone, and
|
|
2005
|
-
an optional `maxWidth` mirrors `textLineBoxes` for nodes the `kit:text` painter
|
|
2006
|
-
draws unwrapped. `useSceneTextEdit` no longer acquires a 2D context, so a
|
|
2007
|
-
double-click always seeds the caret instead of falling back to editing from
|
|
2008
|
-
offset 0. `@weasel-js/text` gains a `./test-seams` entry point exporting
|
|
2009
|
-
`_resetLayoutCacheForTests`.
|
|
2010
|
-
- 4f1ef0b: Lay text out from font bytes alone — no baked atlas.
|
|
2011
|
-
|
|
2012
|
-
`registerFontOutlines` was a paint upgrade for a family that already had an
|
|
2013
|
-
MSDF atlas; a family with only font bytes could not resolve, so it rendered
|
|
2014
|
-
nothing. It is now a tier in its own right: `OutlineFace` reports `ascender`,
|
|
2015
|
-
`advanceOf` and `kernOf` in em units, `resolveFontVariant` resolves an
|
|
2016
|
-
outline-only family, and `layoutRuns` reads advances, kerning and the baseline
|
|
2017
|
-
through one source the atlas and a parsed face both satisfy. `outlineMinSize`
|
|
2018
|
-
does not gate such a family — there is no other tier to prefer.
|
|
2019
|
-
|
|
2020
|
-
This does not touch metric neutrality where it applies: a family that has an
|
|
2021
|
-
atlas still resolves to the atlas, so registering outlines cannot move text
|
|
2022
|
-
that was already rendering.
|
|
2023
|
-
|
|
2024
|
-
Also fixes the outline tier in Node. opentype.js publishes ESM under `module`
|
|
2025
|
-
and UMD under `main`; Node takes the UMD build, whose named exports it cannot
|
|
2026
|
-
detect, so `parse` was undefined and every face failed to load — silently, via
|
|
2027
|
-
the fallback to SDF. A browser bundler reading `module` never saw it.
|
|
2028
|
-
|
|
2029
|
-
Breaking for a consumer-supplied `OutlineParser`: a face must now report
|
|
2030
|
-
metrics as well as geometry.
|
|
2031
|
-
- 0114abf: Add `PaintInput`, a control that edits a whole `FillStyle`.
|
|
2032
|
-
|
|
2033
|
-
A kind bar over a per-kind body, driven by the paint-kind registry rather than
|
|
2034
|
-
a fixed list, so a consumer's registered kind appears in the bar and renders
|
|
2035
|
-
that entry's `Editor`. `SelectionPanel`'s `paint` leaf renders it in place of
|
|
2036
|
-
the chip that showed a gradient as indeterminate and wrote a solid over it on
|
|
2037
|
-
first touch — so the checkerboard now means a mixed selection and nothing else,
|
|
2038
|
-
and a gradient stroke is editable rather than merely paintable.
|
|
2039
|
-
|
|
2040
|
-
Switching kinds keeps a per-kind memory for the control's lifetime, so
|
|
2041
|
-
linear -> solid -> linear comes back with its stops instead of the ramp
|
|
2042
|
-
`withGradientKind` cannot carry.
|
|
2043
|
-
|
|
2044
|
-
`PatternPicker` moves from WeaselDraw into `@weasel-js/ui`, which now depends
|
|
2045
|
-
on `@weasel-js/svg` for its tile previews.
|
|
2046
|
-
|
|
2047
|
-
The bar offers **None**: "what kind of paint is this?" takes no-paint as an
|
|
2048
|
-
answer. `setFill` and `setStroke` accept `paint: null` to write it — a fill
|
|
2049
|
-
becomes `null`, and a stroke goes away entirely rather than keeping a width
|
|
2050
|
-
that draws no ink. `PaintKindEntry` gains an optional `icon`, and the five
|
|
2051
|
-
built-in kinds carry glyphs so six segments fit a property row.
|
|
2052
|
-
|
|
2053
|
-
`FILL` and `STROKE` are now peer sections: the `appearance` group goes headless
|
|
2054
|
-
and `data.fill` becomes a block leaf. The stroke's paint is no longer paired
|
|
2055
|
-
with its width — a whole paint editor cannot share a row with a slider.
|
|
2056
|
-
- 50bc909: `FillStyle` is open: register a sixth paint kind and it renders, converts
|
|
2057
|
-
frames and serializes.
|
|
2058
|
-
|
|
2059
|
-
`registerPaintKind(entry)` returns a disposer and `_resetPaintKindsForTests`
|
|
2060
|
-
re-seeds the five built-ins, matching the kit's other module-global
|
|
2061
|
-
registries. An entry carries the editor's slots (`label`, `seed`, `colorOf`,
|
|
2062
|
-
`Editor`), a render slot, both frame-conversion directions, and an SVG
|
|
2063
|
-
`<defs>` slot. `listPaintKinds()` enumerates them, and `asPaint` types a
|
|
2064
|
-
consumer's own paint as a `FillStyle` — the union itself stays closed, because
|
|
2065
|
-
opening its discriminant would widen every built-in member.
|
|
2066
|
-
|
|
2067
|
-
Three defects fall out of the same change, each of which a sixth kind hit
|
|
2068
|
-
immediately. The renderer's fill dispatch fell off the end of its switch into
|
|
2069
|
-
an unguarded cast to the gradient union, so an unknown kind read `stops` off a
|
|
2070
|
-
paint with none and threw mid-frame. `fillInPoseFrame` and its inverse returned
|
|
2071
|
-
an unknown kind untouched, leaving it painting in screen space on a node that
|
|
2072
|
-
moves. `<defs>` emitted nothing for a kind `gradientXml` did not know while
|
|
2073
|
-
still writing the `url(#id)` that referenced it.
|
|
2074
|
-
|
|
2075
|
-
Registering a kind now bumps the node memo generation, so a node painted
|
|
2076
|
-
before the registration repaints rather than holding the frame it resolved
|
|
2077
|
-
when the kind was unknown.
|
|
2078
|
-
- 6a06f6d: Node paint is an object: `data.fill` is a `FillStyle`, `data.stroke` a `Stroke`
|
|
2079
|
-
|
|
2080
|
-
Each concept now has exactly one shape. `data.fill` holds a `FillStyle`,
|
|
2081
|
-
`data.stroke` a whole `Stroke`, and `null` on either is an explicit "no paint"
|
|
2082
|
-
where `undefined` takes the painter's fallback. Two new authoring helpers keep
|
|
2083
|
-
hand-written node data short:
|
|
2084
|
-
|
|
2085
|
-
```ts
|
|
2086
|
-
data: { path, fill: solid('#7fb069'), stroke: strokeOf('#1c1c1c', 2) }
|
|
2087
|
-
```
|
|
2088
|
-
|
|
2089
|
-
**Breaking, with no compatibility path.** A document written against the old
|
|
2090
|
-
shapes renders wrong rather than failing, which is accepted:
|
|
2091
|
-
|
|
2092
|
-
- `NodeFill = string | FillStyle` and `NodeStroke = string | Stroke` are gone,
|
|
2093
|
-
and so are the string branches of `resolveNodeFill` / `resolveNodeStroke`.
|
|
2094
|
-
A node holding `fill: '#f00'` now paints the default grey.
|
|
2095
|
-
- `data.strokeWidth` is deleted. A stroke's width is `Stroke.width`.
|
|
2096
|
-
- `data.color` — the legacy alias `kit:path` and the rect fallback read — is
|
|
2097
|
-
deleted. The fallback painter reads `data.fill` like everything else.
|
|
2098
|
-
- `fill: 'none'` is now `fill: null`; `stroke: 'none'` is `stroke: null`.
|
|
2099
|
-
- `NodeInkResult` is gone: a painter's `ink` returns `NodeInk` and nothing
|
|
2100
|
-
else. A painter returning `{ filled, strokeWidth }` no longer type-checks
|
|
2101
|
-
and its reach is read as zero.
|
|
2102
|
-
- `@weasel-js/ui` drops `isStrokeObject`, which existed only to discriminate
|
|
2103
|
-
the union; `strokeColorOf` and `strokeWithColor` lose their string branches.
|
|
2104
|
-
- `@weasel-js/svg`'s `strokeDataFromSvg` returns `Stroke | undefined` instead
|
|
2105
|
-
of a `{ stroke, strokeWidth }` pair, and stops flattening a plain solid
|
|
2106
|
-
stroke into a color. SVG's `fill="none"` imports as `fill: null`.
|
|
2107
|
-
|
|
2108
|
-
**A paint's alpha lives in `opacity`, one slot for every paint kind.** That is
|
|
2109
|
-
the only slot a gradient or a pattern has, so it is the slot all of them use,
|
|
2110
|
-
and the renderer multiplies a hex alpha by it — the two would fight if both
|
|
2111
|
-
carried the value. `solid()` therefore moves an alpha channel out of the hex:
|
|
2112
|
-
`solid('#ff000080')` is `{ color: '#ff0000', opacity: 0.502 }`.
|
|
2113
|
-
|
|
2114
|
-
The four setter actions follow: `setFillOpacity` / `setStrokeOpacity` write
|
|
2115
|
-
`opacity` rather than splicing hex, so they now work on a gradient fill, which
|
|
2116
|
-
they used to leave untouched. `setFill` / `setStroke` given a `color` recolor
|
|
2117
|
-
the node's existing paint through the new `paintWithColor`, keeping its opacity
|
|
2118
|
-
unless the picked color states an alpha of its own — and `setStroke` keeps the
|
|
2119
|
-
stroke's width, cap, join and dash instead of replacing the whole value.
|
|
2120
|
-
|
|
2121
|
-
New exports: `solid`, `strokeOf`, `paintAlpha`, `paintWithAlpha`,
|
|
2122
|
-
`paintWithColor`, `DEFAULT_SHAPE_FILL`.
|
|
2123
|
-
|
|
2124
|
-
`defaultNodeProperties` moves `data.fill` from a `color` leaf to a `paint` one
|
|
2125
|
-
— a color control pointed at a `FillStyle` reads `undefined` off a gradient and
|
|
2126
|
-
writes a bare string over it — and the `data.stroke` object leaf drops its
|
|
2127
|
-
`fromScalar`, which had nothing left to lift.
|
|
2128
|
-
- a37ee0b: Separate a text node's content from its typography, and draw depth only where a label marks it
|
|
2129
|
-
|
|
2130
|
-
The text schema put `data.text` in a group named Text, so the section read
|
|
2131
|
-
TEXT and the row inside it read Text — one word nested in itself — and the
|
|
2132
|
-
style groups below it read as fields of the content string rather than as its
|
|
2133
|
-
siblings. Content is its own section now, with the field full-width because
|
|
2134
|
-
the section already names it.
|
|
2135
|
-
|
|
2136
|
-
A group with an empty `name` renders no heading. That already worked for
|
|
2137
|
-
sections and is now documented on `ToolPrefGroup`, since it is how a schema
|
|
2138
|
-
says "this group organises, it doesn't name": `Character` and `Paragraph`
|
|
2139
|
-
carry the labels, and a `Typography` heading over them named nothing new.
|
|
2140
|
-
It stays opt-in rather than a rule that rolls up any all-group parent —
|
|
2141
|
-
a `Border` over `Top` / `Right` / `Bottom` needs its name.
|
|
2142
|
-
|
|
2143
|
-
Rows under a suppressed heading no longer indent. Depth drawn without a
|
|
2144
|
-
visible parent put `Character` a level deeper than `Content` while being its
|
|
2145
|
-
peer, which is the panel's own tree discipline broken by its own hand.
|
|
2146
|
-
- 611b30e: Layers and deps answer for the view they are drawn for
|
|
2147
|
-
|
|
2148
|
-
Nine lookups closed over the *surface's* state at construction, so they answered
|
|
2149
|
-
for view zero in every view. `<CanvasView>` draws the surface's layer array
|
|
2150
|
-
unchanged and only the draw envelope differs, which makes a `draw: (_data, …)`
|
|
2151
|
-
a guarantee of answering for the wrong view rather than merely an unused
|
|
2152
|
-
argument. A drag in view B ghosted in view A, the marquee painted in the wrong
|
|
2153
|
-
view, chrome-caps resolved against the surface's selection, every Cmd+V centered
|
|
2154
|
-
on the wrong camera, and Escape in view B cancelled view A.
|
|
2155
|
-
|
|
2156
|
-
**New on `CanvasViewHelpers`** — `getPreviewSources()`, `getGestureOverlays()`
|
|
2157
|
-
and `getIsVisible()`. All three are **required members**: anyone hand-writing a
|
|
2158
|
-
`CanvasViewHelpers` (a test double, a wrapper) has to add them.
|
|
2159
|
-
`getIsVisible` **moves off `CanvasSurfaceHelpers`**, where it could only ever
|
|
2160
|
-
have answered for one view.
|
|
2161
|
-
|
|
2162
|
-
**New on `GestureSource`** — `previewSources()` and `overlays()`, also required,
|
|
2163
|
-
alongside the newly exported `GesturePreviewSource`. `toolPreviewSources(tools)`
|
|
2164
|
-
is the tool half.
|
|
2165
|
-
|
|
2166
|
-
**Layer options changed.** `createPathEditingOverlayLayer` and
|
|
2167
|
-
`createSlopsDebugLayer` take `getPose(id, previews)` and have lost their
|
|
2168
|
-
`isVisible` / `selectionRef` / `boundsOf` options — those come off the envelope
|
|
2169
|
-
now. `usePreviewGhostLayer` has lost `tools`. Both it and
|
|
2170
|
-
`useDispatcherOverlayLayer` keep `dispatcher` **only** to subscribe for repaint.
|
|
2171
|
-
|
|
2172
|
-
**Picking takes a camera.** `pickEvery`, `pickBest` and `makeGetNodeAtPoint`'s
|
|
2173
|
-
result accept an optional trailing `PickCamera`. A world point does not carry
|
|
2174
|
-
the scale it was produced under and picking has no draw envelope, so the caller
|
|
2175
|
-
that produced the point supplies it; omitting it keeps the surface camera.
|
|
2176
|
-
|
|
2177
|
-
`useHoverTracking` took a `clientToWorld` thunk beside a world-space
|
|
2178
|
-
`getNodeAtPoint` — the first resolved the view and the second did not, so hover
|
|
2179
|
-
picked at the surface's scale inside a panel. It takes one
|
|
2180
|
-
`nodeAtClientPoint(clientX, clientY)` now.
|
|
2181
|
-
|
|
2182
|
-
Anchor-editing target state stays surface-wide; only the preview resolution on
|
|
2183
|
-
that path is per-view.
|
|
2184
|
-
- 9ad8cb2: Picking answers for what was painted
|
|
2185
|
-
|
|
2186
|
-
Three defects in `<SceneCanvas>`'s hit paths, all one shape — a pick answering
|
|
2187
|
-
from something other than what the renderer drew.
|
|
2188
|
-
|
|
2189
|
-
**Pose overrides were painted through and picked around.** `PoseOverride.pose`
|
|
2190
|
-
is documented as replacing the document pose *everywhere the render and
|
|
2191
|
-
hit-test paths read one*, and `sceneAdapter.getPose` honored it. But
|
|
2192
|
-
`<SceneCanvas>` supplies its own `pickEvery`, which read `node.pose` raw — as
|
|
2193
|
-
did the bounds resolver feeding selection chrome and the affordance
|
|
2194
|
-
`ChromeState`, and the marquee/lasso scan. A consumer animating nodes through
|
|
2195
|
-
overrides painted them at one place and picked them at another. `effectivePose`
|
|
2196
|
-
is now the single rule and every one of those reads through it.
|
|
2197
|
-
|
|
2198
|
-
**A clipped-away child was still clickable.** A container clips its subtree and
|
|
2199
|
-
the renderer honors it, so a child outside the clip is not painted.
|
|
2200
|
-
`useSelectTool`'s own walk has rejected those since clipping shipped; the walk
|
|
2201
|
-
`<SceneCanvas>` installs instead had no clip term at all. The new
|
|
2202
|
-
`passesAncestorClips` walks the parent chain per surviving candidate, so a flat
|
|
2203
|
-
render-order scan can apply the same test.
|
|
2204
|
-
|
|
2205
|
-
**The marquee's fast-reject used the unrotated pose box.** A 100×20 rect turned
|
|
2206
|
-
45° puts a corner 32 units above that box; a rubber-band over that corner was
|
|
2207
|
-
rejected before the rotation-correct silhouette test ran, while a click on the
|
|
2208
|
-
same pixel selected the shape.
|
|
2209
|
-
- c1b8511: Repaint the scene-graph side-scroller demo's world from `data.fill`. Its
|
|
2210
|
-
tiles, coins, enemies and flagpole still declared `data.color`, the alias
|
|
2211
|
-
removed when node paint became an object, so every one of them rendered in
|
|
2212
|
-
the default gray — the demo whose whole point is being the visual twin of the
|
|
2213
|
-
immediate-mode load test.
|
|
2214
|
-
- d793d3c: Flip negates rotation; alignment guides and `gaps` distribute measure ink
|
|
2215
|
-
|
|
2216
|
-
Three paths read a pose's stored, unrotated box where the rotated extent was
|
|
2217
|
-
wanted.
|
|
2218
|
-
|
|
2219
|
-
`flipPoseAboutBounds` carried rotation through untouched, so a mirrored shape
|
|
2220
|
-
came back turned the same way — invisible on a rectangle, whose AABB is
|
|
2221
|
-
symmetric under a sign flip, and plainly wrong on an asymmetric one, which
|
|
2222
|
-
translated instead of mirroring. It now negates the pose's rotation.
|
|
2223
|
-
|
|
2224
|
-
`deriveAlignmentGuides` advertised a stationary rotated sibling's lines at its
|
|
2225
|
-
stored edges, while the dragged selection matched against them by its ink.
|
|
2226
|
-
`RECT_ALIGN_PROJECTION.boundsOf` now returns the rotated AABB and
|
|
2227
|
-
`deriveAlignmentGuides` reads its targets through the same projection — a new
|
|
2228
|
-
`projection` option defaulting to the rect one, so existing callers get the fix
|
|
2229
|
-
without a change.
|
|
2230
|
-
|
|
2231
|
-
`useDistribute`'s `gaps` mode divided the leftover span by stored widths, so a
|
|
2232
|
-
rotated member ended up with a gap short by the difference; `centers` shared the
|
|
2233
|
-
line and the blind spot. Both now measure with `visualBoundsViaDescriptor`.
|
|
2234
|
-
`distributeHorizontalAction` / `distributeVerticalAction` also take
|
|
2235
|
-
`params.mode`, so `gaps` is reachable from a binding rather than only from the
|
|
2236
|
-
hook.
|
|
2237
|
-
|
|
2238
|
-
Flip and distribute return different poses than before for rotated shapes.
|
|
2239
|
-
That is the fix, but it is a behavior change for anything depending on the
|
|
2240
|
-
old output.
|
|
2241
|
-
- 3386d64: `@weasel-js/core/routing` exports the route-string projection
|
|
2242
|
-
|
|
2243
|
-
Anything rendering a `GestureSpec` as a route string had to re-implement the
|
|
2244
|
-
projection, and the copy in WeaselDraw's registry inspector had drifted three
|
|
2245
|
-
ways: it answered `drop` and `paste` with no gesture name, so every binding of
|
|
2246
|
-
either vanished from the route list; its argument lookup missed a spec field;
|
|
2247
|
-
and it gated targets on a hand-listed set of kinds, dropping them for
|
|
2248
|
-
`pointerDown`, `longPress` and `wheel`.
|
|
2249
|
-
|
|
2250
|
-
New from the routing subpath: `routesForSpec(spec)` — every route string one
|
|
2251
|
-
spec declares — plus `routeGestureForSpecKind(kind)` over the single spec-kind
|
|
2252
|
-
map, and `PREDICATE_TARGET`, which `registry.ts` already exported but the
|
|
2253
|
-
subpath index did not, so consumers reading `RegistryEntry.target` had no way
|
|
2254
|
-
to compare against the sentinel its own docs name.
|
|
2255
|
-
- ce2b5c7: Make the inline run grammar a parameter instead of a hardcoded branch.
|
|
2256
|
-
|
|
2257
|
-
`runsToMarkdown` and `markdownToRuns` each had the markdown subset spelled out
|
|
2258
|
-
in their control flow — `***`/`**`/`*` and a two-character escape set — so
|
|
2259
|
-
reading or writing any other spelling meant forking both. They now take a
|
|
2260
|
-
`RunGrammar`: a table of markers pairing a repeated delimiter with the run
|
|
2261
|
-
flags it toggles, defaulting to `MARKDOWN_RUN_GRAMMAR`, which is exactly
|
|
2262
|
-
today's behavior. Escaping follows the grammar's own delimiters.
|
|
2263
|
-
|
|
2264
|
-
Nothing changes for a caller that passes no grammar. `underline` and
|
|
2265
|
-
`strikethrough` still have no markdown spelling and are still dropped by
|
|
2266
|
-
`runsToMarkdown` — a grammar that wants `~~struck~~` now adds one marker
|
|
2267
|
-
rather than editing the parser.
|
|
2268
|
-
- 2ea772f: `createSelectionOutlineLayer` and `createSelectionHandlesLayer` now do what the overlay layer does
|
|
2269
|
-
|
|
2270
|
-
`createSelectionOverlayLayer` documents itself as equivalent to stacking the
|
|
2271
|
-
other two, and it was not. It reads `ChromeState` off the draw envelope,
|
|
2272
|
-
resolves the synthetic multi-resize id to the union AABB, honors chrome-caps
|
|
2273
|
-
visibility and suppressed ids, and takes selection and poses from the envelope
|
|
2274
|
-
when they are omitted. The two primitives did none of that: they ignored the
|
|
2275
|
-
draw envelope entirely, required a construction-time `getPose` cascade, and
|
|
2276
|
-
knew nothing about the multi-selection union — so a consumer who stacked them,
|
|
2277
|
-
on the wrapper's own promise, got chrome in the wrong place with no way to
|
|
2278
|
-
tell.
|
|
2279
|
-
|
|
2280
|
-
All three now run one body and differ only in which passes they enable, so the
|
|
2281
|
-
promise holds by construction. `SelectionOutlineLayerOpts` and
|
|
2282
|
-
`SelectionHandlesLayerOpts` become the overlay's option set minus the visuals
|
|
2283
|
-
that don't apply, which makes `getSelection` and `getPose` optional on both and
|
|
2284
|
-
adds `getOutlineIds` and `getSuppressedIds`. Handle visuals are now the named
|
|
2285
|
-
`SelectionHandleStyle`.
|
|
2286
|
-
- 3fb3a46: Key `usePublishSelection` on the publish callback, not the context value
|
|
2287
|
-
|
|
2288
|
-
The effect depended on the whole selection-context value, and the provider
|
|
2289
|
-
mints a new value object on every publish. So one publisher publishing refired
|
|
2290
|
-
the effect for every other publisher in scope, each of which republished its
|
|
2291
|
-
own ids — a newer selection got stomped back to an older one, and two
|
|
2292
|
-
publishers holding different ids under one provider never settled at all.
|
|
2293
|
-
|
|
2294
|
-
`publishSelection` is already a stable `useCallback`, so the effect now depends
|
|
2295
|
-
on it directly. No provider change and no API change.
|
|
2296
|
-
- 84db1f6: Close four gaps that produced wrong answers with no error
|
|
2297
|
-
|
|
2298
|
-
Three path walkers — `pathToMultiPolygon` in core and in `@weasel-js/geom`, and
|
|
2299
|
-
`enumerateAnchors` behind the bezier-edit overlay — handled M/L/C/Q/Z with no
|
|
2300
|
-
`default:` arm, so a command code they did not know fell out of the switch
|
|
2301
|
-
without advancing the coordinate cursor and every segment after it read the
|
|
2302
|
-
wrong floats. They now throw, matching the six sibling walkers. This is a
|
|
2303
|
-
behavior change for anyone feeding these a path built with an opcode outside
|
|
2304
|
-
`PATH_COMMANDS`: what used to come back subtly wrong now raises.
|
|
2305
|
-
|
|
2306
|
-
A `<CanvasView>` built its affordance hit-test without a device profile, so a
|
|
2307
|
-
nested view resolved fine-pointer radii even under a coarse pointer — 8px grab
|
|
2308
|
-
zones against the 14px chrome the surface paints. It reads the profile
|
|
2309
|
-
`<SceneCanvas>` publishes.
|
|
2310
|
-
|
|
2311
|
-
`moveGestureAdapter`'s `insertNode` took no `index`, and the adapter carried
|
|
2312
|
-
neither `getChildren` nor `setChildOrder`, so the sibling slot a delete op
|
|
2313
|
-
records had nowhere to land: undoing a delete through the move pipeline
|
|
2314
|
-
appended the node to the end of its parent instead of putting it back where it
|
|
2315
|
-
was. All three are there now.
|
|
2316
|
-
|
|
2317
|
-
The dev inspector's gesture panel formatted bindings with a private formatter
|
|
2318
|
-
that reported only modifiers set to `true`. The `ingest` action marks every
|
|
2319
|
-
modifier `'optional'`, so its drop and paste bindings rendered blank and the
|
|
2320
|
-
action was invisible on both gestures. Both of the panel's plain-text
|
|
2321
|
-
formatters now go through the kit's `routesForSpec`.
|
|
2322
|
-
- 3386d64: Undoing a multi-node delete or group restores document order
|
|
2323
|
-
|
|
2324
|
-
Restoring by stored index cannot survive replay: history runs a batch's
|
|
2325
|
-
inverses in reverse, while indices captured before the mutation are only
|
|
2326
|
-
correct in ascending order. Deleting `b, c, d` from `[a, b, c, d, e]` and
|
|
2327
|
-
undoing gave `a, b, e, c, d`; Cmd+G on the same three did the same.
|
|
2328
|
-
|
|
2329
|
-
Ops now record a `Slot` — an ordinal plus the id of the following sibling at
|
|
2330
|
-
capture. The anchor is the source of truth whenever it resolves, and it
|
|
2331
|
-
resolves whatever else the batch has already restored. The ordinal remains as
|
|
2332
|
-
the fallback for an adapter that can place by index but cannot enumerate
|
|
2333
|
-
children. `before: null` means "last" and needs no sibling list; an absent
|
|
2334
|
-
`before` means "unobserved", and the two survive `History.serialize` because
|
|
2335
|
-
`undefined` drops out of JSON and `null` does not.
|
|
2336
|
-
|
|
2337
|
-
The ops observe their own slot during `apply()` rather than taking one from the
|
|
2338
|
-
caller, so every existing emitter gets this without a call-site change.
|
|
2339
|
-
`createDeleteOp`'s `index` argument is now a seed that `apply` supersedes; its
|
|
2340
|
-
docstring said it was sufficient on its own, which it never was.
|
|
2341
|
-
|
|
2342
|
-
Adapters without an ordering seam still append, as they did before:
|
|
2343
|
-
`arrayAdapter` has no `setChildOrder`, and the move gesture's adapter has
|
|
2344
|
-
neither that nor an `index` parameter on `insertNode`.
|
|
2345
|
-
- 7a746df: A stroke's dash is edited as a style, not as an array
|
|
2346
|
-
|
|
2347
|
-
`Stroke.dash` already rendered, imported and exported; it had no control,
|
|
2348
|
-
because a `number[]` has no leaf kind. It doesn't need one — the thing a person
|
|
2349
|
-
chooses is a style, and the array is how it is stored. The stroke block gains a
|
|
2350
|
-
Solid / Dashed / Dotted / Custom bar under cap, join and align.
|
|
2351
|
-
|
|
2352
|
-
`ToolPrefEnum` gains `encoding`: `read`/`write` between the stored value and
|
|
2353
|
-
the option string, the counterpart of the `unit` a number leaf already has for
|
|
2354
|
-
a value stored in a canonical unit. Both directions are handed the object the
|
|
2355
|
-
leaf is a field of, because a dash pattern is meaningless without the width it
|
|
2356
|
-
scales by — SVG dash lengths are absolute, so a fixed `[6, 3]` is dots on a
|
|
2357
|
-
hairline and a railroad on a 20px stroke. `dashForStrokeStyle` /
|
|
2358
|
-
`strokeDashStyleOf` are the mapping, exported: **dashed is 3× the width on and
|
|
2359
|
-
2× off, dotted 1× on and 2× off**. An array matching neither reads as `custom`,
|
|
2360
|
-
a new `disabled` option — one a control reports but refuses to author, since
|
|
2361
|
-
there is no array behind it. `solid` is stored as no dash at all, and an object
|
|
2362
|
-
leaf's field written as `undefined` is now removed rather than left holding it.
|
|
2363
|
-
- 4f19274: Cap, join and align are chosen by glyph, and the stroke block drops its labels
|
|
2364
|
-
|
|
2365
|
-
Nine option glyphs and four category glyphs join the icon set. The option
|
|
2366
|
-
glyphs are filled silhouettes — the glyph is the ink, so a choice reads as a
|
|
2367
|
-
shape rather than as a diagram of one. `align` is a circle zoomed until the
|
|
2368
|
-
ink band's far edge leaves the box: `inner` closes into a disc, `outer` into
|
|
2369
|
-
the box's complement of it, and `center` is the annulus straddling the path,
|
|
2370
|
-
so the three are one band at three offsets. The categories are the bare path
|
|
2371
|
-
each row treats, drawn in the outlined register.
|
|
2372
|
-
|
|
2373
|
-
A schema carries a glyph *id*, not a component: `ToolPrefEnum`'s options gain
|
|
2374
|
-
`icon`, and every leaf gains one for rows whose own label is spent on a
|
|
2375
|
-
`pair`. Core ships no icon set and cannot depend on one, so the field is a
|
|
2376
|
-
plain string; weasel-ui resolves it against `ICON_PATHS` and falls back to
|
|
2377
|
-
`short` where it names no glyph.
|
|
2378
|
-
|
|
2379
|
-
`SelectionPanel` now honours `block` inside an object leaf, not only at the
|
|
2380
|
-
section level. A row whose fields are all `block` drops the 64px label column
|
|
2381
|
-
and spans the block. The default stroke schema uses both: paint and width
|
|
2382
|
-
share one label-less row, and cap/join/align share the next.
|
|
2383
|
-
|
|
2384
|
-
`align`'s options run inner, center, outer — the order the ink moves outward.
|
|
2385
|
-
- 94f2446: Add stroke markers — arrowheads and other line terminators as stroke style.
|
|
2386
|
-
|
|
2387
|
-
`markerStart` / `markerMid` / `markerEnd` on `Stroke` take a key resolved
|
|
2388
|
-
through a new registry (`registerMarker`), shipping eight built-in shapes.
|
|
2389
|
-
Unlike SVG, the stroke stops short of a filled head rather than running under
|
|
2390
|
-
it to the tip; the distance is declared per marker, so an open V still reaches
|
|
2391
|
-
the vertex. Round-trips through `@weasel-js/svg` as `marker-*` attributes plus
|
|
2392
|
-
`<marker>` defs.
|
|
2393
|
-
- 07fd2de: `setStroke` takes a whole paint, so a gradient or pattern stroke is writable.
|
|
2394
|
-
|
|
2395
|
-
It accepted `{ color }` only, and merged through `paintWithColor`, which
|
|
2396
|
-
supersedes a non-solid paint with a solid one — a gradient stroke was
|
|
2397
|
-
unreachable even though `setStrokeOpacity` could already reach its alpha.
|
|
2398
|
-
`paint` now wins over `color`, a color arriving later in the gesture supersedes
|
|
2399
|
-
an earlier paint, and the stroke's width, cap, join, dash and align survive
|
|
2400
|
-
either. New `strokeWith(paint, width?)` is `strokeOf`'s sibling for a paint
|
|
2401
|
-
that has no color to pass.
|
|
2402
|
-
|
|
2403
|
-
Two fixes alongside it: `setFill` started with no `color` and no `paint` seeded
|
|
2404
|
-
from `DEFAULT_STROKE_COLOR`, painting the selection black where
|
|
2405
|
-
`setFillOpacity` seeds the same slot from `DEFAULT_FILL_COLOR`; and
|
|
2406
|
-
`gradientForBounds`'s doc comment claimed a corner-to-corner linear gradient
|
|
2407
|
-
where the body builds a left-edge-to-right-edge one.
|
|
2408
|
-
|
|
2409
|
-
`@weasel-js/ui` no longer exports `strokeWithColor`. It shared a name with
|
|
2410
|
-
core's and disagreed with it — core's keeps the paint's opacity, ui's dropped
|
|
2411
|
-
it — and nothing imported it.
|
|
2412
|
-
- 81213fc: Edit a node's stroke as the union it is
|
|
2413
|
-
|
|
2414
|
-
`data.stroke` holds `string | Stroke`, and the schema described it with a
|
|
2415
|
-
`color` leaf — which reads `undefined` off the object form, shows its own
|
|
2416
|
-
default, and writes a bare hex back over the stroke's width, cap, join and
|
|
2417
|
-
dash on the first edit. The same trap `ToolPrefPaint` was introduced to avoid
|
|
2418
|
-
for `FillStyle`.
|
|
2419
|
-
|
|
2420
|
-
A `stroke` pref kind now describes it, and `defaultNodeProperties` uses it.
|
|
2421
|
-
Its control shows whichever color the value has — the string itself, or a
|
|
2422
|
-
solid paint's color — gives a gradient stroke the indeterminate chip rather
|
|
2423
|
-
than claiming a color it doesn't have, and preserves the form on write.
|
|
2424
|
-
|
|
2425
|
-
`PrefsForm` gained the `stroke` case and the `paint` case it never had; a
|
|
2426
|
-
`paint` leaf used to render as the literal text `(paint: no renderer)`.
|
|
2427
|
-
`solidColorOf`, `strokeColorOf`, `strokeWithColor` and `isStrokeObject` are
|
|
2428
|
-
exported from `@weasel-js/ui` for consumers writing their own property
|
|
2429
|
-
renderers against either union.
|
|
2430
|
-
|
|
2431
|
-
Cap, join and dash are not editable from a panel yet, and `data.strokeWidth`
|
|
2432
|
-
remains its own leaf — see `docs/proposals/2026-08-26-node-stroke-union.md`
|
|
2433
|
-
for why that waits on the SVG mapping.
|
|
2434
|
-
- 2f225d7: A thick stroke is clickable across its whole width
|
|
2435
|
-
|
|
2436
|
-
`shapeCoversPoint` grants a grab out to a stroke's outward reach — a full
|
|
2437
|
-
stroke width for an `outer` align — but the AABB pre-filter that runs before it
|
|
2438
|
-
grew only by the pointer slop. So half a thick outer stroke's ink was
|
|
2439
|
-
unclickable: the point was rejected before the refinement that would have
|
|
2440
|
-
claimed it ever ran. `poseContains` carried a comment claiming the pre-filter
|
|
2441
|
-
was at least as generous as the refinement, which it cannot be on its own,
|
|
2442
|
-
since it never sees the stroke. That budget is the caller's, and the comment
|
|
2443
|
-
says so now.
|
|
2444
|
-
|
|
2445
|
-
`ShapeCoversPointOptions.scale` was never passed either, so a stroke width
|
|
2446
|
-
declared in `px` resolved as world units and the reach was wrong at every zoom
|
|
2447
|
-
but 1 — while the caller computed `meanScale(view.scale)` one line above.
|
|
2448
|
-
- 68069dc: Right-to-left text lays out in visual order
|
|
2449
|
-
|
|
2450
|
-
`LayoutRunsOpts` takes an optional `bidi` engine. Given one, `layoutRuns`
|
|
2451
|
-
analyses the paragraph, reorders each line after the wrap, and mirrors brackets
|
|
2452
|
-
in right-to-left runs. Given none, nothing changes: text lays out logically,
|
|
2453
|
-
exactly as before.
|
|
2454
|
-
|
|
2455
|
-
`@weasel-js/text` declares the `BidiResolver` interface and does not depend on
|
|
2456
|
-
`@weasel-js/bidi` — the dependency runs the other way from the usual, so a
|
|
2457
|
-
consumer who renders no right-to-left text never installs the Unicode tables,
|
|
2458
|
-
and a different implementation can be substituted. `@weasel-js/bidi` is a
|
|
2459
|
-
devDependency here only, for a test that drives real Hebrew through the real
|
|
2460
|
-
engine; types lining up is not evidence the semantics do.
|
|
2461
|
-
|
|
2462
|
-
`LaidOutCell` gains `advance` and `level`, and **`x` is no longer monotonic
|
|
2463
|
-
across `cells`**. Cells stay in logical order — slot `i` is still character `i`
|
|
2464
|
-
— while their x values follow the reordering. Sort on `x` for visual order, and
|
|
2465
|
-
read a cell's extent as `[x, x + advance)` rather than reaching for the next
|
|
2466
|
-
cell's `x`. Hit-testing was doing exactly that and now sweeps in visual order
|
|
2467
|
-
against each cell's own extent, taking a right-to-left cell's visually-leading
|
|
2468
|
-
half as the character's logical end.
|
|
2469
|
-
|
|
2470
|
-
Kerning is a gap between two adjacent characters, and the wrap measures it
|
|
2471
|
-
logically. Reordering can put a different pair side by side, so the gap taken
|
|
2472
|
-
is the one belonging to whichever of the two is logically second, and none at
|
|
2473
|
-
all across a direction boundary — where the pair never touched in the source.
|
|
2474
|
-
|
|
2475
|
-
Laying out right-to-left text with no engine now warns once, naming the import.
|
|
2476
|
-
The alternative is glyphs silently appearing reversed, which is the one real
|
|
2477
|
-
hazard of making this opt-in.
|
|
2478
|
-
- 5d0ff9c: Every code point on a line gets a cell
|
|
2479
|
-
|
|
2480
|
-
`LaidOutLineBox` replaces its `caretXs` / `caretIndices` pair with
|
|
2481
|
-
`cells: LaidOutCell[]` plus a `srcEnd` closing offset. A cell carries
|
|
2482
|
-
`srcIndex`, `srcEnd`, `cp`, `x` and `drawsInk`, so slot `i` is `cells[i]` and
|
|
2483
|
-
a consumer indexing per character no longer has to reconcile a sparse array
|
|
2484
|
-
against the source string.
|
|
2485
|
-
|
|
2486
|
-
The old arrays were documented as non-contiguous, and two causes were real:
|
|
2487
|
-
|
|
2488
|
-
- A code point no tier could serve was dropped outright, taking its caret stop
|
|
2489
|
-
with it. It now occupies a zero-advance cell. This is reachable whenever the
|
|
2490
|
-
dynamic canvas fallback is off — which is the normal configuration for a
|
|
2491
|
-
consumer registering its own outlines, where the outline tier has no rung
|
|
2492
|
-
below it.
|
|
2493
|
-
- A space opening a line — at the start of the text, or after a newline — was
|
|
2494
|
-
discarded. It now keeps its cell and still consumes no width, so a line is
|
|
2495
|
-
addressable per character without gaining an indent. A space that opens a
|
|
2496
|
-
*wrapped* line was never affected: the wrap leaves it as a trailing cell on
|
|
2497
|
-
the line before.
|
|
2498
|
-
|
|
2499
|
-
Neither changes any geometry: both cells carry zero advance, zero tracking and
|
|
2500
|
-
no kerning, so bounds, line widths and glyph positions are unchanged.
|
|
2501
|
-
|
|
2502
|
-
A newline still has no cell, since it separates cells rather than being one.
|
|
2503
|
-
`srcEnd` is what a blank line carries in its place.
|
|
2504
|
-
|
|
2505
|
-
`drawsInk` is a property of the code point and the face, not of the call that
|
|
2506
|
-
produced it: it does not flip when a dynamic bake lands or the outline
|
|
2507
|
-
threshold is crossed, so the same text reports the same slots every time. A
|
|
2508
|
-
zero-advance combining mark is `true` — it inks without advancing.
|
|
2509
|
-
- c1b8511: **Breaking:** paint leaves `TextStyle`. A text node's color and outline are
|
|
2510
|
-
`data.fill` and `data.stroke` — the same two leaves every other node kind
|
|
2511
|
-
paints from — and `TextStyle` holds typography only. `TextStyle.fill` and
|
|
2512
|
-
`TextStyle.stroke` are gone, with no compatibility read: a document that put
|
|
2513
|
-
its color in `style.fill` now renders in the default black rather than
|
|
2514
|
-
erroring, so check documents that predate this.
|
|
2515
|
-
|
|
2516
|
-
This fixes a real asymmetry rather than only moving fields. `data.stroke`
|
|
2517
|
-
already reached text through a fold in the painter, but `data.fill` did not:
|
|
2518
|
-
picking a fill color with a text node selected wrote a field nothing read, so
|
|
2519
|
-
the canvas did not change. `setFill`, `setFillOpacity`, the opacity scrub and
|
|
2520
|
-
the Appearance leaf now all mean the same thing on text as on a rect. The
|
|
2521
|
-
duplicate `data.style.fill` control is gone from the text schema with them.
|
|
2522
|
-
|
|
2523
|
-
`resolveTextStyle(style, paint)` takes the node's paint as a second argument
|
|
2524
|
-
and is what derives the caret and selection colors, so the edit overlay
|
|
2525
|
-
matches the glyphs it sits on; `useTextEdit` gained a `getPaint` option for
|
|
2526
|
-
the same reason, defaulted by `useSceneTextEdit` from `data.fill` /
|
|
2527
|
-
`data.stroke`. `TextPose` gained `fill` / `stroke`, so text drawn through
|
|
2528
|
-
`createTextLayer` is painted rather than black. `SvgTextNode` gained the same
|
|
2529
|
-
two, and SVG import and export carry text paint there instead of inside the
|
|
2530
|
-
style. `StyledRun.fill` and `.stroke` are unchanged and still override the
|
|
2531
|
-
node's per range — which is also where a caller with no node at all, a HUD
|
|
2532
|
-
widget or a debug overlay, now states its color.
|
|
2533
|
-
|
|
2534
|
-
`textCommandFromRuns` is exported from the package root.
|
|
2535
|
-
- 546f67d: Draw text from a ring of reused vertex buffers instead of minting a vertex
|
|
2536
|
-
array and two buffers per draw. `drawTextGroup` and `drawTextDecorations` were
|
|
2537
|
-
the last paths still doing what `drawImage` stopped doing; text now costs
|
|
2538
|
-
**3.3 us/command, down from 6.65** at 512 commands a frame on an M2 Max via
|
|
2539
|
-
ANGLE (`tests/perf/transition-matrix.spec.ts`), which puts it level with an
|
|
2540
|
-
image draw. No other command kind moved.
|
|
2541
|
-
|
|
2542
|
-
A text group is as many quads as it has glyphs, so unlike the image ring a
|
|
2543
|
-
slot's buffer grows to the largest run it has seen rather than being fixed at
|
|
2544
|
-
four vertices. The quad index pattern is a pure function of the quad count —
|
|
2545
|
-
the pattern for N quads is a prefix of the pattern for any larger N — so one
|
|
2546
|
-
index buffer serves every slot, grown the same way and written only when it
|
|
2547
|
-
grows.
|
|
2548
|
-
- c2ffa49: Alignment can resolve against reading direction
|
|
2549
|
-
|
|
2550
|
-
`align` gains `start` and `end` alongside `left` / `center` / `right`, and
|
|
2551
|
-
`TextStyle` gains `direction: 'ltr' | 'rtl'`. The split is CSS `text-align`'s:
|
|
2552
|
-
the relative pair resolves against the direction, the absolute pair ignores it.
|
|
2553
|
-
`resolveAlign(align, direction)` collapses one to the other and is exported for
|
|
2554
|
-
consumers that need an edge rather than an intent.
|
|
2555
|
-
|
|
2556
|
-
Direction is an input, not something this package discovers. `@weasel-js/text`
|
|
2557
|
-
has no DOM, so a consumer that reads `getComputedStyle(box).direction` passes
|
|
2558
|
-
what it found; nothing here sniffs an environment.
|
|
2559
|
-
|
|
2560
|
-
Defaults are unchanged — `align: 'left'`, `direction: 'ltr'` — so no existing
|
|
2561
|
-
layout moves. Making `start` the default alignment is a separate call.
|
|
2562
|
-
|
|
2563
|
-
`@weasel-js/svg` carries the direction through: `direction` joins the
|
|
2564
|
-
inheritable presentation properties, and `text-anchor` is now written and read
|
|
2565
|
-
against it. Two things were wrong before and are worth naming, because both
|
|
2566
|
-
rendered plausible output:
|
|
2567
|
-
|
|
2568
|
-
- `align: 'start'` serialized to `text-anchor="end"` — the opposite edge — via
|
|
2569
|
-
a mapping that assumed three values and read the fourth as its `else`.
|
|
2570
|
-
- SVG's initial `text-anchor` is `start`, which under `direction="rtl"` is the
|
|
2571
|
-
right edge, while this model's default `align` is `left`. They agree under
|
|
2572
|
-
`ltr` and only there, so an RTL document with no explicit anchor imported as
|
|
2573
|
-
left-aligned.
|
|
2574
|
-
|
|
2575
|
-
This is alignment and round-tripping only. Layout still walks code points in
|
|
2576
|
-
logical order with the pen always increasing: there is no bidi reordering and
|
|
2577
|
-
no shaping, so a Hebrew or Arabic string aligns to the correct edge and still
|
|
2578
|
-
renders in logical order, and Arabic still renders unjoined.
|
|
2579
|
-
- 4c097ef: Sit every run on a line on one baseline
|
|
2580
|
-
|
|
2581
|
-
Mixed-size text hung each run off the *line top* at its own ascent instead of
|
|
2582
|
-
off a shared baseline, so a 16-unit run beside a 40-unit run floated up level
|
|
2583
|
-
with the big run's cap rather than standing on the line with it. Two faces with
|
|
2584
|
-
different ascents at the same size diverged the same way. Baseline alignment is
|
|
2585
|
-
what inline text does everywhere else, and the module header already claimed
|
|
2586
|
-
this behavior — the walk just never implemented it.
|
|
2587
|
-
|
|
2588
|
-
A line now sinks one baseline far enough to clear its tallest run's ascent and
|
|
2589
|
-
places every glyph against it. Glyph quads derive their top from that baseline
|
|
2590
|
-
rather than from the pen's line top, which is the whole of the change:
|
|
2591
|
-
`qy0 = baselineY + (yoffset - metrics.base) * scale`.
|
|
2592
|
-
|
|
2593
|
-
Uniform-size text — nearly all text — is unchanged, since the maximum over one
|
|
2594
|
-
value is that value. Only lines that actually mix sizes or faces move, and they
|
|
2595
|
-
move to where they always should have been.
|
|
2596
|
-
|
|
2597
|
-
The test named "mixed-size runs share a baseline on the same line" asserted only
|
|
2598
|
-
a quad count and passed throughout; it now asserts the baselines.
|
|
2599
|
-
- 2b86e00: A text node's style is one value, not ten sibling paths
|
|
2600
|
-
|
|
2601
|
-
`data.style.fontSize`, `.fontWeight`, `.align` and the rest addressed into one
|
|
2602
|
-
`TextStyle` from ten independent leaves, each control writing a field of a
|
|
2603
|
-
value it could only half see. `data.style` is an object leaf now, with
|
|
2604
|
-
Character and Paragraph as groups inside it — groups head their fields and
|
|
2605
|
-
contribute nothing to the path, so a field is still a field of the style and
|
|
2606
|
-
one commit writes the whole thing.
|
|
2607
|
-
|
|
2608
|
-
An object leaf whose fields are entirely grouped no longer prints its own
|
|
2609
|
-
heading, which would stack straight onto the first group's, and a group's
|
|
2610
|
-
fields sit under a rule so the nesting reads. WeaselDraw's inspector descends
|
|
2611
|
-
into an object leaf when listing what a kind exposes — the fields are the
|
|
2612
|
-
editable surface; the leaf is the container.
|
|
2613
|
-
|
|
2614
|
-
`SelectionPanel` has a story now, which is how the two layout defects above
|
|
2615
|
-
were found.
|
|
2616
|
-
- d933a89: Superscript, subscript and overline for styled runs
|
|
2617
|
-
|
|
2618
|
-
`StyledRun` gains `script: 'super' | 'sub'` — a raised or lowered baseline and
|
|
2619
|
-
a smaller size together, the pair `<sup>` and `<sub>` imply. It is a preset
|
|
2620
|
-
over two new primitives rather than a mechanism of its own:
|
|
2621
|
-
|
|
2622
|
-
- `baselineShift` — raise (positive) or lower (negative) a run off the line's
|
|
2623
|
-
shared baseline, in ems of the inherited font size.
|
|
2624
|
-
- `fontScale` — a multiplier on the inherited font size, the relative
|
|
2625
|
-
counterpart to `fontSize`. An absolute `fontSize` still wins over it.
|
|
2626
|
-
|
|
2627
|
-
Naming either directly overrides that half of `script` and leaves the other
|
|
2628
|
-
alone. The preset's numbers are exported as `SCRIPT_METRICS` (58.3% size,
|
|
2629
|
-
±33.3% position — Adobe's defaults, so a character panel can show percentages
|
|
2630
|
-
its users already recognize) and are derived, not read from the font: `OS/2`
|
|
2631
|
-
carries real `ySuperscript*` metrics but the baked atlas tier has no slot for
|
|
2632
|
-
them, and metrics that applied on one glyph tier and not the other would
|
|
2633
|
-
reflow text as it crossed the size threshold.
|
|
2634
|
-
|
|
2635
|
-
`resolveRuns` folds all of it into one world-unit `baselineShift` and a final
|
|
2636
|
-
`fontSize`, so layout never learns superscripts exist — it places a run against
|
|
2637
|
-
a baseline and an offset. The shift moves a run's glyphs, its outline geometry
|
|
2638
|
-
and its own decoration rules together, and deliberately does not feed back into
|
|
2639
|
-
the line's baseline or height: a superscript rides the line rather than
|
|
2640
|
-
reflowing it.
|
|
2641
|
-
|
|
2642
|
-
`overline` joins `underline` and `strikethrough` on both `TextStyle` and
|
|
2643
|
-
`StyledRun`, additive over the node style like the other two, and is now
|
|
2644
|
-
available to a custom `RunGrammar` as a `RunFlag`. The default markdown grammar
|
|
2645
|
-
is unchanged — it stays silent on the decorations, as it always has been.
|
|
2646
|
-
- 5923c8b: `Animator.tween` no longer fires `onDone` for a tween that was cancelled during
|
|
2647
|
-
its own final `onTick`. The last tick emitted the value and completed in one
|
|
2648
|
-
pass, so a write made from that tick — cancelling the tween — still got the
|
|
2649
|
-
completion callback, against the documented "not called on cancel" contract.
|
|
2650
|
-
- 2ea772f: Undo of a delete restores the subtree; undo of a group restores the slot
|
|
2651
|
-
|
|
2652
|
-
Two ops inverted to something narrower than what they applied, so undo
|
|
2653
|
-
silently lost data.
|
|
2654
|
-
|
|
2655
|
-
`createDeleteOp.invert()` re-inserted a single node while `apply()` called
|
|
2656
|
-
`removeNode`, which cascades the whole subtree. Delete a container with two
|
|
2657
|
-
children, undo, and the container came back with `children: []` while both
|
|
2658
|
-
children were gone. The op now snapshots its descendants preorder through the
|
|
2659
|
-
adapter's optional `getNode` / `getChildren` — the snapshot is written back
|
|
2660
|
-
into `args`, so an op rebuilt from a serialized entry still inverts — and
|
|
2661
|
-
re-inserts each descendant at its captured slot. A flat adapter's `removeNode`
|
|
2662
|
-
does not cascade, so the inverse skips any descendant the adapter still reports
|
|
2663
|
-
as live rather than duplicating it.
|
|
2664
|
-
|
|
2665
|
-
`createReparentOp` carried only the parent ids, so undoing a Cmd+G appended
|
|
2666
|
-
instead of restoring the sibling slot and paint order changed. `ReparentArgs`
|
|
2667
|
-
now carries `fromIndex` / `toIndex` and places through the existing
|
|
2668
|
-
`getChildren` / `setChildOrder` seam that `createReorderOp` already uses —
|
|
2669
|
-
`setParent`'s signature is unchanged. Adapters without that seam no-op as
|
|
2670
|
-
before. `groupAction` captures each member's index before mutating; `move` and
|
|
2671
|
-
`snapToContainer` pass none and are byte-identical.
|
|
2672
|
-
|
|
2673
|
-
`ops/delete.test.ts` stubbed `removeNode` as a one-id delete that did not
|
|
2674
|
-
cascade, which is why nothing caught the first bug. It now runs against a
|
|
2675
|
-
tree-backed fake.
|
|
2676
|
-
- 2ea772f: Selection chrome, gesture bounds and SVG export fold rotated ink, not pose boxes
|
|
2677
|
-
|
|
2678
|
-
Every union a user looks at or clicks folded each member's *unrotated* box.
|
|
2679
|
-
Select two shapes, rotate one, and the multi-selection frame and its handles
|
|
2680
|
-
sat inside the rotated shape's ink — affordances hand `ChromeState.unionBounds`
|
|
2681
|
-
out as the target bounds for paint *and* hit-test, so the handles were both
|
|
2682
|
-
drawn and grabbable in the wrong place, while `getGestureBounds()` reported the
|
|
2683
|
-
correct larger box.
|
|
2684
|
-
|
|
2685
|
-
`unionAABB` expands each rotated member via `axisAlignedBounds` before folding
|
|
2686
|
-
and is now the one implementation. It lives in `core/geometry/unionBounds.ts`
|
|
2687
|
-
beside the rotation-free `unionBounds`, which stays correct for commit-time
|
|
2688
|
-
actions that write poses back in the unrotated frame; the module says which to
|
|
2689
|
-
reach for. `unionGestureBounds` is **removed** — it was `unionAABB` under
|
|
2690
|
-
another name. Both new functions are exported from the package root.
|
|
2691
|
-
|
|
2692
|
-
Moved onto it: `ChromeState.unionBounds`, the selection overlay's
|
|
2693
|
-
container-to-leaves resolver, the multi-rotate pivot (which put the pivot in
|
|
2694
|
-
the wrong place whenever a member was rotated), and WeaselDraw's export
|
|
2695
|
-
viewBox, which clipped rotated shapes out of the copied SVG.
|
|
2696
|
-
- 3fb3a46: Warn in dev when `useAction` finds no `ActionsProvider`
|
|
2697
|
-
|
|
2698
|
-
`useAction` returned early on a null registry, so an action registered above
|
|
2699
|
-
the provider — or with no provider mounted — silently never fired its
|
|
2700
|
-
bindings. It now warns in dev, naming the action id. Runtime behavior in
|
|
2701
|
-
production builds is unchanged.
|
|
2702
|
-
- Updated dependencies [5c8e9e6]
|
|
2703
|
-
- Updated dependencies [2621cbf]
|
|
2704
|
-
- Updated dependencies [0f936da]
|
|
2705
|
-
- Updated dependencies [4180095]
|
|
2706
|
-
- Updated dependencies [9977908]
|
|
2707
|
-
- Updated dependencies [52c7b2a]
|
|
2708
|
-
- Updated dependencies [3386d64]
|
|
2709
|
-
- Updated dependencies [c6c499d]
|
|
2710
|
-
- Updated dependencies [20097e6]
|
|
2711
|
-
- Updated dependencies [84db1f6]
|
|
2712
|
-
- Updated dependencies [94f2446]
|
|
2713
|
-
- Updated dependencies [68069dc]
|
|
2714
|
-
- Updated dependencies [5d0ff9c]
|
|
2715
|
-
- Updated dependencies [0bb27a5]
|
|
2716
|
-
- Updated dependencies [c2ffa49]
|
|
2717
|
-
- Updated dependencies [4c097ef]
|
|
2718
|
-
- Updated dependencies [d933a89]
|
|
2719
|
-
- @weasel-js/text@1.3.0
|
|
2720
|
-
- @weasel-js/geom@1.3.0
|
|
2721
|
-
- @weasel-js/font@1.3.0
|
|
2722
|
-
- @weasel-js/gestures@1.3.0
|
|
2723
|
-
- @weasel-js/history@1.3.0
|
|
2724
|
-
- @weasel-js/modes@1.3.0
|
|
2725
|
-
- @weasel-js/paint@1.3.0
|
|
2726
|
-
|
|
2727
|
-
## 2.0.0-pre.0
|
|
2728
|
-
|
|
2729
|
-
### Minor Changes
|
|
2730
|
-
|
|
2731
|
-
- bca99e3: Extract the typography layer into `@weasel-js/text`, and the paint vocabulary
|
|
2732
|
-
into `@weasel-js/paint` — two new Tier A leaves.
|
|
2733
|
-
|
|
2734
|
-
`@weasel-js/text` owns the run model, style resolution, `layoutRuns`, wrap and
|
|
2735
|
-
measurement. It depends on `@weasel-js/font`, `@weasel-js/geom` and
|
|
2736
|
-
`@weasel-js/paint`, and on nothing else: a consumer with its own renderer can
|
|
2737
|
-
lay out text without taking the scene graph or a React peer dependency.
|
|
2738
|
-
`layoutRuns` is now public — it was previously reachable only from inside core.
|
|
2739
|
-
|
|
2740
|
-
`@weasel-js/paint` holds `FillStyle`, `Stroke`, gradients, dashes and
|
|
2741
|
-
`TextureHandle`. It was the blocker named in the 2026-07-28 font split: the
|
|
2742
|
-
layout could not move while its fill type lived in the renderer's graph.
|
|
2743
|
-
|
|
2744
|
-
`@weasel-js/core` re-exports both surfaces, so its own API is unchanged.
|
|
2745
|
-
`Rect` moves to `@weasel-js/geom`, beside `Box`.
|
|
2746
|
-
|
|
2747
|
-
Breaking for anyone importing these through core's internal paths rather than
|
|
2748
|
-
its public entry (`core/paint-types`, `features/text/*`); those paths are gone.
|
|
2749
|
-
|
|
2750
|
-
Advances and kerning still come from a baked MSDF atlas — laying out from font
|
|
2751
|
-
bytes alone needs the metrics seam in
|
|
2752
|
-
`docs/superpowers/specs/2026-08-28-text-package-extraction-design.md`.
|
|
2753
|
-
|
|
2754
|
-
<!-- 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" -->
|
|
2755
|
-
|
|
2756
|
-
### Patch Changes
|
|
2757
|
-
|
|
2758
|
-
- 3386d64: Align, distribute and flip use visual bounds
|
|
2759
|
-
|
|
2760
|
-
These folded each member's unrotated pose box, so "Align Left" on a selection
|
|
2761
|
-
containing a rotated shape lined up the boxes and left the rotated shape's ink
|
|
2762
|
-
sticking out past the others. They now work on the visual bounding box, as
|
|
2763
|
-
Figma and Illustrator do.
|
|
2764
|
-
|
|
2765
|
-
Both ends moved together — expanding only the union would have made alignment
|
|
2766
|
-
worse, since the delta runs from an edge of the union to the same edge of each
|
|
2767
|
-
member's box. The new exported `visualBoundsViaDescriptor(pose, geometry)`
|
|
2768
|
-
reads a pose's bounds, recovers its rotation and expands via
|
|
2769
|
-
`axisAlignedBounds`; the union folds those with `unionAABB`. The delta is still
|
|
2770
|
-
applied as a translation of the stored pose through
|
|
2771
|
-
`translatePoseViaDescriptor`, so a shape moves rather than being re-posed.
|
|
2772
|
-
|
|
2773
|
-
Flip needed only its union pivot changed: mirroring maps a centre and preserves
|
|
2774
|
-
size, and an expanded box is concentric with the box it came from.
|
|
2775
|
-
|
|
2776
|
-
`alignMoveBehavior` folds the dragged selection the same way, so a drag snaps
|
|
2777
|
-
by its ink.
|
|
2778
|
-
|
|
2779
|
-
- ffafb7d: Never let an animation's virtual clock run backwards.
|
|
2780
|
-
|
|
2781
|
-
`useAnimator` seeds each animation's `lastRealNow` from `now()` at register
|
|
2782
|
-
time, then advances its virtual clock by the difference against the timestamp
|
|
2783
|
-
the frame loop supplies. Those two share a time origin in a browser, where the
|
|
2784
|
-
rAF timestamp and `performance.now()` are both page-relative — but that is a
|
|
2785
|
-
browser guarantee, not a universal one, and jsdom starts them roughly 600ms
|
|
2786
|
-
apart. The first frame's delta then came out hugely negative and `virtualNow`
|
|
2787
|
-
spent dozens of frames climbing back toward zero before a tween advanced at
|
|
2788
|
-
all: a 40ms glide took 95 frames and over a second of wall time, growing worse
|
|
2789
|
-
the longer the process had been alive.
|
|
2790
|
-
|
|
2791
|
-
A frame's elapsed time is never negative, so the sample is now clamped at
|
|
2792
|
-
zero. Under a shared origin this is a no-op.
|
|
2793
|
-
|
|
2794
|
-
- ba8b139: Camera animation: `viewport.animatedZoom` now does something
|
|
2795
|
-
|
|
2796
|
-
`animatedZoom` has been declared on `SceneCanvasProps.viewport` and read by
|
|
2797
|
-
nothing; Cmd+=/-/0 was a bare `view.set`. It now routes the discrete zoom steps
|
|
2798
|
-
through the kit's `Animator`. Wheel and pinch are unchanged and never animate —
|
|
2799
|
-
their input already delivers a sample per frame.
|
|
2800
|
-
|
|
2801
|
-
Camera animation is a general surface, not a zoom flag. Three ways in, one
|
|
2802
|
-
runner behind them:
|
|
2803
|
-
|
|
2804
|
-
- `useViewAnimation(view, animator?)` — `animate`, `animateToBounds`, `stop`,
|
|
2805
|
-
`isAnimating`, `target`.
|
|
2806
|
-
- The `view` dep gains optional `animate` / `stopAnimation` / `animationTarget`,
|
|
2807
|
-
so any action can glide the camera.
|
|
2808
|
-
- `SceneCanvasApi` gains `animateView` / `stopViewAnimation` /
|
|
2809
|
-
`isViewAnimating` for fit-to-selection, recenter, or a scripted tour. All
|
|
2810
|
-
three are **required** members: anyone hand-implementing `SceneCanvasApi`
|
|
2811
|
-
(a test double, a wrapper) has to add them, the way `CanvasExtensionApi`
|
|
2812
|
-
grew `getPaintedVersion`.
|
|
2813
|
-
|
|
2814
|
-
Scale interpolates geometrically and translation is derived from the screen
|
|
2815
|
-
point the two views agree on, so a zoom stays anchored instead of drifting and
|
|
2816
|
-
each frame changes the view by the same ratio. One animation runs at a time; any
|
|
2817
|
-
other view write cancels it, and a cancel leaves the camera where it is rather
|
|
2818
|
-
than jumping to the target. On an uncontrolled canvas the whole animation costs
|
|
2819
|
-
no React render.
|
|
2820
|
-
|
|
2821
|
-
**Breaking:** `useViewTween` is removed. `useViewAnimation` keeps its name and
|
|
2822
|
-
changes signature — it takes a `{ get, set }` view channel plus an optional
|
|
2823
|
-
`Animator`, and `animateTo(from, to, { duration, easing })` becomes
|
|
2824
|
-
`animate(to, { ms, easing })`. The `from` argument is gone because the runner
|
|
2825
|
-
reads the live view, which is what lets an interrupted camera resume from where
|
|
2826
|
-
it actually is instead of snapping back to a captured start. `cancel()` is now
|
|
2827
|
-
`stop()`, and `animateToBounds(bounds, currentView, dims, { duration })` is now
|
|
2828
|
-
`animateToBounds(bounds, dims, { ms })` — the `currentView` argument goes for
|
|
2829
|
-
the same reason `from` does.
|
|
2830
|
-
|
|
2831
|
-
**Breaking:** `viewport.recenter` and `ViewApi.recenter` widen to
|
|
2832
|
-
`() => View | void`. Returning the target view lets Cmd+0 animate there;
|
|
2833
|
-
returning nothing keeps the existing behavior. `animatedZoom`'s config fields
|
|
2834
|
-
are `ms` / `resetMs` rather than `duration` / `resetDuration`, matching the
|
|
2835
|
-
animator's vocabulary.
|
|
2836
|
-
|
|
2837
|
-
- 3fb3a46: Forward `onFocus` and `onBlur` from the canvas element
|
|
2838
|
-
|
|
2839
|
-
The canvas is focusable by default (`tabIndex` 0) but exposed no way to
|
|
2840
|
-
observe focus, so consumers driving focus-dependent chrome had to attach a
|
|
2841
|
-
listener to an ancestor and infer it. Both are now props on `CanvasProps`, and
|
|
2842
|
-
so reach `SceneCanvasProps` and the canvas element unchanged.
|
|
2843
|
-
|
|
2844
|
-
- 67bcb05: Drop four values the canvas layer memo no longer reads
|
|
2845
|
-
|
|
2846
2002
|
`hit-test affordances against the painted chrome state` moved the selection
|
|
2847
2003
|
overlay to reading bounds off the chrome state at paint time, which left
|
|
2848
2004
|
`selectedIds`, `multiActive`, `previewToolPose` and `previewToolBounds`
|
|
2849
2005
|
referenced only by the `layers` memo's dependency array — nothing in the body
|
|
2850
2006
|
used them. Removing them from the array made all four dead locals, so they go
|
|
2851
2007
|
too.
|
|
2852
|
-
|
|
2008
|
+
|
|
2853
2009
|
The memo now rebuilds the layer array on layer/tool/geometry changes rather
|
|
2854
2010
|
than additionally on every selection and preview-pose change. Selection chrome
|
|
2855
2011
|
is unaffected: it repaints from chrome state, not from the identity of this
|
|
2856
2012
|
array.
|
|
2857
|
-
|
|
2858
2013
|
- 47cbb08: A closed subpath's dash no longer seams at its start vertex
|
|
2859
|
-
|
|
2014
|
+
|
|
2860
2015
|
`splitForDash` flushed the run still open when a closed subpath's walk returned
|
|
2861
2016
|
to the vertex it started from as its own open sub-polyline, so it and the run
|
|
2862
2017
|
that began there rendered as two butt-capped ribbons meeting at a point — a
|
|
@@ -2865,17 +2020,16 @@
|
|
|
2865
2020
|
is drawn across the seam like any other corner. A pattern whose first "on"
|
|
2866
2021
|
length covers the whole perimeter emits a closed ribbon, identical to the
|
|
2867
2022
|
undashed stroke.
|
|
2868
|
-
|
|
2869
2023
|
- f43e9c2: A derived edge follows the drag that moves its endpoint
|
|
2870
|
-
|
|
2024
|
+
|
|
2871
2025
|
`move`, `resize` and `rotate` kept their in-flight poses in action-local
|
|
2872
2026
|
scratch and published them only as `previewIds` / `previewPose`. That surface
|
|
2873
2027
|
is enough to paint a ghost and size selection chrome, but nothing that asks
|
|
2874
|
-
the
|
|
2028
|
+
the *scene* where a node is can see it — and `scenePoseLookup`, which resolves
|
|
2875
2029
|
a derived node's geometry, asks the scene. So dragging a box left its edge
|
|
2876
2030
|
anchored to the pre-drag position until the drop, when the commit invalidated
|
|
2877
2031
|
the dependents and the edge jumped.
|
|
2878
|
-
|
|
2032
|
+
|
|
2879
2033
|
The three actions now also publish each frame into the scene's ephemeral pose
|
|
2880
2034
|
overrides (`syncPreviewOverrides` / `dropPreviewOverrides` in
|
|
2881
2035
|
`interactions/actions/previewOverrides.ts`). Overrides bypass `executeAndLog`,
|
|
@@ -2883,101 +2037,98 @@
|
|
|
2883
2037
|
avoided per-frame scene writes in the first place was history, and this writes
|
|
2884
2038
|
no history. Entries are set once and mutated in place, published with a single
|
|
2885
2039
|
`commit()` per frame.
|
|
2886
|
-
|
|
2040
|
+
|
|
2887
2041
|
Picking follows for free: the pick source resolves a derived path through its
|
|
2888
2042
|
own override-aware `poseOf`, so an edge is grabbable where it is drawn
|
|
2889
2043
|
mid-gesture rather than where it used to be.
|
|
2890
|
-
|
|
2044
|
+
|
|
2891
2045
|
`clone` is deliberately untouched — its previews are the new ghosts at the
|
|
2892
2046
|
drag target, and the originals never move, so nothing derives from a changed
|
|
2893
2047
|
pose.
|
|
2894
|
-
|
|
2048
|
+
|
|
2895
2049
|
Also closes the matching gap in the preview-ghost layer, which built a
|
|
2896
2050
|
container's clip with no derived path and so ghosted a derived container
|
|
2897
2051
|
without one.
|
|
2898
|
-
|
|
2052
|
+
|
|
2899
2053
|
Note for anyone with a hand-written `Scene` stand-in: `overrides` is now read
|
|
2900
2054
|
on every gesture frame. It was already required by the `Scene` contract, but a
|
|
2901
2055
|
partial fake that omitted it will now throw rather than silently skip.
|
|
2902
|
-
|
|
2903
2056
|
- bb27e83: A derived node is clickable where it paints
|
|
2904
|
-
|
|
2057
|
+
|
|
2905
2058
|
A node whose geometry comes from `derivePath` had no silhouette and no `ink`:
|
|
2906
2059
|
`NodeShapeEntry.silhouette` took only `(node, pose)`, and a derived path is
|
|
2907
|
-
resolved from the
|
|
2060
|
+
resolved from the *dependencies'* poses, which a painter has no handle on. So
|
|
2908
2061
|
`kit:derived` could not report one, `shapeCoversPoint` read the resulting null
|
|
2909
2062
|
as "no opinion" and answered `true` everywhere, and picking fell back to the
|
|
2910
2063
|
node's own pose — for an edge, a zero-sized placeholder at the origin. An edge
|
|
2911
2064
|
was unpickable, and a derived container contributed no clip.
|
|
2912
|
-
|
|
2065
|
+
|
|
2913
2066
|
`silhouette` now takes a `NodeSilhouetteCtx` carrying `derivedPath`, on the
|
|
2914
2067
|
same convention `NodePaintCtx` already uses, and `kit:derived` reports the
|
|
2915
2068
|
derived path as its silhouette and its declared stroke as its `ink`.
|
|
2916
|
-
|
|
2917
|
-
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
|
|
2918
2071
|
the painter: `PickSource.derivedPathOf`, a matching optional argument to
|
|
2919
2072
|
`buildSceneTree`, and `SceneSlotConfig.derivedPathOf` — the slot already
|
|
2920
|
-
carried the derived path a node
|
|
2921
|
-
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
|
|
2922
2075
|
bare-adapter paths supply none of them and behave exactly as before.
|
|
2923
|
-
|
|
2076
|
+
|
|
2924
2077
|
The pre-filter had to move with it. `useSceneSelectTool` grew its region test
|
|
2925
2078
|
from the node's pose, which for a derived node is the wrong box entirely, so
|
|
2926
2079
|
the edge was rejected before the shape test could claim it. It now tests the
|
|
2927
2080
|
derived path when there is one — `poseContains` already reads a path-like pose
|
|
2928
2081
|
as a path, so this reuses it rather than adding a second reach calculation.
|
|
2929
|
-
|
|
2082
|
+
|
|
2930
2083
|
`findShapeSilhouette` skips its memo when handed a derived path. That slot is
|
|
2931
2084
|
keyed on `(node, pose, data)` and cannot see the path, so it would serve one
|
|
2932
2085
|
caller's silhouette to a caller that passed a different one — the same reason
|
|
2933
2086
|
`kit:derived` already skips `PAINT_SLOT`.
|
|
2934
|
-
|
|
2935
2087
|
- 6a33c3f: A node's path can be derived from other nodes' poses
|
|
2936
|
-
|
|
2088
|
+
|
|
2937
2089
|
A node declares `dependsOn: NodeId[]` and a `derivePath` function resolved by key
|
|
2938
2090
|
through `SceneRegistry`, and the scene walks resolve its path before painting
|
|
2939
2091
|
rather than it being authored. An edge drawn between two boxes is then an
|
|
2940
2092
|
ordinary scene node — selectable, styleable, exportable — whose geometry never
|
|
2941
2093
|
enters undo history. The seam and its traps are in `docs/extending.md`.
|
|
2942
|
-
|
|
2094
|
+
|
|
2943
2095
|
New surface: `scene.removeMany(ids)`; `dependsOn` and `derivePath` on
|
|
2944
2096
|
`NodeBase` and on `AddNodeSpec`, which is what a consumer writes;
|
|
2945
2097
|
`SceneRegistry.derivePath`; `SerializedNode.dependsOn` and
|
|
2946
2098
|
`SerializedNode.derivePathKey`, both additions to the serialization format;
|
|
2947
2099
|
`NodePaintCtx.derivedPath`.
|
|
2948
|
-
|
|
2100
|
+
|
|
2949
2101
|
Deleting a node now deletes everything that derives from it, transitively,
|
|
2950
2102
|
including those nodes' own subtrees, in one undo entry — so `scene.remove` can
|
|
2951
2103
|
remove nodes anywhere in the tree that the caller never named, and `removeLayer`
|
|
2952
2104
|
reaches nodes on other layers. Undo after the built-in **Delete** key does not
|
|
2953
2105
|
yet restore the cascaded nodes; see "Derived geometry follow-ups" in
|
|
2954
2106
|
`docs/TODO.md`.
|
|
2955
|
-
|
|
2107
|
+
|
|
2956
2108
|
**Breaking: `defaultDrawOne` takes `(node, pose, view?, ctx?)`.** The paint
|
|
2957
2109
|
context moves to a fourth parameter, so a call passing a `NodePaintCtx` third is
|
|
2958
2110
|
now a type error rather than a silent slide into the `view` slot. The same
|
|
2959
2111
|
fourth parameter is added to the `SceneViewDrawOne` and `SceneSlotConfig.drawOne`
|
|
2960
2112
|
callback types, which is not a break: an existing three-parameter implementation
|
|
2961
2113
|
still satisfies them, and an existing three-argument call still compiles.
|
|
2962
|
-
|
|
2114
|
+
|
|
2963
2115
|
**Breaking: `Scene` gained a required `removeMany`.** A hand-written object
|
|
2964
2116
|
typed as a `Scene` — a test double, most likely — no longer typechecks until it
|
|
2965
2117
|
implements it.
|
|
2966
|
-
|
|
2118
|
+
|
|
2967
2119
|
**Breaking: `kit:remove`'s op payload changed shape.** `rootId` / `parent` /
|
|
2968
2120
|
`index` became `detached: { id, parent, index }[]`, because a cascaded dependent
|
|
2969
2121
|
is not a descendant of the removed node and the tree has to be told about every
|
|
2970
2122
|
subtree that came out of it. A history persisted by an older build now throws
|
|
2971
2123
|
mid-undo rather than degrading. The break is deliberate; kit op payloads are not
|
|
2972
2124
|
versioned.
|
|
2973
|
-
|
|
2974
2125
|
- c24e7de: Detached views follow pose overrides
|
|
2975
|
-
|
|
2126
|
+
|
|
2976
2127
|
`<SceneViewCanvas>` and `<MinimapCanvas>` re-rendered off `scene.getVersion()`,
|
|
2977
2128
|
which a pose override deliberately never bumps — so they kept painting document
|
|
2978
2129
|
poses while `<SceneCanvas>` painted the overridden ones. A minimap beside a
|
|
2979
2130
|
canvas driving a drag or a simulation silently disagreed with it.
|
|
2980
|
-
|
|
2131
|
+
|
|
2981
2132
|
`<SceneViewCanvas>` now paints through `useFrameLoop` instead of from React, and
|
|
2982
2133
|
subscribes to `scene.overrides`. A render (prop change or version bump) and an
|
|
2983
2134
|
override commit both just mark the surface dirty, and one animation frame
|
|
@@ -2985,39 +2136,37 @@
|
|
|
2985
2136
|
render, and a backgrounded tab stops painting them entirely. The mount paint
|
|
2986
2137
|
stays synchronous, so the first frame is still the scene rather than a blank
|
|
2987
2138
|
canvas. `<MinimapCanvas>` inherits all of this through it.
|
|
2988
|
-
|
|
2139
|
+
|
|
2989
2140
|
Repaints driven by a prop change are now asynchronous: they land on the next
|
|
2990
2141
|
animation frame rather than in the layout effect of the render that caused them.
|
|
2991
2142
|
Code that renders and then reads pixels in the same tick needs to wait a frame.
|
|
2992
|
-
|
|
2993
|
-
A minimap's
|
|
2143
|
+
|
|
2144
|
+
A minimap's *framing* still derives from document poses, so a node overridden
|
|
2994
2145
|
outside the document bounds paints outside the fitted frame — recomputing the
|
|
2995
2146
|
fit per frame would rescale the whole minimap throughout a settle.
|
|
2996
|
-
|
|
2997
2147
|
- ce82f4a: An enum leaf can ask for a segmented control, and `pair` works inside an object
|
|
2998
|
-
|
|
2148
|
+
|
|
2999
2149
|
`ToolPrefEnumControl` gains `'toggle'`: a three-option enum shows all three at
|
|
3000
2150
|
once instead of hiding two behind a select. Options carry an optional `short`
|
|
3001
2151
|
label — a capital or two — for the width a property row has; the full `label`
|
|
3002
2152
|
stays the accessible name, so the abbreviation never becomes the only thing
|
|
3003
2153
|
naming the option. A mixed selection selects no segment rather than picking a
|
|
3004
2154
|
winner.
|
|
3005
|
-
|
|
2155
|
+
|
|
3006
2156
|
`pair` now merges fields inside an object leaf, as it already did for section
|
|
3007
2157
|
rows — a hint shouldn't mean something different for being a field of a value
|
|
3008
|
-
rather than a sibling of one. It merges
|
|
2158
|
+
rather than a sibling of one. It merges *adjacent* leaves in both places, so
|
|
3009
2159
|
the schema orders family, size, weight: size and weight pair, and family (which
|
|
3010
2160
|
sat between them) moves ahead of the pair rather than splitting it.
|
|
3011
|
-
|
|
2161
|
+
|
|
3012
2162
|
A stroke's cap, join and align share one row; property rows wrap rather than
|
|
3013
2163
|
overflow when the controls in them don't fit.
|
|
3014
|
-
|
|
3015
2164
|
- be697dc: Add ephemeral pose overrides to the scene
|
|
3016
|
-
|
|
2165
|
+
|
|
3017
2166
|
`scene.overrides` holds a per-node `{ pose?, alpha? }` that the render and
|
|
3018
2167
|
hit-test paths read through and that history, `toJSON()` and `getVersion()`
|
|
3019
2168
|
never see. It is additive: a scene with no overrides behaves exactly as before.
|
|
3020
|
-
|
|
2169
|
+
|
|
3021
2170
|
This is where per-frame motion belongs. A 60 Hz loop previously had to write
|
|
3022
2171
|
through `setPose`, which records an undo entry (one per frame at best, batched)
|
|
3023
2172
|
and bumps the scene version, re-rendering every `useSyncExternalStore`
|
|
@@ -3025,47 +2174,45 @@
|
|
|
3025
2174
|
frame, because the painter memo keys on pose reference. An override entry is
|
|
3026
2175
|
hoisted once and mutated in place; `overrides.commit()` publishes the frame and
|
|
3027
2176
|
invalidates the memo for the overridden nodes only.
|
|
3028
|
-
|
|
2177
|
+
|
|
3029
2178
|
`commit()` is required after an in-place mutation — without it the memo serves
|
|
3030
2179
|
the previous frame's draw. Overrides are cleared when a node is removed, since
|
|
3031
2180
|
ids are reusable. To make a frame permanent, write it once through `setPose`
|
|
3032
2181
|
and clear the override; that single step is the undo entry.
|
|
3033
|
-
|
|
2182
|
+
|
|
3034
2183
|
`ForceGraphDemo` now settles with zero history entries and bakes the result as
|
|
3035
2184
|
one, replacing a per-tick batch of 24 `setPose` calls.
|
|
3036
|
-
|
|
3037
2185
|
- e909a3b: `fitTextPose` sizes a box the renderer will actually fill
|
|
3038
|
-
|
|
2186
|
+
|
|
3039
2187
|
It was the fourth site measuring text its own way: `ctx.measureText` per
|
|
3040
2188
|
character against system fonts, no kerning, `pose.text` only. Nothing masked
|
|
3041
2189
|
it the way the WebGL context masked the caret — a consumer calling it got a
|
|
3042
2190
|
box that disagreed with the paint, narrower by a kern on every pair and wrong
|
|
3043
2191
|
by the whole difference between the installed family and the registered face.
|
|
3044
2192
|
It goes through the shared layout now, so it sees kerning and per-run styling.
|
|
3045
|
-
|
|
2193
|
+
|
|
3046
2194
|
**Breaking:** `fitTextPose(ctx, pose, opts)` is now `fitTextPose(pose, opts)`.
|
|
3047
|
-
|
|
3048
2195
|
- 26bbdcf: Paint the canvas from its own animation frame instead of from a React render
|
|
3049
|
-
|
|
2196
|
+
|
|
3050
2197
|
`requestRedraw()` marks the surface dirty and the next frame paints, so many
|
|
3051
2198
|
redraws in one tick cost one paint. The view gains an imperative path on the
|
|
3052
2199
|
canvas handle — `setView` / `getView` / `subscribeView` — and `SceneCanvas` no
|
|
3053
2200
|
longer holds it in React state, so a camera moving at 60 Hz costs no renders.
|
|
3054
2201
|
Consumers passing a `view` prop stay controlled and are unaffected.
|
|
3055
|
-
|
|
2202
|
+
|
|
3056
2203
|
Opt-ins that come with it: `syncPaint` paints inside the commit for a consumer
|
|
3057
2204
|
that wants the old whole-cloth guarantee, `useScene(…, { subscribe: false })`
|
|
3058
2205
|
gives a host the scene without a render per mutation, `useSceneTextEdit`'s
|
|
3059
2206
|
`view` option accepts a thunk so the overlay tracks a ref-driven camera, and a
|
|
3060
2207
|
`contentVersion` prop feeds the version that `getPaintedVersion()` reports.
|
|
3061
|
-
|
|
2208
|
+
|
|
3062
2209
|
Two public signatures changed. `usePinchZoomTool` takes a view getter,
|
|
3063
2210
|
`getView: () => View`, where it took a `View` — nothing re-renders to refresh a
|
|
3064
2211
|
captured value any more. `CanvasExtensionApi` gained five required members —
|
|
3065
2212
|
`getView`, `setView`, `subscribeView`, `subscribeFrame`, `getPaintedVersion` —
|
|
3066
2213
|
so external code hand-implementing that interface stops typechecking; code that
|
|
3067
2214
|
only calls through the ref is unaffected.
|
|
3068
|
-
|
|
2215
|
+
|
|
3069
2216
|
Pixels and DOM can now be a frame apart, in whichever direction the change came
|
|
3070
2217
|
from. A view change leads with pixels: `setView` paints without rendering, so
|
|
3071
2218
|
DOM built from the view is stale until something re-renders it — position
|
|
@@ -3074,64 +2221,60 @@
|
|
|
3074
2221
|
pixels land next frame — compare `getPaintedVersion()` against the version you
|
|
3075
2222
|
are about to render when chrome must be in lockstep. Do not render scene-derived
|
|
3076
2223
|
DOM inside `startTransition`: React defers it and nothing forces it to catch up.
|
|
3077
|
-
|
|
2224
|
+
|
|
3078
2225
|
Anything reading the drawing buffer back outside a paint — the hud loupe's pixel
|
|
3079
2226
|
mode is the one in-tree case — can likewise see a buffer one frame older;
|
|
3080
2227
|
`subscribeFrame` runs on the frame that painted and removes the lag. Nothing
|
|
3081
2228
|
paints while `document.hidden` is true, `syncPaint` included, so a readback from
|
|
3082
2229
|
a background tab returns the frame from before the tab was hidden.
|
|
3083
|
-
|
|
3084
2230
|
- 546f67d: Copy typed-array arguments into `makeGLRecorder`'s call log as they are
|
|
3085
2231
|
recorded. A caller is entitled to reuse the array it uploads from, so storing
|
|
3086
2232
|
the reference recorded a value that later frames overwrote — a test reading
|
|
3087
2233
|
two frames back saw the same numbers twice and passed. Test-only surface.
|
|
3088
2234
|
- 3fb3a46: Release held keys when the window loses focus
|
|
3089
|
-
|
|
2235
|
+
|
|
3090
2236
|
A window that blurs mid-hold never delivers the keyup, so every in-flight
|
|
3091
2237
|
`key-held` handle stayed engaged until that key was pressed again — holding
|
|
3092
2238
|
Space and tabbing away left the hand tool on the hotkey stack indefinitely.
|
|
3093
|
-
|
|
2239
|
+
|
|
3094
2240
|
The gesture dispatcher now fires the `key-held` up phase for each held key on
|
|
3095
2241
|
window blur. Consumers that hand-rolled this reset can drop it; ongoing
|
|
3096
2242
|
invocations see a normal `onEnd`.
|
|
3097
|
-
|
|
3098
2243
|
- ccd51cc: Add a 43-glyph monochrome icon set to `@weasel-js/ui`.
|
|
3099
|
-
|
|
2244
|
+
|
|
3100
2245
|
One register: a 20x20 viewBox drawn in `currentColor` at stroke-width 1.5 with
|
|
3101
2246
|
round caps and joins, hairline weight reserved for structure, and filled
|
|
3102
2247
|
regions only where an action has a subject. Covers transport, history, view,
|
|
3103
2248
|
trial lifecycle, collection, state, instrument and status vocabulary. Import a
|
|
3104
2249
|
named component (`CloneIcon`), or `Icon` when the glyph is chosen at runtime.
|
|
3105
|
-
|
|
2250
|
+
|
|
3106
2251
|
`@weasel-js/ui` also re-exports the tool glyphs that live in `@weasel-js/core`,
|
|
3107
2252
|
so consumers have one import site for the whole set. `ImageIcon` was reachable
|
|
3108
2253
|
from core's icons folder but missing from its public barrel; it is exported
|
|
3109
2254
|
now.
|
|
3110
|
-
|
|
2255
|
+
|
|
3111
2256
|
Glyph geometry is generated (`npm run gen:icons`) from `packages/ui/scripts/icons/`
|
|
3112
2257
|
rather than hand-placed, because arrowheads and joins that miss their terminus
|
|
3113
2258
|
are invisible at chrome size.
|
|
3114
|
-
|
|
3115
2259
|
- 3fb3a46: Compose `before` and `after` layer chains in both directions
|
|
3116
|
-
|
|
2260
|
+
|
|
3117
2261
|
`composeOrderedLayers` walked the two anchor maps separately: a chain hanging
|
|
3118
2262
|
off an `after` anchor only followed further `after` links, and likewise for
|
|
3119
2263
|
`before`. A custom layer anchored `before: 'scene'` carrying a second custom
|
|
3120
2264
|
anchored `after` it dropped that second layer to the tail with a spurious
|
|
3121
2265
|
dangling-reference warning.
|
|
3122
|
-
|
|
2266
|
+
|
|
3123
2267
|
Both walks now emit a layer's `before` chain, the layer, then its `after`
|
|
3124
2268
|
chain, so the two mix freely. Cycle detection and orphan fallback are
|
|
3125
2269
|
unchanged.
|
|
3126
|
-
|
|
3127
2270
|
- d9f110e: Stop every frame loop while nothing can see it
|
|
3128
|
-
|
|
2271
|
+
|
|
3129
2272
|
New public hook `useVisibleRaf` in `@weasel-js/core` owns the question of
|
|
3130
2273
|
whether a frame may run: nothing runs while `document.hidden`, and a loop that
|
|
3131
2274
|
names an element also stops while that element is outside the viewport. A
|
|
3132
2275
|
request made while suspended is held rather than dropped and re-armed on
|
|
3133
2276
|
resume, so a loop never polls visibility or needs restarting by hand.
|
|
3134
|
-
|
|
2277
|
+
|
|
3135
2278
|
Ten loops now run behind it — `useFrameLoop`, `useAnimator`, `useSimulation`,
|
|
3136
2279
|
`useDecayLoop`, `useTextEdit`'s overlay follow, `CursorCoordsHud`'s FPS
|
|
3137
2280
|
counter, `Badge`'s crawl, and labkit's `FpsMeter`, `useTiledSurface` and
|
|
@@ -3139,30 +2282,29 @@
|
|
|
3139
2282
|
the rest ran on any page left open. `useLayerScheduler` looked safe and wasn't:
|
|
3140
2283
|
it paints only dirty layers, but a hidden tab still commits React updates and
|
|
3141
2284
|
its view/size effect marks every layer dirty.
|
|
3142
|
-
|
|
2285
|
+
|
|
3143
2286
|
Loops measuring elapsed time rebase their clock through the new `onResume`
|
|
3144
2287
|
option, so an hour spent hidden does not arrive as one hour-long frame — an FPS
|
|
3145
2288
|
meter reporting a rate nobody achieved, a tween jumping to its end value on
|
|
3146
2289
|
return. `dangerouslyRunWhenHidden` opts a loop out for offscreen recording or
|
|
3147
2290
|
export; nothing in the tree sets it.
|
|
3148
|
-
|
|
2291
|
+
|
|
3149
2292
|
`npm run check:frame-loops` fails the build on a bare `requestAnimationFrame`
|
|
3150
2293
|
in kit source, and runs in CI.
|
|
3151
|
-
|
|
3152
2294
|
- 0dd35a1: Fix pinch-to-zoom: mac trackpads zoomed the page, and `viewport.pinchZoom` zoomed twice
|
|
3153
|
-
|
|
2295
|
+
|
|
3154
2296
|
A trackpad pinch reaches the page as `wheel { ctrlKey: true }`. On a mac
|
|
3155
2297
|
`viewport.zoom`'s `mods: { mod: true }` binding requires metaKey and forbids
|
|
3156
2298
|
ctrl, and `viewport.wheelPan` forbids ctrl too, so nothing claimed the event
|
|
3157
2299
|
and the browser's own ctrl+wheel page zoom ran. `viewport.zoom` now carries a
|
|
3158
2300
|
second wheel binding on bare ctrl. Off mac it duplicates the `mod` binding,
|
|
3159
2301
|
where the matcher picks a single winner.
|
|
3160
|
-
|
|
2302
|
+
|
|
3161
2303
|
Nothing caught that because `IS_MAC` read `navigator.platform ?? userAgent`,
|
|
3162
2304
|
and jsdom reports an empty-string platform — not nullish, so the fallback never
|
|
3163
2305
|
fired and every mac binding in the kit was exercised only on the non-mac
|
|
3164
2306
|
branch. It reads `||` now.
|
|
3165
|
-
|
|
2307
|
+
|
|
3166
2308
|
Separately, `viewport.pinchZoom: true` mounted `<Canvas>`'s `usePinchZoomTool`
|
|
3167
2309
|
alongside the `viewport.pinchZoom` action that already handled the same
|
|
3168
2310
|
gesture, applying one pinch's factor twice — the opt-in broke the path that
|
|
@@ -3171,36 +2313,34 @@
|
|
|
3171
2313
|
the kit's 0.1–8 clamp now applied by default. `pinchZoom: false` disables pinch
|
|
3172
2314
|
for real; it previously left the action running. Bare `<Canvas>` keeps the hook
|
|
3173
2315
|
as its own pinch path.
|
|
3174
|
-
|
|
3175
2316
|
- 1a0bea3: `useNodeOverlayFrame`: the coordinate frame a DOM overlay pinned to a node needs
|
|
3176
|
-
|
|
2317
|
+
|
|
3177
2318
|
Nothing in the kit exported one, so consumers hand-rolled it — their own
|
|
3178
2319
|
`ResizeObserver` next to the existing `useCanvasSize`, and a translate-and-scale
|
|
3179
2320
|
inverse built by projecting two points. That inverse silently drops
|
|
3180
2321
|
`pose.rotation`, which is why on-canvas gradient handles on a rotated node sat
|
|
3181
2322
|
beside the paint instead of on it.
|
|
3182
|
-
|
|
2323
|
+
|
|
3183
2324
|
```ts
|
|
3184
|
-
useNodeOverlayFrame(scene, containerRef, nodeId, { view })
|
|
2325
|
+
useNodeOverlayFrame(scene, containerRef, nodeId, { view })
|
|
3185
2326
|
// → { box, toScreen, toLocal, width, height } | null
|
|
3186
2327
|
```
|
|
3187
|
-
|
|
2328
|
+
|
|
3188
2329
|
`box` is the node's composed world box, unrotated — the frame `toScreen` maps
|
|
3189
2330
|
from, and the box to hand `fillInPoseFrame` / `fillToBoundsFrame`. Rotation
|
|
3190
2331
|
lives in the pose→world leg, where it belongs: a node's stored geometry and its
|
|
3191
2332
|
bounds-frame paint are pre-rotation by definition, so neither of those two
|
|
3192
2333
|
changes.
|
|
3193
|
-
|
|
2334
|
+
|
|
3194
2335
|
`@weasel-js/ui` gains `SceneGradientHandles`, the scene-aware half of
|
|
3195
2336
|
`GradientHandles`: it reads the gradient out of a node's `fill` **or** its
|
|
3196
2337
|
`stroke` — `slot` is a prop — and commits each drag through `setFill` or
|
|
3197
2338
|
`setStroke` as one undo entry. `GradientHandles` itself stays frame-agnostic.
|
|
3198
|
-
|
|
2339
|
+
|
|
3199
2340
|
Also: `isGradientFill` narrows a `FillStyle` to its three gradient members, and
|
|
3200
2341
|
`useCanvasSize` accepts any `HTMLElement` rather than only a `div`.
|
|
3201
|
-
|
|
3202
2342
|
- 9d95836: A node's `data.stroke` takes a whole `Stroke`, not just a color
|
|
3203
|
-
|
|
2343
|
+
|
|
3204
2344
|
`NodeStroke = string | Stroke`, mirroring `NodeFill`. A string is still a
|
|
3205
2345
|
color and `'none'` still skips the stroke; an object is a core `Stroke` whose
|
|
3206
2346
|
`width`, `cap`, `join`, `dash`, `miterLimit` and `align` all reach the
|
|
@@ -3209,99 +2349,94 @@
|
|
|
3209
2349
|
rule `withLeafStroke` already applied to text. A bounds-relative stroke paint
|
|
3210
2350
|
is baked onto the pose box the way a fill is, so a gradient stroke resolves
|
|
3211
2351
|
against the box it was authored against.
|
|
3212
|
-
|
|
2352
|
+
|
|
3213
2353
|
`kit:shape` now honors `stroke: 'none'`, which only `kit:path` checked before.
|
|
3214
|
-
|
|
2354
|
+
|
|
3215
2355
|
`NodeInk` reports `{ filled, outset, inset }` instead of `{ filled,
|
|
3216
|
-
strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
2356
|
+
strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
3217
2357
|
`'outer'` none inside, which one number could not say, so picking grabbed the
|
|
3218
2358
|
wrong side. `ink` takes an optional context carrying the view scale, so a
|
|
3219
2359
|
`{ px }` stroke width resolves to world units. A painter that still returns
|
|
3220
2360
|
`{ filled, strokeWidth }` is read as a centered stroke and keeps working.
|
|
3221
|
-
|
|
2361
|
+
|
|
3222
2362
|
`setStroke` and `setStrokeOpacity` no longer stringify a node's `Stroke`: a
|
|
3223
2363
|
color pick replaces its paint and keeps width, cap, join and dash, and an
|
|
3224
2364
|
opacity drag sets the paint's `opacity`, which is the only form that works on
|
|
3225
2365
|
a gradient stroke.
|
|
3226
|
-
|
|
2366
|
+
|
|
3227
2367
|
Editing UI for the rich form is not here yet — a schema-driven color control
|
|
3228
2368
|
still writes a bare string over the object, so nodes carrying one are for
|
|
3229
2369
|
programmatic authorship until `SelectionPanel` learns the union. See
|
|
3230
2370
|
`docs/proposals/2026-08-26-node-stroke-union.md`.
|
|
3231
|
-
|
|
3232
2371
|
- 62a3c46: Paint a gradient or pattern stroke instead of throwing.
|
|
3233
|
-
|
|
2372
|
+
|
|
3234
2373
|
`Stroke.paint` has always been a full `FillStyle`, and SVG import puts paint
|
|
3235
2374
|
servers there deliberately, but the renderer refused anything but a solid — so
|
|
3236
2375
|
importing a shape with `stroke="url(#grad)"` produced a scene that threw on the
|
|
3237
2376
|
next frame. Both stroke paths now paint the ribbon through the same route a
|
|
3238
2377
|
fill takes, including under the inner/outer alignment stencil. A non-solid
|
|
3239
2378
|
even-odd fill no longer renders black.
|
|
3240
|
-
|
|
3241
2379
|
- 5f6c28e: An object leaf's fields can be organised into groups
|
|
3242
|
-
|
|
2380
|
+
|
|
3243
2381
|
`ToolPrefObject.children` takes a `ToolPrefGroup` as well as a leaf. A group
|
|
3244
2382
|
heads its fields under a label and contributes nothing to the path — the same
|
|
3245
2383
|
rule group keys follow at the top level of a schema, so a field inside one is
|
|
3246
2384
|
still addressed as a field of the object.
|
|
3247
|
-
|
|
2385
|
+
|
|
3248
2386
|
Without it, a value with many fields renders as one undifferentiated list. A
|
|
3249
2387
|
`TextStyle` is the case that needs it: its character and paragraph fields are
|
|
3250
2388
|
one value but read as two lists.
|
|
3251
|
-
|
|
3252
2389
|
- 3cd1ee8: A schema leaf can hold an object, with its fields hanging off it
|
|
3253
|
-
|
|
2390
|
+
|
|
3254
2391
|
A compound value — a stroke, a shadow, a pattern spec — could be described as
|
|
3255
2392
|
sibling leaves addressing into it (`data.stroke.width`, `data.stroke.cap`).
|
|
3256
2393
|
It shouldn't be: each control then writes one field of a value it can only
|
|
3257
2394
|
half see, and writing a field into something that isn't an object yet corrupts
|
|
3258
2395
|
it outright.
|
|
3259
|
-
|
|
2396
|
+
|
|
3260
2397
|
`ToolPrefObject` describes the value instead. Its `children` are ordinary
|
|
3261
2398
|
leaves whose paths are relative to the object, and every child edit commits
|
|
3262
2399
|
the parent object whole. A field that is itself a union declares the kind that
|
|
3263
2400
|
edits that union — a stroke's `paint` is a `paint` leaf. `fromScalar` lifts a
|
|
3264
2401
|
value still held in a scalar form before a child edit lands on it, which is
|
|
3265
2402
|
how a stroke stored as a bare colour string gains a width.
|
|
3266
|
-
|
|
2403
|
+
|
|
3267
2404
|
`defaultNodeProperties` describes `data.stroke` this way, so the panel shows
|
|
3268
2405
|
Color, Width, Cap, Join and Align under one Stroke block, and the separate
|
|
3269
2406
|
`data.strokeWidth` leaf is gone. `SelectionPanel` now honours `block`, which
|
|
3270
2407
|
`PrefsForm` already did. The one-off `stroke` pref kind added days ago is
|
|
3271
2408
|
replaced by this general one.
|
|
3272
|
-
|
|
2409
|
+
|
|
3273
2410
|
`dash` has no leaf: it is a `number[]` and no kind edits one. It survives
|
|
3274
2411
|
import, export and rendering untouched.
|
|
3275
|
-
|
|
3276
2412
|
- 2ea772f: Selection handles are hit-tested at the size they are painted
|
|
3277
|
-
|
|
2413
|
+
|
|
3278
2414
|
Handles painted at `HANDLE_BASE_PX * targetScale` and hit-tested at the bare
|
|
3279
2415
|
constant, and neither `buildAffordanceAt` call site passed the option that
|
|
3280
2416
|
would have scaled it. A coarse pointer got a bigger picture and exactly the
|
|
3281
2417
|
same 8px grab zone it had on a mouse — the touch forgiveness the coarse profile
|
|
3282
2418
|
exists to provide never reached the hit-test. The slops debug overlay was a
|
|
3283
2419
|
third unscaled copy, so it drew hit regions where they were not.
|
|
3284
|
-
|
|
2420
|
+
|
|
3285
2421
|
`core/device/targets.ts` now holds one base table and one accessor,
|
|
3286
2422
|
`targetSizesPx(targetScale)`. Paint, hit-test and the debug overlay all resolve
|
|
3287
2423
|
through it. `HANDLE_BASE_PX`, `ANCHOR_HIT_BASE_PX` and
|
|
3288
2424
|
`ROTATION_HANDLE_BASE_PX` keep their names and values and now read off the
|
|
3289
2425
|
table; the internal `HANDLE_HIT_RADIUS` and `ANCHOR_HIT_RADIUS` are gone.
|
|
3290
|
-
|
|
2426
|
+
|
|
3291
2427
|
`buildAffordanceAt` and `createSlopsDebugLayer` take an optional `targetScale`.
|
|
3292
2428
|
`selectTool.handleHitRadius` now actually reaches the hit-test — it previously
|
|
3293
2429
|
reached nothing.
|
|
3294
|
-
|
|
2430
|
+
|
|
3295
2431
|
`useRotateTool`'s `handleHitRadius` option is **removed**. The rotation
|
|
3296
2432
|
affordance is an annulus with a band thickness and no point radius, so the
|
|
3297
2433
|
option could only ever have been a second name for `rotationHandleDistance`,
|
|
3298
2434
|
which is live and now defaults from the same table.
|
|
3299
|
-
|
|
2435
|
+
|
|
3300
2436
|
Known gap: `CanvasView` is a second `buildAffordanceAt` call site that reads no
|
|
3301
2437
|
device profile, so a nested view still hit-tests at the fine-pointer size.
|
|
3302
|
-
|
|
3303
2438
|
- f77bd95: `getChildren` means one thing on an adapter
|
|
3304
|
-
|
|
2439
|
+
|
|
3305
2440
|
`MoveAdapter` declared `getChildren(id)` — a node's direct children, for the
|
|
3306
2441
|
drag cascade — and `OrderedAdapter` declared `getChildren(parentId | null)`,
|
|
3307
2442
|
the z-ordering seam where `null` means the root. Both land on the same adapter
|
|
@@ -3309,36 +2444,35 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3309
2444
|
under the name the ops read with the second meaning. An op asking for root
|
|
3310
2445
|
order got `[]`, which reads as "the root has no siblings", and the slot it
|
|
3311
2446
|
captured was silently lost.
|
|
3312
|
-
|
|
2447
|
+
|
|
3313
2448
|
The two declarations are now one contract, and `arrayAdapter` answers the root
|
|
3314
2449
|
from its own item array rather than delegating — a consumer callback written
|
|
3315
2450
|
for node ids returns `[]` there, which cannot be told apart from a genuine
|
|
3316
2451
|
empty answer. A consumer's `getChildren` config is still only ever asked about
|
|
3317
2452
|
a node id.
|
|
3318
|
-
|
|
2453
|
+
|
|
3319
2454
|
`arrayAdapter` still exposes no `setChildOrder`, so it places by ordinal rather
|
|
3320
2455
|
than by anchor. That is unchanged, and it is why the ordinal fallback exists.
|
|
3321
|
-
|
|
3322
2456
|
- 2ea772f: The canvas and the gradient editor now sample one gradient
|
|
3323
|
-
|
|
2457
|
+
|
|
3324
2458
|
`buildGradientRamp` carried its own interpolation beside
|
|
3325
2459
|
`sampleGradientStops`, and the two disagreed three ways: the ramp had no guard
|
|
3326
2460
|
at either end and extrapolated past the first and last stop, the two picked
|
|
3327
2461
|
opposite sides of a coincident pair, and they parsed color differently — a stop
|
|
3328
2462
|
written as a CSS named color rendered on the canvas and threw in the editor.
|
|
3329
|
-
|
|
2463
|
+
|
|
3330
2464
|
`sampleGradientStops` keeps its semantics and is now the only implementation.
|
|
3331
2465
|
`resolveGradientStops` sorts and parses the list once; `sampleResolvedStops`
|
|
3332
2466
|
returns the color at `t`. The ramp cache builds its texels through those, so
|
|
3333
2467
|
there is no interpolation math left in the renderer.
|
|
3334
|
-
|
|
2468
|
+
|
|
3335
2469
|
Two behavior changes worth naming. `resolveColor` is the surviving parser, so
|
|
3336
2470
|
gradient stops accept named and functional colors everywhere — but no longer
|
|
3337
2471
|
hex without a leading `#`, which only the editor path had tolerated and the
|
|
3338
2472
|
canvas never accepted. And `sampleGradientStops` returns normalized hex at the
|
|
3339
2473
|
endpoints instead of echoing the raw stop string, so `'red'` comes back as
|
|
3340
2474
|
`'#ff0000'`.
|
|
3341
|
-
|
|
2475
|
+
|
|
3342
2476
|
**SVG export:** a conic gradient left the exporter as a dangling `url(#…)` —
|
|
3343
2477
|
the element already carried the reference, the built-in serializer returned
|
|
3344
2478
|
nothing, and the registry's `toSvg` slot has no in-repo implementation, so the
|
|
@@ -3346,18 +2480,17 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3346
2480
|
through to the same warning the pattern path already emits when nothing can
|
|
3347
2481
|
produce a paint server. A consumer that registers a `toSvg` for
|
|
3348
2482
|
`conic-gradient` still serializes and gets no warning.
|
|
3349
|
-
|
|
3350
2483
|
- aba8d91: Answer "can this node be hit" in one place
|
|
3351
|
-
|
|
2484
|
+
|
|
3352
2485
|
Four tree walks answered it separately — the generic-adapter point pick, the
|
|
3353
2486
|
one `<SceneCanvas>` installs, `sceneToAdapter`'s area walk, and the live
|
|
3354
2487
|
marquee/lasso — plus a fifth that shadowed the third. They agreed on every case
|
|
3355
2488
|
that had a test and disagreed on the rest, three times, silently. `pickWalk`
|
|
3356
2489
|
now owns every gate; a query supplies only its own shape test and the clip
|
|
3357
2490
|
predicate for its region.
|
|
3358
|
-
|
|
2491
|
+
|
|
3359
2492
|
Behavior that changes as a result:
|
|
3360
|
-
|
|
2493
|
+
|
|
3361
2494
|
- **A node painted at alpha 0 is no longer clickable.** The pick path reads the
|
|
3362
2495
|
same number the painter does — the view's `alphaFor` times any per-node
|
|
3363
2496
|
override alpha — so a node faded out of sight stops claiming clicks. The
|
|
@@ -3373,13 +2506,12 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3373
2506
|
- An ancestor clip now rejects an area query that reaches into the clip where
|
|
3374
2507
|
the node is not, or reaches the node where the clip is not — the two terms
|
|
3375
2508
|
together, where one alone let false positives through.
|
|
3376
|
-
|
|
2509
|
+
|
|
3377
2510
|
`useSceneSelectTool` takes `alphaOf` and `layerIsPainted` for the asking view.
|
|
3378
2511
|
`passesAncestorClips` and its module are gone; `pickWalk`, `scenePickSource`,
|
|
3379
2512
|
`adapterPickSource` and `ownClipOf` replace them.
|
|
3380
|
-
|
|
3381
2513
|
- 2ea772f: A drag-to-insert reports the bounds it paints
|
|
3382
|
-
|
|
2514
|
+
|
|
3383
2515
|
The painter, the commit factory and `getGestureBounds()` each sized an
|
|
3384
2516
|
in-flight insert differently. The reporter read the drag rect alone, so a
|
|
3385
2517
|
centered Alt-drag reported a half-extent of `d` against a painted circumradius
|
|
@@ -3388,45 +2520,43 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3388
2520
|
all. The painter and the commit agreed on polygon and star but not on line or
|
|
3389
2521
|
pencil: the commit posed the drag AABB for a line the painter drew endpoint to
|
|
3390
2522
|
endpoint, and fell back to the drag rect for a trail under four samples.
|
|
3391
|
-
|
|
2523
|
+
|
|
3392
2524
|
One function now answers it for all three. The zero-area skip in the painter
|
|
3393
2525
|
and the reporter tests the resolved extent rather than the raw drag rect, and
|
|
3394
2526
|
an `InsertNodeFactory` that returns no `pose` falls back to the extent. The
|
|
3395
2527
|
`bounds` argument handed to a factory is unchanged.
|
|
3396
|
-
|
|
3397
2528
|
- 3386d64: Path command opcodes derive from one table
|
|
3398
|
-
|
|
2529
|
+
|
|
3399
2530
|
`M`/`L`/`C`/`Q`/`Z` and their coordinate counts were declared five times —
|
|
3400
2531
|
once in core, once in `@weasel-js/geom`, and three more as `COORD_COUNT`
|
|
3401
2532
|
literals in the path transform, pose-rotation and pose-descriptor walkers. They
|
|
3402
2533
|
agreed, and nothing held them to each other: a sixth opcode desynchronizes two
|
|
3403
2534
|
packages' reading of the same `Uint8Array` with no exception and no type error,
|
|
3404
2535
|
and every walker misparses the coordinate stream from that command on.
|
|
3405
|
-
|
|
2536
|
+
|
|
3406
2537
|
`PATH_COMMANDS` in `@weasel-js/geom` is now the table. `PATH_M`…`PATH_Z`,
|
|
3407
2538
|
`PATH_CMD_LENGTHS` and the new `pathCommandCoordCount` all derive from it, and
|
|
3408
2539
|
core re-exports them by name, so the opcode constants keep their names, values
|
|
3409
2540
|
and literal types. The three walkers moved onto `forEachSegment` rather than
|
|
3410
2541
|
onto the accessor alone — they were duplicating the coordinate-cursor advance
|
|
3411
2542
|
as well as the length, and the cursor is the half that actually misreads.
|
|
3412
|
-
|
|
2543
|
+
|
|
3413
2544
|
Eight further files switch on these opcodes with inline literals. Five throw on
|
|
3414
2545
|
an unknown code; three — the path boolean adapter, the anchor-editing geometry,
|
|
3415
2546
|
and geom's own boolean adapter — have no `default` arm and would silently stop
|
|
3416
2547
|
advancing. Left as-is; they need per-command semantics, not one walker.
|
|
3417
|
-
|
|
3418
2548
|
- 68d2651: Pref leaf kinds are declared once, and every renderer is exhaustive
|
|
3419
|
-
|
|
2549
|
+
|
|
3420
2550
|
`@weasel-js/ui` carried its own copy of the pref-leaf union under a comment
|
|
3421
2551
|
saying to keep it in sync with core's field-for-field. It had drifted: ui's enum
|
|
3422
2552
|
leaf had neither `encoding` nor `options[].disabled`, so a dash-array
|
|
3423
2553
|
preference did not merely fail to select — choosing an option wrote the option
|
|
3424
2554
|
string over the stored dash array. labkit's two renderers were missing the
|
|
3425
2555
|
`paint` and `object` kinds outright.
|
|
3426
|
-
|
|
2556
|
+
|
|
3427
2557
|
ui's schema is now a rename re-export of core's declaration. The public `Pref*`
|
|
3428
2558
|
names are unchanged, and there is nothing left to keep in sync.
|
|
3429
|
-
|
|
2559
|
+
|
|
3430
2560
|
More importantly, all four renderer switches ended in `default:`, so adding a
|
|
3431
2561
|
built-in kind produced no error at any site and simply rendered nothing —
|
|
3432
2562
|
verified by adding one and typechecking. `ToolPrefLeaf` widens `kind` to
|
|
@@ -3436,34 +2566,32 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3436
2566
|
`isBuiltinToolPref(leaf)`, which narrows to the closed union so each renderer
|
|
3437
2567
|
can discriminate and end in a `never`. App-defined kinds take the placeholder
|
|
3438
2568
|
path as before.
|
|
3439
|
-
|
|
2569
|
+
|
|
3440
2570
|
Dash-array preferences now select and commit correctly in `PrefsForm`: the enum
|
|
3441
2571
|
arm threads sibling values, routes through `encoding.read` / `encoding.write`,
|
|
3442
2572
|
and honors `option.disabled`. `SelectionPanel` already did all of this — it was
|
|
3443
2573
|
only the forked copy that could not express it.
|
|
3444
|
-
|
|
3445
2574
|
- 3386d64: Dragging out a text box shows a live preview
|
|
3446
|
-
|
|
2575
|
+
|
|
3447
2576
|
The set of insertable kinds and the `KitInsertShape` union sat on adjacent
|
|
3448
2577
|
lines with no linkage, and seven more sites restated one list or the other. The
|
|
3449
2578
|
drift was already live: the text tool binds `actionId: 'insert'` and commits
|
|
3450
2579
|
through the insert dep, but the runtime set never listed `text`, so a
|
|
3451
2580
|
drag-to-insert text box had no preview.
|
|
3452
|
-
|
|
2581
|
+
|
|
3453
2582
|
`SHAPE_KINDS` is now one descriptor table — a row per kind, flagged for whether
|
|
3454
2583
|
it has a built-in tool and whether it takes an insert preview. Both unions,
|
|
3455
2584
|
`KIT_SHAPE_KINDS`, `BUNDLE_TOOLS.exhaustive`, the known-builtin-id list and the
|
|
3456
2585
|
preview gate all derive from it.
|
|
3457
|
-
|
|
2586
|
+
|
|
3458
2587
|
Two type-surface consequences. `KIT_SHAPE_KINDS` is typed
|
|
3459
2588
|
`readonly BuiltinShapeToolId[]` rather than a literal tuple — same contents,
|
|
3460
2589
|
same order, and `(typeof KIT_SHAPE_KINDS)[number]` is unchanged; what goes is
|
|
3461
2590
|
positional and length typing, which nothing uses. And `OngoingOverlay['shape']`
|
|
3462
2591
|
gains `'text'`, which is the fix itself: a consumer switching exhaustively over
|
|
3463
2592
|
it gains a case, handled by the existing box arm.
|
|
3464
|
-
|
|
3465
2593
|
- c6c499d: Text layout is computed once, and the caret reads the layout that was painted
|
|
3466
|
-
|
|
2594
|
+
|
|
3467
2595
|
The paint, the pose silhouette and the click-to-edit caret each ran their own
|
|
3468
2596
|
walk. The paint went through a memoized `layoutRuns`; the silhouette re-ran
|
|
3469
2597
|
`layoutRuns` on every pose change, because it allocates a fresh `ResolvedRun[]`
|
|
@@ -3473,7 +2601,7 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3473
2601
|
caret could therefore answer with a different line, and a different glyph, than
|
|
3474
2602
|
the one under the pointer — masked in practice only because it asked a WebGL
|
|
3475
2603
|
canvas for a 2D context and got `null`, degrading silently to no caret at all.
|
|
3476
|
-
|
|
2604
|
+
|
|
3477
2605
|
`cachedLayoutRuns` now lives in `@weasel-js/text` beside the function it caches,
|
|
3478
2606
|
and all three go through it. It keeps the array-identity `WeakMap` as the
|
|
3479
2607
|
renderer's zero-cost path and falls through to a bounded LRU keyed on the runs'
|
|
@@ -3481,7 +2609,7 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3481
2609
|
about 230× cheaper than laying out again, at roughly 4× the cost of the
|
|
3482
2610
|
identity hit. `LaidOutLineBox` carries the caret stops the pen produced, so
|
|
3483
2611
|
snapping is to the advance cells the glyphs were actually painted in.
|
|
3484
|
-
|
|
2612
|
+
|
|
3485
2613
|
**Breaking:** `caretIndexAt(ctx, x, y, pose)` is now
|
|
3486
2614
|
`caretIndexAt(x, y, pose, opts?)` — the `CanvasRenderingContext2D` is gone, and
|
|
3487
2615
|
an optional `maxWidth` mirrors `textLineBoxes` for nodes the `kit:text` painter
|
|
@@ -3489,9 +2617,8 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3489
2617
|
double-click always seeds the caret instead of falling back to editing from
|
|
3490
2618
|
offset 0. `@weasel-js/text` gains a `./test-seams` entry point exporting
|
|
3491
2619
|
`_resetLayoutCacheForTests`.
|
|
3492
|
-
|
|
3493
2620
|
- 4f1ef0b: Lay text out from font bytes alone — no baked atlas.
|
|
3494
|
-
|
|
2621
|
+
|
|
3495
2622
|
`registerFontOutlines` was a paint upgrade for a family that already had an
|
|
3496
2623
|
MSDF atlas; a family with only font bytes could not resolve, so it rendered
|
|
3497
2624
|
nothing. It is now a tier in its own right: `OutlineFace` reports `ascender`,
|
|
@@ -3499,48 +2626,46 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3499
2626
|
outline-only family, and `layoutRuns` reads advances, kerning and the baseline
|
|
3500
2627
|
through one source the atlas and a parsed face both satisfy. `outlineMinSize`
|
|
3501
2628
|
does not gate such a family — there is no other tier to prefer.
|
|
3502
|
-
|
|
2629
|
+
|
|
3503
2630
|
This does not touch metric neutrality where it applies: a family that has an
|
|
3504
2631
|
atlas still resolves to the atlas, so registering outlines cannot move text
|
|
3505
2632
|
that was already rendering.
|
|
3506
|
-
|
|
2633
|
+
|
|
3507
2634
|
Also fixes the outline tier in Node. opentype.js publishes ESM under `module`
|
|
3508
2635
|
and UMD under `main`; Node takes the UMD build, whose named exports it cannot
|
|
3509
2636
|
detect, so `parse` was undefined and every face failed to load — silently, via
|
|
3510
2637
|
the fallback to SDF. A browser bundler reading `module` never saw it.
|
|
3511
|
-
|
|
2638
|
+
|
|
3512
2639
|
Breaking for a consumer-supplied `OutlineParser`: a face must now report
|
|
3513
2640
|
metrics as well as geometry.
|
|
3514
|
-
|
|
3515
2641
|
- 0114abf: Add `PaintInput`, a control that edits a whole `FillStyle`.
|
|
3516
|
-
|
|
2642
|
+
|
|
3517
2643
|
A kind bar over a per-kind body, driven by the paint-kind registry rather than
|
|
3518
2644
|
a fixed list, so a consumer's registered kind appears in the bar and renders
|
|
3519
2645
|
that entry's `Editor`. `SelectionPanel`'s `paint` leaf renders it in place of
|
|
3520
2646
|
the chip that showed a gradient as indeterminate and wrote a solid over it on
|
|
3521
2647
|
first touch — so the checkerboard now means a mixed selection and nothing else,
|
|
3522
2648
|
and a gradient stroke is editable rather than merely paintable.
|
|
3523
|
-
|
|
2649
|
+
|
|
3524
2650
|
Switching kinds keeps a per-kind memory for the control's lifetime, so
|
|
3525
2651
|
linear -> solid -> linear comes back with its stops instead of the ramp
|
|
3526
2652
|
`withGradientKind` cannot carry.
|
|
3527
|
-
|
|
2653
|
+
|
|
3528
2654
|
`PatternPicker` moves from WeaselDraw into `@weasel-js/ui`, which now depends
|
|
3529
2655
|
on `@weasel-js/svg` for its tile previews.
|
|
3530
|
-
|
|
2656
|
+
|
|
3531
2657
|
The bar offers **None**: "what kind of paint is this?" takes no-paint as an
|
|
3532
2658
|
answer. `setFill` and `setStroke` accept `paint: null` to write it — a fill
|
|
3533
2659
|
becomes `null`, and a stroke goes away entirely rather than keeping a width
|
|
3534
2660
|
that draws no ink. `PaintKindEntry` gains an optional `icon`, and the five
|
|
3535
2661
|
built-in kinds carry glyphs so six segments fit a property row.
|
|
3536
|
-
|
|
2662
|
+
|
|
3537
2663
|
`FILL` and `STROKE` are now peer sections: the `appearance` group goes headless
|
|
3538
2664
|
and `data.fill` becomes a block leaf. The stroke's paint is no longer paired
|
|
3539
2665
|
with its width — a whole paint editor cannot share a row with a slider.
|
|
3540
|
-
|
|
3541
2666
|
- 50bc909: `FillStyle` is open: register a sixth paint kind and it renders, converts
|
|
3542
2667
|
frames and serializes.
|
|
3543
|
-
|
|
2668
|
+
|
|
3544
2669
|
`registerPaintKind(entry)` returns a disposer and `_resetPaintKindsForTests`
|
|
3545
2670
|
re-seeds the five built-ins, matching the kit's other module-global
|
|
3546
2671
|
registries. An entry carries the editor's slots (`label`, `seed`, `colorOf`,
|
|
@@ -3548,7 +2673,7 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3548
2673
|
`<defs>` slot. `listPaintKinds()` enumerates them, and `asPaint` types a
|
|
3549
2674
|
consumer's own paint as a `FillStyle` — the union itself stays closed, because
|
|
3550
2675
|
opening its discriminant would widen every built-in member.
|
|
3551
|
-
|
|
2676
|
+
|
|
3552
2677
|
Three defects fall out of the same change, each of which a sixth kind hit
|
|
3553
2678
|
immediately. The renderer's fill dispatch fell off the end of its switch into
|
|
3554
2679
|
an unguarded cast to the gradient union, so an unknown kind read `stops` off a
|
|
@@ -3556,25 +2681,24 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3556
2681
|
an unknown kind untouched, leaving it painting in screen space on a node that
|
|
3557
2682
|
moves. `<defs>` emitted nothing for a kind `gradientXml` did not know while
|
|
3558
2683
|
still writing the `url(#id)` that referenced it.
|
|
3559
|
-
|
|
2684
|
+
|
|
3560
2685
|
Registering a kind now bumps the node memo generation, so a node painted
|
|
3561
2686
|
before the registration repaints rather than holding the frame it resolved
|
|
3562
2687
|
when the kind was unknown.
|
|
3563
|
-
|
|
3564
2688
|
- 6a06f6d: Node paint is an object: `data.fill` is a `FillStyle`, `data.stroke` a `Stroke`
|
|
3565
|
-
|
|
2689
|
+
|
|
3566
2690
|
Each concept now has exactly one shape. `data.fill` holds a `FillStyle`,
|
|
3567
2691
|
`data.stroke` a whole `Stroke`, and `null` on either is an explicit "no paint"
|
|
3568
2692
|
where `undefined` takes the painter's fallback. Two new authoring helpers keep
|
|
3569
2693
|
hand-written node data short:
|
|
3570
|
-
|
|
2694
|
+
|
|
3571
2695
|
```ts
|
|
3572
2696
|
data: { path, fill: solid('#7fb069'), stroke: strokeOf('#1c1c1c', 2) }
|
|
3573
2697
|
```
|
|
3574
|
-
|
|
2698
|
+
|
|
3575
2699
|
**Breaking, with no compatibility path.** A document written against the old
|
|
3576
2700
|
shapes renders wrong rather than failing, which is accepted:
|
|
3577
|
-
|
|
2701
|
+
|
|
3578
2702
|
- `NodeFill = string | FillStyle` and `NodeStroke = string | Stroke` are gone,
|
|
3579
2703
|
and so are the string branches of `resolveNodeFill` / `resolveNodeStroke`.
|
|
3580
2704
|
A node holding `fill: '#f00'` now paints the default grey.
|
|
@@ -3590,176 +2714,169 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3590
2714
|
- `@weasel-js/svg`'s `strokeDataFromSvg` returns `Stroke | undefined` instead
|
|
3591
2715
|
of a `{ stroke, strokeWidth }` pair, and stops flattening a plain solid
|
|
3592
2716
|
stroke into a color. SVG's `fill="none"` imports as `fill: null`.
|
|
3593
|
-
|
|
2717
|
+
|
|
3594
2718
|
**A paint's alpha lives in `opacity`, one slot for every paint kind.** That is
|
|
3595
2719
|
the only slot a gradient or a pattern has, so it is the slot all of them use,
|
|
3596
2720
|
and the renderer multiplies a hex alpha by it — the two would fight if both
|
|
3597
2721
|
carried the value. `solid()` therefore moves an alpha channel out of the hex:
|
|
3598
2722
|
`solid('#ff000080')` is `{ color: '#ff0000', opacity: 0.502 }`.
|
|
3599
|
-
|
|
2723
|
+
|
|
3600
2724
|
The four setter actions follow: `setFillOpacity` / `setStrokeOpacity` write
|
|
3601
2725
|
`opacity` rather than splicing hex, so they now work on a gradient fill, which
|
|
3602
2726
|
they used to leave untouched. `setFill` / `setStroke` given a `color` recolor
|
|
3603
2727
|
the node's existing paint through the new `paintWithColor`, keeping its opacity
|
|
3604
2728
|
unless the picked color states an alpha of its own — and `setStroke` keeps the
|
|
3605
2729
|
stroke's width, cap, join and dash instead of replacing the whole value.
|
|
3606
|
-
|
|
2730
|
+
|
|
3607
2731
|
New exports: `solid`, `strokeOf`, `paintAlpha`, `paintWithAlpha`,
|
|
3608
2732
|
`paintWithColor`, `DEFAULT_SHAPE_FILL`.
|
|
3609
|
-
|
|
2733
|
+
|
|
3610
2734
|
`defaultNodeProperties` moves `data.fill` from a `color` leaf to a `paint` one
|
|
3611
2735
|
— a color control pointed at a `FillStyle` reads `undefined` off a gradient and
|
|
3612
2736
|
writes a bare string over it — and the `data.stroke` object leaf drops its
|
|
3613
2737
|
`fromScalar`, which had nothing left to lift.
|
|
3614
|
-
|
|
3615
2738
|
- a37ee0b: Separate a text node's content from its typography, and draw depth only where a label marks it
|
|
3616
|
-
|
|
2739
|
+
|
|
3617
2740
|
The text schema put `data.text` in a group named Text, so the section read
|
|
3618
2741
|
TEXT and the row inside it read Text — one word nested in itself — and the
|
|
3619
2742
|
style groups below it read as fields of the content string rather than as its
|
|
3620
2743
|
siblings. Content is its own section now, with the field full-width because
|
|
3621
2744
|
the section already names it.
|
|
3622
|
-
|
|
2745
|
+
|
|
3623
2746
|
A group with an empty `name` renders no heading. That already worked for
|
|
3624
2747
|
sections and is now documented on `ToolPrefGroup`, since it is how a schema
|
|
3625
2748
|
says "this group organises, it doesn't name": `Character` and `Paragraph`
|
|
3626
2749
|
carry the labels, and a `Typography` heading over them named nothing new.
|
|
3627
2750
|
It stays opt-in rather than a rule that rolls up any all-group parent —
|
|
3628
2751
|
a `Border` over `Top` / `Right` / `Bottom` needs its name.
|
|
3629
|
-
|
|
2752
|
+
|
|
3630
2753
|
Rows under a suppressed heading no longer indent. Depth drawn without a
|
|
3631
2754
|
visible parent put `Character` a level deeper than `Content` while being its
|
|
3632
2755
|
peer, which is the panel's own tree discipline broken by its own hand.
|
|
3633
|
-
|
|
3634
2756
|
- 611b30e: Layers and deps answer for the view they are drawn for
|
|
3635
|
-
|
|
3636
|
-
Nine lookups closed over the
|
|
2757
|
+
|
|
2758
|
+
Nine lookups closed over the *surface's* state at construction, so they answered
|
|
3637
2759
|
for view zero in every view. `<CanvasView>` draws the surface's layer array
|
|
3638
2760
|
unchanged and only the draw envelope differs, which makes a `draw: (_data, …)`
|
|
3639
2761
|
a guarantee of answering for the wrong view rather than merely an unused
|
|
3640
2762
|
argument. A drag in view B ghosted in view A, the marquee painted in the wrong
|
|
3641
2763
|
view, chrome-caps resolved against the surface's selection, every Cmd+V centered
|
|
3642
2764
|
on the wrong camera, and Escape in view B cancelled view A.
|
|
3643
|
-
|
|
2765
|
+
|
|
3644
2766
|
**New on `CanvasViewHelpers`** — `getPreviewSources()`, `getGestureOverlays()`
|
|
3645
2767
|
and `getIsVisible()`. All three are **required members**: anyone hand-writing a
|
|
3646
2768
|
`CanvasViewHelpers` (a test double, a wrapper) has to add them.
|
|
3647
2769
|
`getIsVisible` **moves off `CanvasSurfaceHelpers`**, where it could only ever
|
|
3648
2770
|
have answered for one view.
|
|
3649
|
-
|
|
2771
|
+
|
|
3650
2772
|
**New on `GestureSource`** — `previewSources()` and `overlays()`, also required,
|
|
3651
2773
|
alongside the newly exported `GesturePreviewSource`. `toolPreviewSources(tools)`
|
|
3652
2774
|
is the tool half.
|
|
3653
|
-
|
|
2775
|
+
|
|
3654
2776
|
**Layer options changed.** `createPathEditingOverlayLayer` and
|
|
3655
2777
|
`createSlopsDebugLayer` take `getPose(id, previews)` and have lost their
|
|
3656
2778
|
`isVisible` / `selectionRef` / `boundsOf` options — those come off the envelope
|
|
3657
2779
|
now. `usePreviewGhostLayer` has lost `tools`. Both it and
|
|
3658
2780
|
`useDispatcherOverlayLayer` keep `dispatcher` **only** to subscribe for repaint.
|
|
3659
|
-
|
|
2781
|
+
|
|
3660
2782
|
**Picking takes a camera.** `pickEvery`, `pickBest` and `makeGetNodeAtPoint`'s
|
|
3661
2783
|
result accept an optional trailing `PickCamera`. A world point does not carry
|
|
3662
2784
|
the scale it was produced under and picking has no draw envelope, so the caller
|
|
3663
2785
|
that produced the point supplies it; omitting it keeps the surface camera.
|
|
3664
|
-
|
|
2786
|
+
|
|
3665
2787
|
`useHoverTracking` took a `clientToWorld` thunk beside a world-space
|
|
3666
2788
|
`getNodeAtPoint` — the first resolved the view and the second did not, so hover
|
|
3667
2789
|
picked at the surface's scale inside a panel. It takes one
|
|
3668
2790
|
`nodeAtClientPoint(clientX, clientY)` now.
|
|
3669
|
-
|
|
2791
|
+
|
|
3670
2792
|
Anchor-editing target state stays surface-wide; only the preview resolution on
|
|
3671
2793
|
that path is per-view.
|
|
3672
|
-
|
|
3673
2794
|
- 9ad8cb2: Picking answers for what was painted
|
|
3674
|
-
|
|
2795
|
+
|
|
3675
2796
|
Three defects in `<SceneCanvas>`'s hit paths, all one shape — a pick answering
|
|
3676
2797
|
from something other than what the renderer drew.
|
|
3677
|
-
|
|
2798
|
+
|
|
3678
2799
|
**Pose overrides were painted through and picked around.** `PoseOverride.pose`
|
|
3679
|
-
is documented as replacing the document pose
|
|
3680
|
-
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
|
|
3681
2802
|
`<SceneCanvas>` supplies its own `pickEvery`, which read `node.pose` raw — as
|
|
3682
2803
|
did the bounds resolver feeding selection chrome and the affordance
|
|
3683
2804
|
`ChromeState`, and the marquee/lasso scan. A consumer animating nodes through
|
|
3684
2805
|
overrides painted them at one place and picked them at another. `effectivePose`
|
|
3685
2806
|
is now the single rule and every one of those reads through it.
|
|
3686
|
-
|
|
2807
|
+
|
|
3687
2808
|
**A clipped-away child was still clickable.** A container clips its subtree and
|
|
3688
2809
|
the renderer honors it, so a child outside the clip is not painted.
|
|
3689
2810
|
`useSelectTool`'s own walk has rejected those since clipping shipped; the walk
|
|
3690
2811
|
`<SceneCanvas>` installs instead had no clip term at all. The new
|
|
3691
2812
|
`passesAncestorClips` walks the parent chain per surviving candidate, so a flat
|
|
3692
2813
|
render-order scan can apply the same test.
|
|
3693
|
-
|
|
2814
|
+
|
|
3694
2815
|
**The marquee's fast-reject used the unrotated pose box.** A 100×20 rect turned
|
|
3695
2816
|
45° puts a corner 32 units above that box; a rubber-band over that corner was
|
|
3696
2817
|
rejected before the rotation-correct silhouette test ran, while a click on the
|
|
3697
2818
|
same pixel selected the shape.
|
|
3698
|
-
|
|
3699
2819
|
- c1b8511: Repaint the scene-graph side-scroller demo's world from `data.fill`. Its
|
|
3700
2820
|
tiles, coins, enemies and flagpole still declared `data.color`, the alias
|
|
3701
2821
|
removed when node paint became an object, so every one of them rendered in
|
|
3702
2822
|
the default gray — the demo whose whole point is being the visual twin of the
|
|
3703
2823
|
immediate-mode load test.
|
|
3704
2824
|
- d793d3c: Flip negates rotation; alignment guides and `gaps` distribute measure ink
|
|
3705
|
-
|
|
2825
|
+
|
|
3706
2826
|
Three paths read a pose's stored, unrotated box where the rotated extent was
|
|
3707
2827
|
wanted.
|
|
3708
|
-
|
|
2828
|
+
|
|
3709
2829
|
`flipPoseAboutBounds` carried rotation through untouched, so a mirrored shape
|
|
3710
2830
|
came back turned the same way — invisible on a rectangle, whose AABB is
|
|
3711
2831
|
symmetric under a sign flip, and plainly wrong on an asymmetric one, which
|
|
3712
2832
|
translated instead of mirroring. It now negates the pose's rotation.
|
|
3713
|
-
|
|
2833
|
+
|
|
3714
2834
|
`deriveAlignmentGuides` advertised a stationary rotated sibling's lines at its
|
|
3715
2835
|
stored edges, while the dragged selection matched against them by its ink.
|
|
3716
2836
|
`RECT_ALIGN_PROJECTION.boundsOf` now returns the rotated AABB and
|
|
3717
2837
|
`deriveAlignmentGuides` reads its targets through the same projection — a new
|
|
3718
2838
|
`projection` option defaulting to the rect one, so existing callers get the fix
|
|
3719
2839
|
without a change.
|
|
3720
|
-
|
|
2840
|
+
|
|
3721
2841
|
`useDistribute`'s `gaps` mode divided the leftover span by stored widths, so a
|
|
3722
2842
|
rotated member ended up with a gap short by the difference; `centers` shared the
|
|
3723
2843
|
line and the blind spot. Both now measure with `visualBoundsViaDescriptor`.
|
|
3724
2844
|
`distributeHorizontalAction` / `distributeVerticalAction` also take
|
|
3725
2845
|
`params.mode`, so `gaps` is reachable from a binding rather than only from the
|
|
3726
2846
|
hook.
|
|
3727
|
-
|
|
2847
|
+
|
|
3728
2848
|
Flip and distribute return different poses than before for rotated shapes.
|
|
3729
2849
|
That is the fix, but it is a behavior change for anything depending on the
|
|
3730
2850
|
old output.
|
|
3731
|
-
|
|
3732
2851
|
- 3386d64: `@weasel-js/core/routing` exports the route-string projection
|
|
3733
|
-
|
|
2852
|
+
|
|
3734
2853
|
Anything rendering a `GestureSpec` as a route string had to re-implement the
|
|
3735
2854
|
projection, and the copy in WeaselDraw's registry inspector had drifted three
|
|
3736
2855
|
ways: it answered `drop` and `paste` with no gesture name, so every binding of
|
|
3737
2856
|
either vanished from the route list; its argument lookup missed a spec field;
|
|
3738
2857
|
and it gated targets on a hand-listed set of kinds, dropping them for
|
|
3739
2858
|
`pointerDown`, `longPress` and `wheel`.
|
|
3740
|
-
|
|
2859
|
+
|
|
3741
2860
|
New from the routing subpath: `routesForSpec(spec)` — every route string one
|
|
3742
2861
|
spec declares — plus `routeGestureForSpecKind(kind)` over the single spec-kind
|
|
3743
2862
|
map, and `PREDICATE_TARGET`, which `registry.ts` already exported but the
|
|
3744
2863
|
subpath index did not, so consumers reading `RegistryEntry.target` had no way
|
|
3745
2864
|
to compare against the sentinel its own docs name.
|
|
3746
|
-
|
|
3747
2865
|
- ce2b5c7: Make the inline run grammar a parameter instead of a hardcoded branch.
|
|
3748
|
-
|
|
2866
|
+
|
|
3749
2867
|
`runsToMarkdown` and `markdownToRuns` each had the markdown subset spelled out
|
|
3750
2868
|
in their control flow — `***`/`**`/`*` and a two-character escape set — so
|
|
3751
2869
|
reading or writing any other spelling meant forking both. They now take a
|
|
3752
2870
|
`RunGrammar`: a table of markers pairing a repeated delimiter with the run
|
|
3753
2871
|
flags it toggles, defaulting to `MARKDOWN_RUN_GRAMMAR`, which is exactly
|
|
3754
2872
|
today's behavior. Escaping follows the grammar's own delimiters.
|
|
3755
|
-
|
|
2873
|
+
|
|
3756
2874
|
Nothing changes for a caller that passes no grammar. `underline` and
|
|
3757
2875
|
`strikethrough` still have no markdown spelling and are still dropped by
|
|
3758
2876
|
`runsToMarkdown` — a grammar that wants `~~struck~~` now adds one marker
|
|
3759
2877
|
rather than editing the parser.
|
|
3760
|
-
|
|
3761
2878
|
- 2ea772f: `createSelectionOutlineLayer` and `createSelectionHandlesLayer` now do what the overlay layer does
|
|
3762
|
-
|
|
2879
|
+
|
|
3763
2880
|
`createSelectionOverlayLayer` documents itself as equivalent to stacking the
|
|
3764
2881
|
other two, and it was not. It reads `ChromeState` off the draw envelope,
|
|
3765
2882
|
resolves the synthetic multi-resize id to the union AABB, honors chrome-caps
|
|
@@ -3769,27 +2886,25 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3769
2886
|
knew nothing about the multi-selection union — so a consumer who stacked them,
|
|
3770
2887
|
on the wrapper's own promise, got chrome in the wrong place with no way to
|
|
3771
2888
|
tell.
|
|
3772
|
-
|
|
2889
|
+
|
|
3773
2890
|
All three now run one body and differ only in which passes they enable, so the
|
|
3774
2891
|
promise holds by construction. `SelectionOutlineLayerOpts` and
|
|
3775
2892
|
`SelectionHandlesLayerOpts` become the overlay's option set minus the visuals
|
|
3776
2893
|
that don't apply, which makes `getSelection` and `getPose` optional on both and
|
|
3777
2894
|
adds `getOutlineIds` and `getSuppressedIds`. Handle visuals are now the named
|
|
3778
2895
|
`SelectionHandleStyle`.
|
|
3779
|
-
|
|
3780
2896
|
- 3fb3a46: Key `usePublishSelection` on the publish callback, not the context value
|
|
3781
|
-
|
|
2897
|
+
|
|
3782
2898
|
The effect depended on the whole selection-context value, and the provider
|
|
3783
2899
|
mints a new value object on every publish. So one publisher publishing refired
|
|
3784
2900
|
the effect for every other publisher in scope, each of which republished its
|
|
3785
2901
|
own ids — a newer selection got stomped back to an older one, and two
|
|
3786
2902
|
publishers holding different ids under one provider never settled at all.
|
|
3787
|
-
|
|
2903
|
+
|
|
3788
2904
|
`publishSelection` is already a stable `useCallback`, so the effect now depends
|
|
3789
2905
|
on it directly. No provider change and no API change.
|
|
3790
|
-
|
|
3791
2906
|
- 84db1f6: Close four gaps that produced wrong answers with no error
|
|
3792
|
-
|
|
2907
|
+
|
|
3793
2908
|
Three path walkers — `pathToMultiPolygon` in core and in `@weasel-js/geom`, and
|
|
3794
2909
|
`enumerateAnchors` behind the bezier-edit overlay — handled M/L/C/Q/Z with no
|
|
3795
2910
|
`default:` arm, so a command code they did not know fell out of the switch
|
|
@@ -3797,31 +2912,30 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3797
2912
|
wrong floats. They now throw, matching the six sibling walkers. This is a
|
|
3798
2913
|
behavior change for anyone feeding these a path built with an opcode outside
|
|
3799
2914
|
`PATH_COMMANDS`: what used to come back subtly wrong now raises.
|
|
3800
|
-
|
|
2915
|
+
|
|
3801
2916
|
A `<CanvasView>` built its affordance hit-test without a device profile, so a
|
|
3802
2917
|
nested view resolved fine-pointer radii even under a coarse pointer — 8px grab
|
|
3803
2918
|
zones against the 14px chrome the surface paints. It reads the profile
|
|
3804
2919
|
`<SceneCanvas>` publishes.
|
|
3805
|
-
|
|
2920
|
+
|
|
3806
2921
|
`moveGestureAdapter`'s `insertNode` took no `index`, and the adapter carried
|
|
3807
2922
|
neither `getChildren` nor `setChildOrder`, so the sibling slot a delete op
|
|
3808
2923
|
records had nowhere to land: undoing a delete through the move pipeline
|
|
3809
2924
|
appended the node to the end of its parent instead of putting it back where it
|
|
3810
2925
|
was. All three are there now.
|
|
3811
|
-
|
|
2926
|
+
|
|
3812
2927
|
The dev inspector's gesture panel formatted bindings with a private formatter
|
|
3813
2928
|
that reported only modifiers set to `true`. The `ingest` action marks every
|
|
3814
2929
|
modifier `'optional'`, so its drop and paste bindings rendered blank and the
|
|
3815
2930
|
action was invisible on both gestures. Both of the panel's plain-text
|
|
3816
2931
|
formatters now go through the kit's `routesForSpec`.
|
|
3817
|
-
|
|
3818
2932
|
- 3386d64: Undoing a multi-node delete or group restores document order
|
|
3819
|
-
|
|
2933
|
+
|
|
3820
2934
|
Restoring by stored index cannot survive replay: history runs a batch's
|
|
3821
2935
|
inverses in reverse, while indices captured before the mutation are only
|
|
3822
2936
|
correct in ascending order. Deleting `b, c, d` from `[a, b, c, d, e]` and
|
|
3823
2937
|
undoing gave `a, b, e, c, d`; Cmd+G on the same three did the same.
|
|
3824
|
-
|
|
2938
|
+
|
|
3825
2939
|
Ops now record a `Slot` — an ordinal plus the id of the following sibling at
|
|
3826
2940
|
capture. The anchor is the source of truth whenever it resolves, and it
|
|
3827
2941
|
resolves whatever else the batch has already restored. The ordinal remains as
|
|
@@ -3829,23 +2943,22 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3829
2943
|
children. `before: null` means "last" and needs no sibling list; an absent
|
|
3830
2944
|
`before` means "unobserved", and the two survive `History.serialize` because
|
|
3831
2945
|
`undefined` drops out of JSON and `null` does not.
|
|
3832
|
-
|
|
2946
|
+
|
|
3833
2947
|
The ops observe their own slot during `apply()` rather than taking one from the
|
|
3834
2948
|
caller, so every existing emitter gets this without a call-site change.
|
|
3835
2949
|
`createDeleteOp`'s `index` argument is now a seed that `apply` supersedes; its
|
|
3836
2950
|
docstring said it was sufficient on its own, which it never was.
|
|
3837
|
-
|
|
2951
|
+
|
|
3838
2952
|
Adapters without an ordering seam still append, as they did before:
|
|
3839
2953
|
`arrayAdapter` has no `setChildOrder`, and the move gesture's adapter has
|
|
3840
2954
|
neither that nor an `index` parameter on `insertNode`.
|
|
3841
|
-
|
|
3842
2955
|
- 7a746df: A stroke's dash is edited as a style, not as an array
|
|
3843
|
-
|
|
2956
|
+
|
|
3844
2957
|
`Stroke.dash` already rendered, imported and exported; it had no control,
|
|
3845
2958
|
because a `number[]` has no leaf kind. It doesn't need one — the thing a person
|
|
3846
2959
|
chooses is a style, and the array is how it is stored. The stroke block gains a
|
|
3847
2960
|
Solid / Dashed / Dotted / Custom bar under cap, join and align.
|
|
3848
|
-
|
|
2961
|
+
|
|
3849
2962
|
`ToolPrefEnum` gains `encoding`: `read`/`write` between the stored value and
|
|
3850
2963
|
the option string, the counterpart of the `unit` a number leaf already has for
|
|
3851
2964
|
a value stored in a canonical unit. Both directions are handed the object the
|
|
@@ -3857,9 +2970,8 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3857
2970
|
a new `disabled` option — one a control reports but refuses to author, since
|
|
3858
2971
|
there is no array behind it. `solid` is stored as no dash at all, and an object
|
|
3859
2972
|
leaf's field written as `undefined` is now removed rather than left holding it.
|
|
3860
|
-
|
|
3861
2973
|
- 4f19274: Cap, join and align are chosen by glyph, and the stroke block drops its labels
|
|
3862
|
-
|
|
2974
|
+
|
|
3863
2975
|
Nine option glyphs and four category glyphs join the icon set. The option
|
|
3864
2976
|
glyphs are filled silhouettes — the glyph is the ink, so a choice reads as a
|
|
3865
2977
|
shape rather than as a diagram of one. `align` is a circle zoomed until the
|
|
@@ -3867,31 +2979,29 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3867
2979
|
the box's complement of it, and `center` is the annulus straddling the path,
|
|
3868
2980
|
so the three are one band at three offsets. The categories are the bare path
|
|
3869
2981
|
each row treats, drawn in the outlined register.
|
|
3870
|
-
|
|
3871
|
-
A schema carries a glyph
|
|
2982
|
+
|
|
2983
|
+
A schema carries a glyph *id*, not a component: `ToolPrefEnum`'s options gain
|
|
3872
2984
|
`icon`, and every leaf gains one for rows whose own label is spent on a
|
|
3873
2985
|
`pair`. Core ships no icon set and cannot depend on one, so the field is a
|
|
3874
2986
|
plain string; weasel-ui resolves it against `ICON_PATHS` and falls back to
|
|
3875
2987
|
`short` where it names no glyph.
|
|
3876
|
-
|
|
2988
|
+
|
|
3877
2989
|
`SelectionPanel` now honours `block` inside an object leaf, not only at the
|
|
3878
2990
|
section level. A row whose fields are all `block` drops the 64px label column
|
|
3879
2991
|
and spans the block. The default stroke schema uses both: paint and width
|
|
3880
2992
|
share one label-less row, and cap/join/align share the next.
|
|
3881
|
-
|
|
2993
|
+
|
|
3882
2994
|
`align`'s options run inner, center, outer — the order the ink moves outward.
|
|
3883
|
-
|
|
3884
2995
|
- 94f2446: Add stroke markers — arrowheads and other line terminators as stroke style.
|
|
3885
|
-
|
|
2996
|
+
|
|
3886
2997
|
`markerStart` / `markerMid` / `markerEnd` on `Stroke` take a key resolved
|
|
3887
2998
|
through a new registry (`registerMarker`), shipping eight built-in shapes.
|
|
3888
2999
|
Unlike SVG, the stroke stops short of a filled head rather than running under
|
|
3889
3000
|
it to the tip; the distance is declared per marker, so an open V still reaches
|
|
3890
3001
|
the vertex. Round-trips through `@weasel-js/svg` as `marker-*` attributes plus
|
|
3891
3002
|
`<marker>` defs.
|
|
3892
|
-
|
|
3893
3003
|
- 07fd2de: `setStroke` takes a whole paint, so a gradient or pattern stroke is writable.
|
|
3894
|
-
|
|
3004
|
+
|
|
3895
3005
|
It accepted `{ color }` only, and merged through `paintWithColor`, which
|
|
3896
3006
|
supersedes a non-solid paint with a solid one — a gradient stroke was
|
|
3897
3007
|
unreachable even though `setStrokeOpacity` could already reach its alpha.
|
|
@@ -3899,42 +3009,40 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3899
3009
|
an earlier paint, and the stroke's width, cap, join, dash and align survive
|
|
3900
3010
|
either. New `strokeWith(paint, width?)` is `strokeOf`'s sibling for a paint
|
|
3901
3011
|
that has no color to pass.
|
|
3902
|
-
|
|
3012
|
+
|
|
3903
3013
|
Two fixes alongside it: `setFill` started with no `color` and no `paint` seeded
|
|
3904
3014
|
from `DEFAULT_STROKE_COLOR`, painting the selection black where
|
|
3905
3015
|
`setFillOpacity` seeds the same slot from `DEFAULT_FILL_COLOR`; and
|
|
3906
3016
|
`gradientForBounds`'s doc comment claimed a corner-to-corner linear gradient
|
|
3907
3017
|
where the body builds a left-edge-to-right-edge one.
|
|
3908
|
-
|
|
3018
|
+
|
|
3909
3019
|
`@weasel-js/ui` no longer exports `strokeWithColor`. It shared a name with
|
|
3910
3020
|
core's and disagreed with it — core's keeps the paint's opacity, ui's dropped
|
|
3911
3021
|
it — and nothing imported it.
|
|
3912
|
-
|
|
3913
3022
|
- 81213fc: Edit a node's stroke as the union it is
|
|
3914
|
-
|
|
3023
|
+
|
|
3915
3024
|
`data.stroke` holds `string | Stroke`, and the schema described it with a
|
|
3916
3025
|
`color` leaf — which reads `undefined` off the object form, shows its own
|
|
3917
3026
|
default, and writes a bare hex back over the stroke's width, cap, join and
|
|
3918
3027
|
dash on the first edit. The same trap `ToolPrefPaint` was introduced to avoid
|
|
3919
3028
|
for `FillStyle`.
|
|
3920
|
-
|
|
3029
|
+
|
|
3921
3030
|
A `stroke` pref kind now describes it, and `defaultNodeProperties` uses it.
|
|
3922
3031
|
Its control shows whichever color the value has — the string itself, or a
|
|
3923
3032
|
solid paint's color — gives a gradient stroke the indeterminate chip rather
|
|
3924
3033
|
than claiming a color it doesn't have, and preserves the form on write.
|
|
3925
|
-
|
|
3034
|
+
|
|
3926
3035
|
`PrefsForm` gained the `stroke` case and the `paint` case it never had; a
|
|
3927
3036
|
`paint` leaf used to render as the literal text `(paint: no renderer)`.
|
|
3928
3037
|
`solidColorOf`, `strokeColorOf`, `strokeWithColor` and `isStrokeObject` are
|
|
3929
3038
|
exported from `@weasel-js/ui` for consumers writing their own property
|
|
3930
3039
|
renderers against either union.
|
|
3931
|
-
|
|
3040
|
+
|
|
3932
3041
|
Cap, join and dash are not editable from a panel yet, and `data.strokeWidth`
|
|
3933
3042
|
remains its own leaf — see `docs/proposals/2026-08-26-node-stroke-union.md`
|
|
3934
3043
|
for why that waits on the SVG mapping.
|
|
3935
|
-
|
|
3936
3044
|
- 2f225d7: A thick stroke is clickable across its whole width
|
|
3937
|
-
|
|
3045
|
+
|
|
3938
3046
|
`shapeCoversPoint` grants a grab out to a stroke's outward reach — a full
|
|
3939
3047
|
stroke width for an `outer` align — but the AABB pre-filter that runs before it
|
|
3940
3048
|
grew only by the pointer slop. So half a thick outer stroke's ink was
|
|
@@ -3943,25 +3051,24 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3943
3051
|
was at least as generous as the refinement, which it cannot be on its own,
|
|
3944
3052
|
since it never sees the stroke. That budget is the caller's, and the comment
|
|
3945
3053
|
says so now.
|
|
3946
|
-
|
|
3054
|
+
|
|
3947
3055
|
`ShapeCoversPointOptions.scale` was never passed either, so a stroke width
|
|
3948
3056
|
declared in `px` resolved as world units and the reach was wrong at every zoom
|
|
3949
3057
|
but 1 — while the caller computed `meanScale(view.scale)` one line above.
|
|
3950
|
-
|
|
3951
3058
|
- 68069dc: Right-to-left text lays out in visual order
|
|
3952
|
-
|
|
3059
|
+
|
|
3953
3060
|
`LayoutRunsOpts` takes an optional `bidi` engine. Given one, `layoutRuns`
|
|
3954
3061
|
analyses the paragraph, reorders each line after the wrap, and mirrors brackets
|
|
3955
3062
|
in right-to-left runs. Given none, nothing changes: text lays out logically,
|
|
3956
3063
|
exactly as before.
|
|
3957
|
-
|
|
3064
|
+
|
|
3958
3065
|
`@weasel-js/text` declares the `BidiResolver` interface and does not depend on
|
|
3959
3066
|
`@weasel-js/bidi` — the dependency runs the other way from the usual, so a
|
|
3960
3067
|
consumer who renders no right-to-left text never installs the Unicode tables,
|
|
3961
3068
|
and a different implementation can be substituted. `@weasel-js/bidi` is a
|
|
3962
3069
|
devDependency here only, for a test that drives real Hebrew through the real
|
|
3963
3070
|
engine; types lining up is not evidence the semantics do.
|
|
3964
|
-
|
|
3071
|
+
|
|
3965
3072
|
`LaidOutCell` gains `advance` and `level`, and **`x` is no longer monotonic
|
|
3966
3073
|
across `cells`**. Cells stay in logical order — slot `i` is still character `i`
|
|
3967
3074
|
— while their x values follow the reordering. Sort on `x` for visual order, and
|
|
@@ -3969,26 +3076,25 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3969
3076
|
cell's `x`. Hit-testing was doing exactly that and now sweeps in visual order
|
|
3970
3077
|
against each cell's own extent, taking a right-to-left cell's visually-leading
|
|
3971
3078
|
half as the character's logical end.
|
|
3972
|
-
|
|
3079
|
+
|
|
3973
3080
|
Kerning is a gap between two adjacent characters, and the wrap measures it
|
|
3974
3081
|
logically. Reordering can put a different pair side by side, so the gap taken
|
|
3975
3082
|
is the one belonging to whichever of the two is logically second, and none at
|
|
3976
3083
|
all across a direction boundary — where the pair never touched in the source.
|
|
3977
|
-
|
|
3084
|
+
|
|
3978
3085
|
Laying out right-to-left text with no engine now warns once, naming the import.
|
|
3979
3086
|
The alternative is glyphs silently appearing reversed, which is the one real
|
|
3980
3087
|
hazard of making this opt-in.
|
|
3981
|
-
|
|
3982
3088
|
- 5d0ff9c: Every code point on a line gets a cell
|
|
3983
|
-
|
|
3089
|
+
|
|
3984
3090
|
`LaidOutLineBox` replaces its `caretXs` / `caretIndices` pair with
|
|
3985
3091
|
`cells: LaidOutCell[]` plus a `srcEnd` closing offset. A cell carries
|
|
3986
3092
|
`srcIndex`, `srcEnd`, `cp`, `x` and `drawsInk`, so slot `i` is `cells[i]` and
|
|
3987
3093
|
a consumer indexing per character no longer has to reconcile a sparse array
|
|
3988
3094
|
against the source string.
|
|
3989
|
-
|
|
3095
|
+
|
|
3990
3096
|
The old arrays were documented as non-contiguous, and two causes were real:
|
|
3991
|
-
|
|
3097
|
+
|
|
3992
3098
|
- A code point no tier could serve was dropped outright, taking its caret stop
|
|
3993
3099
|
with it. It now occupies a zero-advance cell. This is reachable whenever the
|
|
3994
3100
|
dynamic canvas fallback is off — which is the normal configuration for a
|
|
@@ -3997,34 +3103,33 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
3997
3103
|
- A space opening a line — at the start of the text, or after a newline — was
|
|
3998
3104
|
discarded. It now keeps its cell and still consumes no width, so a line is
|
|
3999
3105
|
addressable per character without gaining an indent. A space that opens a
|
|
4000
|
-
|
|
3106
|
+
*wrapped* line was never affected: the wrap leaves it as a trailing cell on
|
|
4001
3107
|
the line before.
|
|
4002
|
-
|
|
3108
|
+
|
|
4003
3109
|
Neither changes any geometry: both cells carry zero advance, zero tracking and
|
|
4004
3110
|
no kerning, so bounds, line widths and glyph positions are unchanged.
|
|
4005
|
-
|
|
3111
|
+
|
|
4006
3112
|
A newline still has no cell, since it separates cells rather than being one.
|
|
4007
3113
|
`srcEnd` is what a blank line carries in its place.
|
|
4008
|
-
|
|
3114
|
+
|
|
4009
3115
|
`drawsInk` is a property of the code point and the face, not of the call that
|
|
4010
3116
|
produced it: it does not flip when a dynamic bake lands or the outline
|
|
4011
3117
|
threshold is crossed, so the same text reports the same slots every time. A
|
|
4012
3118
|
zero-advance combining mark is `true` — it inks without advancing.
|
|
4013
|
-
|
|
4014
3119
|
- c1b8511: **Breaking:** paint leaves `TextStyle`. A text node's color and outline are
|
|
4015
3120
|
`data.fill` and `data.stroke` — the same two leaves every other node kind
|
|
4016
3121
|
paints from — and `TextStyle` holds typography only. `TextStyle.fill` and
|
|
4017
3122
|
`TextStyle.stroke` are gone, with no compatibility read: a document that put
|
|
4018
3123
|
its color in `style.fill` now renders in the default black rather than
|
|
4019
3124
|
erroring, so check documents that predate this.
|
|
4020
|
-
|
|
3125
|
+
|
|
4021
3126
|
This fixes a real asymmetry rather than only moving fields. `data.stroke`
|
|
4022
3127
|
already reached text through a fold in the painter, but `data.fill` did not:
|
|
4023
3128
|
picking a fill color with a text node selected wrote a field nothing read, so
|
|
4024
3129
|
the canvas did not change. `setFill`, `setFillOpacity`, the opacity scrub and
|
|
4025
3130
|
the Appearance leaf now all mean the same thing on text as on a rect. The
|
|
4026
3131
|
duplicate `data.style.fill` control is gone from the text schema with them.
|
|
4027
|
-
|
|
3132
|
+
|
|
4028
3133
|
`resolveTextStyle(style, paint)` takes the node's paint as a second argument
|
|
4029
3134
|
and is what derives the caret and selection colors, so the edit overlay
|
|
4030
3135
|
matches the glyphs it sits on; `useTextEdit` gained a `getPaint` option for
|
|
@@ -4035,105 +3140,100 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4035
3140
|
style. `StyledRun.fill` and `.stroke` are unchanged and still override the
|
|
4036
3141
|
node's per range — which is also where a caller with no node at all, a HUD
|
|
4037
3142
|
widget or a debug overlay, now states its color.
|
|
4038
|
-
|
|
3143
|
+
|
|
4039
3144
|
`textCommandFromRuns` is exported from the package root.
|
|
4040
|
-
|
|
4041
3145
|
- 546f67d: Draw text from a ring of reused vertex buffers instead of minting a vertex
|
|
4042
3146
|
array and two buffers per draw. `drawTextGroup` and `drawTextDecorations` were
|
|
4043
3147
|
the last paths still doing what `drawImage` stopped doing; text now costs
|
|
4044
3148
|
**3.3 us/command, down from 6.65** at 512 commands a frame on an M2 Max via
|
|
4045
3149
|
ANGLE (`tests/perf/transition-matrix.spec.ts`), which puts it level with an
|
|
4046
3150
|
image draw. No other command kind moved.
|
|
4047
|
-
|
|
3151
|
+
|
|
4048
3152
|
A text group is as many quads as it has glyphs, so unlike the image ring a
|
|
4049
3153
|
slot's buffer grows to the largest run it has seen rather than being fixed at
|
|
4050
3154
|
four vertices. The quad index pattern is a pure function of the quad count —
|
|
4051
3155
|
the pattern for N quads is a prefix of the pattern for any larger N — so one
|
|
4052
3156
|
index buffer serves every slot, grown the same way and written only when it
|
|
4053
3157
|
grows.
|
|
4054
|
-
|
|
4055
3158
|
- c2ffa49: Alignment can resolve against reading direction
|
|
4056
|
-
|
|
3159
|
+
|
|
4057
3160
|
`align` gains `start` and `end` alongside `left` / `center` / `right`, and
|
|
4058
3161
|
`TextStyle` gains `direction: 'ltr' | 'rtl'`. The split is CSS `text-align`'s:
|
|
4059
3162
|
the relative pair resolves against the direction, the absolute pair ignores it.
|
|
4060
3163
|
`resolveAlign(align, direction)` collapses one to the other and is exported for
|
|
4061
3164
|
consumers that need an edge rather than an intent.
|
|
4062
|
-
|
|
3165
|
+
|
|
4063
3166
|
Direction is an input, not something this package discovers. `@weasel-js/text`
|
|
4064
3167
|
has no DOM, so a consumer that reads `getComputedStyle(box).direction` passes
|
|
4065
3168
|
what it found; nothing here sniffs an environment.
|
|
4066
|
-
|
|
3169
|
+
|
|
4067
3170
|
Defaults are unchanged — `align: 'left'`, `direction: 'ltr'` — so no existing
|
|
4068
3171
|
layout moves. Making `start` the default alignment is a separate call.
|
|
4069
|
-
|
|
3172
|
+
|
|
4070
3173
|
`@weasel-js/svg` carries the direction through: `direction` joins the
|
|
4071
3174
|
inheritable presentation properties, and `text-anchor` is now written and read
|
|
4072
3175
|
against it. Two things were wrong before and are worth naming, because both
|
|
4073
3176
|
rendered plausible output:
|
|
4074
|
-
|
|
3177
|
+
|
|
4075
3178
|
- `align: 'start'` serialized to `text-anchor="end"` — the opposite edge — via
|
|
4076
3179
|
a mapping that assumed three values and read the fourth as its `else`.
|
|
4077
3180
|
- SVG's initial `text-anchor` is `start`, which under `direction="rtl"` is the
|
|
4078
3181
|
right edge, while this model's default `align` is `left`. They agree under
|
|
4079
3182
|
`ltr` and only there, so an RTL document with no explicit anchor imported as
|
|
4080
3183
|
left-aligned.
|
|
4081
|
-
|
|
3184
|
+
|
|
4082
3185
|
This is alignment and round-tripping only. Layout still walks code points in
|
|
4083
3186
|
logical order with the pen always increasing: there is no bidi reordering and
|
|
4084
3187
|
no shaping, so a Hebrew or Arabic string aligns to the correct edge and still
|
|
4085
3188
|
renders in logical order, and Arabic still renders unjoined.
|
|
4086
|
-
|
|
4087
3189
|
- 4c097ef: Sit every run on a line on one baseline
|
|
4088
|
-
|
|
4089
|
-
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
|
|
4090
3192
|
off a shared baseline, so a 16-unit run beside a 40-unit run floated up level
|
|
4091
3193
|
with the big run's cap rather than standing on the line with it. Two faces with
|
|
4092
3194
|
different ascents at the same size diverged the same way. Baseline alignment is
|
|
4093
3195
|
what inline text does everywhere else, and the module header already claimed
|
|
4094
3196
|
this behavior — the walk just never implemented it.
|
|
4095
|
-
|
|
3197
|
+
|
|
4096
3198
|
A line now sinks one baseline far enough to clear its tallest run's ascent and
|
|
4097
3199
|
places every glyph against it. Glyph quads derive their top from that baseline
|
|
4098
3200
|
rather than from the pen's line top, which is the whole of the change:
|
|
4099
3201
|
`qy0 = baselineY + (yoffset - metrics.base) * scale`.
|
|
4100
|
-
|
|
3202
|
+
|
|
4101
3203
|
Uniform-size text — nearly all text — is unchanged, since the maximum over one
|
|
4102
3204
|
value is that value. Only lines that actually mix sizes or faces move, and they
|
|
4103
3205
|
move to where they always should have been.
|
|
4104
|
-
|
|
3206
|
+
|
|
4105
3207
|
The test named "mixed-size runs share a baseline on the same line" asserted only
|
|
4106
3208
|
a quad count and passed throughout; it now asserts the baselines.
|
|
4107
|
-
|
|
4108
3209
|
- 2b86e00: A text node's style is one value, not ten sibling paths
|
|
4109
|
-
|
|
3210
|
+
|
|
4110
3211
|
`data.style.fontSize`, `.fontWeight`, `.align` and the rest addressed into one
|
|
4111
3212
|
`TextStyle` from ten independent leaves, each control writing a field of a
|
|
4112
3213
|
value it could only half see. `data.style` is an object leaf now, with
|
|
4113
3214
|
Character and Paragraph as groups inside it — groups head their fields and
|
|
4114
3215
|
contribute nothing to the path, so a field is still a field of the style and
|
|
4115
3216
|
one commit writes the whole thing.
|
|
4116
|
-
|
|
3217
|
+
|
|
4117
3218
|
An object leaf whose fields are entirely grouped no longer prints its own
|
|
4118
3219
|
heading, which would stack straight onto the first group's, and a group's
|
|
4119
3220
|
fields sit under a rule so the nesting reads. WeaselDraw's inspector descends
|
|
4120
3221
|
into an object leaf when listing what a kind exposes — the fields are the
|
|
4121
3222
|
editable surface; the leaf is the container.
|
|
4122
|
-
|
|
3223
|
+
|
|
4123
3224
|
`SelectionPanel` has a story now, which is how the two layout defects above
|
|
4124
3225
|
were found.
|
|
4125
|
-
|
|
4126
3226
|
- d933a89: Superscript, subscript and overline for styled runs
|
|
4127
|
-
|
|
3227
|
+
|
|
4128
3228
|
`StyledRun` gains `script: 'super' | 'sub'` — a raised or lowered baseline and
|
|
4129
3229
|
a smaller size together, the pair `<sup>` and `<sub>` imply. It is a preset
|
|
4130
3230
|
over two new primitives rather than a mechanism of its own:
|
|
4131
|
-
|
|
3231
|
+
|
|
4132
3232
|
- `baselineShift` — raise (positive) or lower (negative) a run off the line's
|
|
4133
3233
|
shared baseline, in ems of the inherited font size.
|
|
4134
3234
|
- `fontScale` — a multiplier on the inherited font size, the relative
|
|
4135
3235
|
counterpart to `fontSize`. An absolute `fontSize` still wins over it.
|
|
4136
|
-
|
|
3236
|
+
|
|
4137
3237
|
Naming either directly overrides that half of `script` and leaves the other
|
|
4138
3238
|
alone. The preset's numbers are exported as `SCRIPT_METRICS` (58.3% size,
|
|
4139
3239
|
±33.3% position — Adobe's defaults, so a character panel can show percentages
|
|
@@ -4141,28 +3241,27 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4141
3241
|
carries real `ySuperscript*` metrics but the baked atlas tier has no slot for
|
|
4142
3242
|
them, and metrics that applied on one glyph tier and not the other would
|
|
4143
3243
|
reflow text as it crossed the size threshold.
|
|
4144
|
-
|
|
3244
|
+
|
|
4145
3245
|
`resolveRuns` folds all of it into one world-unit `baselineShift` and a final
|
|
4146
3246
|
`fontSize`, so layout never learns superscripts exist — it places a run against
|
|
4147
3247
|
a baseline and an offset. The shift moves a run's glyphs, its outline geometry
|
|
4148
3248
|
and its own decoration rules together, and deliberately does not feed back into
|
|
4149
3249
|
the line's baseline or height: a superscript rides the line rather than
|
|
4150
3250
|
reflowing it.
|
|
4151
|
-
|
|
3251
|
+
|
|
4152
3252
|
`overline` joins `underline` and `strikethrough` on both `TextStyle` and
|
|
4153
3253
|
`StyledRun`, additive over the node style like the other two, and is now
|
|
4154
3254
|
available to a custom `RunGrammar` as a `RunFlag`. The default markdown grammar
|
|
4155
3255
|
is unchanged — it stays silent on the decorations, as it always has been.
|
|
4156
|
-
|
|
4157
3256
|
- 5923c8b: `Animator.tween` no longer fires `onDone` for a tween that was cancelled during
|
|
4158
3257
|
its own final `onTick`. The last tick emitted the value and completed in one
|
|
4159
3258
|
pass, so a write made from that tick — cancelling the tween — still got the
|
|
4160
3259
|
completion callback, against the documented "not called on cancel" contract.
|
|
4161
3260
|
- 2ea772f: Undo of a delete restores the subtree; undo of a group restores the slot
|
|
4162
|
-
|
|
3261
|
+
|
|
4163
3262
|
Two ops inverted to something narrower than what they applied, so undo
|
|
4164
3263
|
silently lost data.
|
|
4165
|
-
|
|
3264
|
+
|
|
4166
3265
|
`createDeleteOp.invert()` re-inserted a single node while `apply()` called
|
|
4167
3266
|
`removeNode`, which cascades the whole subtree. Delete a container with two
|
|
4168
3267
|
children, undo, and the container came back with `children: []` while both
|
|
@@ -4172,7 +3271,7 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4172
3271
|
re-inserts each descendant at its captured slot. A flat adapter's `removeNode`
|
|
4173
3272
|
does not cascade, so the inverse skips any descendant the adapter still reports
|
|
4174
3273
|
as live rather than duplicating it.
|
|
4175
|
-
|
|
3274
|
+
|
|
4176
3275
|
`createReparentOp` carried only the parent ids, so undoing a Cmd+G appended
|
|
4177
3276
|
instead of restoring the sibling slot and paint order changed. `ReparentArgs`
|
|
4178
3277
|
now carries `fromIndex` / `toIndex` and places through the existing
|
|
@@ -4180,39 +3279,42 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4180
3279
|
`setParent`'s signature is unchanged. Adapters without that seam no-op as
|
|
4181
3280
|
before. `groupAction` captures each member's index before mutating; `move` and
|
|
4182
3281
|
`snapToContainer` pass none and are byte-identical.
|
|
4183
|
-
|
|
3282
|
+
|
|
4184
3283
|
`ops/delete.test.ts` stubbed `removeNode` as a one-id delete that did not
|
|
4185
3284
|
cascade, which is why nothing caught the first bug. It now runs against a
|
|
4186
3285
|
tree-backed fake.
|
|
4187
|
-
|
|
4188
3286
|
- 2ea772f: Selection chrome, gesture bounds and SVG export fold rotated ink, not pose boxes
|
|
4189
|
-
|
|
4190
|
-
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.
|
|
4191
3289
|
Select two shapes, rotate one, and the multi-selection frame and its handles
|
|
4192
3290
|
sat inside the rotated shape's ink — affordances hand `ChromeState.unionBounds`
|
|
4193
|
-
out as the target bounds for paint
|
|
3291
|
+
out as the target bounds for paint *and* hit-test, so the handles were both
|
|
4194
3292
|
drawn and grabbable in the wrong place, while `getGestureBounds()` reported the
|
|
4195
3293
|
correct larger box.
|
|
4196
|
-
|
|
3294
|
+
|
|
4197
3295
|
`unionAABB` expands each rotated member via `axisAlignedBounds` before folding
|
|
4198
3296
|
and is now the one implementation. It lives in `core/geometry/unionBounds.ts`
|
|
4199
3297
|
beside the rotation-free `unionBounds`, which stays correct for commit-time
|
|
4200
3298
|
actions that write poses back in the unrotated frame; the module says which to
|
|
4201
3299
|
reach for. `unionGestureBounds` is **removed** — it was `unionAABB` under
|
|
4202
3300
|
another name. Both new functions are exported from the package root.
|
|
4203
|
-
|
|
3301
|
+
|
|
4204
3302
|
Moved onto it: `ChromeState.unionBounds`, the selection overlay's
|
|
4205
3303
|
container-to-leaves resolver, the multi-rotate pivot (which put the pivot in
|
|
4206
3304
|
the wrong place whenever a member was rotated), and WeaselDraw's export
|
|
4207
3305
|
viewBox, which clipped rotated shapes out of the copied SVG.
|
|
4208
|
-
|
|
4209
3306
|
- 3fb3a46: Warn in dev when `useAction` finds no `ActionsProvider`
|
|
4210
|
-
|
|
3307
|
+
|
|
4211
3308
|
`useAction` returned early on a null registry, so an action registered above
|
|
4212
3309
|
the provider — or with no provider mounted — silently never fired its
|
|
4213
3310
|
bindings. It now warns in dev, naming the action id. Runtime behavior in
|
|
4214
3311
|
production builds is unchanged.
|
|
4215
|
-
|
|
3312
|
+
- Updated dependencies [5c8e9e6]
|
|
3313
|
+
- Updated dependencies [2621cbf]
|
|
3314
|
+
- Updated dependencies [0f936da]
|
|
3315
|
+
- Updated dependencies [4180095]
|
|
3316
|
+
- Updated dependencies [9977908]
|
|
3317
|
+
- Updated dependencies [52c7b2a]
|
|
4216
3318
|
- Updated dependencies [3386d64]
|
|
4217
3319
|
- Updated dependencies [c6c499d]
|
|
4218
3320
|
- Updated dependencies [20097e6]
|
|
@@ -4224,13 +3326,13 @@ strokeWidth }`: `align: 'inner'` puts no ink outside the silhouette and
|
|
|
4224
3326
|
- Updated dependencies [c2ffa49]
|
|
4225
3327
|
- Updated dependencies [4c097ef]
|
|
4226
3328
|
- Updated dependencies [d933a89]
|
|
4227
|
-
- @weasel-js/
|
|
4228
|
-
- @weasel-js/
|
|
4229
|
-
- @weasel-js/
|
|
4230
|
-
- @weasel-js/
|
|
4231
|
-
- @weasel-js/
|
|
4232
|
-
- @weasel-js/
|
|
4233
|
-
- @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
|
|
4234
3336
|
|
|
4235
3337
|
## 1.2.0
|
|
4236
3338
|
|