@weasel-js/core 1.4.0-pre.0 → 1.4.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 +433 -0
- package/README.md +22 -18
- package/dist/{chunk-IMFXEINV.js → chunk-HYJL7OOJ.js} +219 -48
- package/dist/chunk-HYJL7OOJ.js.map +1 -0
- package/dist/{chunk-H6E6KPTU.js → chunk-UTIFY2UU.js} +2 -2
- package/dist/chunk-UTIFY2UU.js.map +1 -0
- package/dist/index.d.ts +95 -6
- package/dist/index.js +2 -2
- package/dist/{registry-sBLZYWI1.d.ts → registry-CxSEyDka.d.ts} +15 -15
- package/dist/renderer.js +2 -2
- package/dist/routing.d.ts +2 -1
- package/dist/routing.js +1 -1
- package/package.json +9 -8
- package/dist/chunk-H6E6KPTU.js.map +0 -1
- package/dist/chunk-IMFXEINV.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,438 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1214ff5: Split a canvas's paint target from its input target.
|
|
8
|
+
|
|
9
|
+
`<SceneCanvas paintInto={{ canvas, x, y }} inputElement={el}>` paints into a
|
|
10
|
+
rect of a canvas you own and takes pointer input from an element you own, so N
|
|
11
|
+
canvases share one GL context and one buffer. Each needs its own
|
|
12
|
+
`<WeaselProvider isolate>`.
|
|
13
|
+
|
|
14
|
+
The ref handle names both elements: `element` is where input, focus and the
|
|
15
|
+
cursor live, and is now typed `HTMLElement` because detached it is not a canvas;
|
|
16
|
+
`surface` is where pixels land. Attached, they are the same `<canvas>` and
|
|
17
|
+
`element` keeps working as before. The HUDs render when detached too, anchored
|
|
18
|
+
to the input box rather than to the shared surface every pane sits in.
|
|
19
|
+
|
|
20
|
+
Breaking, narrowly: `createLoupe`'s `element` option is now `canvas`, with an
|
|
21
|
+
optional `input` for the element aim is measured against.
|
|
22
|
+
`CanvasExtensionApi.element` no longer satisfies an `HTMLCanvasElement` — read
|
|
23
|
+
`surface` for pixels. And `clientToWorld`'s first parameter widens to
|
|
24
|
+
`HTMLElement`, which stops compiling for a consumer who annotated that parameter
|
|
25
|
+
as `HTMLCanvasElement`; one who let it infer is unaffected.
|
|
26
|
+
|
|
27
|
+
<!-- bump-approved: minor: Mike — the labkit annotations arcs 1-4 (a shared drawing surface, a mark store, the overlay, and capture/export) plus this split of a canvas's paint target from its input target, on top of ~30 patch changesets carrying new public surface across core, ui and labkit; called explicitly in conversation on 2026-09-03: "we were going to cut a 1.4.0-pre release" -->
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- eb16573: Alt-drag-to-duplicate shows the duplicate cursor.
|
|
32
|
+
|
|
33
|
+
`clone` declared no cursor, so the one gesture in the kit that copies instead
|
|
34
|
+
of moving looked exactly like a move until you released. It now declares
|
|
35
|
+
`cursor: 'copy'` and `activeCursor: 'copy'`, which needs no modifier gate of
|
|
36
|
+
its own: the select tool binds clone behind `mods: { alt: true }`, so the hover
|
|
37
|
+
pump predicts the action — and shows the cursor — only while Alt is held over a
|
|
38
|
+
body, and drops it the moment Alt is released.
|
|
39
|
+
|
|
40
|
+
This replaces the `apps/draw` stub that arc 4 deleted. That stub forced
|
|
41
|
+
`cursor: copy` from a CSS rule fed by a hand-rolled Alt listener, scoped to
|
|
42
|
+
path-edit mode and applied over the whole canvas including where nothing would
|
|
43
|
+
happen. The affordance now shows wherever Alt-drag actually duplicates, in
|
|
44
|
+
every mode, through the cursor pipeline.
|
|
45
|
+
|
|
46
|
+
`AffordanceRegion.cursor` and a layer claim cannot express this — neither
|
|
47
|
+
`affordanceAt` nor `RenderLayer.hitTest` receives the event, so `Action.cursor`
|
|
48
|
+
is the kit's only modifier-gated cursor channel.
|
|
49
|
+
- 6650d67: Correct the actions-registry documentation, which described the API that was
|
|
50
|
+
replaced in May 2026.
|
|
51
|
+
|
|
52
|
+
`packages/core/README.md` said `<ActionsProvider>` wires a keydown listener,
|
|
53
|
+
showed an `Action` with a `run` callback, and told consumers to reach for
|
|
54
|
+
`useSelectAll` / `useEscape` / `useDuplicate` / `useNudge` / `useReorder`. None
|
|
55
|
+
of that is true: keystrokes and pointer gestures both route through the gesture
|
|
56
|
+
dispatcher matching `defaultBinding`, an action does its work through `invoker`,
|
|
57
|
+
and those five hooks were deleted. `docs/taxonomy.md` likewise still listed
|
|
58
|
+
`Action.run` and a fallback to a `useKeybinding` path that no longer exists.
|
|
59
|
+
- 04ea2e8: The painted cursor tier, and cursors that tell the five shape tools apart.
|
|
60
|
+
|
|
61
|
+
A cursor that cannot be a CSS cursor is now drawn into the canvas instead of
|
|
62
|
+
being dropped. `resolveCursorTier` escalates when a glyph is sized in world
|
|
63
|
+
units, or past the 128 CSS px above which the browser silently discards the
|
|
64
|
+
image; `<Canvas>` then sets `cursor: none` and a screen-space layer paints the
|
|
65
|
+
glyph at the pointer. Tools never choose the tier — they declare what they
|
|
66
|
+
want, and a brush stays a brush across the radius where it stops being
|
|
67
|
+
expressible as a CSS cursor.
|
|
68
|
+
|
|
69
|
+
`brush` ships with it, sized by `worldRadius` so its ring measures the brush at
|
|
70
|
+
every zoom. A glyph that measures something declares `CursorGlyph.radius` to
|
|
71
|
+
name the circle being sized, and world-sized glyphs hold their line weight in
|
|
72
|
+
CSS px while their geometry scales.
|
|
73
|
+
|
|
74
|
+
`paint.ts` is the shared half: the baker and the painter both consume its
|
|
75
|
+
ordered paint ops, so the two tiers draw one glyph rather than two drawings
|
|
76
|
+
that resemble each other. The layer itself lives in core, which is where
|
|
77
|
+
`RenderLayer` and `Path` live.
|
|
78
|
+
|
|
79
|
+
rect, ellipse, line, star and polygon were all bare `crosshair` and so
|
|
80
|
+
indistinguishable while in use; each now shows a crosshair badged with its
|
|
81
|
+
shape, hotspotted on the cross. `Tool.cursor` on a built `Tool` widens from
|
|
82
|
+
`string` to `CursorSpec`, which it should have done when `ToolDef.cursor` did.
|
|
83
|
+
|
|
84
|
+
The `apps/draw` `cursor: copy` stub and its hand-rolled Alt listener are gone.
|
|
85
|
+
They advertised an add-anchor sub-tool that does not exist, so the affordance
|
|
86
|
+
was removed rather than relocated; `docs/TODO.md` records where it returns.
|
|
87
|
+
- b656ebf: Rotatable cursors, and a `CursorSpec` for the four fields that declare one.
|
|
88
|
+
|
|
89
|
+
`bakeCursor` takes an `angle` in radians, quantized to 16 steps of 22.5°, and
|
|
90
|
+
turns both the glyph and its hotspot. `Tool.cursor`, `Action.cursor`,
|
|
91
|
+
`Action.activeCursor` and `AffordanceRegion.cursor` widen from `string` to
|
|
92
|
+
`CursorSpec` — either a CSS keyword, which passes through untouched, or
|
|
93
|
+
`{ glyph, size?, angle?, fallback? }`. Every cursor declaration written before
|
|
94
|
+
this keeps working.
|
|
95
|
+
|
|
96
|
+
Two glyphs ship with it. The selection's rotation ring now shows a real `rotate`
|
|
97
|
+
cursor instead of a bare `grab`, and the resize corners show a `resize` arrow
|
|
98
|
+
turned to the corner's actual axis — a rotated selection used to keep the
|
|
99
|
+
unrotated diagonal, because CSS has four diagonal keywords and a rotation needs
|
|
100
|
+
sixteen. The keyword remains as each spec's `fallback`.
|
|
101
|
+
- 5295c34: Draw on a lab's instrument: the `annotations` capability gets its overlay.
|
|
102
|
+
|
|
103
|
+
An instrument that declares `annotations` now gets a drawing surface on every
|
|
104
|
+
target it names — weasel tools, weasel selection, marks that pan and zoom with
|
|
105
|
+
what they mark — plus a palette (select, freehand, line, arrow, rectangle,
|
|
106
|
+
ellipse, text) and its own tool slot. `useAnnotations()` reaches the store from
|
|
107
|
+
the instrument's render or from a chrome contribution, and re-renders its
|
|
108
|
+
caller as marks change.
|
|
109
|
+
|
|
110
|
+
The lab's shared surface grew the buffer that makes this possible: one
|
|
111
|
+
`<canvas>` over `.lk-lab__body`, and `SurfaceHandle.registerPainter`, which is
|
|
112
|
+
how a resize of that buffer reaches every tile rather than the one that moved.
|
|
113
|
+
`getContainer()` names the element tile rects are measured against.
|
|
114
|
+
|
|
115
|
+
A mark is a weasel scene node in a scene of its own per target — a pane's
|
|
116
|
+
hit-test, marquee and paint walk the whole scene they are handed, so one shared
|
|
117
|
+
scene would put a neighbour's marks under the pointer. An annotation's id is
|
|
118
|
+
therefore `<target>/<node>`, and `createAnnotationStore` takes `targets` alone
|
|
119
|
+
plus an optional `restore`; `SerializedAnnotations` carries `scenes`, keyed by
|
|
120
|
+
target. Marks still do not survive a reload — the storage slot is the next arc.
|
|
121
|
+
|
|
122
|
+
Core adds `ArrowIcon` to the built-in tool glyphs.
|
|
123
|
+
- 2fbf611: Give a canvas its own provider scope with `<WeaselProvider isolate>`
|
|
124
|
+
|
|
125
|
+
An actions registry holds exactly one dispatcher, so a second `<SceneCanvas>`
|
|
126
|
+
joining a scope displaced the first and took its input away. Worse, the
|
|
127
|
+
detach was unconditional: whichever canvas unmounted — or merely re-rendered
|
|
128
|
+
with a new dispatcher identity — cleared the slot for the one still on screen.
|
|
129
|
+
The symptom was a canvas that stopped responding, naming neither canvas nor the
|
|
130
|
+
registry they shared.
|
|
131
|
+
|
|
132
|
+
`isolate` mounts every provider unconditionally instead of deferring to one
|
|
133
|
+
already in scope, so canvases that merely coexist get a scope each. This is the
|
|
134
|
+
shape consumers had already reached for by hand: `AnimationDemo` and
|
|
135
|
+
`BooleanOpsDemo` both mounted raw `ActionsProvider` / `SelectionContextProvider`
|
|
136
|
+
/ `DepRegistryProvider` to shadow the ambient scope, and both now say `isolate`
|
|
137
|
+
instead.
|
|
138
|
+
|
|
139
|
+
`setDispatcher` and `setDepRegistry` return a release that clears the slot only
|
|
140
|
+
while the caller still holds it, so a departing canvas can no longer disable a
|
|
141
|
+
surviving one. A second dispatcher claiming an occupied registry warns once,
|
|
142
|
+
naming `isolate` as the fix.
|
|
143
|
+
|
|
144
|
+
Two canvases still cannot *share* one registry: a toolbar outside both has
|
|
145
|
+
nothing to say which one it drives. That needs a focused-canvas concept and is
|
|
146
|
+
not in this change.
|
|
147
|
+
- 36b6ee7: Add `@weasel-js/cursor` and give three tools real cursors.
|
|
148
|
+
|
|
149
|
+
A cursor glyph is SVG path `d` strings tagged with a paint role plus a hotspot
|
|
150
|
+
in glyph units — the one geometry form both a data-URI baker and a `Path2D`
|
|
151
|
+
painter consume without translating. `bakeCursor` renders one to a
|
|
152
|
+
`url(data:image/svg+xml,…)` string with the hotspot scaled to integer CSS px,
|
|
153
|
+
and `cursorFor` memoizes that per name and size. The pencil, pen and eyedropper
|
|
154
|
+
tools now show their own glyph instead of a shared `crosshair`.
|
|
155
|
+
|
|
156
|
+
Cursors ship as SVG with no bitmap fallback: Chrome rasterizes an SVG data-URI
|
|
157
|
+
cursor at device scale, so it is already crisp on a retina display. `bakeCursor`
|
|
158
|
+
throws above 128 CSS px rather than emitting a cursor the browser would drop
|
|
159
|
+
silently — that size is where a later painted tier will take over.
|
|
160
|
+
|
|
161
|
+
Glyph geometry is authored in `scripts/glyphs/` and generated to resolved
|
|
162
|
+
literals by `npm run gen:cursors`; `npm run proof:cursors` renders the baked
|
|
163
|
+
assets over three backgrounds for inspection.
|
|
164
|
+
|
|
165
|
+
New API: `bakeCursor`, `cursorFor`, `GLYPHS`, `haloFitsInBox`, `CursorGlyph`,
|
|
166
|
+
`CursorPath`, `CursorGlyphName`, `BakeOptions`, and the register constants
|
|
167
|
+
`CURSOR_INK`, `CURSOR_HALO`, `CURSOR_HALO_WIDTH`, `CURSOR_MAX_CSS_PX`.
|
|
168
|
+
- 7a0c568: Tell an event handler how late its crossing is
|
|
169
|
+
|
|
170
|
+
`EventTrack`'s `fire` took no arguments, so a handler could only ask its own
|
|
171
|
+
clock for "now" — when the frame was processed, not when the playhead crossed
|
|
172
|
+
the edge. That held footstep scheduling in the side-scroller at frame
|
|
173
|
+
resolution against an audio engine built for sample resolution: a measured peak
|
|
174
|
+
spread of 33–47 ms on the looping run cycle.
|
|
175
|
+
|
|
176
|
+
`fire(lateBy)` reports the gap between the crossing and the frame carrying it,
|
|
177
|
+
in timeline ms. It is never negative, including on the loop seam, where the
|
|
178
|
+
outgoing lap's tail fires after the playhead has already wrapped — the case
|
|
179
|
+
that makes a handler comparing against `handle.time()` read a negative
|
|
180
|
+
lateness. A nested timeline's events report the same figure as a top-level
|
|
181
|
+
one's; the track's offset cancels.
|
|
182
|
+
|
|
183
|
+
Nothing has to change to compile: a zero-argument function is assignable to the
|
|
184
|
+
new signature.
|
|
185
|
+
|
|
186
|
+
`lateBy` is a delta, not a clock reading, so events from two different
|
|
187
|
+
timelines still cannot be ordered against each other. That would need the
|
|
188
|
+
animator's virtual clock made public, which this does not do.
|
|
189
|
+
|
|
190
|
+
`SideScrollerDemo` now places each footfall a fixed budget after its true
|
|
191
|
+
crossing, so which frame happened to notice a contact turns into constant
|
|
192
|
+
latency rather than audible spread.
|
|
193
|
+
- a7fa697: Add an anchored-placement solver and keep HUD windows on their host.
|
|
194
|
+
|
|
195
|
+
`@weasel-js/geom` gains `placeRect` and `clampRectWithin`. `placeRect` resolves an
|
|
196
|
+
overlay against an anchor: it picks a side, flips to the opposite one when the
|
|
197
|
+
preferred side has no room, and slides along the alignment axis to stay inside a
|
|
198
|
+
boundary. `clampRectWithin` is the containment half on its own — move a rect the
|
|
199
|
+
shortest distance that puts it inside a boundary, keeping its size. Both are pure
|
|
200
|
+
and take an explicit boundary rect, so a boundary that does not start at the
|
|
201
|
+
origin resolves correctly.
|
|
202
|
+
|
|
203
|
+
A HUD window could previously be dragged fully off its host with no way to
|
|
204
|
+
recover it: `createWindow` clamped size but never position. Move drags and
|
|
205
|
+
`setBounds` now keep the window on the host. Resize drags are deliberately left
|
|
206
|
+
alone, so pulling an edge past the host does not fight the gesture.
|
|
207
|
+
|
|
208
|
+
`@weasel-js/core` gains `hostAnchorRect`, `hostAnchorCss` and `useHostAnchor`,
|
|
209
|
+
which hold a fixed-position panel against a host element's corner and keep it
|
|
210
|
+
inside the viewport. The corner is an alignment per axis rather than a fixed
|
|
211
|
+
one, and `useHostAnchor` takes a function that resolves the host, so a host held
|
|
212
|
+
in a ref and one found by selector work the same way.
|
|
213
|
+
|
|
214
|
+
`hostAnchorCss` pins whichever edges the alignment names. That is not cosmetic:
|
|
215
|
+
a panel whose width tracks its content holds the anchored edge still and grows
|
|
216
|
+
away from it, so pinning the wrong edge makes the anchored corner drift on every
|
|
217
|
+
content change.
|
|
218
|
+
|
|
219
|
+
Four places were carrying their own copy of that anchor math and now share this
|
|
220
|
+
one — `CursorCoordsHud`, `PickHud`, `ModalityHud`, and WeaselDraw's
|
|
221
|
+
`DispatchTracePanel`, which anchors the opposite corner. None of the four
|
|
222
|
+
clamped, so a panel could hang off the edge when the host was scrolled or the
|
|
223
|
+
panel was tall.
|
|
224
|
+
- 2272682: `createParallaxLayer` takes an optional `getOuterView`, so a plane can derive
|
|
225
|
+
from a ref-driven camera. It previously derived only from the canvas's `view`
|
|
226
|
+
prop; a consumer keeping a 60 Hz camera out of React state pins that prop to
|
|
227
|
+
identity and got identity back for every `pan` value — a backdrop that silently
|
|
228
|
+
never moved.
|
|
229
|
+
|
|
230
|
+
`useHandTool` no longer builds a velocity tracker and a decay loop it never
|
|
231
|
+
uses. `inertia` and `axis` were already inert; they are now documented as such
|
|
232
|
+
until the `viewport.dragPan` action implements them.
|
|
233
|
+
- 503b56d: Fix two path-walker bugs that produced wrong geometry with no error.
|
|
234
|
+
|
|
235
|
+
`tessellate` treated `Z` as a no-op, so a command following a close flattened
|
|
236
|
+
from the last point drawn rather than from the subpath start — SVG puts the pen
|
|
237
|
+
back at the start. `pathDistanceToPoint` dispatched through an `if`/`else if`
|
|
238
|
+
chain with no final `else`, so an unrecognized command code left the coordinate
|
|
239
|
+
cursor unadvanced and silently misaligned every later read; it now throws.
|
|
240
|
+
- ac2deea: Add `polylineFromPoints` — the open counterpart to `polygonFromPoints`.
|
|
241
|
+
|
|
242
|
+
Same geometry, without the closing edge. A freehand stroke or a measurement
|
|
243
|
+
line wants this; a region wants the closed one. The pencil tool's drag preview
|
|
244
|
+
was building its ghost with `polygonFromPoints`, so the edge from the newest
|
|
245
|
+
sample back to the first swept across the drawing as the stroke grew and read
|
|
246
|
+
as a marquee.
|
|
247
|
+
- 23ffb2f: `WeaselRenderer` can draw into a rect of a buffer it does not own.
|
|
248
|
+
`setTarget({ origin, clear })` applies a viewport and scissor inside `render()`,
|
|
249
|
+
so N renderers can share one WebGL context and one canvas without a frame clear
|
|
250
|
+
erasing a co-tenant. The rect's size is the renderer's own `width`/`height`, so
|
|
251
|
+
`resize()` remains the single source of it.
|
|
252
|
+
|
|
253
|
+
Adds API. Two behaviour changes for existing callers: `render()` now
|
|
254
|
+
re-establishes blend, depth, cull and clear colour every frame instead of once at
|
|
255
|
+
construction, so a co-tenant moving that state no longer corrupts weasel's
|
|
256
|
+
frames; and the constructor now throws when handed a WebGL2 context whose
|
|
257
|
+
attributes report no stencil buffer, which previously rendered clips and even-odd
|
|
258
|
+
fills wrong rather than failing. A context that cannot report its attributes is
|
|
259
|
+
unaffected.
|
|
260
|
+
- 016851c: Stop a stroke with no paint from blanking the whole document.
|
|
261
|
+
|
|
262
|
+
`SelectionPanel`'s object leaf started from `{}` when the node held no value
|
|
263
|
+
yet, so editing any non-paint field of `data.stroke` on an unstroked node
|
|
264
|
+
committed that field alone — a `Stroke` with no `paint`, which the type
|
|
265
|
+
forbids. The leaf's declared `default` was dead for writes; it now seeds from
|
|
266
|
+
it, so writing one field materializes a complete value.
|
|
267
|
+
|
|
268
|
+
Such a stroke threw out of `fillInPoseFrame`, and the throw escaped the painter
|
|
269
|
+
and took the frame with it: the document page and every other node vanished,
|
|
270
|
+
and the canvas stayed stale until something unrelated requested a redraw — so
|
|
271
|
+
WeaselDraw opened on an empty workspace and only drew once the pointer moved.
|
|
272
|
+
`resolveNodeStroke` now reads a paintless stroke as no stroke, and the text
|
|
273
|
+
painter routes through it like every other painter. The frame loop no longer
|
|
274
|
+
loses its dirty flag when a paint throws, so one bad frame is retried rather
|
|
275
|
+
than stranding the surface.
|
|
276
|
+
- c9dd37f: Render text decorations as a toggle row, and ship a builtin font-family control
|
|
277
|
+
|
|
278
|
+
`SelectionPanel` rendered every boolean leaf as a `Switch`, ignoring the leaf's
|
|
279
|
+
`control` entirely — so the three text decorations arrived as three switch rows
|
|
280
|
+
where every text editor puts one row of U / S / O. `ToolPrefBooleanControl` now
|
|
281
|
+
accepts `'toggle'`, `ToolPrefBoolean` carries a `short` label for it (the pair
|
|
282
|
+
takes the row's name, leaving the leaf only a glyph's worth of room), and the
|
|
283
|
+
panel honors both. Core's text schema asks for it: `underline`,
|
|
284
|
+
`strikethrough` and `overline` share a `Decoration` pair.
|
|
285
|
+
|
|
286
|
+
A run of adjacent leaves sharing a `pair` renders as one `ToggleBar`, not one
|
|
287
|
+
bar per leaf — the same segmented control the `Align` row beside it already
|
|
288
|
+
draws. Each segment still writes only its own leaf, so flipping one decoration
|
|
289
|
+
never invents values for the other two. An unset toggle is left unselected
|
|
290
|
+
rather than dimmed: unselected is what a toggle button's off state means, and
|
|
291
|
+
the dimming the `Switch` path uses for the same case reads as disabled on one.
|
|
292
|
+
A leaf a consumer claims with its own `renderers` entry drops out of the run.
|
|
293
|
+
|
|
294
|
+
`FontFamilySelect` moves from WeaselDraw into `@weasel-js/ui`, and
|
|
295
|
+
`SelectionPanel` reaches for it on a `font-family` leaf. Core's own default
|
|
296
|
+
text schema declares that kind, so a consumer passing no `renderers` — the
|
|
297
|
+
Storybook story, any app taking the defaults — got the literal
|
|
298
|
+
`(font-family: no renderer)` placeholder where the font picker belongs. The
|
|
299
|
+
control offers both tiers that can actually paint and probes substitution at
|
|
300
|
+
the node's own weight and style, so its label names the variant that will
|
|
301
|
+
render. `@weasel-js/ui` now depends on `@weasel-js/font`.
|
|
302
|
+
- 9a000ea: A stroked text node now gets hit reach from its stroke. `TEXT_PAINTER` declared
|
|
303
|
+
no `ink`, so picking fell back to a zero-outset default and a heavily outlined
|
|
304
|
+
glyph was unpickable across the width of its own outline.
|
|
305
|
+
|
|
306
|
+
`kit:derived` also now evaluates ahead of `kit:path` / `kit:shape` / `kit:image`.
|
|
307
|
+
A derived node whose `data` happens to carry a `path`, `shape` or `image` field
|
|
308
|
+
was silently painted by those painters instead of from its derived path.
|
|
309
|
+
- 016851c: Add an editor surface for superscript, subscript and overline.
|
|
310
|
+
|
|
311
|
+
`StyledRun.script`, `baselineShift`, `fontScale` and `overline` reached layout,
|
|
312
|
+
SVG and the DOM overlay but nothing could apply them. The character bar now
|
|
313
|
+
carries an x² / x₂ pair, an overline toggle beside B / I / U / S, and the two
|
|
314
|
+
primitives `script` presets — baseline shift and scale — as percentage fields
|
|
315
|
+
that show what the preset supplies and override just that half when typed over.
|
|
316
|
+
`overline` also joins the sidebar's node-level Character group. Superscript and
|
|
317
|
+
subscript take Cmd+Shift+= and Cmd+Shift+-; the unshifted pair is browser zoom,
|
|
318
|
+
which a page cannot cancel.
|
|
319
|
+
|
|
320
|
+
A styling written at a collapsed caret now arms `useTextEdit`'s new
|
|
321
|
+
`pendingStyle` and applies to the next character typed, instead of being
|
|
322
|
+
dropped or restyling the whole node. That is what `script` needs — it has no
|
|
323
|
+
node-level counterpart to write to by design — and it makes the bar agree with
|
|
324
|
+
Cmd+B, which already behaved this way. `rangeStyle` reports the styling *at* a
|
|
325
|
+
collapsed caret rather than `{}`, and `toggleStyle` is public.
|
|
326
|
+
|
|
327
|
+
Three fixes fall out of putting both paths through one implementation:
|
|
328
|
+
lowering a flag the node sets now works from the bar and from a collapsed
|
|
329
|
+
caret, not only from the keyboard over a range; a toggle reads the node's flags
|
|
330
|
+
as well as the runs, so Cmd+B inside a `fontWeight: 700` node clears bold
|
|
331
|
+
instead of adding it; and focus returns to the text after a styling control is
|
|
332
|
+
clicked, so typing continues in the document rather than reaching the app as
|
|
333
|
+
tool shortcuts.
|
|
334
|
+
- 8ddec11: Accept a named or cubic-bezier easing wherever a curve is taken, and let a
|
|
335
|
+
timeline's loop policy change after it is created.
|
|
336
|
+
|
|
337
|
+
`easing` was a bare function everywhere, which is fine to call and impossible to
|
|
338
|
+
name back, show in a picker, or serialize. It now also accepts the name of a
|
|
339
|
+
built-in (`'easeOutBack'`) or control points (`{ bezier: [0.4, 0, 0.2, 1] }`),
|
|
340
|
+
resolved by `resolveEasing` at the four places a curve is actually invoked. The
|
|
341
|
+
union is additive, so every existing function value stays assignable. Bezier x
|
|
342
|
+
control points are clamped to 0..1, which is what keeps the solve monotone, and
|
|
343
|
+
the control-point tuple is `readonly` so an `as const` preset is assignable.
|
|
344
|
+
|
|
345
|
+
`TimelineHandle.setLoop(loop)` sets policy and nothing else. A timeline already
|
|
346
|
+
parked at its duration does not restart — `rearm` declines to revive one — so
|
|
347
|
+
play it again by seeking to 0 and resuming. Restoring saved transport state
|
|
348
|
+
therefore cannot start playback as a side effect.
|
|
349
|
+
|
|
350
|
+
Both settings now read back. `AnimationHandle.timeScale()` returns an
|
|
351
|
+
animation's own scale, and `Animator.timeScale()` the global one, the way
|
|
352
|
+
`isPaused()` already pairs with `pause()`. `TimelineHandle.loop()` returns the
|
|
353
|
+
policy as it stands — `true`, `false`, or the laps a finite loop has left, which
|
|
354
|
+
falls as they are consumed. A transport UI can drive itself off the handle
|
|
355
|
+
instead of mirroring what it last wrote, which drifts as soon as anything else
|
|
356
|
+
holding the handle sets it.
|
|
357
|
+
- 28894b9: Fix the viewport primitives on an axis with negative scale.
|
|
358
|
+
|
|
359
|
+
`View.scale` is documented as pixels per world unit _per axis_, so `scale.y < 0`
|
|
360
|
+
is the ordinary way to spell a y-up camera. Two primitives did not read it that
|
|
361
|
+
way, and both failed silently rather than erroring.
|
|
362
|
+
|
|
363
|
+
`zoomAt` clamped the signed scale against positive bounds
|
|
364
|
+
(`min(max, max(min, scale * factor))`), so one wheel step on a y-up view
|
|
365
|
+
returned `scale.y = +0.1`: the axis flipped and the zoom collapsed to the
|
|
366
|
+
minimum. It now bounds the magnitude and restores the sign, so a clamp limits a
|
|
367
|
+
flipped axis instead of unflipping it.
|
|
368
|
+
|
|
369
|
+
`clampView` computed the visible world extent as `canvas.height / scale.y`,
|
|
370
|
+
which is negative on a flipped axis. That made the "is the view zoomed out past
|
|
371
|
+
the bounds" test never fire, and put the scroll interval on the wrong side of
|
|
372
|
+
the anchor — a y-up view could be panned outside its own bounds. It now takes
|
|
373
|
+
the extent as a magnitude and anchors the interval at the rect's far edge when
|
|
374
|
+
the axis is flipped.
|
|
375
|
+
|
|
376
|
+
Found while giving labkit's instrument canvas a declarable coordinate system:
|
|
377
|
+
routing its wheel through `zoomAt` looked like the obvious way to stop
|
|
378
|
+
reimplementing fixed-point zoom, and would have been a bug.
|
|
379
|
+
- c4ccd0a: Zoom now has one clamp. `DEFAULT_MIN_ZOOM` / `DEFAULT_MAX_ZOOM` are exported from
|
|
380
|
+
`@weasel-js/core` and every zoom path defaults from them — `zoomAt`, the
|
|
381
|
+
`viewport.zoom` and pinch actions, `usePinchZoomTool`, `fitViewToBounds`,
|
|
382
|
+
`computeWheelAction` and `useZoom`.
|
|
383
|
+
|
|
384
|
+
**Behavior change:** the three paths that carried the second, undocumented pair
|
|
385
|
+
now cap at 8x rather than 10x. `fitViewToBounds` could previously land at 10x and
|
|
386
|
+
the next pinch frame would clamp it straight back to 8x. Pass an explicit
|
|
387
|
+
`maxScale` / `max` to keep 10x.
|
|
388
|
+
- Updated dependencies [04ea2e8]
|
|
389
|
+
- Updated dependencies [b656ebf]
|
|
390
|
+
- Updated dependencies [36b6ee7]
|
|
391
|
+
- Updated dependencies [a7fa697]
|
|
392
|
+
- @weasel-js/cursor@1.4.0
|
|
393
|
+
- @weasel-js/geom@1.4.0
|
|
394
|
+
- @weasel-js/text@1.4.0
|
|
395
|
+
- @weasel-js/font@1.4.0
|
|
396
|
+
- @weasel-js/gestures@1.4.0
|
|
397
|
+
- @weasel-js/history@1.4.0
|
|
398
|
+
- @weasel-js/modes@1.4.0
|
|
399
|
+
- @weasel-js/paint@1.4.0
|
|
400
|
+
|
|
401
|
+
## 1.4.0-pre.1
|
|
402
|
+
|
|
403
|
+
### Patch Changes
|
|
404
|
+
|
|
405
|
+
- 36b6ee7: Add `@weasel-js/cursor` and give three tools real cursors.
|
|
406
|
+
|
|
407
|
+
A cursor glyph is SVG path `d` strings tagged with a paint role plus a hotspot
|
|
408
|
+
in glyph units — the one geometry form both a data-URI baker and a `Path2D`
|
|
409
|
+
painter consume without translating. `bakeCursor` renders one to a
|
|
410
|
+
`url(data:image/svg+xml,…)` string with the hotspot scaled to integer CSS px,
|
|
411
|
+
and `cursorFor` memoizes that per name and size. The pencil, pen and eyedropper
|
|
412
|
+
tools now show their own glyph instead of a shared `crosshair`.
|
|
413
|
+
|
|
414
|
+
Cursors ship as SVG with no bitmap fallback: Chrome rasterizes an SVG data-URI
|
|
415
|
+
cursor at device scale, so it is already crisp on a retina display. `bakeCursor`
|
|
416
|
+
throws above 128 CSS px rather than emitting a cursor the browser would drop
|
|
417
|
+
silently — that size is where a later painted tier will take over.
|
|
418
|
+
|
|
419
|
+
Glyph geometry is authored in `scripts/glyphs/` and generated to resolved
|
|
420
|
+
literals by `npm run gen:cursors`; `npm run proof:cursors` renders the baked
|
|
421
|
+
assets over three backgrounds for inspection.
|
|
422
|
+
|
|
423
|
+
New API: `bakeCursor`, `cursorFor`, `GLYPHS`, `haloFitsInBox`, `CursorGlyph`,
|
|
424
|
+
`CursorPath`, `CursorGlyphName`, `BakeOptions`, and the register constants
|
|
425
|
+
`CURSOR_INK`, `CURSOR_HALO`, `CURSOR_HALO_WIDTH`, `CURSOR_MAX_CSS_PX`.
|
|
426
|
+
- Updated dependencies [36b6ee7]
|
|
427
|
+
- @weasel-js/cursor@1.4.0-pre.1
|
|
428
|
+
- @weasel-js/font@1.4.0-pre.1
|
|
429
|
+
- @weasel-js/geom@1.4.0-pre.1
|
|
430
|
+
- @weasel-js/gestures@1.4.0-pre.1
|
|
431
|
+
- @weasel-js/history@1.4.0-pre.1
|
|
432
|
+
- @weasel-js/modes@1.4.0-pre.1
|
|
433
|
+
- @weasel-js/paint@1.4.0-pre.1
|
|
434
|
+
- @weasel-js/text@1.4.0-pre.1
|
|
435
|
+
|
|
3
436
|
## 1.4.0-pre.0
|
|
4
437
|
|
|
5
438
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -70,28 +70,32 @@ Core doesn't ship a prebuilt atlas — bake one with `npm run gen:font -- <font.
|
|
|
70
70
|
|
|
71
71
|
## Actions registry
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
An `Action` is a named operation — `delete`, `duplicate`, `group`, `insert`, `viewport.dragPan` — paired with the input that triggers it. `<ActionsProvider>` holds the registered descriptors, and the gesture dispatcher matches live input against each one's `defaultBinding`. Keystrokes and pointer gestures take the same path, so a keyboard shortcut and a drag are two bindings on one action rather than two mechanisms.
|
|
74
|
+
|
|
75
|
+
`<SceneCanvas>` auto-mounts a provider when none is above it and registers the kit-standard descriptors, derived from the scene, selection, view and history it already owns.
|
|
74
76
|
|
|
75
77
|
```tsx
|
|
76
|
-
import {
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
import { SceneCanvas } from '@weasel-js/core';
|
|
79
|
+
|
|
80
|
+
<SceneCanvas
|
|
81
|
+
scene={scene}
|
|
82
|
+
selection={selection}
|
|
83
|
+
actions={{
|
|
84
|
+
duplicate: null, // drop the default
|
|
85
|
+
'app.publish': { // add your own
|
|
86
|
+
id: 'app.publish',
|
|
87
|
+
label: 'Publish',
|
|
88
|
+
defaultBinding: { kind: 'key', key: 'p', mods: { mod: true } },
|
|
89
|
+
requires: ['selection'],
|
|
90
|
+
invoker: { timing: 'immediate', run: ({ selection }) => publish(selection.get()) },
|
|
91
|
+
},
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
92
94
|
```
|
|
93
95
|
|
|
94
|
-
The `actions` prop
|
|
96
|
+
The `actions` prop takes `null` to unregister every default, or a record keyed by action id. Each value is `null` to drop that one id, a partial `Action` to merge onto the default of the same id, or a complete `Action` to register a new one.
|
|
97
|
+
|
|
98
|
+
An action does its work through `invoker`, not a bare callback. `{ timing: 'immediate' }` runs once; `{ timing: 'ongoing' }` returns a handle so a drag can preview while it moves and commit at the end. The deps an invoker reads (`selection`, `scene`, `applyOps`, …) are declared in `requires` and resolved at invocation time, which is what lets a consumer swap one — see `useDepSource`.
|
|
95
99
|
|
|
96
100
|
## Custom shaders (`@experimental`)
|
|
97
101
|
|