@weasel-js/core 1.1.0 → 1.2.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 CHANGED
@@ -1,5 +1,485 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 53016f7: Add `actionShortcuts(action)` — an action's keyboard bindings, flattened into
8
+ the shape `formatShortcut` / `formatShortcutParts` render.
9
+
10
+ Binding lists are written for a matcher, not a reader, so two things collapse:
11
+ a spec's `key` may list spellings of one keycap (`['[', '{']` — the shifted
12
+ bracket reports as `'{'`), and a modifier declared `'optional'` matches held or
13
+ unheld, so it isn't part of what anyone presses. Non-keyboard bindings have no
14
+ chip form and are skipped.
15
+
16
+ Every keyboard binding is returned, in declaration order — an action can answer
17
+ to several (`reorder.forward` has three) and nothing marks one canonical.
18
+
19
+ WeaselDraw's command palette shows its shortcut chip again. It had been
20
+ suppressed since `Action.defaultBinding: KeyBinding` was removed, pending a
21
+ formatter for the replacement shape.
22
+
23
+ - e25e77b: `<Canvas>` accepts `layerVisibility` and `layerOrder`.
24
+
25
+ `drawLayers` has resolved layer visibility and draw order since it was written,
26
+ but `<Canvas>` passed it `{}` and `undefined`, so the only way to control either
27
+ was a layer's own `defaultVisible`. Both are now props: `layerVisibility` maps
28
+ layer id to shown, falling back to `defaultVisible` for ids it omits and ignored
29
+ entirely by `alwaysOn` layers; `layerOrder` lists ids bottom-first, and any
30
+ layer it omits is not drawn.
31
+
32
+ Hiding a layer also stops it claiming pointer events through `hitTestExtras`,
33
+ which previously walked every registered layer regardless. Draw and hit-test
34
+ resolve visibility through one exported `isLayerVisible`, so a layer nobody can
35
+ see cannot swallow a click.
36
+
37
+ - 8e00c13: Give `<CanvasView>` a selection of its own.
38
+
39
+ `selection` and `selectionOptions` on a view mirror the props of the same name
40
+ on `<SceneCanvas>`: supply a `SelectionApi` to control one, or pass
41
+ `selectionOptions` to have the view build its own. Either goes into the view's
42
+ dep overlay, so an action dispatched inside that view reads and writes the
43
+ view's selection and leaves the surface's alone. Pass neither and the view
44
+ shares the surface's selection.
45
+
46
+ Nothing paints it yet — a view's chrome is still drawn from the surface's
47
+ selection.
48
+
49
+ - c91e186: Add `<CanvasView>`: a second camera over a rect of an existing canvas, drawn
50
+ through the same GL context, with input routed to it.
51
+
52
+ Declare one through `<SceneCanvas views={[...]}>`, or mount the component as a
53
+ child — the same declaration either way, with children landing after every prop
54
+ entry in paint and hit order. A view paints the surface's own layer stack
55
+ through its camera, or a narrowed slice of it via `layers`, and owns its camera
56
+ in the same hybrid controlled/uncontrolled mode `<Canvas>` uses. Wheel and drag
57
+ inside its rect move that camera rather than the canvas's, and a gesture that
58
+ wanders out of the rect stays with the camera it began under.
59
+
60
+ Two supporting changes: `createViewportLayer`'s `source` accepts a thunk, so a
61
+ viewport can paint a stack assembled elsewhere rather than one closed over at
62
+ construction; and `<SceneCanvas>` mounts a view registry, which is how a view
63
+ and its surface find each other.
64
+
65
+ A canvas with no views declared is unchanged — no registry entries, so every
66
+ point resolves to the canvas as before.
67
+
68
+ Not yet per-view: selection and chrome, affordance hit-testing, and pinch-zoom.
69
+ A gesture inside a view reaches the ambient viewport actions and nothing else.
70
+
71
+ - cada4da: Answer chrome bounds and layer-helper bounds with one function.
72
+
73
+ `buildChromeState`'s `effectiveBoundsOf` and `CanvasHelpers.getEffectiveBounds`
74
+ each spelled out the same cascade — the active tool's published preview, then
75
+ the dispatcher's preview extras, then committed bounds — in two places that had
76
+ to agree for a resize handle to sit on the shape it belongs to. They now share
77
+ `boundsWithPreview`.
78
+
79
+ The helpers copy carried an extra committed-pose fallback for when no bounds
80
+ resolver is wired. That branch was unreachable: a missing resolver means no
81
+ `boundsOf` prop and no adapter, and without an adapter the pose lookup returns
82
+ `null` too. Behavior is unchanged.
83
+
84
+ - 889b1d0: Give each of `useGestureDispatcher`'s view records its own camera.
85
+
86
+ A record may now carry a `ViewApi`. An event routed to it dispatches against
87
+ the canvas dep registry with the `view` dep — and only that dep — replaced by
88
+ the record's, so `viewport.dragPan` and the rest of the viewport actions move
89
+ the view the gesture began in rather than the whole canvas. No second `setView`
90
+ channel was needed: every viewport action already reads its camera from that
91
+ dep.
92
+
93
+ This is what makes routing correct rather than merely wired. Records without a
94
+ `ViewApi`, which is every record today, resolve `view` exactly as before.
95
+
96
+ - 9e6927a: Route each input event to the view it landed in, inside `useGestureDispatcher`.
97
+
98
+ The hook takes an optional `views` — a thunk returning the non-root dispatch
99
+ records and a resolver that names one for a client point. `createViewResolver`
100
+ satisfies the resolver shape, so a canvas holding several viewports can hand
101
+ the two straight over. Pointerdown pins its pointer to the view it began in and
102
+ pointerup releases it, so a drag that leaves a panel keeps reporting
103
+ coordinates in the camera it started under. Keyboard and paste carry no
104
+ coordinates and run on the view the last coordinate-bearing event resolved to.
105
+ A resolved id with no live record falls back to the root, so a view that
106
+ unmounts mid-gesture degrades rather than dropping the event.
107
+
108
+ No public change, and no change at all with `views` omitted: every event then
109
+ runs on the record the flat options describe. Cancelling in-flight gestures —
110
+ on unmount and on tool change — now reaches every view's dispatcher rather than
111
+ only the root's.
112
+
113
+ - eafe4be: Resolve the dispatcher and its coordinate lookups per event inside
114
+ `useGestureDispatcher`.
115
+
116
+ The hook took `dispatcher`, `affordanceAt`, `classifyTarget` and `clientToWorld`
117
+ as four sibling options and read each through its own ref, and it bound the
118
+ dispatcher once when the listener effect ran. Those four are one thing —
119
+ everything about handling an event that depends on which view it landed in — so
120
+ they are now one internal record, read fresh on each event.
121
+
122
+ No public change: the four options stay exactly as they are and become that
123
+ record. They are the single-view façade, the same way `SceneCanvasProps` is.
124
+
125
+ This is groundwork for routing input to one of several views. Doing it this way
126
+ means the hook keeps mounting once: a canvas with N views gets N dispatchers
127
+ behind one listener set, rather than N copies of the hook all firing on every
128
+ event.
129
+
130
+ - ae84ca1: Move one view's overlay-aware state into a `useViewHelpers` hook.
131
+
132
+ `<Canvas>` built its chrome state and its layer helpers inline: the bounds
133
+ fallbacks, the committed-pose lookup, the tool preview cascade, `buildChromeState`
134
+ and the `CanvasViewHelpers` object were about 120 lines of the component body
135
+ closing over its props. They are now one hook taking explicit dependencies —
136
+ adapter, geometry, bounds resolver, selection, tools, gesture source and the
137
+ dispatcher's preview extras — and `<Canvas>` calls it for its own view.
138
+
139
+ Being a hook is the point. A canvas hosting several viewports cannot loop this
140
+ work inside one component, but N components can each call it once, which is what
141
+ per-view selection and chrome will be built from.
142
+
143
+ `CanvasHelpers`, `CanvasViewHelpers` and `CanvasSurfaceHelpers` moved to the new
144
+ module and are still re-exported from `./canvas/Canvas` and the package root, so
145
+ imports are unchanged. The hook takes only the `getPose` slice of the adapter
146
+ rather than the full contract.
147
+
148
+ - 0514a37: `hitTestExtras` takes an optional `frame` naming the camera to test under.
149
+
150
+ The method read the canvas's own `view` and `dims` off refs, which is right for
151
+ every existing caller and wrong for a point routed to a viewport node: there the
152
+ world point is in the node's inner view and a layer resolving a screen-pixel
153
+ tolerance needs that view and the node's rect size. `hitTestExtras(x, y, { view,
154
+ dims })` supplies both; omitting `frame` keeps the previous behavior, so no
155
+ existing call site changes.
156
+
157
+ - daa5ce6: Add a source rect and flip to `ImageDrawCommand`, so one bitmap can be drawn as
158
+ many frames.
159
+
160
+ `source` is a sub-rectangle in bitmap pixels; `flipX` / `flipY` mirror the
161
+ sampled region within the destination rect without moving the quad. Both are
162
+ additive and optional — a command that sets neither draws exactly as before.
163
+ Until now a sprite sheet needed a custom `ShaderDrawCommand` to do what is
164
+ arithmetic on the quad's four UV pairs.
165
+
166
+ `source` is not range-checked: a rect past the bitmap edge samples outside
167
+ `[0..1]`, which `CLAMP_TO_EDGE` smears. With `sampling: 'linear'` the filter
168
+ also reaches half a texel beyond `source`, so an atlas whose frames touch will
169
+ bleed at the seams — pad frames with a gutter or use `'nearest'`. The renderer
170
+ deliberately does not inset for this, which would make an exact 1:1 blit soft.
171
+
172
+ New `frameRect(sheet, index)` and the `SpriteSheet` type turn a uniform grid
173
+ (`frameWidth`, `frameHeight`, `columns`, optional `margin` and `spacing`,
174
+ following the Tiled / Aseprite convention) into that source rect. It is
175
+ row-major from 0 and does not wrap past the last cell — wrapping belongs to the
176
+ animation, since a sheet does not know how many of its cells are filled.
177
+
178
+ `@weasel-js/hud`'s image widget takes `source`, `flipX` and `flipY` as options
179
+ and gains `setSource` and `setFlip` to change them in place. `setFlip` merges,
180
+ leaving an omitted axis alone. Without the setters a sprite animation would
181
+ have to dispose and rebuild the widget every frame.
182
+
183
+ - 144e70a: Export `keySpecShortcut(spec)` — the chip form of one gesture spec, or
184
+ `undefined` where a spec has none — and `actionBindings(action)`, the flat
185
+ binding list `actionShortcuts` already reads.
186
+
187
+ `actionShortcuts` is now a dedupe over `keySpecShortcut`, so the spec-to-chip
188
+ mapping has one implementation. It had two: ToolkitBuilder's binding table
189
+ projected key specs inline, and rendered a `'optional'` modifier as a keycap
190
+ the reader has to press. Surfaces that render drag, click and wheel specs
191
+ alongside keyboard ones can now share the keyboard half without taking
192
+ `actionShortcuts`' action-at-a-time shape.
193
+
194
+ - 2627cde: Fix a hook-order defect in the Badge effects and several stale-closure bugs,
195
+ found by turning on a correctness lint baseline.
196
+
197
+ Six Badge effects (`Aqua`, `Bevel`, `Bevel2`, `Metal`, `Sheen`, `Woodgrain`)
198
+ called `useId` after an early return keyed on `variant`. Changing a `<Badge>`'s
199
+ variant to one those effects don't render, and back, remounted the component
200
+ and issued fresh ids — so the `<clipPath>` and gradient ids their `url(#…)`
201
+ references point at changed identity mid-life.
202
+
203
+ Also fixed: `Canvas.tsx`'s paint effect read a stale `helpersForLayers` through
204
+ its closure rather than the ref the file maintains, and `useDeviceProfile`
205
+ ignored a `targetScale` supplied by a provider.
206
+
207
+ `composeOrderedLayers` is now generic over the `LayersMap` it receives instead
208
+ of taking `any`; inference at existing call sites is unchanged.
209
+
210
+ - 8b583b4: Default an unset miter limit to 4, not 10.
211
+
212
+ A stroke that sets no `miterLimit` used Canvas2D's 10. The SVG serializer omits
213
+ the attribute for an unset field, so the same stroke exported and opened
214
+ anywhere else renders at SVG's default of 4 — the kit disagreed with its own
215
+ export format, and re-importing the file did not reconcile them.
216
+
217
+ 10 also lets an acute corner throw a miter spike four times the half-width. A
218
+ stroked capital W put one in the middle of the letter, from the apex of a V
219
+ most of the way down: measured at 7.99 units out on a half-width of 2, and
220
+ inside the letterform where a bounding box never sees it. Glyph outlines are
221
+ where this shows first because a type designer's sharpest vertices were never
222
+ drawn to be stroked.
223
+
224
+ Strokes that set `miterLimit` explicitly are unaffected. Anything relying on
225
+ the old default can set `miterLimit: 10`. No visual baseline moved: nothing in
226
+ the demo set strokes a corner sharp enough to have been spiking.
227
+
228
+ - e61d3e3: Resolve selection chrome's bounds through one cascade, not two.
229
+
230
+ `createSelectionOverlayLayer`'s `getPose` is now optional alongside
231
+ `getSelection`. Omitted, the layer takes bounds from the `ChromeState` on the
232
+ draw envelope — the cascade its selection was already built with. `<Canvas>`
233
+ and `<SceneCanvas>` each carried a `poseById` chain of their own for this;
234
+ both are gone, and a consumer's `poseById` override still wins where it is set.
235
+
236
+ The two chains were supposed to agree and did not: they consulted the same
237
+ preview sources in opposite priority, and only one of them carried rotation
238
+ through. With one camera the disagreement was hard to see; per-view chrome
239
+ would have made it visible.
240
+
241
+ - f0cc29c: Hit-test affordances against the chrome state that was painted.
242
+
243
+ The gesture dispatcher's `affordanceAt` built a `ChromeState` of its own —
244
+ selection off a ref, bounds straight from the resolver, its own union AABB —
245
+ next to the one the canvas helpers had already built. The two differed by the
246
+ in-flight overlay: mid-drag, resize handles painted at the ghost while their
247
+ hit regions stayed at the committed pose.
248
+
249
+ A surface now publishes its view chrome on the handle it attaches to the view
250
+ registry, and the dispatcher reads it from there. `ChromeState` gains an
251
+ `EMPTY_CHROME_STATE` for the before-attach case, and `anchorStateFrom` is the
252
+ dep-registry read the mounter used to inline.
253
+
254
+ - 438970b: Draw selection chrome for the view that asked, not for the canvas.
255
+
256
+ `createSelectionOverlayLayer`'s `getSelection` is now optional. Omitted, the
257
+ layer takes its ids from the `ChromeState` on the draw envelope — the same
258
+ channel it already read the multi-selection union AABB from. `<Canvas>` and
259
+ `<SceneCanvas>` stop passing one, so the single overlay layer a surface builds
260
+ outlines whichever view is drawing it.
261
+
262
+ The multi-selection split is unchanged: the handle pass works against the
263
+ synthetic union id, the outline pass against the real members.
264
+
265
+ - f2ba2ab: Let a view answer any dep for itself, not just `view`.
266
+
267
+ A dispatcher view record carried a `ViewApi`; it now carries a thunked
268
+ `Partial<DepSchema>`, and an event routed to that view resolves every name in
269
+ it from the view, everything else from the canvas registry. `view` becomes one
270
+ entry rather than a special case, which is what per-view selection needs next.
271
+
272
+ This is also the answer to whether a view should get a `DepRegistryProvider` of
273
+ its own: it should not. The registry is where a consumer registers _sources_,
274
+ and one per view would fragment that — overriding `insert` would mean knowing
275
+ how many views exist and overriding each. An overlay keeps one place to
276
+ register and one authority per dep, with a view claiming only what is genuinely
277
+ its own.
278
+
279
+ - 4ac9273: Route pinch and hover to the view under the pointer.
280
+
281
+ Both attached to the canvas and targeted the outer camera, so a pinch inside a
282
+ panel zoomed the canvas beneath it and hover resolved the wrong node. The view
283
+ registry now owns one `ViewResolver` for the surface, and the dispatcher, pinch
284
+ and hover all ask it — one authority, so they cannot disagree about where a
285
+ point landed.
286
+
287
+ `usePinchZoomTool` takes a `resolveTarget` option naming the camera an anchor
288
+ belongs to, and measures the anchor from that camera's origin. Omitted, it is
289
+ the canvas's own, as before.
290
+
291
+ - 8570a23: Export `resolveParams` from the package entry
292
+
293
+ `BindingOpts.params` may be a thunk, and its own doc comment tells callers to
294
+ read it "via `resolveParams(opts?.params)`" — but that helper was defined and
295
+ used internally, never re-exported. Every consumer writing a parametric
296
+ `key-held` (or other) binding was stuck reimplementing the thunk check by
297
+ hand. `resolveParams` is now importable from `@weasel-js/core`.
298
+
299
+ - 7c202d2: Put the selection on the scene, and restore it on undo.
300
+
301
+ `scene.getSelection()` / `scene.setSelection()` own the transient set of active
302
+ ids. It is not document content — `toJSON` never carries it — but every history
303
+ entry now records the selection its edit was made under, so undo and redo put
304
+ back what was selected. Changing the selection is still never an undo step of
305
+ its own.
306
+
307
+ Undoing a boolean op used to leave the selection pointing at the result node
308
+ undo had just deleted; deleting a multi-selection and undoing left it empty.
309
+
310
+ `useSelection({ scene })` keeps the selection on the scene rather than in the
311
+ hook. `<SceneCanvas>` does that by default, so every view over one scene shares
312
+ a selection; a `<CanvasView>` opts out with `selection` / `selectionOptions`.
313
+
314
+ `@weasel-js/history` gains `CreateHistoryOptions.selection`, a get/set pair the
315
+ engine reads and writes on the way past — supply it and entries carry
316
+ `selectionBefore` / `selectionAfter`, omit it and the engine touches selection
317
+ never. `recordEntry` takes the pre-batch selection as an option, because by the
318
+ time it runs the live selection has already moved on.
319
+
320
+ `defaultCommitAdapter` carries `getSelection` / `setSelection` now, so
321
+ selection-carrying ops replay without splicing `SelectionApi.adapterMethods`
322
+ over it.
323
+
324
+ - c7b4705: Add a side-scroller demo that load-tests the animation timeline and the audio
325
+ engine. The player is an eleven-joint rig posed by cross-faded
326
+ `SampledTrack<Pose>` clips — the run cycle plays on a real `animator.timeline`
327
+ whose time scale tracks ground speed, while jump and fall are seeked by vertical
328
+ velocity rather than played. Footsteps fire from an `EventTrack` on that looping
329
+ timeline, which is the timeline-to-audio bridge under the heaviest load it will
330
+ see. Every sound is synthesized into an `AudioBuffer` at load, so the demo ships
331
+ no assets.
332
+
333
+ Its HUD is the point: frame time, active voice count, footstep timing spread and
334
+ a swarm control that pushes the voice pool past its limit, so the demo measures
335
+ the two arcs rather than merely exercising them.
336
+
337
+ Findings are recorded in `docs/TODO.md` under Animation. The load-bearing one:
338
+ `EventTrack` events are `{ t, fire: () => void }`, and `fire` receives no
339
+ arguments, so an audio handler cannot learn the playhead's crossing time and is
340
+ quantized to the animation frame instead of the audio clock.
341
+
342
+ - 6a5c047: Split `CanvasHelpers` into its per-view and per-surface halves.
343
+
344
+ `CanvasViewHelpers` is what one camera's own tools, gestures and selection
345
+ answer — `getEffectivePose`, `getEffectiveBounds`, `getGestureBounds`,
346
+ `subscribeGestures`, `getGestureVersion`, `getChromeState`.
347
+ `CanvasSurfaceHelpers` is what a GL context has one of — `getDebug`,
348
+ `getIsVisible`. `CanvasHelpers` extends both and is unchanged for layers, which
349
+ still receive the whole object as their `data`.
350
+
351
+ The two are now built separately inside `<Canvas>`, so which side a lookup
352
+ belongs on is a compile-time fact instead of a claim in a design doc. That is
353
+ the boundary a canvas hosting several viewports has to build N of one side and
354
+ one of the other across.
355
+
356
+ - 49e450c: Add `createViewResolver` — which view owns a client point, held steady for a
357
+ gesture.
358
+
359
+ A canvas with viewport nodes on it has more than one camera, and a pointer event
360
+ belongs to exactly one of them. The resolver hit-tests a list of
361
+ `ResolvableView`s (a camera plus the rect it paints into) in reverse paint
362
+ order, right and bottom edges exclusive, and falls back to the root view.
363
+
364
+ It pins a pointer on `begin` and releases it on `end`, so a drag that leaves its
365
+ view's rect — over a neighbour, or off the canvas — keeps reporting coordinates
366
+ in the space it started in. Without that, a marquee crossing a panel edge
367
+ silently starts measuring against the wrong camera. A pointer that began on the
368
+ root canvas is pinned to the root for the same reason. The pinned view is looked
369
+ up fresh each call, so a rect that moves mid-gesture is honored.
370
+
371
+ `ViewTarget.origin` is the resolved view's client-space origin, ready to pass
372
+ straight to `clientToWorld`. `ViewportLayer.resolvable(outer, dims)` supplies a
373
+ viewport node as a candidate.
374
+
375
+ Nothing is wired into the dispatcher yet: tools still target the outer view.
376
+
377
+ - 6031085: Stroke text at any size, not only above the outline threshold.
378
+
379
+ A glyph escalates from its SDF tier to tessellated outlines once it covers
380
+ `OUTLINE_MIN_SCREEN_PX` (48) on screen, and only the outline tier has geometry
381
+ to stroke. Text below that silently dropped its stroke: the control was live,
382
+ the paint never arrived, and the same text stroked correctly inside a magnifier
383
+ that happened to lift it over the threshold.
384
+
385
+ A run carrying a stroke now escalates at any size. The threshold still governs
386
+ unstroked text, where it is a choice between two correct renderings rather than
387
+ between a stroke and nothing. A zero-width stroke does not escalate, and an
388
+ explicit opt-out of the tier still wins — as does a run the tier cannot serve
389
+ (no registered outlines, or synthetic bold, whose emboldening is an SDF
390
+ threshold shift with no geometric equivalent).
391
+
392
+ - ccaaecd: `Stroke.width` accepts `{ px }` for a width in screen pixels, resolved against the accumulated transform scale at draw time. Callers previously divided by `meanScale(view.scale)` at each site; this moves that into the renderer and lets the stroke mesh cache key see the resolved width.
393
+ - ec0eb08: Rename the viewport `computeFitView` to `computeFitViewport`. It was unreachable from the package entry: an identically-named export from the minimap module shadowed it. This is a breaking rename of a symbol nobody could import.
394
+ - 726f85e: Make what a view paints hittable inside that view.
395
+
396
+ `<CanvasView>` registers an `affordanceAt` and a `classifyTarget` of its own,
397
+ built the same way the surface builds its pair but against this view's chrome
398
+ state and camera. A press inside a panel now lands on that panel's resize
399
+ handles, rotation band and path anchors, and a body under the point classifies
400
+ against the panel's selection — until now a gesture inside a panel reached only
401
+ the ambient viewport actions.
402
+
403
+ Externally registered layers keep first refusal on the point, hit-tested
404
+ against the view's frame and draw envelope rather than the canvas's.
405
+
406
+ The surface's context to its views widens to carry the hit-test half it is the
407
+ authority on: the pickers, the node-kind resolver and the chrome-caps
408
+ predicate.
409
+
410
+ - 601aa6b: Let a view build its own helpers and hand them to its layers.
411
+
412
+ `<CanvasView>` now calls `useViewHelpers` and passes the result through the
413
+ viewport node's `data` thunk: its source layers draw against this view's chrome
414
+ state, effective poses and gesture bounds, with the surface half of the
415
+ envelope — debug sink, chrome-caps predicate — passing through untouched.
416
+
417
+ The inputs that hook needs are surface-wide (adapter, geometry, bounds
418
+ resolver, tools, gesture source) and are read during a view's render, so
419
+ `<SceneCanvas>` publishes them as context rather than on the `SurfaceHandle`,
420
+ which is not attached until an effect runs.
421
+
422
+ Layers that read chrome off the draw envelope — affordance layers, the
423
+ selection overlay's multi-union — follow the view. The selection overlay's
424
+ per-id outline and handles still come from closures over the surface's
425
+ selection.
426
+
427
+ - 9607185: Read a view's gesture previews from the view's own dispatcher.
428
+
429
+ `<CanvasView>` builds its helpers with a `GestureSource` and preview extras
430
+ over the dispatcher it owns, rather than inheriting the surface's. A view has
431
+ had its own dispatcher since routing landed, so a gesture inside a panel put
432
+ its in-flight handles somewhere the panel's own chrome was not looking: no
433
+ ghost, no tracking resize handles, no gesture bounds.
434
+
435
+ The dispatcher's contribution to those lookups is now one factory
436
+ (`createDispatcherPreviewSources`) next to `createGestureSource`, instead of
437
+ two closures inlined in `<SceneCanvas>`. The context a surface publishes to its
438
+ views narrows to the scene-shaped half — adapter, geometry, bounds resolver,
439
+ tools.
440
+
441
+ - 58f43e7: Apply the inner view transform to a viewport node's source layers.
442
+
443
+ A world-space `RenderLayer` emits world coords and relies on its caller to wrap
444
+ them in `viewToMat3(view)`. `drawLayers` did that; `createViewportLayer` did
445
+ not — it concatenated `layer.draw(...)` output under a bare translate to the
446
+ rect origin. So a viewport's inner `view.x/y/scale` never reached the pixels,
447
+ while its `reproject` inverse assumed they had. Content drew at raw world
448
+ coords and hit-testing disagreed with what was on screen; at the identity inner
449
+ view the two happened to coincide, which is why it looked right in the demo.
450
+
451
+ Both paths now go through one exported helper, `drawOneLayer`, which puts a
452
+ layer's commands in the space its `space` field declares.
453
+
454
+ A screen-space source layer keeps drawing untransformed, but that means the
455
+ viewport's own CSS-pixel space — coords relative to the rect's top-left,
456
+ clipped to the rect. The previous doc comment claimed such layers rendered to
457
+ the outer canvas instead; they never did.
458
+
459
+ - 2e22d99: A viewport node can host a live camera and its own per-view data.
460
+
461
+ `view` now accepts a thunk as well as a `View`. It is read fresh on every
462
+ `draw`, `reproject` and `resolvable`, so those three cannot disagree about where
463
+ the viewport is looking part-way through a gesture. The thunk receives the outer
464
+ view and dims, so a derived camera — parallax, node-anchored scroll — is a
465
+ function of the one hosting it.
466
+
467
+ A `data` thunk derives what the source layers receive from what the outer canvas
468
+ passed down. Without it they get the outer canvas's `data`, as before. This is
469
+ what lets a viewport showing the same scene through a second camera give its
470
+ layers their own selection, chrome state and gesture previews instead of the
471
+ hosting view's.
472
+
473
+ Both are additive: `CreateViewportLayerOpts` gained a second type parameter that
474
+ defaults to the first, so existing call sites infer exactly as they did.
475
+
476
+ - Updated dependencies [7c202d2]
477
+ - @weasel-js/history@1.2.0
478
+ - @weasel-js/font@1.2.0
479
+ - @weasel-js/geom@1.2.0
480
+ - @weasel-js/gestures@1.2.0
481
+ - @weasel-js/modes@1.2.0
482
+
3
483
  ## 1.1.0
4
484
 
5
485
  ### Minor Changes
@@ -1,5 +1,5 @@
1
1
  import { P as Path } from './path-B6MMiodD.js';
2
- import { F as FillStyle, S as Stroke, a as TextureHandle } from './paint-types-CnLIzqq1.js';
2
+ import { F as FillStyle, S as Stroke, a as TextureHandle } from './paint-types-1fUdZUaH.js';
3
3
 
4
4
  /**
5
5
  * 2D affine matrix utilities. Column-major 9-element Float32Array, matching
@@ -33,6 +33,13 @@ declare function apply(m: Mat3, x: number, y: number): [number, number];
33
33
  * matches clip-down).
34
34
  */
35
35
  declare function screenToClip(width: number, height: number): Mat3;
36
+ /**
37
+ * Uniform-equivalent scale factor: the square root of the absolute
38
+ * determinant of the linear part, i.e. the geometric mean of the two axis
39
+ * scales. Rotation-invariant. Under non-uniform scale it is between the two
40
+ * axes and exact on neither — the same compromise `meanScale` documents.
41
+ */
42
+ declare function meanScaleOf(m: Mat3): number;
36
43
  /** The renderer's 3x3 matrix operations, as one namespace. These work on the
37
44
  * 9-element `Float32Array` form the GL uniform upload wants — distinct from
38
45
  * `@weasel-js/geom`'s 6-element affine `Mat3`, though the logical element
@@ -45,6 +52,7 @@ declare const mat3: {
45
52
  invert: typeof invert;
46
53
  apply: typeof apply;
47
54
  screenToClip: typeof screenToClip;
55
+ meanScaleOf: typeof meanScaleOf;
48
56
  };
49
57
 
50
58
  /**
@@ -389,6 +397,21 @@ interface ImageDrawCommand {
389
397
  * device pixels as hard squares — required by anything magnifying a
390
398
  * framebuffer readback, where blur destroys the point of the readback. */
391
399
  sampling?: 'linear' | 'nearest';
400
+ /** Sub-rectangle of `image` to draw, in bitmap pixels from the top-left.
401
+ * Omitted draws the whole bitmap. Not range-checked: a rect past the edge
402
+ * samples outside [0..1], which CLAMP_TO_EDGE smears. With
403
+ * `sampling: 'linear'` the filter reaches half a texel beyond `source`, so
404
+ * atlas frames need a gutter (see `SpriteSheet.spacing`) or `'nearest'`. */
405
+ source?: {
406
+ x: number;
407
+ y: number;
408
+ w: number;
409
+ h: number;
410
+ };
411
+ /** Mirror the sampled region within the destination rect. The quad does not
412
+ * move — a flipped draw covers exactly the pixels an unflipped one does. */
413
+ flipX?: boolean;
414
+ flipY?: boolean;
392
415
  }
393
416
  /**
394
417
  * Custom shader draw command. The renderer generates a quad over `bounds`