@weasel-js/core 0.7.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +541 -0
- package/dist/{DrawCommand-CJtqqt8H.d.ts → DrawCommand-DCmiNkBj.d.ts} +38 -170
- package/dist/{chunk-CQNKCG34.js → chunk-6SHZHXXR.js} +47 -10
- package/dist/chunk-6SHZHXXR.js.map +1 -0
- package/dist/{chunk-5F3PIERN.js → chunk-D2C6K6FO.js} +1420 -652
- package/dist/chunk-D2C6K6FO.js.map +1 -0
- package/dist/chunk-DCIU3SRK.js +505 -0
- package/dist/chunk-DCIU3SRK.js.map +1 -0
- package/dist/chunk-IO5Z75X4.js +167 -0
- package/dist/chunk-IO5Z75X4.js.map +1 -0
- package/dist/{chunk-VOVKONXA.js → chunk-UB6A6L77.js} +3 -3
- package/dist/{chunk-VOVKONXA.js.map → chunk-UB6A6L77.js.map} +1 -1
- package/dist/{chunk-BGGZ4CVF.js → chunk-Z3KNFTTS.js} +6 -6
- package/dist/chunk-Z3KNFTTS.js.map +1 -0
- package/dist/clipboard.d.ts +2 -2
- package/dist/clone.d.ts +1 -1
- package/dist/{geometry-D9BDMiQi.d.ts → geometry-DVeZ2i-c.d.ts} +1 -1
- package/dist/{grid-CaSK9bHV.d.ts → grid-XcFQVS0f.d.ts} +1 -1
- package/dist/{index-DOYRTfP0.d.ts → index-ChM3ZJ2v.d.ts} +278 -93
- package/dist/index.d.ts +442 -57
- package/dist/index.js +6 -6
- package/dist/insert.d.ts +2 -2
- package/dist/insert.js +5 -5
- package/dist/insert.js.map +1 -1
- package/dist/move.d.ts +3 -3
- package/dist/move.js +2 -2
- package/dist/{options-DMWeTELe.d.ts → options-wAvCnOZd.d.ts} +1 -1
- package/dist/paint-types-CnLIzqq1.d.ts +261 -0
- package/dist/patterns-builtin.d.ts +57 -2
- package/dist/patterns-builtin.js +1 -97
- package/dist/patterns-builtin.js.map +1 -1
- package/dist/{pointSnapToGrid-C3EruUwt.d.ts → pointSnapToGrid-BuDZWBrL.d.ts} +2 -2
- package/dist/renderer.d.ts +25 -8
- package/dist/renderer.js +6 -6
- package/dist/resize.d.ts +3 -3
- package/dist/resize.js +2 -2
- package/dist/routing.d.ts +7 -7
- package/dist/routing.js +1 -1
- package/dist/{types-Dcaa0tPq.d.ts → types-BhJJ2OCM.d.ts} +9 -1
- package/dist/{types-Cpb4hii1.d.ts → types-DUpI7Apc.d.ts} +3 -95
- package/dist/{viewToMat3-D4lrBigW.d.ts → viewToMat3-BvaWCN99.d.ts} +1 -1
- package/package.json +6 -6
- package/dist/chunk-5F3PIERN.js.map +0 -1
- package/dist/chunk-BGGZ4CVF.js.map +0 -1
- package/dist/chunk-CQNKCG34.js.map +0 -1
- package/dist/chunk-SYM6RAM4.js +0 -232
- package/dist/chunk-SYM6RAM4.js.map +0 -1
- package/dist/chunk-Y52N27PF.js +0 -39
- package/dist/chunk-Y52N27PF.js.map +0 -1
- package/dist/registerTexture-BzHTLhD9.d.ts +0 -25
- /package/dist/{types-B6MMiodD.d.ts → path-B6MMiodD.d.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,546 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ffd9713: Chrome hit-tests are axis-aware under non-uniform zoom.
|
|
8
|
+
|
|
9
|
+
Chrome declares its hit zones in screen pixels and paints in screen space, but
|
|
10
|
+
the hit-test converted those pixels to world units through `meanScale`, a
|
|
11
|
+
single geometric mean of the two axis scales. Under per-axis zoom that made the
|
|
12
|
+
pickable region larger than the painted one on the squashed axis and smaller on
|
|
13
|
+
the stretched one — an 8px handle at `scale: { x: 4, y: 1 }` was grabbable 16
|
|
14
|
+
screen px to the side and only 4 px above.
|
|
15
|
+
|
|
16
|
+
New primitives in `core/viewport/pxExtent`, all exported: `pxExtent(px, scale)`
|
|
17
|
+
for a per-axis world length, and `withinPxBox` / `withinPxRadius` to compare a
|
|
18
|
+
world-space delta in screen space directly, which can't drift from the paint at
|
|
19
|
+
all.
|
|
20
|
+
|
|
21
|
+
`AffordanceRegion`'s `point` hit is now that screen-space comparison, so a
|
|
22
|
+
handle's hit region is the square you see — which also fixes it under a rotated
|
|
23
|
+
target, where the old local-frame test was a tilted rectangle on screen. The
|
|
24
|
+
annulus band floor (`minBandPx`) and the annulus paint inset (`insetPx`) are
|
|
25
|
+
per-axis, keeping the ring you can see the ring you can grab. The pen's
|
|
26
|
+
close-hit radius is a screen-space circle. Snap-guide tolerances — the shared
|
|
27
|
+
guide strategy, insert, resize, and alignment — are per-axis, which is exact
|
|
28
|
+
rather than approximate there: a vertical guide is matched by a horizontal
|
|
29
|
+
distance, so it answers to `scale.x` alone.
|
|
30
|
+
|
|
31
|
+
`matchAlignment`'s `worldTolerance` parameter is now `{ x, y }` rather than a
|
|
32
|
+
number. Pass the same value twice for a world-space tolerance.
|
|
33
|
+
|
|
34
|
+
`meanScale` stays, for the two things that legitimately have no per-axis
|
|
35
|
+
answer: hairline stroke widths, where the renderer takes one width, and painted
|
|
36
|
+
chrome placement, whose per-axis form doesn't separate under a rotated target.
|
|
37
|
+
Its doc now says so.
|
|
38
|
+
|
|
39
|
+
- 8853e73: Affordance hits are claims, and an exclusive claim outranks the scope tier.
|
|
40
|
+
|
|
41
|
+
`AffordanceHit` gains `strength`, and `owner` naming what produced the hit. Kit
|
|
42
|
+
chrome claims `'shared'`, which is what it always did: compete on scope and
|
|
43
|
+
specificity. Registered layers, whose hits previously flattened to a bare kind
|
|
44
|
+
string and a payload, now return a `LayerHit` that can also carry `cursor` and
|
|
45
|
+
`strength`, so a consumer's own chrome says the things kit chrome already said
|
|
46
|
+
through `AffordanceRegion`. `owner` is groundwork — nothing binds on it yet, and
|
|
47
|
+
today only diagnostics read it; target it with `kindOf` or `affordance:<kind>`.
|
|
48
|
+
|
|
49
|
+
**Behavior change.** When a press carries an exclusive claim, only bindings
|
|
50
|
+
whose `target` consults the affordance — a `kindOf` predicate, or the
|
|
51
|
+
`affordance:<kind>` string form — are candidates. Scope ordering applies within
|
|
52
|
+
that filtered set, unchanged. Body-class targets (`'empty'`, `'selected-body'`,
|
|
53
|
+
`'unselected-body'`) and `kind:` targets resolve from the body classification
|
|
54
|
+
and never see the affordance, so they no longer win presses on chrome floating
|
|
55
|
+
over the body they name.
|
|
56
|
+
|
|
57
|
+
This is the dispatcher rule the previous release's changeset said was the real
|
|
58
|
+
fix. `select`'s marquee no longer needs its hand-written predicate declining
|
|
59
|
+
chrome affordances, and it is deleted; `rect`, `ellipse`, `polygon`, `star`,
|
|
60
|
+
`hand`, `pen` and `lasso` keep their bare `{ kind: 'drag' }` bindings and stop
|
|
61
|
+
swallowing drags on HUD chrome anyway, which is the point — seven copies of a
|
|
62
|
+
predicate was the alternative. (`select`'s _click_ binding keeps a predicate of
|
|
63
|
+
its own, for an unrelated reason: resize and rotate bind only drag, so a click
|
|
64
|
+
exactly on a handle would otherwise clear the selection.)
|
|
65
|
+
|
|
66
|
+
The filter is hard: if an exclusive claim leaves nothing eligible, the press
|
|
67
|
+
does nothing. A dev-only warning names the owner the first time that happens for
|
|
68
|
+
each owner, because the failure is otherwise silent.
|
|
69
|
+
|
|
70
|
+
**What a claim does not reach.** Only gestures that carry an affordance are
|
|
71
|
+
filtered, and keyboard events never do. Which pointer-family gestures carry one
|
|
72
|
+
is settled by the per-kind claim work later in this release — see "A widget
|
|
73
|
+
declares which gestures it consumes." The filter outranks hotkey scope as well
|
|
74
|
+
as active scope, so holding space to pan no longer pans while the pointer is
|
|
75
|
+
over HUD chrome.
|
|
76
|
+
|
|
77
|
+
`@weasel-js/hud` claims exclusively, and `Widget` gains `cursorAt(x, y)`, which
|
|
78
|
+
resolves a cursor per point rather than from hover state; `hud.window()`
|
|
79
|
+
implements it, so hovering a resize band shows `nwse-resize` instead of the
|
|
80
|
+
active tool's cursor. A widget can also stay transparent to input — `rect`,
|
|
81
|
+
`text`, `label` and `image` are decoration — so a backdrop widget no longer
|
|
82
|
+
eats presses meant for the canvas or for widgets beneath it. That occlusion
|
|
83
|
+
predates this release, but an exclusive claim would have widened it from "HUD
|
|
84
|
+
elements occlude each other" to "HUD elements kill every tool underneath."
|
|
85
|
+
|
|
86
|
+
`WindowWidget.cursor` is removed. Nothing read it; `cursorAt` replaces it.
|
|
87
|
+
|
|
88
|
+
- 43482ce: A registry entry declares what it contributes and when it is eligible.
|
|
89
|
+
|
|
90
|
+
`Contribution` is the entry type: `bindings`, `actions`, an `overlay`, a
|
|
91
|
+
`presentation`, each optional and independent. `Tool<TScratch>` is now the
|
|
92
|
+
**focus-declaring case** of one — it keeps only what a mode the user switches
|
|
93
|
+
into needs (`initScratch`, the lifecycle hooks, the preview hooks, a `cursor`
|
|
94
|
+
closing over its own scratch). An entry that only routes input declares only
|
|
95
|
+
bindings and actions, which is what `@weasel-js/hud` always was.
|
|
96
|
+
|
|
97
|
+
`Eligibility` is a set of conditions rather than one value, because one entry
|
|
98
|
+
holds several: the hand tool is palette-selectable _and_ space-held. `focus`,
|
|
99
|
+
`offhand: HotkeyTrigger`, `always`, `claimed`, plus `capabilities` as a modality
|
|
100
|
+
filter — `@weasel-js/modes` now reads its tags from there. The scope tier a
|
|
101
|
+
binding matches at is derived from whichever condition is live, ordered to match
|
|
102
|
+
the dispatcher's existing hotkey > active > ambient walk. Nothing about that
|
|
103
|
+
walk changed; what changed is that an entry lands in a tier because of what it
|
|
104
|
+
declares about itself rather than which argument a consumer passed it in.
|
|
105
|
+
|
|
106
|
+
`useContributions` is the assembly point, and `useTools` is a shim over it with
|
|
107
|
+
its shape unchanged.
|
|
108
|
+
|
|
109
|
+
**Breaking — `@weasel-js/hud`:** `createHudTool` / `useHudTool` are now
|
|
110
|
+
`createHudContribution` / `useHudContribution`, with no alias. The old names were
|
|
111
|
+
half of the same misstatement as the `as unknown as Tool<null>` cast they
|
|
112
|
+
required; keeping them would preserve exactly what this corrects.
|
|
113
|
+
|
|
114
|
+
**Breaking — `DispatcherContext.ambientToolIds` is removed.** A host driving the
|
|
115
|
+
dispatcher directly declares `eligibility: { always: true }` on always-on entries
|
|
116
|
+
instead. `useTools` consumers are unaffected; the shim sets it.
|
|
117
|
+
|
|
118
|
+
**Behavior — a declared held-key trigger now wires itself.** `ToolDef.hotkey` was
|
|
119
|
+
read by the inspector and wired nothing; the engagement lived in a host-side
|
|
120
|
+
registration keyed by tool id (`BUILTIN_OFFHAND_ACTIONS`, now gone). Assembly
|
|
121
|
+
registers the consolidated `tool.offhand` action from the declarations, so a tool
|
|
122
|
+
that wants space declares it — `useHandTool` does. A host that also registered
|
|
123
|
+
`tool.offhand` by hand should stop.
|
|
124
|
+
|
|
125
|
+
**Behavior — `useTools` returns shallow copies** for ambient entries and
|
|
126
|
+
`hotkey`-declaring tools, since it adds the declaration they were missing.
|
|
127
|
+
Registry tools from `defineTool` are returned unchanged.
|
|
128
|
+
|
|
129
|
+
**Behavior — route-conflict reporting now also sees action `defaultBinding`s.**
|
|
130
|
+
The dispatcher always matched against them; the reporter never saw them, so a
|
|
131
|
+
tool binding colliding with an action default went unreported. It no longer does.
|
|
132
|
+
Dispatch is unchanged.
|
|
133
|
+
|
|
134
|
+
Also added: `mergeContributions(...bundles)`, which concatenates and throws on a
|
|
135
|
+
duplicate id rather than silently dropping one — the recorded plugin/bundling v1,
|
|
136
|
+
whose deferral condition was "≥2 plugin-shaped features in flight."
|
|
137
|
+
|
|
138
|
+
**Not done, so the seam is stated rather than implied:**
|
|
139
|
+
`EligibilityState.heldTriggers` exists and `liveScope` honors it, but nothing
|
|
140
|
+
populates it. `tool.offhand`'s invoker still reports engagement by pushing a tool
|
|
141
|
+
_id_, which `engagedIds` reads, so the declaration registers the binding while
|
|
142
|
+
the id carries the tier. Retiring that means changing `tool.offhand`'s contract.
|
|
143
|
+
|
|
144
|
+
- 9ed1139: Gradient fills that stay attached to their shape, and an editor for them.
|
|
145
|
+
|
|
146
|
+
Gradient geometry was interpreted in screen space: `draw.ts` set the shader's
|
|
147
|
+
`u_worldInv` to the identity matrix, with a comment saying a later step would
|
|
148
|
+
wire the real view inverse. Nothing did. Every gradient therefore slid across
|
|
149
|
+
its own geometry under pan and zoom, which is why the gradient demo shipped
|
|
150
|
+
with pan and zoom disabled. Fine for a viewport-fixed wash, useless for a
|
|
151
|
+
paint on a shape.
|
|
152
|
+
|
|
153
|
+
Gradient paints now carry `units`, mirroring SVG's `gradientUnits`:
|
|
154
|
+
|
|
155
|
+
- `'bounds'` — fractions of the painted node's box, `0..1` per axis (SVG
|
|
156
|
+
`objectBoundingBox`). Resolved by the node painter, so the paint follows the
|
|
157
|
+
node through moves, resizes and rotation.
|
|
158
|
+
- `'local'` — the frame the geometry was handed to the renderer in.
|
|
159
|
+
- `'world'` — scene coordinates; the paint holds still while geometry moves
|
|
160
|
+
through it (SVG `userSpaceOnUse`).
|
|
161
|
+
- `'screen'` — surface pixels, and the default, so every existing gradient
|
|
162
|
+
keeps the behavior it had. WeaselDraw's workspace tint wants exactly this.
|
|
163
|
+
|
|
164
|
+
`WeaselRenderer.render` takes an optional view matrix for `'world'`, and falls
|
|
165
|
+
back to screen space without one. `fillInPoseFrame` resolves `'bounds'` against
|
|
166
|
+
a box and `fillToBoundsFrame` inverts it; `mat3.invert` is new alongside them.
|
|
167
|
+
Supporting helpers: `sampleGradientStops`, `withGradientKind`,
|
|
168
|
+
`gradientGeometry`, `gradientForBounds`.
|
|
169
|
+
|
|
170
|
+
`setFill` takes a whole `paint` as well as a `color`, so a gradient edit is one
|
|
171
|
+
undo entry like any color edit. A `color` no longer tries to inherit alpha from
|
|
172
|
+
a fill that is a gradient.
|
|
173
|
+
|
|
174
|
+
New in `@weasel-js/ui`: `<GradientEditor>` (kind switch, stop strip, per-stop
|
|
175
|
+
color) and `<GradientHandles>` (on-canvas endpoint / center / radius / angle
|
|
176
|
+
handles, positioned through consumer-supplied `toScreen` / `toLocal` so it
|
|
177
|
+
needs no view or scene of its own). Both split live `onInput` from committed
|
|
178
|
+
`onChange`.
|
|
179
|
+
|
|
180
|
+
Converting between kinds is lossy in ways the data makes unavoidable: a radial
|
|
181
|
+
gradient stores no angle, so a round trip through one leaves the segment
|
|
182
|
+
horizontal, and a conic stores no radius, so a round trip through one resets
|
|
183
|
+
the segment's length.
|
|
184
|
+
|
|
185
|
+
`'bounds'` is not a frame you can do polar math in: `x` and `y` are fractions
|
|
186
|
+
of two different lengths, so a circle in it is an ellipse on screen.
|
|
187
|
+
`<GradientHandles>` therefore takes a gradient already resolved by
|
|
188
|
+
`fillInPoseFrame`, and consumers convert edits back with `fillToBoundsFrame`.
|
|
189
|
+
|
|
190
|
+
- 6aaa469: Group resize scales a rotated child along its own axes.
|
|
191
|
+
|
|
192
|
+
Resizing a group applied the group's per-axis scale straight to each leaf's
|
|
193
|
+
axis-aligned `width` and `height` and carried `rotation` through untouched.
|
|
194
|
+
That is correct when `src`/`dst` are already in the leaf's own frame — the
|
|
195
|
+
single-leaf path, where the drag delta is projected into that frame before the
|
|
196
|
+
anchor math runs — but in a group they are world-frame, and a rotated leaf's
|
|
197
|
+
local axes are not the axes being scaled. At 90° a horizontal stretch grew the
|
|
198
|
+
leaf's `width`, when its world-horizontal extent is its `height`.
|
|
199
|
+
|
|
200
|
+
New `remapRotatedLeaf(pose, src, dst)` in `interactions/actions/resize/geometry`
|
|
201
|
+
applies the group affine to the leaf's local frame and drops the shear, which
|
|
202
|
+
the `{x, y, width, height, rotation}` pose model cannot represent. The centre
|
|
203
|
+
moves exactly; each local axis takes the length of its own image; the rotation
|
|
204
|
+
follows the image of the local x-axis. `resizeAction` uses it on the group path
|
|
205
|
+
for any leaf with a rotation, and nowhere else — the unrotated leaf and the
|
|
206
|
+
single-leaf path are byte-identical to before.
|
|
207
|
+
|
|
208
|
+
Rotation and `width` compose exactly under repeated application; only `height`
|
|
209
|
+
drifts, since the perpendicular of a mapped axis is not the image of the
|
|
210
|
+
perpendicular — that gap is exactly the dropped shear. It does not accumulate
|
|
211
|
+
during a drag: every move remaps from the gesture's start poses, not from the
|
|
212
|
+
previous preview.
|
|
213
|
+
|
|
214
|
+
- 40dd97d: A widget declares which gestures it consumes, and a claim bars only those.
|
|
215
|
+
|
|
216
|
+
A HUD widget could be pressed, dragged and hovered and nothing else. The gap
|
|
217
|
+
was in the dispatcher: `affordanceAt` ran on `pointerdown` and hover only, so
|
|
218
|
+
`doubleclick`, `contextmenu` and `wheel` carried no affordance and no binding
|
|
219
|
+
could gate on one. All four now do — `doubleclick` replays it from its press,
|
|
220
|
+
`contextmenu` classifies its own position (a secondary button never reaches
|
|
221
|
+
`onPointerDown`, so there is nothing to replay from) and gains world
|
|
222
|
+
coordinates it never carried, and `wheel` classifies the point under the
|
|
223
|
+
cursor. The immediate-invoker param bag, which forwarded position for `click`
|
|
224
|
+
and `pointerdown` only, now covers `contextmenu`, `longpress` and the
|
|
225
|
+
affordance on all four.
|
|
226
|
+
|
|
227
|
+
**Behavior change, and the reason for the rest of this entry.** An exclusive
|
|
228
|
+
claim previously meant _this pixel is mine_. Under that rule, giving `wheel` an
|
|
229
|
+
affordance would have killed scroll-to-zoom over every floating panel. It now
|
|
230
|
+
means _these gestures are mine_: `LayerHit` and `AffordanceHit` gain
|
|
231
|
+
`claimedKinds`, a set over the new `ClaimableGesture` union (`'pointer'` —
|
|
232
|
+
covering `pointerDown` / `click` / `drag`, one press protocol — plus
|
|
233
|
+
`'doubleClick'`, `'contextMenu'`, `'longPress'`, `'wheel'`). Omitting it bars
|
|
234
|
+
everything, which is what an exclusive claim did before.
|
|
235
|
+
|
|
236
|
+
`Widget.claims` is that set. Absent means every kind but `wheel`, so
|
|
237
|
+
right-clicking or double-clicking HUD chrome stops acting on the scene
|
|
238
|
+
underneath while scroll-to-zoom over a panel is unchanged; a widget that wants
|
|
239
|
+
the wheel asks for it. `claims: []` is decoration and replaces `claimsPointer`,
|
|
240
|
+
which is removed. `HudPointerEvent` gains `doubleclick`, `contextmenu`,
|
|
241
|
+
`longpress` and `wheel` arms.
|
|
242
|
+
|
|
243
|
+
`PointerClaim` and `onPointer`'s return type are removed. The return was
|
|
244
|
+
discarded at every call site, and it cannot be made live: the claim filter runs
|
|
245
|
+
at match time, before any widget is consulted, so there is no later moment at
|
|
246
|
+
which returning `'pass'` could restore a binding the filter already dropped.
|
|
247
|
+
|
|
248
|
+
**Two matcher fixes this depends on.** `doubleClick` and `contextMenu` passed
|
|
249
|
+
the DOM target to `matchTarget` where `click`, `drag` and `longPress` pass the
|
|
250
|
+
affordance; a `kindOf` predicate on either kind therefore received an element
|
|
251
|
+
no predicate in the kit expects. They now match the others. And the kit's body
|
|
252
|
+
predicates — `isBody`, `isSelectedBody`, `isUnselectedBody`, `isEmpty` — carry
|
|
253
|
+
`readsAffordance: false`, which `targetConsultsAffordance` honors. Each has a
|
|
254
|
+
`kindOf` but reads only `bodyTarget`, so the filter inferred that they consult
|
|
255
|
+
the hit and let them survive a claim; with `doubleclick` now carrying one,
|
|
256
|
+
`enterPathEdit`'s `kindOf: isBody` would otherwise have entered path-edit mode
|
|
257
|
+
on a double-click over chrome.
|
|
258
|
+
|
|
259
|
+
`WheelSpec` gains `target`, and `wheel` gains a route-grammar target slot —
|
|
260
|
+
a wheel binding could not gate on anything at all before.
|
|
261
|
+
|
|
262
|
+
- 531150f: A loupe, and the window primitive it needed.
|
|
263
|
+
|
|
264
|
+
`hud.window()` is a draggable, resizable frame drawn in WebGL over the canvas —
|
|
265
|
+
titlebar, eight resize bands, close box. It paints no interior. Interiors come
|
|
266
|
+
from a new optional `content` painter on `Widget`, which `attachHud` draws
|
|
267
|
+
beneath every widget frame in the same layer, clipped to `contentRect`. That
|
|
268
|
+
painter receives `HudContentCtx`, carrying the scene data and view the hud layer
|
|
269
|
+
was already handed; `HudDrawCtx` stays data-free, so widgets remain renderable
|
|
270
|
+
headlessly. Painter commands are in absolute canvas coordinates — the group
|
|
271
|
+
carries a clip, not a transform.
|
|
272
|
+
|
|
273
|
+
`createLoupe()` is the first consumer: a window whose interior shows either the
|
|
274
|
+
scene re-rendered through a magnified inner view, or the actual framebuffer read
|
|
275
|
+
back and magnified 1:1. Both modes exist because neither answers both questions
|
|
276
|
+
honestly — a re-render is crisp at any magnification but its antialiased edge
|
|
277
|
+
colors are not the colors on screen, so the hex readout samples the framebuffer
|
|
278
|
+
in either mode. The frame stays parked and the pointer aims it; content freezes
|
|
279
|
+
while the pointer is over the window, which is what keeps the borders reachable.
|
|
280
|
+
Aiming uses its own `pointermove` listener rather than hud hover, because hud
|
|
281
|
+
hover comes from the layer's `onUncapturedMove` and stops during a captured drag
|
|
282
|
+
— exactly when a magnifier is most wanted.
|
|
283
|
+
|
|
284
|
+
Also fixed in the HUD: `hud.drag` pumped **world** coordinates into widgets
|
|
285
|
+
while `hud.press` sent screen coordinates, because the dispatcher builds
|
|
286
|
+
move/end contexts with an empty dep bag and the `view` lookup silently fell
|
|
287
|
+
through. Invisible at zoom 1, and a window that jumped and tracked backwards at
|
|
288
|
+
any other zoom. The drag action now captures its deps at gesture start.
|
|
289
|
+
|
|
290
|
+
`ImageDrawCommand` gains `sampling: 'linear' | 'nearest'`, applied per draw at
|
|
291
|
+
bind time rather than at upload, since `GLImageCache` keys textures by bitmap
|
|
292
|
+
identity. Without `nearest`, magnifying a framebuffer readback comes back
|
|
293
|
+
blurred, which defeats the readback.
|
|
294
|
+
|
|
295
|
+
**Behavior change in `@weasel-js/core`:** the select tool's area-select drag now
|
|
296
|
+
declines presses that a registered layer's hit-test claimed. It bound
|
|
297
|
+
`{ kind: 'drag', target: 'empty' }`, and the string form of `target` resolves
|
|
298
|
+
from the body only — chrome floating over empty canvas read as empty canvas, so
|
|
299
|
+
area-select swallowed drags on HUD widgets. The adjacent `clearSelection` click
|
|
300
|
+
binding already had the correct shape. Other tools that bind a bare
|
|
301
|
+
`{ kind: 'drag' }` still have this hole.
|
|
302
|
+
|
|
303
|
+
- 596253e: The built-in path and shape painters accept a `FillStyle` in `data.fill`.
|
|
304
|
+
|
|
305
|
+
`kit:path` and `kit:shape` typed `data.fill` as a color string and emitted
|
|
306
|
+
`{ color }`, so a node could not carry a gradient or a pattern without the
|
|
307
|
+
consumer registering a painter of its own — even though the renderer has taken
|
|
308
|
+
every `FillStyle` variant since the paint model landed. The two painters were
|
|
309
|
+
the narrow point, not the renderer.
|
|
310
|
+
|
|
311
|
+
`data.fill` is now `string | FillStyle` (exported as `NodeFill`). A string
|
|
312
|
+
still means a solid color, `'none'` still skips the fill, `undefined` still
|
|
313
|
+
falls back to `data.color` and then to the default — and only when there is no
|
|
314
|
+
stroke, so a stroke-only pencil path stays unfilled. An object is used as-is.
|
|
315
|
+
`ink()` agrees with `paint()` on all of it, which the existing agreement test
|
|
316
|
+
plus a new one both check.
|
|
317
|
+
|
|
318
|
+
One behavior change beyond the widening: `kit:shape` used to paint `'none'` as
|
|
319
|
+
a literal color, since only `kit:path` special-cased it. It now skips the fill,
|
|
320
|
+
matching `kit:path` and matching what the string obviously means.
|
|
321
|
+
|
|
322
|
+
This is the kit half of gradient and texture fills. The app half — widening
|
|
323
|
+
WeaselDraw's own data shape, a fill-kind switch in the properties panel, a
|
|
324
|
+
gradient editor with on-canvas handles, and matching SVG `<linearGradient>` /
|
|
325
|
+
`<pattern>` export — is untouched.
|
|
326
|
+
|
|
327
|
+
- 22eafe6: Pattern fills tile, persist, and round-trip through SVG.
|
|
328
|
+
|
|
329
|
+
The `pattern` variant of `FillStyle` never tiled. `drawPathFillPattern`
|
|
330
|
+
borrowed the `imageFill` program while binding the path fill mesh VAO, which
|
|
331
|
+
enables `a_position` only — `a_uv` was never bound, so `v_uv` was the constant
|
|
332
|
+
`(0, 0)` and every fragment sampled texel (0, 0) of the tile. Textures also
|
|
333
|
+
uploaded with `CLAMP_TO_EDGE`, so correct UVs alone would have smeared rather
|
|
334
|
+
than repeated. The variant had no visual consumer, which is why it went
|
|
335
|
+
unnoticed.
|
|
336
|
+
|
|
337
|
+
`patternFill` is now its own program, taking `gradFill`'s vertex stage:
|
|
338
|
+
paint-space coordinates come from the screen position through `u_worldInv`
|
|
339
|
+
rather than a UV attribute, so the path mesh keeps its position-only layout.
|
|
340
|
+
`GLTextureCache.upload` takes a wrap argument and pattern textures bind
|
|
341
|
+
`REPEAT`.
|
|
342
|
+
|
|
343
|
+
Patterns pick up `units` alongside gradients. For a pattern it names the space
|
|
344
|
+
the tile's **origin and scale** live in — not geometry, which a pattern hasn't
|
|
345
|
+
got. `'bounds'` anchors the tile to the painted node's box, so dragging the
|
|
346
|
+
node carries the pattern and resizing reveals more tiles instead of stretching
|
|
347
|
+
them; `fillInPoseFrame` rebases it by translation only.
|
|
348
|
+
|
|
349
|
+
`TilePatternSpec` is the serializable payload — plain data naming a built-in
|
|
350
|
+
tile (`hatch`, `crosshatch`, `dots`, `chunks`) plus its parameters:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
{ fill: 'pattern', pattern: { tile: 'hatch', color: '#0fb5a8', size: 8 }, units: 'bounds' }
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`resolvePatternSpec` turns one into a `TextureHandle` at paint time, memoized
|
|
357
|
+
on the spec's values so identical specs share a texture. The built-in painters
|
|
358
|
+
resolve it alongside `fillInPoseFrame`; a consumer emitting its own draw
|
|
359
|
+
commands resolves it at the same place it calls that one. A `TextureHandle`
|
|
360
|
+
payload still works untouched, but cannot be persisted or exported — prefer
|
|
361
|
+
the spec.
|
|
362
|
+
|
|
363
|
+
`@weasel-js/svg` serializes a tile spec as a `<pattern patternUnits=
|
|
364
|
+
"userSpaceOnUse">` whose children come from the same tile description that
|
|
365
|
+
rasterizes the texture, so the vector and raster forms cannot drift. The spec
|
|
366
|
+
rides along on `data-weasel-tile` for lossless re-import; a hand-authored
|
|
367
|
+
`<pattern>` without it is dropped with a warning rather than guessed at.
|
|
368
|
+
`SerializeOptions.onWarn` is new, and reports paint that SVG cannot express —
|
|
369
|
+
a conic gradient, or a pattern carrying a `TextureHandle`.
|
|
370
|
+
|
|
371
|
+
`tilePreviewSvg` / `tilePreviewCssUrl` render a single tile as a standalone
|
|
372
|
+
`<svg>`, for pickers that need to show a tile outside a document.
|
|
373
|
+
|
|
374
|
+
- cd23624: A text run can turn off a flag its node sets.
|
|
375
|
+
|
|
376
|
+
Run-level `bold` / `italic` / `underline` / `strikethrough` are additive over
|
|
377
|
+
the node's `TextStyle` — a run turns a flag on, never off. So "select a word
|
|
378
|
+
inside an underlined node and hit U" was unrepresentable, and the character bar
|
|
379
|
+
could only refuse.
|
|
380
|
+
|
|
381
|
+
New `setFlagOverRange(runs, style, start, end, key, value)` in
|
|
382
|
+
`features/text/runs/flagRange`, exported alongside `nodeHasFlag`. Turning a
|
|
383
|
+
flag on, or off in a node that doesn't set it, is the ordinary additive write
|
|
384
|
+
and returns the style untouched. Turning it off in a node that _does_ set it
|
|
385
|
+
clears the node flag and raises it on the runs outside the range: identical
|
|
386
|
+
rendered result, expressible edit, and `StyledRun` unchanged — so nothing a
|
|
387
|
+
document can already contain changes meaning.
|
|
388
|
+
|
|
389
|
+
That is the answer to the recorded question of tri-state versus
|
|
390
|
+
normalize-on-write, and it is not close. A tri-state run flag cannot cover
|
|
391
|
+
`bold` or `italic`: those are booleans on a run but `fontWeight` and
|
|
392
|
+
`fontStyle` on the node, so a run's `false` has no node-level boolean to
|
|
393
|
+
override. Tri-state fixes two of the four flags; this fixes all four, and
|
|
394
|
+
without widening the persisted shape.
|
|
395
|
+
|
|
396
|
+
One case is declined rather than approximated. `run.bold` resolves to exactly
|
|
397
|
+
700 everywhere, so a node at `fontWeight: 900` cannot have its weight pushed
|
|
398
|
+
onto its runs without lightening the text that was _not_ edited. That returns
|
|
399
|
+
`applied: false` and writes nothing; a control should disable rather than
|
|
400
|
+
silently downgrade.
|
|
401
|
+
|
|
402
|
+
`useTextEdit` takes an optional `setStyle(id, style)` for this — the hook could
|
|
403
|
+
read the node style but had nowhere to write one back. Omit it and the toggle
|
|
404
|
+
declines exactly as it does today.
|
|
405
|
+
|
|
406
|
+
### Patch Changes
|
|
407
|
+
|
|
408
|
+
- Updated dependencies [43482ce]
|
|
409
|
+
- Updated dependencies [40dd97d]
|
|
410
|
+
- @weasel-js/modes@1.0.0
|
|
411
|
+
- @weasel-js/gestures@1.0.0
|
|
412
|
+
- @weasel-js/font@1.0.0
|
|
413
|
+
- @weasel-js/geom@1.0.0
|
|
414
|
+
- @weasel-js/history@1.0.0
|
|
415
|
+
|
|
416
|
+
## 0.8.0
|
|
417
|
+
|
|
418
|
+
### Minor Changes
|
|
419
|
+
|
|
420
|
+
- bdcdfe5: Clipboard keyboard actions, and two bugs the wiring flushed out.
|
|
421
|
+
|
|
422
|
+
`clipboard.copy` (Cmd/Ctrl+C) and `clipboard.cut` (Cmd/Ctrl+X) are kit-standard
|
|
423
|
+
descriptors. Publish the imperative surface `useClipboardOps` returns as the new
|
|
424
|
+
`clipboard` dep and both work; the buttons a consumer already has and the
|
|
425
|
+
shortcut then route through one implementation instead of two. Cut is copy plus
|
|
426
|
+
the same batched delete `deleteAction` performs — one undo entry.
|
|
427
|
+
|
|
428
|
+
There is deliberately no `clipboard.paste`. Cmd/Ctrl+V already arrives as a DOM
|
|
429
|
+
`paste` event, which the dispatcher routes to `ingest` and the content-handler
|
|
430
|
+
registry — the path that reaches the OS payload. A key binding would fire
|
|
431
|
+
alongside it and paste twice.
|
|
432
|
+
|
|
433
|
+
Two older bugs, both found by actually pressing the keys:
|
|
434
|
+
|
|
435
|
+
- **Deleting a container together with its children threw mid-batch.**
|
|
436
|
+
`removeNode` cascades the subtree, so a group's op already takes its members
|
|
437
|
+
with it and the members' own ops then hit `unknown node id`. Selecting a group
|
|
438
|
+
and its contents at once — what Cmd+A does — was enough. `deleteAction` and
|
|
439
|
+
`clipboard.cut` now share one op builder that skips any id with a selected
|
|
440
|
+
ancestor.
|
|
441
|
+
- **Cmd+D did nothing at all.** Two faults stacked. `duplicate` threw before
|
|
442
|
+
its `enabled` gate could answer, because the gate reads `deps.selection` and
|
|
443
|
+
the descriptor never declared it — an undeclared read the dev-build deps
|
|
444
|
+
Proxy treats as an error. Declared now, with a sweep test over every
|
|
445
|
+
`requiresSelection`-gated descriptor so the next one can't ship the same way.
|
|
446
|
+
And underneath that, the invoker was a stub whose body was `void params`,
|
|
447
|
+
deferring to a "legacy bridge" that no longer exists. `duplicateAction` now
|
|
448
|
+
really duplicates: each selected node with its whole subtree (so a duplicated
|
|
449
|
+
group comes out populated, not empty), offset by the same 12 units a paste
|
|
450
|
+
gets, as one undoable batch, with the copies selected afterward. Descendants
|
|
451
|
+
are offset only for absolute-pose scenes — with a `poseComposition`
|
|
452
|
+
registered, poses are relative and moving the root already carries them.
|
|
453
|
+
|
|
454
|
+
`polygonHitTestRect` moved from `features/paths/` to `core/geometry/`: it is
|
|
455
|
+
pure geometry, `core/adapters/arrayAdapter.ts` needs it, and core may not import
|
|
456
|
+
from features. Same exports from the package barrel.
|
|
457
|
+
|
|
458
|
+
- e0ab60e: Device profile: the kit stops assuming a mouse.
|
|
459
|
+
|
|
460
|
+
`DeviceProfile` is one object holding pointer coarseness, hover capability and
|
|
461
|
+
pixel density, resolved once per `<SceneCanvas>` and published to its subtree.
|
|
462
|
+
Two things read it. The chrome-caps rule layer gains `coarsePointer:` and
|
|
463
|
+
`canHover:` selectors (plus matching fluent atoms), so consumers can gate
|
|
464
|
+
chrome on the device. And every handle size and hit radius — six independent
|
|
465
|
+
literal `8`s and one `24` before this — now derives from one base module times
|
|
466
|
+
`DeviceProfile.targetScale`, so a coarse pointer gets 14px handles and a 42px
|
|
467
|
+
rotation distance without paint and hit-test ever drifting apart. The public
|
|
468
|
+
`DEFAULT_HANDLE_SIZE` / `DEFAULT_ROTATION_HANDLE_DISTANCE` constants keep
|
|
469
|
+
their unscaled values.
|
|
470
|
+
|
|
471
|
+
`longPress` is a real gesture kind: spec, event, matcher, and route grammar.
|
|
472
|
+
The dispatcher synthesizes it for touch and pen presses held 500ms without
|
|
473
|
+
crossing the drag threshold — never for a mouse, and cancelled by movement,
|
|
474
|
+
release, cancel, or a second finger landing. An unmatched long-press
|
|
475
|
+
re-dispatches as `contextmenu`, so existing `contextMenu` bindings become
|
|
476
|
+
reachable by touch with no consumer change.
|
|
477
|
+
|
|
478
|
+
Also fixes a density bug: `useCanvasSize` read `devicePixelRatio` only inside
|
|
479
|
+
its `ResizeObserver` callback, so moving a window to a different-density
|
|
480
|
+
display without resizing it left the snapshot stale. Density now comes from
|
|
481
|
+
the profile, which watches a re-armed resolution media query.
|
|
482
|
+
|
|
483
|
+
Override any of it with the new `<SceneCanvas device={{ coarsePointer: true }}>`
|
|
484
|
+
prop — for tests, for demos that want touch-sized chrome on a desktop, and for
|
|
485
|
+
hybrid devices where the media query guesses wrong.
|
|
486
|
+
|
|
487
|
+
- 3d693c7: Underline and strikethrough shortcuts, and the core boundary goes strict.
|
|
488
|
+
|
|
489
|
+
Cmd/Ctrl+U toggles underline and Cmd/Ctrl+Shift+X toggles strikethrough, both
|
|
490
|
+
through `toggleFlagInRange` like bold and italic — so they toggle _off_, a mixed
|
|
491
|
+
range turns fully on, and a collapsed caret gets a pending style that styles the
|
|
492
|
+
next character only. `rangeStyle.ts` had listed both flags all along; only
|
|
493
|
+
`useTextEdit`'s `StyleFlag` and its keydown switch were narrower.
|
|
494
|
+
|
|
495
|
+
Underline in particular had to be intercepted rather than merely supported.
|
|
496
|
+
Left alone, the browser ran its own `formatUnderline` and `domToRuns`' `<u>`
|
|
497
|
+
flattening made that look like it had worked while bypassing the run algebra
|
|
498
|
+
entirely. The flattening stays — it's what lets pasted decoration survive — but
|
|
499
|
+
it was never the mechanism. Bare Cmd+X is deliberately left to the browser so
|
|
500
|
+
cutting text mid-edit still works; only Cmd+Shift+X is claimed.
|
|
501
|
+
|
|
502
|
+
The `core/` ← `features/`/`interactions/` lint rule no longer exempts type
|
|
503
|
+
imports. Three types core named across the boundary moved down to where core
|
|
504
|
+
can own them — `Path` to `core/geometry/path.ts`, `RectPose` to
|
|
505
|
+
`core/scene/types.ts` (whose doc comment already claimed it lived in core), and
|
|
506
|
+
`ModifierState` to `core/modifierState.ts` — each with a re-export left at its
|
|
507
|
+
old address, so no importer changes.
|
|
508
|
+
|
|
509
|
+
- e264d62: Stroked text.
|
|
510
|
+
|
|
511
|
+
`TextStyle.stroke` and `StyledRun.stroke` carry a real `Stroke`, and the
|
|
512
|
+
outline tier paints it as a second batched draw call over the group's merged
|
|
513
|
+
geometry — so a glyph above `textOutlineMinScreenSize` gets real joins, caps
|
|
514
|
+
and miters in any paint, because by then it is an ordinary `PolygonPath`.
|
|
515
|
+
Width stays a world measure: it crosses into the cached em-space tessellation
|
|
516
|
+
by dividing by the glyph's scale, so it does not grow with `fontSize`. Below
|
|
517
|
+
the threshold a glyph is a sampled distance field with no geometry to stroke,
|
|
518
|
+
and renders unstroked rather than approximated.
|
|
519
|
+
|
|
520
|
+
`kit:text` also reads the kit-native `data.stroke` / `data.strokeWidth` leaf
|
|
521
|
+
fields that `kit:shape` already honors, so one pair of stroke controls means
|
|
522
|
+
the same thing on a text node as on a rect.
|
|
523
|
+
|
|
524
|
+
`@weasel-js/svg` round-trips all of it — node-level and per-`<tspan>` — where
|
|
525
|
+
it previously parsed a text stroke into a warning and dropped it.
|
|
526
|
+
|
|
527
|
+
Two older bugs fell out of building it, both invisible to fills and both
|
|
528
|
+
fixed: `extractPolylines` kept a closed contour's duplicate final point
|
|
529
|
+
(zero-length closing segment, dropped wrap-around join), and glyph path data
|
|
530
|
+
whose contours carried no `Z` stroked as open polylines — a missing closing
|
|
531
|
+
edge with a cap at each loose end. A fill closes a contour implicitly; only a
|
|
532
|
+
stroke reads the difference.
|
|
533
|
+
|
|
534
|
+
### Patch Changes
|
|
535
|
+
|
|
536
|
+
- Updated dependencies [e0ab60e]
|
|
537
|
+
- Updated dependencies [e264d62]
|
|
538
|
+
- @weasel-js/gestures@0.8.0
|
|
539
|
+
- @weasel-js/font@0.8.0
|
|
540
|
+
- @weasel-js/geom@0.8.0
|
|
541
|
+
- @weasel-js/history@0.8.0
|
|
542
|
+
- @weasel-js/modes@0.8.0
|
|
543
|
+
|
|
3
544
|
## 0.7.2
|
|
4
545
|
|
|
5
546
|
### Patch Changes
|