@weasel-js/core 1.15.1 → 1.15.2
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 +115 -0
- package/dist/animation/poseTarget.js +3 -3
- package/dist/animation/poseTarget.js.map +1 -1
- package/dist/animation/reflow.js +7 -7
- package/dist/animation/reflow.js.map +1 -1
- package/dist/animation/rig/rigNode.js +2 -2
- package/dist/animation/rig/rigNode.js.map +1 -1
- package/dist/barrel/input.js +2 -1
- package/dist/barrel/paint.js +2 -2
- package/dist/barrel/viewport.js +3 -3
- package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +17 -64
- package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js.map +1 -1
- package/dist/canvas/accentColor.js +3 -3
- package/dist/canvas/accentColor.js.map +1 -1
- package/dist/canvas/deps/enteredGroup.js +5 -4
- package/dist/canvas/deps/enteredGroup.js.map +1 -1
- package/dist/canvas/deps/selectionSnapshot.js +7 -2
- package/dist/canvas/deps/selectionSnapshot.js.map +1 -1
- package/dist/canvas/slopsDebugLayer.js +8 -2
- package/dist/canvas/slopsDebugLayer.js.map +1 -1
- package/dist/canvas/useViewRegistrySurface.js +6 -3
- package/dist/canvas/useViewRegistrySurface.js.map +1 -1
- package/dist/canvas/viewRegistry.js +5 -5
- package/dist/canvas/viewRegistry.js.map +1 -1
- package/dist/core/ops/reorder/index.js +2 -6
- package/dist/core/ops/reorder/index.js.map +1 -1
- package/dist/core/pages/fitViewToPage.js +2 -7
- package/dist/core/pages/fitViewToPage.js.map +1 -1
- package/dist/core/pages/pages.js +3 -9
- package/dist/core/pages/pages.js.map +1 -1
- package/dist/core/paint/meshKindLazy.js +9 -5
- package/dist/core/paint/meshKindLazy.js.map +1 -1
- package/dist/core/scene/pathEffects/blend.js +2 -2
- package/dist/core/scene/pathEffects/blend.js.map +1 -1
- package/dist/core/scene/pathEffects/textAlong.js +5 -4
- package/dist/core/scene/pathEffects/textAlong.js.map +1 -1
- package/dist/core/scene/scene.js +12 -1
- package/dist/core/scene/scene.js.map +1 -1
- package/dist/core/viewport/clientToWorld.js +7 -1
- package/dist/core/viewport/clientToWorld.js.map +1 -1
- package/dist/core/viewport/zoomAt.js +6 -6
- package/dist/core/viewport/zoomAt.js.map +1 -1
- package/dist/devtools/root.js +11 -4
- package/dist/devtools/root.js.map +1 -1
- package/dist/devtools/transport.js +12 -16
- package/dist/devtools/transport.js.map +1 -1
- package/dist/devtools-protocol.d.ts +0 -11
- package/dist/domains/measure/measureHit.js +2 -8
- package/dist/domains/measure/measureHit.js.map +1 -1
- package/dist/domains/paths/anchorMarkers.js +5 -11
- package/dist/domains/paths/anchorMarkers.js.map +1 -1
- package/dist/domains/paths/pathEditingOverlayLayer.js +5 -9
- package/dist/domains/paths/pathEditingOverlayLayer.js.map +1 -1
- package/dist/domains/paths/penPreviewLayer.js +9 -12
- package/dist/domains/paths/penPreviewLayer.js.map +1 -1
- package/dist/domains/paths/shapeGenerator.js +1 -2
- package/dist/domains/paths/shapeGenerator.js.map +1 -1
- package/dist/domains/snap/angle2d.js +2 -1
- package/dist/domains/snap/angle2d.js.map +1 -1
- package/dist/domains/snap/outcomeStore.js +5 -8
- package/dist/domains/snap/outcomeStore.js.map +1 -1
- package/dist/domains/text/useSceneTextEdit.js +9 -4
- package/dist/domains/text/useSceneTextEdit.js.map +1 -1
- package/dist/domains/viewports/aim.js +3 -8
- package/dist/domains/viewports/aim.js.map +1 -1
- package/dist/domains/viewports/viewResolver.js +5 -3
- package/dist/domains/viewports/viewResolver.js.map +1 -1
- package/dist/index.d.ts +20 -21
- package/dist/index.js +5 -4
- package/dist/input/usePublishPointer.js +59 -0
- package/dist/input/usePublishPointer.js.map +1 -0
- package/dist/interactions/actions/defaults/containerScroll.js +9 -3
- package/dist/interactions/actions/defaults/containerScroll.js.map +1 -1
- package/dist/interactions/actions/defaults/enterGroup.js +3 -3
- package/dist/interactions/actions/defaults/enterGroup.js.map +1 -1
- package/dist/interactions/actions/defaults/pathEditMarks.js +2 -5
- package/dist/interactions/actions/defaults/pathEditMarks.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/handleDrag.js +4 -7
- package/dist/interactions/actions/edit-anchors/handleDrag.js.map +1 -1
- package/dist/interactions/actions/move/behaviors/clampMove.js +42 -0
- package/dist/interactions/actions/move/behaviors/clampMove.js.map +1 -0
- package/dist/interactions/actions/move/layout/container.js +3 -6
- package/dist/interactions/actions/move/layout/container.js.map +1 -1
- package/dist/interactions/actions/move/layout/exits.js +2 -2
- package/dist/interactions/actions/move/layout/exits.js.map +1 -1
- package/dist/interactions/actions/transform-again/memory.js +2 -2
- package/dist/interactions/actions/transform-again/memory.js.map +1 -1
- package/dist/move.d.ts +18 -3
- package/dist/move.js +2 -1
- package/dist/version.js +1 -1
- package/package.json +25 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,120 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.15.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 946faff: `clampMove({ bounds })` from `@weasel-js/core/move` is a move behavior that keeps the dragged nodes inside a
|
|
8
|
+
world rect: the drag's translation is clamped so the box around every dragged node stays in `bounds`, in
|
|
9
|
+
the preview and the commit alike. `bounds` can be a function, read each frame, for a rect that changes
|
|
10
|
+
size.
|
|
11
|
+
- 507f539: paint exports `relativeLuminance` and `luminanceContrast`, the WCAG luminance and ratio `contrastRatio` is built on; core re-exports both. theme's `luminance`/`contrast`, its test helpers, ui's painted-contrast helpers and forge's pixel contrast readout now use them.
|
|
12
|
+
|
|
13
|
+
Color strings that theme's `withAlpha`/`atAlpha`, ui's `TokenPanel` hex conversion and diagram-formats' DOT and Mermaid writers read are parsed by paint's `parseColor`. The `TokenPanel` color input now converts float and percent `rgb()` channels, named colors and `hsl()` without a browser, and diagram-formats writes percent `rgb()` channels as hex instead of passing them through. diagram-formats now depends on `@weasel-js/paint`.
|
|
14
|
+
|
|
15
|
+
labkit's loupe and ui's `Timeline` ruler and lane graph measure their element with core's `useElementSize`.
|
|
16
|
+
- a4138b6: `@weasel-js/geom` states the nearest point of a segment, point distance and lerp once, in any
|
|
17
|
+
dimension, and every tier answers through that one rule.
|
|
18
|
+
|
|
19
|
+
- `@weasel-js/geom/nd` adds `segmentT`, `closestPointOnSegment`, `pointSegmentDist`,
|
|
20
|
+
`pointSegmentDist2`, `distance`, `distance2` and `lerp` over plain number arrays.
|
|
21
|
+
- The root barrel adds `segmentT` and `pointSegmentDist` over loose scalars, which allocate nothing,
|
|
22
|
+
beside the existing `pointSegmentDist2`; `distance`, `distance2` and `lerpPoint` over `Point`; and a
|
|
23
|
+
scalar `lerp`.
|
|
24
|
+
- `@weasel-js/geom/3d`'s `closestPointOnSegment` and `lerp` now go through the same rule, so a planar
|
|
25
|
+
problem gets the same numbers in 2D, 3D and `nd`.
|
|
26
|
+
|
|
27
|
+
A segment's parameter is clamped to [0, 1] and is 0 on a zero-length segment; at `t = 1` the nearest
|
|
28
|
+
point is the far end exactly. `nearestOnLine` and the private copies in core's measurement hit test,
|
|
29
|
+
animator's auto-skinning, diagram's route and label code, stroke's edge test, path-tools'
|
|
30
|
+
extend-to-meet and guides' layout snaps now call these, so their distances can move by a rounding
|
|
31
|
+
step (`Math.hypot` became a square root of the summed squares).
|
|
32
|
+
|
|
33
|
+
Corner shapes (`cornerShapePieces`) now fit the true superellipse within `CORNER_FIT_TOLERANCE` for
|
|
34
|
+
every finite `k`. Past `|k| ≈ 9` they strayed up to nine times the tolerance off the curve's straight
|
|
35
|
+
legs, because the fit stepped in an angle that crowds those legs into less than a double resolves;
|
|
36
|
+
it now steps evenly along each leg. The same corners draw with about a third fewer cubics.
|
|
37
|
+
- f1906a8: The rest of the kit's stores notify through registry's `createEmitter`: the entered group, the snap
|
|
38
|
+
outcome, devtools roots and transports, kit settings, the menu host and menu controller, tool
|
|
39
|
+
options, `RunSlots`, the seam preview, the HUD list, the effect runners, the symmetry controller,
|
|
40
|
+
the devtools client, and forge's loupe and CSS-variable stores. A listener that throws is logged
|
|
41
|
+
and no longer stops the others, and a listener unsubscribed mid-notify is no longer called; the
|
|
42
|
+
menu host, `RunSlots` and the HUD list used to walk their live set. diagram, diagram-formats, menu,
|
|
43
|
+
symmetry and devtools now depend on `@weasel-js/registry`.
|
|
44
|
+
|
|
45
|
+
Structural comparisons that went through `JSON.stringify` use `deepEqual` instead, so key order no
|
|
46
|
+
longer makes two equal values differ (theme baking, diagram export's style diffs, trial marks, path
|
|
47
|
+
constraints, unit rows, toolbar dock drops). PrefSchemaEditor's three-way merge keeps
|
|
48
|
+
`JSON.stringify`, because there a reordered key is an edit.
|
|
49
|
+
|
|
50
|
+
New: `sameList(a, b, same?)` in `@weasel-js/registry`, whether two lists hold equal elements in
|
|
51
|
+
order. `@weasel-js/react`'s `sameList` is now this function, and ui's `samePath` is an alias of it.
|
|
52
|
+
- e804540: **Behavior change:** `matchesKeyBinding` and `historyKey` now read `mod` the way `@weasel-js/gestures`
|
|
53
|
+
does: Command on an Apple platform, Ctrl elsewhere, and the other key forbidden. Before, either key
|
|
54
|
+
fired a `mod` binding on every platform, so Ctrl+Z undid on a Mac and Cmd+Z undid on Windows; now
|
|
55
|
+
neither does, and neither does Cmd+Ctrl+Z. Both take an optional `isMac` argument, defaulting to
|
|
56
|
+
`isApplePlatform()`. Everything built on them follows: labkit's trial chrome and `useUndoKeys`, and
|
|
57
|
+
the undo keys in `AssetEditor`, `LayeredCurveEditor` and forge's CSS-vars panel. A trial's snapshot
|
|
58
|
+
chord is Mod+S alone; Shift+Mod+S no longer takes a snapshot.
|
|
59
|
+
|
|
60
|
+
`rowKey` takes a sixth argument, `apple`, and selects all on Mod+A as above; Ctrl+A on a Mac no
|
|
61
|
+
longer selects all in a `Tree` or any list on `useRowNavigation`, which passes `isApplePlatform()`.
|
|
62
|
+
`rowKey` called without it reads `mod` as Ctrl.
|
|
63
|
+
|
|
64
|
+
`@weasel-js/react` reads the system color scheme: `systemColorScheme()`, `watchColorScheme(fn)` and
|
|
65
|
+
`useSystemColorScheme(follow?)`, with `DEFAULT_COLOR_SCHEME`, dark, wherever `matchMedia` is missing or
|
|
66
|
+
the system states no preference. Dark is the mode the theme's tokens paint at `:root`. theme's
|
|
67
|
+
`useResolvedColorMode`, core's `watchThemeOf` and forge's loupe conditions use it. **Behavior change:**
|
|
68
|
+
forge's loupe read a missing `matchMedia` as light; it now reads it as dark, as the theme always has.
|
|
69
|
+
|
|
70
|
+
`scene.mintId()`, and the id `add` gives a spec with none, now pass over any id the scene already
|
|
71
|
+
holds. A document reopened in a new session keeps the ids the last one minted, and a `generateId` that
|
|
72
|
+
starts over with the session could mint them again; a generator that gives 1000 taken ids in a row
|
|
73
|
+
throws.
|
|
74
|
+
- 2f6b10d: `usePublishPointer(elementRef, locate)` from `@weasel-js/core` publishes the pointer over an element into
|
|
75
|
+
the pointer store in scope, placed by `locate(pointerId, clientX, clientY)`. It is what `<SceneCanvas>`
|
|
76
|
+
does for its own canvas: a pointer pressed on the element keeps reporting once a drag leaves it, and a
|
|
77
|
+
free pointer leaving clears the store unless another view has published since. The minimap and labkit's
|
|
78
|
+
cameras now publish through it, so a drag that runs off a minimap or a lab stage no longer stops
|
|
79
|
+
reporting where the pointer is.
|
|
80
|
+
- 5f6111b: `@weasel-js/geom` exports `viewWorldToScreen(view, p)`, `viewScreenToWorld(view, p)` and
|
|
81
|
+
`viewRectToScreen(view, rect)`, which convert between world and screen through a `View` directly;
|
|
82
|
+
`@weasel-js/core` re-exports them. Core, guides, diagram, hud, labkit and path-tools now use them in
|
|
83
|
+
place of their own inline copies of the same arithmetic.
|
|
84
|
+
|
|
85
|
+
Under a flipped view (a negative `scale` axis), diagram overlays, labkit lens hit boxes and the
|
|
86
|
+
path-edit marquee now get a rect with a positive width and height. labkit's camera zoom keeps a
|
|
87
|
+
flipped axis flipped instead of turning it back to positive.
|
|
88
|
+
- Updated dependencies [507f539]
|
|
89
|
+
- Updated dependencies [c6edab2]
|
|
90
|
+
- Updated dependencies [a4138b6]
|
|
91
|
+
- Updated dependencies [5a0375b]
|
|
92
|
+
- Updated dependencies [f1906a8]
|
|
93
|
+
- Updated dependencies [e804540]
|
|
94
|
+
- Updated dependencies [6544399]
|
|
95
|
+
- Updated dependencies [5f6111b]
|
|
96
|
+
- @weasel-js/paint@1.15.2
|
|
97
|
+
- @weasel-js/geom@1.15.2
|
|
98
|
+
- @weasel-js/animator@1.15.2
|
|
99
|
+
- @weasel-js/stroke@1.15.2
|
|
100
|
+
- @weasel-js/react@1.15.2
|
|
101
|
+
- @weasel-js/routing@1.15.2
|
|
102
|
+
- @weasel-js/registry@1.15.2
|
|
103
|
+
- @weasel-js/text@1.15.2
|
|
104
|
+
- @weasel-js/ui-logic@1.15.2
|
|
105
|
+
- @weasel-js/prefs@1.15.2
|
|
106
|
+
- @weasel-js/renderer@1.15.2
|
|
107
|
+
- @weasel-js/text-dom@1.15.2
|
|
108
|
+
- @weasel-js/simulation@1.15.2
|
|
109
|
+
- @weasel-js/gestures@1.15.2
|
|
110
|
+
- @weasel-js/cursor@1.15.2
|
|
111
|
+
- @weasel-js/font@1.15.2
|
|
112
|
+
- @weasel-js/history@1.15.2
|
|
113
|
+
- @weasel-js/icons@1.15.2
|
|
114
|
+
- @weasel-js/modes@1.15.2
|
|
115
|
+
- @weasel-js/select@1.15.2
|
|
116
|
+
- @weasel-js/quantity@1.15.2
|
|
117
|
+
|
|
3
118
|
## 1.15.1
|
|
4
119
|
|
|
5
120
|
### Patch Changes
|
|
@@ -112,7 +112,7 @@ function createPoseTarget(opts) {
|
|
|
112
112
|
}
|
|
113
113
|
var sceneTargets = /* @__PURE__ */ new WeakMap();
|
|
114
114
|
/** Whether two poses hold the same fields, compared without allocating: a running target asks on every publish. */
|
|
115
|
-
function
|
|
115
|
+
function samePoseShallow(a, b) {
|
|
116
116
|
if (a === b) return true;
|
|
117
117
|
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
118
118
|
const ra = a;
|
|
@@ -169,7 +169,7 @@ function scenePoseTarget(scene, id, descriptor) {
|
|
|
169
169
|
if (!n) return;
|
|
170
170
|
writing = true;
|
|
171
171
|
try {
|
|
172
|
-
if (!
|
|
172
|
+
if (!samePoseShallow(n.pose, pose)) scene.untracked(() => scene.setPose(nid, pose));
|
|
173
173
|
} finally {
|
|
174
174
|
writing = false;
|
|
175
175
|
}
|
|
@@ -180,7 +180,7 @@ function scenePoseTarget(scene, id, descriptor) {
|
|
|
180
180
|
}, () => {
|
|
181
181
|
if (writing) return false;
|
|
182
182
|
const n = scene.get(nid);
|
|
183
|
-
return !!n && mine !== void 0 && !
|
|
183
|
+
return !!n && mine !== void 0 && !samePoseShallow(n.pose, mine);
|
|
184
184
|
});
|
|
185
185
|
if (!target) return void 0;
|
|
186
186
|
made.set(id, target);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poseTarget.js","names":[],"sources":["../../src/animation/poseTarget.ts"],"sourcesContent":["import { RECT_POSE_DESCRIPTOR, type PoseDescriptor } from '../core/geometry/poseDescriptor';\nimport { documentPose, type PoseSource } from '../core/scene/effectivePose';\nimport type { NodeId, PoseOverride, PoseOverrides } from '../core/scene/types';\nimport { axesOf } from '@weasel-js/animator';\nimport { createTarget, type Target, type TargetAxes } from '@weasel-js/animator';\n\n/** What {@link createPoseTarget} takes. */\nexport interface PoseTargetOptions<TPose> {\n /** The node whose pose it is. */\n id: string;\n /** The node's committed pose: where the document says it is. */\n get(): TPose;\n /** Commits a pose: an animation's destination as it starts, and where a canceled one stopped. */\n set(pose: TPose): void;\n /** Where the pose on screen is published while the node moves. */\n overrides: PoseOverrides<TPose>;\n /** Supplies the pose's axes through `PoseDescriptor.axes`. Without one, or where it answers\n * null, the pose has to be a number, an array of numbers, or an object of numeric fields. */\n descriptor?: PoseDescriptor<TPose>;\n}\n\n/** The part of a scene {@link scenePoseTarget} reads and writes. A `Scene` satisfies it. */\nexport interface PoseTargetScene<TPose> extends PoseSource<TPose> {\n readonly overrides: PoseOverrides<TPose>;\n setPose(id: NodeId, pose: TPose): void;\n untracked<T>(fn: () => T): T;\n}\n\n/** How `like`'s kind of pose maps to axes: the descriptor's own mapping, else its flat numeric\n * fields. Null for a pose with neither. */\nexport function poseAxes<TPose>(\n descriptor: PoseDescriptor<TPose> | undefined,\n like: TPose,\n): TargetAxes<TPose> | null {\n return descriptor?.axes?.(like) ?? (axesOf(like) as TargetAxes<TPose> | null);\n}\n\nconst publishers = new WeakMap<object, () => void>();\n\n/** One function per overrides table, so every target publishing into it shares one commit a frame. */\nfunction publisherOf<TPose>(overrides: PoseOverrides<TPose>): () => void {\n let publish = publishers.get(overrides);\n if (!publish) {\n publish = () => overrides.commit();\n publishers.set(overrides, publish);\n }\n return publish;\n}\n\n/**\n * `poseAxes` for the pose `get` answers, with `from` keeping the fields that are not axes as\n * that pose has them now. Reading them once would leave them stale in every pose a canceled\n * animation commits. Null where the pose has no axes.\n */\nexport function liveAxes<TPose>(\n descriptor: PoseDescriptor<TPose> | undefined,\n get: () => TPose,\n): TargetAxes<TPose> | null {\n let like = get();\n let axes = poseAxes(descriptor, like);\n if (!axes) return null;\n const { count, angles } = axes;\n const current = (): TargetAxes<TPose> => {\n const now = get();\n if (now !== like) {\n const next = poseAxes(descriptor, now);\n if (next && next.count === count) axes = next;\n like = now;\n }\n return axes!;\n };\n return {\n count,\n angles,\n to: (p) => current().to(p),\n from: (a) => current().from(a),\n add: axes.add && ((p, d) => {\n const now = current();\n if (now.add) return now.add(p, d);\n return now.from(now.to(p).map((n, i) => n + d[i]!));\n }),\n write: axes.write && ((p, out, at) => {\n const now = current();\n if (now.write) now.write(p, out, at);\n else now.to(p).forEach((n, i) => { out[at + i] = n; });\n }),\n };\n}\n\nfunction build<TPose>(\n opts: PoseTargetOptions<TPose>,\n onRest?: () => void,\n /** Whether something else has written the committed pose since an animation last did. */\n overwritten?: () => boolean,\n): Target<TPose> | null {\n const { overrides } = opts;\n const id = opts.id as NodeId;\n const axes = liveAxes(opts.descriptor, opts.get);\n if (!axes) return null;\n const entry: PoseOverride<TPose> = {};\n let installed = false;\n /** While installed, stops the run as soon as the table changes after something else wrote\n * the pose: a drag laid over the run commits and then lets go, which would show this run's\n * last frame until its next one. */\n let unwatch: (() => void) | null = null;\n const rest = (): void => {\n installed = false;\n unwatch?.();\n unwatch = null;\n overrides.clear(id, entry);\n onRest?.();\n };\n const target = createTarget<TPose>({\n get: opts.get,\n set: opts.set,\n axes,\n show(pose) {\n // The writer wins: what was left to travel belonged to a pose that is no longer committed.\n if (target.active && overwritten?.()) target.stop({ commit: false });\n if (!target.active) {\n rest();\n return;\n }\n entry.pose = pose;\n // Reinstalled where it stood if the table dropped it; an override laid over it since, a drag's, is not taken back.\n if (!overrides.has(id, entry)) overrides.set(id, entry);\n installed = true;\n if (overwritten && unwatch === null) {\n unwatch = overrides.subscribe(() => {\n if (!target.active || !overwritten()) return;\n target.stop({ commit: false });\n if (installed) rest();\n });\n }\n },\n publish: publisherOf(overrides),\n });\n return target;\n}\n\n/**\n * A {@link Target} over one node's pose. Animations on it commit their destination through\n * `set` as they start, and the pose on screen reaches the paint and pick walks as a pose\n * override: installed while the node moves and cleared when it comes to rest.\n *\n * Throws for a pose with no axes. Make one per node: two targets over one node do not add.\n * {@link scenePoseTarget} keeps that one for a scene's nodes.\n */\nexport function createPoseTarget<TPose>(opts: PoseTargetOptions<TPose>): Target<TPose> {\n const target = build(opts);\n if (!target) {\n throw new Error('createPoseTarget: this pose has no axes; give a descriptor whose `axes` maps it');\n }\n return target;\n}\n\nconst sceneTargets = new WeakMap<object, Map<string, Target<unknown>>>();\n\n/** Whether two poses hold the same fields, compared without allocating: a running target asks on every publish. */\nfunction samePose(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n const ra = a as Record<string, unknown>;\n const rb = b as Record<string, unknown>;\n let count = 0;\n for (const k in ra) {\n if (!Object.hasOwn(ra, k)) continue;\n if (!Object.hasOwn(rb, k) || ra[k] !== rb[k]) return false;\n count++;\n }\n for (const k in rb) if (Object.hasOwn(rb, k)) count--;\n return count === 0;\n}\n\n/**\n * The pose target of node `id` in `scene`: the same one on every call, so everything animating\n * the node adds. Its committed value is the node's document pose, written without recording;\n * a caller that wants the move in history records its own op first.\n *\n * Undefined for a node that is not in the scene, or whose pose has no axes. `descriptor`\n * defaults to `RECT_POSE_DESCRIPTOR`, and is read the first time a node's target is made.\n */\nexport function scenePoseTarget<TPose>(\n scene: PoseTargetScene<TPose>,\n id: string,\n descriptor?: PoseDescriptor<TPose>,\n): Target<TPose> | undefined {\n let made = sceneTargets.get(scene.overrides);\n if (!made) {\n made = new Map();\n sceneTargets.set(scene.overrides, made);\n }\n const nid = id as NodeId;\n const node = scene.get(nid);\n const by = descriptor ?? (RECT_POSE_DESCRIPTOR as unknown as PoseDescriptor<TPose>);\n const held = made.get(id) as Target<TPose> | undefined;\n // A pose that has changed shape at rest, a polygon given more points, needs axes made for it.\n const fits = !held || held.active || !node\n || poseAxes(by, documentPose(scene, node))?.count === held.axisCount;\n if (held && fits) return held;\n if (!node) return undefined;\n // A node removed mid-flight keeps answering with the last pose it had.\n let last = documentPose(scene, node);\n /** The node's pose as an animation last left it, or as it was when the first one started. */\n let mine: TPose | undefined;\n /** While this target's own commit is being written. */\n let writing = false;\n const cache = made;\n let target: Target<TPose> | null = null;\n target = build<TPose>({\n id,\n descriptor: by,\n overrides: scene.overrides,\n get() {\n const n = scene.get(nid);\n if (n) last = documentPose(scene, n);\n if (n && !target?.active) mine = n.pose;\n return last;\n },\n set(pose) {\n const n = scene.get(nid);\n if (!n) return;\n // The write runs the layout's reflow at once, and a reflow publishes glides to the overrides.\n writing = true;\n try {\n if (!samePose(n.pose, pose)) scene.untracked(() => scene.setPose(nid, pose));\n } finally {\n writing = false;\n }\n mine = scene.get(nid)?.pose;\n },\n }, () => {\n if (!scene.get(nid)) cache.delete(id);\n }, () => {\n if (writing) return false;\n const n = scene.get(nid);\n return !!n && mine !== undefined && !samePose(n.pose, mine);\n });\n if (!target) return undefined;\n made.set(id, target as Target<unknown>);\n return target;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"poseTarget.js","names":[],"sources":["../../src/animation/poseTarget.ts"],"sourcesContent":["import { RECT_POSE_DESCRIPTOR, type PoseDescriptor } from '../core/geometry/poseDescriptor';\nimport { documentPose, type PoseSource } from '../core/scene/effectivePose';\nimport type { NodeId, PoseOverride, PoseOverrides } from '../core/scene/types';\nimport { axesOf } from '@weasel-js/animator';\nimport { createTarget, type Target, type TargetAxes } from '@weasel-js/animator';\n\n/** What {@link createPoseTarget} takes. */\nexport interface PoseTargetOptions<TPose> {\n /** The node whose pose it is. */\n id: string;\n /** The node's committed pose: where the document says it is. */\n get(): TPose;\n /** Commits a pose: an animation's destination as it starts, and where a canceled one stopped. */\n set(pose: TPose): void;\n /** Where the pose on screen is published while the node moves. */\n overrides: PoseOverrides<TPose>;\n /** Supplies the pose's axes through `PoseDescriptor.axes`. Without one, or where it answers\n * null, the pose has to be a number, an array of numbers, or an object of numeric fields. */\n descriptor?: PoseDescriptor<TPose>;\n}\n\n/** The part of a scene {@link scenePoseTarget} reads and writes. A `Scene` satisfies it. */\nexport interface PoseTargetScene<TPose> extends PoseSource<TPose> {\n readonly overrides: PoseOverrides<TPose>;\n setPose(id: NodeId, pose: TPose): void;\n untracked<T>(fn: () => T): T;\n}\n\n/** How `like`'s kind of pose maps to axes: the descriptor's own mapping, else its flat numeric\n * fields. Null for a pose with neither. */\nexport function poseAxes<TPose>(\n descriptor: PoseDescriptor<TPose> | undefined,\n like: TPose,\n): TargetAxes<TPose> | null {\n return descriptor?.axes?.(like) ?? (axesOf(like) as TargetAxes<TPose> | null);\n}\n\nconst publishers = new WeakMap<object, () => void>();\n\n/** One function per overrides table, so every target publishing into it shares one commit a frame. */\nfunction publisherOf<TPose>(overrides: PoseOverrides<TPose>): () => void {\n let publish = publishers.get(overrides);\n if (!publish) {\n publish = () => overrides.commit();\n publishers.set(overrides, publish);\n }\n return publish;\n}\n\n/**\n * `poseAxes` for the pose `get` answers, with `from` keeping the fields that are not axes as\n * that pose has them now. Reading them once would leave them stale in every pose a canceled\n * animation commits. Null where the pose has no axes.\n */\nexport function liveAxes<TPose>(\n descriptor: PoseDescriptor<TPose> | undefined,\n get: () => TPose,\n): TargetAxes<TPose> | null {\n let like = get();\n let axes = poseAxes(descriptor, like);\n if (!axes) return null;\n const { count, angles } = axes;\n const current = (): TargetAxes<TPose> => {\n const now = get();\n if (now !== like) {\n const next = poseAxes(descriptor, now);\n if (next && next.count === count) axes = next;\n like = now;\n }\n return axes!;\n };\n return {\n count,\n angles,\n to: (p) => current().to(p),\n from: (a) => current().from(a),\n add: axes.add && ((p, d) => {\n const now = current();\n if (now.add) return now.add(p, d);\n return now.from(now.to(p).map((n, i) => n + d[i]!));\n }),\n write: axes.write && ((p, out, at) => {\n const now = current();\n if (now.write) now.write(p, out, at);\n else now.to(p).forEach((n, i) => { out[at + i] = n; });\n }),\n };\n}\n\nfunction build<TPose>(\n opts: PoseTargetOptions<TPose>,\n onRest?: () => void,\n /** Whether something else has written the committed pose since an animation last did. */\n overwritten?: () => boolean,\n): Target<TPose> | null {\n const { overrides } = opts;\n const id = opts.id as NodeId;\n const axes = liveAxes(opts.descriptor, opts.get);\n if (!axes) return null;\n const entry: PoseOverride<TPose> = {};\n let installed = false;\n /** While installed, stops the run as soon as the table changes after something else wrote\n * the pose: a drag laid over the run commits and then lets go, which would show this run's\n * last frame until its next one. */\n let unwatch: (() => void) | null = null;\n const rest = (): void => {\n installed = false;\n unwatch?.();\n unwatch = null;\n overrides.clear(id, entry);\n onRest?.();\n };\n const target = createTarget<TPose>({\n get: opts.get,\n set: opts.set,\n axes,\n show(pose) {\n // The writer wins: what was left to travel belonged to a pose that is no longer committed.\n if (target.active && overwritten?.()) target.stop({ commit: false });\n if (!target.active) {\n rest();\n return;\n }\n entry.pose = pose;\n // Reinstalled where it stood if the table dropped it; an override laid over it since, a drag's, is not taken back.\n if (!overrides.has(id, entry)) overrides.set(id, entry);\n installed = true;\n if (overwritten && unwatch === null) {\n unwatch = overrides.subscribe(() => {\n if (!target.active || !overwritten()) return;\n target.stop({ commit: false });\n if (installed) rest();\n });\n }\n },\n publish: publisherOf(overrides),\n });\n return target;\n}\n\n/**\n * A {@link Target} over one node's pose. Animations on it commit their destination through\n * `set` as they start, and the pose on screen reaches the paint and pick walks as a pose\n * override: installed while the node moves and cleared when it comes to rest.\n *\n * Throws for a pose with no axes. Make one per node: two targets over one node do not add.\n * {@link scenePoseTarget} keeps that one for a scene's nodes.\n */\nexport function createPoseTarget<TPose>(opts: PoseTargetOptions<TPose>): Target<TPose> {\n const target = build(opts);\n if (!target) {\n throw new Error('createPoseTarget: this pose has no axes; give a descriptor whose `axes` maps it');\n }\n return target;\n}\n\nconst sceneTargets = new WeakMap<object, Map<string, Target<unknown>>>();\n\n/** Whether two poses hold the same fields, compared without allocating: a running target asks on every publish. */\nfunction samePoseShallow(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n const ra = a as Record<string, unknown>;\n const rb = b as Record<string, unknown>;\n let count = 0;\n for (const k in ra) {\n if (!Object.hasOwn(ra, k)) continue;\n if (!Object.hasOwn(rb, k) || ra[k] !== rb[k]) return false;\n count++;\n }\n for (const k in rb) if (Object.hasOwn(rb, k)) count--;\n return count === 0;\n}\n\n/**\n * The pose target of node `id` in `scene`: the same one on every call, so everything animating\n * the node adds. Its committed value is the node's document pose, written without recording;\n * a caller that wants the move in history records its own op first.\n *\n * Undefined for a node that is not in the scene, or whose pose has no axes. `descriptor`\n * defaults to `RECT_POSE_DESCRIPTOR`, and is read the first time a node's target is made.\n */\nexport function scenePoseTarget<TPose>(\n scene: PoseTargetScene<TPose>,\n id: string,\n descriptor?: PoseDescriptor<TPose>,\n): Target<TPose> | undefined {\n let made = sceneTargets.get(scene.overrides);\n if (!made) {\n made = new Map();\n sceneTargets.set(scene.overrides, made);\n }\n const nid = id as NodeId;\n const node = scene.get(nid);\n const by = descriptor ?? (RECT_POSE_DESCRIPTOR as unknown as PoseDescriptor<TPose>);\n const held = made.get(id) as Target<TPose> | undefined;\n // A pose that has changed shape at rest, a polygon given more points, needs axes made for it.\n const fits = !held || held.active || !node\n || poseAxes(by, documentPose(scene, node))?.count === held.axisCount;\n if (held && fits) return held;\n if (!node) return undefined;\n // A node removed mid-flight keeps answering with the last pose it had.\n let last = documentPose(scene, node);\n /** The node's pose as an animation last left it, or as it was when the first one started. */\n let mine: TPose | undefined;\n /** While this target's own commit is being written. */\n let writing = false;\n const cache = made;\n let target: Target<TPose> | null = null;\n target = build<TPose>({\n id,\n descriptor: by,\n overrides: scene.overrides,\n get() {\n const n = scene.get(nid);\n if (n) last = documentPose(scene, n);\n if (n && !target?.active) mine = n.pose;\n return last;\n },\n set(pose) {\n const n = scene.get(nid);\n if (!n) return;\n // The write runs the layout's reflow at once, and a reflow publishes glides to the overrides.\n writing = true;\n try {\n if (!samePoseShallow(n.pose, pose)) scene.untracked(() => scene.setPose(nid, pose));\n } finally {\n writing = false;\n }\n mine = scene.get(nid)?.pose;\n },\n }, () => {\n if (!scene.get(nid)) cache.delete(id);\n }, () => {\n if (writing) return false;\n const n = scene.get(nid);\n return !!n && mine !== undefined && !samePoseShallow(n.pose, mine);\n });\n if (!target) return undefined;\n made.set(id, target as Target<unknown>);\n return target;\n}\n"],"mappings":"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"}
|
package/dist/animation/reflow.js
CHANGED
|
@@ -8,13 +8,13 @@ import { createTarget } from "@weasel-js/animator";
|
|
|
8
8
|
// src/animation/reflow.ts
|
|
9
9
|
/** The animator's cancel-key for one node's glide. */
|
|
10
10
|
var keyFor = (id) => `reflow:${id}`;
|
|
11
|
-
function
|
|
11
|
+
function samePoseDeep(a, b) {
|
|
12
12
|
if (Object.is(a, b)) return true;
|
|
13
13
|
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
14
14
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
15
15
|
const ka = Object.keys(a);
|
|
16
16
|
if (ka.length !== Object.keys(b).length) return false;
|
|
17
|
-
for (const k of ka) if (!
|
|
17
|
+
for (const k of ka) if (!samePoseDeep(a[k], b[k])) return false;
|
|
18
18
|
return true;
|
|
19
19
|
}
|
|
20
20
|
function makeReflow(scene, animator, read) {
|
|
@@ -55,7 +55,7 @@ function makeGlides(scene, animator, read) {
|
|
|
55
55
|
const run = (id, g, to, settling) => {
|
|
56
56
|
const from = g.entry.pose;
|
|
57
57
|
g.settling = settling;
|
|
58
|
-
if (
|
|
58
|
+
if (samePoseDeep(from, to)) {
|
|
59
59
|
animator.cancelKey(keyFor(id));
|
|
60
60
|
g.slot = to;
|
|
61
61
|
g.entry.pose = to;
|
|
@@ -64,7 +64,7 @@ function makeGlides(scene, animator, read) {
|
|
|
64
64
|
unsubscribeIfIdle();
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
if (g.target?.active &&
|
|
67
|
+
if (g.target?.active && samePoseDeep(g.slot, to)) return;
|
|
68
68
|
const o = read();
|
|
69
69
|
const lerp = o.geometry?.lerp;
|
|
70
70
|
const onDone = () => {
|
|
@@ -124,7 +124,7 @@ function makeGlides(scene, animator, read) {
|
|
|
124
124
|
}
|
|
125
125
|
scene.overrides.commit();
|
|
126
126
|
};
|
|
127
|
-
const onCourse = (id, g) => animator.isActive(keyFor(id)) ||
|
|
127
|
+
const onCourse = (id, g) => animator.isActive(keyFor(id)) || samePoseDeep(g.entry.pose, g.slot);
|
|
128
128
|
const stop = (id) => {
|
|
129
129
|
const g = glides.get(id);
|
|
130
130
|
if (!g) return;
|
|
@@ -173,7 +173,7 @@ function makeGlides(scene, animator, read) {
|
|
|
173
173
|
if (!node) return;
|
|
174
174
|
const from = opts?.from;
|
|
175
175
|
const g = glides.get(id);
|
|
176
|
-
if (g && from === void 0 && !g.settling &&
|
|
176
|
+
if (g && from === void 0 && !g.settling && samePoseDeep(g.slot, pose) && onCourse(id, g)) return;
|
|
177
177
|
run(id, claim(id, node, g, from), pose, false);
|
|
178
178
|
},
|
|
179
179
|
settle(id, opts) {
|
|
@@ -187,7 +187,7 @@ function makeGlides(scene, animator, read) {
|
|
|
187
187
|
}
|
|
188
188
|
const home = documentPose(scene, node);
|
|
189
189
|
if (g && from === void 0) {
|
|
190
|
-
if (g.settling &&
|
|
190
|
+
if (g.settling && samePoseDeep(g.slot, home) && onCourse(id, g)) return;
|
|
191
191
|
run(id, g, home, true);
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reflow.js","names":[],"sources":["../../src/animation/reflow.ts"],"sourcesContent":["import { useEffect, useMemo } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { RECT_POSE_DESCRIPTOR, type PoseDescriptor } from '../core/geometry/poseDescriptor';\nimport { documentPose, effectivePose, type PosedNode, type PoseSource } from '../core/scene/effectivePose';\nimport type { NodeId, PoseOverride, PoseOverrides } from '../core/scene/types';\nimport type { ReflowTransition } from '../layout/types';\nimport { liveAxes } from './poseTarget';\nimport { createTarget, type Target } from '@weasel-js/animator';\nimport type { Animator, EasingSpec, SpringPresetName } from '@weasel-js/animator';\n\n/** How a reflowed sibling travels: a duration and easing, or a spring. */\nexport interface ReflowTransitionOptions<TPose> {\n /** Default 200. Ignored under `spring`. */\n ms?: number;\n easing?: EasingSpec;\n /** Travel on a spring instead of a duration. A retarget starts a new spring\n * from the pose on screen at the speed the node was moving. */\n spring?: {\n preset?: SpringPresetName;\n stiffness?: number;\n damping?: number;\n mass?: number;\n };\n /** Supplies the pose's axes, and its `lerp` is the path a timed glide\n * follows. Default `RECT_POSE_DESCRIPTOR`, each axis in a straight line.\n * A pose with no axes glides by `lerp` alone, and a retarget restarts it. */\n geometry?: PoseDescriptor<TPose>;\n /** Names each glide in `animator.watch` and `live()`. Default `'reflow'`. */\n label?: string;\n}\n\n/** A {@link ReflowTransition} the animator drives. `dispose` drops every\n * override it holds; the transition stays usable afterward. */\nexport interface AnimatedReflow<TPose = unknown> extends ReflowTransition<TPose> {\n dispose(): void;\n /** The same transition over another scene: its animator, timing and label,\n * for poses `geometry` describes given this one's descriptor. Option\n * changes reach it as they reach this one. */\n over<TOther>(\n scene: ReflowScene<TOther>,\n geometry: (mine: PoseDescriptor<TPose>) => PoseDescriptor<TOther>,\n ): AnimatedReflow<TOther>;\n}\n\n/** The part of a scene a reflow transition reads and overrides. */\nexport interface ReflowScene<TPose> extends PoseSource<TPose> {\n readonly overrides: PoseOverrides<TPose>;\n}\n\ninterface Glide<TPose> {\n entry: PoseOverride<TPose>;\n /** Where the layout wants the node shown: what its target commits. */\n slot: TPose;\n settling: boolean;\n /** Null for a pose with no axes, which glides by `lerp`. */\n target: Target<TPose> | null;\n}\n\n/** The animator's cancel-key for one node's glide. */\nconst keyFor = (id: string): string => `reflow:${id}`;\n\nfunction samePose(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const ka = Object.keys(a);\n if (ka.length !== Object.keys(b).length) return false;\n for (const k of ka) {\n if (!samePose((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;\n }\n return true;\n}\n\nfunction makeReflow<TPose>(\n scene: ReflowScene<TPose>,\n animator: Animator,\n read: () => ReflowTransitionOptions<TPose>,\n): AnimatedReflow<TPose> {\n return {\n ...makeGlides(scene, animator, read),\n over: (other, geometry) => makeReflow(other, animator, () => {\n const { geometry: own, ...timing } = read();\n return { ...timing, geometry: geometry(own ?? (RECT_POSE_DESCRIPTOR as unknown as PoseDescriptor<TPose>)) };\n }),\n };\n}\n\nfunction makeGlides<TPose>(\n scene: ReflowScene<TPose>,\n animator: Animator,\n read: () => ReflowTransitionOptions<TPose>,\n): Omit<AnimatedReflow<TPose>, 'over'> {\n const glides = new Map<string, Glide<TPose>>();\n let dirty = false;\n let unsubscribe: (() => void) | null = null;\n\n // Ticks only mark the frame dirty; one commit per animator frame publishes\n // every glide at once.\n const subscribe = (): void => {\n if (unsubscribe) return;\n unsubscribe = animator.onTick(() => {\n if (!dirty) return;\n dirty = false;\n scene.overrides.commit();\n });\n };\n const unsubscribeIfIdle = (): void => {\n if (glides.size > 0 || !unsubscribe) return;\n unsubscribe();\n unsubscribe = null;\n };\n\n const release = (id: string, g: Glide<TPose>): void => {\n if (glides.get(id) === g) glides.delete(id);\n const nid = id as NodeId;\n scene.overrides.clear(nid, g.entry);\n dirty = true;\n };\n\n const run = (id: string, g: Glide<TPose>, to: TPose, settling: boolean): void => {\n const from = g.entry.pose as TPose;\n g.settling = settling;\n if (samePose(from, to)) {\n animator.cancelKey(keyFor(id));\n g.slot = to;\n g.entry.pose = to;\n if (settling) release(id, g);\n scene.overrides.commit();\n unsubscribeIfIdle();\n return;\n }\n // Already on its way there: whatever is moving it finishes the job.\n if (g.target?.active && samePose(g.slot, to)) return;\n const o = read();\n const lerp = o.geometry?.lerp;\n const onDone = (): void => {\n if (g.target?.active || !g.settling) return;\n release(id, g);\n unsubscribeIfIdle();\n };\n const cancelKey = keyFor(id);\n const label = o.label ?? 'reflow';\n subscribe();\n if (g.target) {\n // A spring interrupts what is moving the node and takes its velocity; a timed glide adds to it.\n if (o.spring) animator.spring<TPose>({ target: g.target, to, ...o.spring, cancelKey, label, onDone });\n else {\n animator.tween<TPose>({\n target: g.target, to, ms: o.ms ?? 200, easing: o.easing, cancelKey, interrupt: false, label, onDone,\n interpolate: lerp && ((a, b, t) => lerp(a, b, t)),\n });\n }\n } else {\n if (!lerp) throw new Error('useAnimatedReflow: this pose has no axes; supply geometry: { ..., lerp }');\n g.slot = to;\n const onTick = (p: number): void => {\n g.entry.pose = lerp(from, to, p);\n dirty = true;\n };\n // The shared key is what retargets: the new animation interrupts the old.\n if (o.spring) animator.spring<number>({ from: 0, to: 1, ...o.spring, cancelKey, label, onTick, onDone });\n else animator.tween<number>({ from: 0, to: 1, ms: o.ms ?? 200, easing: o.easing, cancelKey, label, onTick, onDone });\n }\n scene.overrides.commit();\n };\n\n // False for a glide canceled through the animator: it stopped short of its\n // slot and nothing is carrying it there.\n const onCourse = (id: string, g: Glide<TPose>): boolean =>\n animator.isActive(keyFor(id)) || samePose(g.entry.pose, g.slot);\n\n const stop = (id: string): void => {\n const g = glides.get(id);\n if (!g) return;\n glides.delete(id);\n animator.cancelKey(keyFor(id));\n const nid = id as NodeId;\n scene.overrides.clear(nid, g.entry);\n unsubscribeIfIdle();\n };\n\n const start = (id: string, pose: TPose): Glide<TPose> => {\n const g: Glide<TPose> = { entry: { pose }, slot: pose, settling: false, target: null };\n const descriptor = read().geometry ?? (RECT_POSE_DESCRIPTOR as unknown as PoseDescriptor<TPose>);\n const axes = liveAxes(descriptor, () => g.slot);\n if (axes) {\n g.target = createTarget<TPose>({\n axes,\n get: () => g.slot,\n set: (slot) => { g.slot = slot; },\n show: (shown) => {\n g.entry.pose = shown;\n dirty = true;\n },\n });\n }\n glides.set(id, g);\n return g;\n };\n\n // The glide for `id`, created if absent, starting from `from` when given,\n // and installed as a ply if it is not one: its place among other writers' is left alone.\n const claim = (\n id: string,\n node: PosedNode<TPose>,\n g: Glide<TPose> | undefined,\n from: TPose | undefined,\n ): Glide<TPose> => {\n if (!g) {\n g = start(id, from ?? effectivePose(scene, node));\n } else if (from !== undefined) {\n animator.cancelKey(keyFor(id));\n g.slot = from;\n g.entry.pose = from;\n }\n if (!scene.overrides.has(id as NodeId, g.entry)) scene.overrides.set(id as NodeId, g.entry);\n return g;\n };\n\n return {\n glide(id, pose, opts) {\n const node = scene.get(id as NodeId);\n if (!node) return;\n const from = opts?.from;\n const g = glides.get(id);\n if (g && from === undefined && !g.settling && samePose(g.slot, pose) && onCourse(id, g)) return;\n run(id, claim(id, node, g, from), pose, false);\n },\n settle(id, opts) {\n const from = opts?.from;\n const g = glides.get(id);\n if (!g && from === undefined) return;\n const node = scene.get(id as NodeId);\n if (!node) { stop(id); return; }\n const home = documentPose(scene, node);\n if (g && from === undefined) {\n if (g.settling && samePose(g.slot, home) && onCourse(id, g)) return;\n run(id, g, home, true);\n return;\n }\n run(id, claim(id, node, g, from), home, true);\n },\n stop,\n poseOf: (id) => glides.get(id)?.entry.pose,\n dispose() {\n for (const id of [...glides.keys()]) stop(id);\n },\n };\n}\n\n/**\n * A reflow transition over `animator`. Each node it moves has an animation\n * target whose committed value is the slot the layout wants it shown at, and\n * a new slot is a new animation on that target under the key `reflow:<id>`:\n * a timed glide adds to the ones still running, a spring interrupts them at\n * their velocity, and `animator.cancelKey` reaches one node's. See\n * {@link useAnimatedReflow} for the hook form.\n */\nexport function createReflowTransition<TPose>(\n scene: ReflowScene<TPose>,\n animator: Animator,\n opts: ReflowTransitionOptions<TPose> = {},\n): AnimatedReflow<TPose> {\n return makeReflow(scene, animator, () => opts);\n}\n\n/**\n * Opt a canvas's layout reflow into animation: pass the result to\n * `<SceneCanvas reflowTransition>` and siblings a drag displaces glide to\n * their slots instead of snapping. `null` (or `false`) turns it off, and\n * whatever it was holding snaps home. Option changes apply from the next\n * glide on, without rebuilding.\n */\nexport function useAnimatedReflow<TPose>(\n scene: ReflowScene<TPose>,\n animator: Animator,\n opts: ReflowTransitionOptions<TPose> | null | false | undefined,\n): AnimatedReflow<TPose> | null {\n const latest = useLatest(opts || {});\n const enabled = !!opts;\n const reflow = useMemo(\n () => (enabled ? makeReflow(scene, animator, () => latest.current) : null),\n [enabled, scene, animator, latest],\n );\n useEffect(() => () => reflow?.dispose(), [reflow]);\n return reflow;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reflow.js","names":[],"sources":["../../src/animation/reflow.ts"],"sourcesContent":["import { useEffect, useMemo } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { RECT_POSE_DESCRIPTOR, type PoseDescriptor } from '../core/geometry/poseDescriptor';\nimport { documentPose, effectivePose, type PosedNode, type PoseSource } from '../core/scene/effectivePose';\nimport type { NodeId, PoseOverride, PoseOverrides } from '../core/scene/types';\nimport type { ReflowTransition } from '../layout/types';\nimport { liveAxes } from './poseTarget';\nimport { createTarget, type Target } from '@weasel-js/animator';\nimport type { Animator, EasingSpec, SpringPresetName } from '@weasel-js/animator';\n\n/** How a reflowed sibling travels: a duration and easing, or a spring. */\nexport interface ReflowTransitionOptions<TPose> {\n /** Default 200. Ignored under `spring`. */\n ms?: number;\n easing?: EasingSpec;\n /** Travel on a spring instead of a duration. A retarget starts a new spring\n * from the pose on screen at the speed the node was moving. */\n spring?: {\n preset?: SpringPresetName;\n stiffness?: number;\n damping?: number;\n mass?: number;\n };\n /** Supplies the pose's axes, and its `lerp` is the path a timed glide\n * follows. Default `RECT_POSE_DESCRIPTOR`, each axis in a straight line.\n * A pose with no axes glides by `lerp` alone, and a retarget restarts it. */\n geometry?: PoseDescriptor<TPose>;\n /** Names each glide in `animator.watch` and `live()`. Default `'reflow'`. */\n label?: string;\n}\n\n/** A {@link ReflowTransition} the animator drives. `dispose` drops every\n * override it holds; the transition stays usable afterward. */\nexport interface AnimatedReflow<TPose = unknown> extends ReflowTransition<TPose> {\n dispose(): void;\n /** The same transition over another scene: its animator, timing and label,\n * for poses `geometry` describes given this one's descriptor. Option\n * changes reach it as they reach this one. */\n over<TOther>(\n scene: ReflowScene<TOther>,\n geometry: (mine: PoseDescriptor<TPose>) => PoseDescriptor<TOther>,\n ): AnimatedReflow<TOther>;\n}\n\n/** The part of a scene a reflow transition reads and overrides. */\nexport interface ReflowScene<TPose> extends PoseSource<TPose> {\n readonly overrides: PoseOverrides<TPose>;\n}\n\ninterface Glide<TPose> {\n entry: PoseOverride<TPose>;\n /** Where the layout wants the node shown: what its target commits. */\n slot: TPose;\n settling: boolean;\n /** Null for a pose with no axes, which glides by `lerp`. */\n target: Target<TPose> | null;\n}\n\n/** The animator's cancel-key for one node's glide. */\nconst keyFor = (id: string): string => `reflow:${id}`;\n\nfunction samePoseDeep(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const ka = Object.keys(a);\n if (ka.length !== Object.keys(b).length) return false;\n for (const k of ka) {\n if (!samePoseDeep((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;\n }\n return true;\n}\n\nfunction makeReflow<TPose>(\n scene: ReflowScene<TPose>,\n animator: Animator,\n read: () => ReflowTransitionOptions<TPose>,\n): AnimatedReflow<TPose> {\n return {\n ...makeGlides(scene, animator, read),\n over: (other, geometry) => makeReflow(other, animator, () => {\n const { geometry: own, ...timing } = read();\n return { ...timing, geometry: geometry(own ?? (RECT_POSE_DESCRIPTOR as unknown as PoseDescriptor<TPose>)) };\n }),\n };\n}\n\nfunction makeGlides<TPose>(\n scene: ReflowScene<TPose>,\n animator: Animator,\n read: () => ReflowTransitionOptions<TPose>,\n): Omit<AnimatedReflow<TPose>, 'over'> {\n const glides = new Map<string, Glide<TPose>>();\n let dirty = false;\n let unsubscribe: (() => void) | null = null;\n\n // Ticks only mark the frame dirty; one commit per animator frame publishes\n // every glide at once.\n const subscribe = (): void => {\n if (unsubscribe) return;\n unsubscribe = animator.onTick(() => {\n if (!dirty) return;\n dirty = false;\n scene.overrides.commit();\n });\n };\n const unsubscribeIfIdle = (): void => {\n if (glides.size > 0 || !unsubscribe) return;\n unsubscribe();\n unsubscribe = null;\n };\n\n const release = (id: string, g: Glide<TPose>): void => {\n if (glides.get(id) === g) glides.delete(id);\n const nid = id as NodeId;\n scene.overrides.clear(nid, g.entry);\n dirty = true;\n };\n\n const run = (id: string, g: Glide<TPose>, to: TPose, settling: boolean): void => {\n const from = g.entry.pose as TPose;\n g.settling = settling;\n if (samePoseDeep(from, to)) {\n animator.cancelKey(keyFor(id));\n g.slot = to;\n g.entry.pose = to;\n if (settling) release(id, g);\n scene.overrides.commit();\n unsubscribeIfIdle();\n return;\n }\n // Already on its way there: whatever is moving it finishes the job.\n if (g.target?.active && samePoseDeep(g.slot, to)) return;\n const o = read();\n const lerp = o.geometry?.lerp;\n const onDone = (): void => {\n if (g.target?.active || !g.settling) return;\n release(id, g);\n unsubscribeIfIdle();\n };\n const cancelKey = keyFor(id);\n const label = o.label ?? 'reflow';\n subscribe();\n if (g.target) {\n // A spring interrupts what is moving the node and takes its velocity; a timed glide adds to it.\n if (o.spring) animator.spring<TPose>({ target: g.target, to, ...o.spring, cancelKey, label, onDone });\n else {\n animator.tween<TPose>({\n target: g.target, to, ms: o.ms ?? 200, easing: o.easing, cancelKey, interrupt: false, label, onDone,\n interpolate: lerp && ((a, b, t) => lerp(a, b, t)),\n });\n }\n } else {\n if (!lerp) throw new Error('useAnimatedReflow: this pose has no axes; supply geometry: { ..., lerp }');\n g.slot = to;\n const onTick = (p: number): void => {\n g.entry.pose = lerp(from, to, p);\n dirty = true;\n };\n // The shared key is what retargets: the new animation interrupts the old.\n if (o.spring) animator.spring<number>({ from: 0, to: 1, ...o.spring, cancelKey, label, onTick, onDone });\n else animator.tween<number>({ from: 0, to: 1, ms: o.ms ?? 200, easing: o.easing, cancelKey, label, onTick, onDone });\n }\n scene.overrides.commit();\n };\n\n // False for a glide canceled through the animator: it stopped short of its\n // slot and nothing is carrying it there.\n const onCourse = (id: string, g: Glide<TPose>): boolean =>\n animator.isActive(keyFor(id)) || samePoseDeep(g.entry.pose, g.slot);\n\n const stop = (id: string): void => {\n const g = glides.get(id);\n if (!g) return;\n glides.delete(id);\n animator.cancelKey(keyFor(id));\n const nid = id as NodeId;\n scene.overrides.clear(nid, g.entry);\n unsubscribeIfIdle();\n };\n\n const start = (id: string, pose: TPose): Glide<TPose> => {\n const g: Glide<TPose> = { entry: { pose }, slot: pose, settling: false, target: null };\n const descriptor = read().geometry ?? (RECT_POSE_DESCRIPTOR as unknown as PoseDescriptor<TPose>);\n const axes = liveAxes(descriptor, () => g.slot);\n if (axes) {\n g.target = createTarget<TPose>({\n axes,\n get: () => g.slot,\n set: (slot) => { g.slot = slot; },\n show: (shown) => {\n g.entry.pose = shown;\n dirty = true;\n },\n });\n }\n glides.set(id, g);\n return g;\n };\n\n // The glide for `id`, created if absent, starting from `from` when given,\n // and installed as a ply if it is not one: its place among other writers' is left alone.\n const claim = (\n id: string,\n node: PosedNode<TPose>,\n g: Glide<TPose> | undefined,\n from: TPose | undefined,\n ): Glide<TPose> => {\n if (!g) {\n g = start(id, from ?? effectivePose(scene, node));\n } else if (from !== undefined) {\n animator.cancelKey(keyFor(id));\n g.slot = from;\n g.entry.pose = from;\n }\n if (!scene.overrides.has(id as NodeId, g.entry)) scene.overrides.set(id as NodeId, g.entry);\n return g;\n };\n\n return {\n glide(id, pose, opts) {\n const node = scene.get(id as NodeId);\n if (!node) return;\n const from = opts?.from;\n const g = glides.get(id);\n if (g && from === undefined && !g.settling && samePoseDeep(g.slot, pose) && onCourse(id, g)) return;\n run(id, claim(id, node, g, from), pose, false);\n },\n settle(id, opts) {\n const from = opts?.from;\n const g = glides.get(id);\n if (!g && from === undefined) return;\n const node = scene.get(id as NodeId);\n if (!node) { stop(id); return; }\n const home = documentPose(scene, node);\n if (g && from === undefined) {\n if (g.settling && samePoseDeep(g.slot, home) && onCourse(id, g)) return;\n run(id, g, home, true);\n return;\n }\n run(id, claim(id, node, g, from), home, true);\n },\n stop,\n poseOf: (id) => glides.get(id)?.entry.pose,\n dispose() {\n for (const id of [...glides.keys()]) stop(id);\n },\n };\n}\n\n/**\n * A reflow transition over `animator`. Each node it moves has an animation\n * target whose committed value is the slot the layout wants it shown at, and\n * a new slot is a new animation on that target under the key `reflow:<id>`:\n * a timed glide adds to the ones still running, a spring interrupts them at\n * their velocity, and `animator.cancelKey` reaches one node's. See\n * {@link useAnimatedReflow} for the hook form.\n */\nexport function createReflowTransition<TPose>(\n scene: ReflowScene<TPose>,\n animator: Animator,\n opts: ReflowTransitionOptions<TPose> = {},\n): AnimatedReflow<TPose> {\n return makeReflow(scene, animator, () => opts);\n}\n\n/**\n * Opt a canvas's layout reflow into animation: pass the result to\n * `<SceneCanvas reflowTransition>` and siblings a drag displaces glide to\n * their slots instead of snapping. `null` (or `false`) turns it off, and\n * whatever it was holding snaps home. Option changes apply from the next\n * glide on, without rebuilding.\n */\nexport function useAnimatedReflow<TPose>(\n scene: ReflowScene<TPose>,\n animator: Animator,\n opts: ReflowTransitionOptions<TPose> | null | false | undefined,\n): AnimatedReflow<TPose> | null {\n const latest = useLatest(opts || {});\n const enabled = !!opts;\n const reflow = useMemo(\n () => (enabled ? makeReflow(scene, animator, () => latest.current) : null),\n [enabled, scene, animator, latest],\n );\n useEffect(() => () => reflow?.dispose(), [reflow]);\n return reflow;\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { AUTO_POSE_DESCRIPTOR } from "../../core/geometry/autoPoseDescriptor.js";
|
|
2
2
|
import { createRigSeat } from "./rigSeat.js";
|
|
3
3
|
import { useSeatedRig } from "./useRig.js";
|
|
4
|
-
import { poseFrameInverseOf, poseFrameOf } from "@weasel-js/geom";
|
|
4
|
+
import { applyToPoint, poseFrameInverseOf, poseFrameOf } from "@weasel-js/geom";
|
|
5
5
|
import { useMemo, useState, useSyncExternalStore } from "react";
|
|
6
6
|
|
|
7
7
|
// src/animation/rig/rigNode.ts
|
|
8
|
-
var through = (m, x, y) => m ?
|
|
8
|
+
var through = (m, x, y) => m ? applyToPoint(m, x, y) : [x, y];
|
|
9
9
|
/**
|
|
10
10
|
* A skeleton as a rig node's data keeps it, moved to where the node's pose puts it: its root
|
|
11
11
|
* joints are offset from the pose box's top-left corner and turned as the node is. Joints
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rigNode.js","names":[],"sources":["../../../src/animation/rig/rigNode.ts"],"sourcesContent":["import { useMemo, useState, useSyncExternalStore } from 'react';\nimport { poseFrameInverseOf, poseFrameOf } from '@weasel-js/geom';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport type { PoseDescriptor } from '../../core/geometry/poseDescriptor';\nimport type { NodeId } from '../../core/scene/types';\nimport type { BindRigOptions, Rig, RigScene } from './bindRig';\nimport type { Skin } from '@weasel-js/animator';\nimport type { Skeleton } from '@weasel-js/animator';\nimport { createRigSeat } from './rigSeat';\nimport { useSeatedRig } from './useRig';\n\n/** What a rig reads from node data: a `skeleton` on the node that is the rig, and on the nodes\n * under it a `skin` for a path that bends or a `joint` for a node that rides one whole. */\nexport interface RigNodeData {\n /** Joints relative to the node's pose: its box's top-left corner, turned as the node is. */\n skeleton?: Skeleton;\n skin?: Skin;\n joint?: string;\n}\n\n/** Weights to bind in place of the document's, and word of when they change. `@weasel-js/rig`'s\n * `RigEditState` is one: it holds what a stroke of the weight tool has painted and not yet written. */\nexport interface PaintedSkins {\n /** By node. A new map whenever the weights change. */\n painted(): ReadonlyMap<NodeId, Skin>;\n listen(fn: () => void): () => void;\n}\n\n/** The part of a scene `rigFromNode` reads. */\nexport interface RigNodeSource<TPose> {\n get(id: NodeId): { pose: TPose; data?: unknown } | undefined;\n childrenOf(id: NodeId): readonly NodeId[];\n}\n\n/** The skeleton, bindings, and skins a rig node declares, as `bindRig` takes them. */\nexport interface RigNodeParts {\n skeleton: Skeleton;\n bindings: Record<string, NodeId[]>;\n skins: Record<NodeId, Skin>;\n}\n\ninterface Reading extends RigNodeParts {\n /** Every node, pose, and data object the parts were read from, to tell when they are stale. */\n refs: unknown[];\n}\n\ntype Affine = readonly [number, number, number, number, number, number];\nconst through = (m: Affine | null, x: number, y: number): [number, number] =>\n (m ? [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]] : [x, y]);\n\n/**\n * A skeleton as a rig node's data keeps it, moved to where the node's pose puts it: its root\n * joints are offset from the pose box's top-left corner and turned as the node is. Joints\n * under a root are relative to their parents and are left as they are.\n */\nexport function placeSkeleton<TPose>(\n skeleton: Skeleton,\n pose: TPose,\n descriptor: PoseDescriptor<TPose> = AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>,\n): Skeleton {\n const box = descriptor.getBounds(pose);\n const frame = poseFrameOf(pose);\n const turn = descriptor.getRotation?.(pose) ?? 0;\n return {\n joints: skeleton.joints.map((joint) => {\n if (joint.parent != null) return joint;\n const [x, y] = through(frame, box.x + joint.bind.x, box.y + joint.bind.y);\n return { ...joint, bind: { ...joint.bind, x, y, rotation: joint.bind.rotation + turn } };\n }),\n };\n}\n\n/** The inverse of {@link placeSkeleton}: a skeleton in the scene's space, as the node at\n * `pose` would keep it in its data. */\nexport function unplaceSkeleton<TPose>(\n skeleton: Skeleton,\n pose: TPose,\n descriptor: PoseDescriptor<TPose> = AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>,\n): Skeleton {\n const box = descriptor.getBounds(pose);\n const inverse = poseFrameInverseOf(pose);\n const turn = descriptor.getRotation?.(pose) ?? 0;\n return {\n joints: skeleton.joints.map((joint) => {\n if (joint.parent != null) return joint;\n const [x, y] = through(inverse, joint.bind.x, joint.bind.y);\n return { ...joint, bind: { ...joint.bind, x: x - box.x, y: y - box.y, rotation: joint.bind.rotation - turn } };\n }),\n };\n}\n\nconst UNPAINTED: ReadonlyMap<NodeId, Skin> = new Map();\nconst noWord = (): (() => void) => () => {};\nconst unpainted = (): ReadonlyMap<NodeId, Skin> => UNPAINTED;\n\nfunction read<TPose>(\n scene: RigNodeSource<TPose>,\n id: NodeId,\n descriptor: PoseDescriptor<TPose>,\n painted: ReadonlyMap<NodeId, Skin> = UNPAINTED,\n): Reading {\n const node = scene.get(id);\n if (!node) throw new Error(`rigFromNode: the rig is node \"${id}\", which the scene lacks.`);\n const local = (node.data as RigNodeData | null | undefined)?.skeleton;\n if (!local) throw new Error(`rigFromNode: node \"${id}\" has no skeleton in its data.`);\n\n const skeleton = placeSkeleton(local, node.pose, descriptor);\n\n const out: Reading = { skeleton, bindings: {}, skins: {}, refs: [node.pose, node.data] };\n const walk = (parent: NodeId): void => {\n for (const child of scene.childrenOf(parent)) {\n const n = scene.get(child);\n if (!n) continue;\n const data = n.data as RigNodeData | null | undefined;\n const skin = painted.get(child) ?? data?.skin;\n out.refs.push(child, n.pose, n.data, skin);\n if (skin) out.skins[child] = skin;\n else if (data?.joint !== undefined) (out.bindings[data.joint] ??= []).push(child);\n walk(child);\n }\n };\n walk(id);\n return out;\n}\n\nconst sameRefs = (a: Reading, b: Reading): boolean =>\n a === b || (a.refs.length === b.refs.length && a.refs.every((r, i) => r === b.refs[i]));\n\n/**\n * Read a rig out of the scene: the skeleton in `id`'s `data.skeleton`, placed by that node's\n * pose, and under it every node whose data has a `skin` or names a `joint`.\n */\nexport function rigFromNode<TPose>(\n scene: RigNodeSource<TPose>,\n id: NodeId,\n descriptor: PoseDescriptor<TPose> = AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>,\n): RigNodeParts {\n const { skeleton, bindings, skins } = read(scene, id, descriptor);\n return { skeleton, bindings, skins };\n}\n\n/** Options for `useRigNode`. */\nexport interface UseRigNodeOptions<TPose>\n extends Pick<BindRigOptions<TPose>, 'apply' | 'descriptor' | 'restPath'> {\n scene: RigScene<TPose> & RigNodeSource<TPose> & {\n subscribe(listener: () => void): () => void;\n getVersion(): number;\n };\n /** The node whose data holds the skeleton. */\n node: NodeId;\n /** Weights bound in place of a node's `data.skin` while they are held. Give it the\n * `RigEditState` `@weasel-js/rig`'s weight tool has, and a stroke bends the path as it is painted. */\n painting?: PaintedSkins;\n}\n\n/**\n * A {@link Rig} read from a node, as {@link rigFromNode} reads it. It is bound again when the\n * rig node or a node under it changes its pose or data, or when nodes under it come or go; an\n * edit elsewhere in the scene leaves it alone. A rig bound again keeps its `target`, with the\n * animations running on it, its committed pose, and its placement, unless the skeleton's\n * joints were added, removed, renamed, or reordered. Then it has a new `target`, which takes\n * over the placement and the pose of every joint the skeleton still names. Animations go on\n * running on the target they were started on and show through the new one, so a joint that\n * was renamed stops moving.\n */\nexport function useRigNode<TPose>(options: UseRigNodeOptions<TPose>): Rig {\n const { scene, node, apply, descriptor, restPath, painting } = options;\n const version = useSyncExternalStore(scene.subscribe, scene.getVersion);\n const painted = useSyncExternalStore(painting?.listen ?? noWord, painting?.painted ?? unpainted);\n const next = useMemo(\n () => read(scene, node, descriptor ?? (AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>), painted),\n // `version` is what says the scene may answer differently.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [scene, node, descriptor, version, painted],\n );\n // Each read makes new objects; the rig must outlive a read that found nothing changed.\n const [held, setHeld] = useState(next);\n const parts = sameRefs(held, next) ? held : next;\n if (parts !== held) setHeld(next);\n const joints = JSON.stringify(parts.skeleton.joints.map((joint) => joint.name));\n // A pose target's axes are its joints' names in order, so the seat holds while those do,\n // and the next one takes over from it.\n const [seated, setSeated] = useState(() => ({ joints, seat: createRigSeat(parts.skeleton) }));\n let { seat } = seated;\n if (seated.joints !== joints) {\n seat = createRigSeat(parts.skeleton, seated.seat);\n setSeated({ joints, seat });\n }\n return useSeatedRig({\n scene, skeleton: parts.skeleton, bindings: parts.bindings, skins: parts.skins, apply, descriptor, restPath,\n }, seat);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rigNode.js","names":[],"sources":["../../../src/animation/rig/rigNode.ts"],"sourcesContent":["import { useMemo, useState, useSyncExternalStore } from 'react';\nimport { applyToPoint, poseFrameInverseOf, poseFrameOf, type Affine } from '@weasel-js/geom';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport type { PoseDescriptor } from '../../core/geometry/poseDescriptor';\nimport type { NodeId } from '../../core/scene/types';\nimport type { BindRigOptions, Rig, RigScene } from './bindRig';\nimport type { Skin } from '@weasel-js/animator';\nimport type { Skeleton } from '@weasel-js/animator';\nimport { createRigSeat } from './rigSeat';\nimport { useSeatedRig } from './useRig';\n\n/** What a rig reads from node data: a `skeleton` on the node that is the rig, and on the nodes\n * under it a `skin` for a path that bends or a `joint` for a node that rides one whole. */\nexport interface RigNodeData {\n /** Joints relative to the node's pose: its box's top-left corner, turned as the node is. */\n skeleton?: Skeleton;\n skin?: Skin;\n joint?: string;\n}\n\n/** Weights to bind in place of the document's, and word of when they change. `@weasel-js/rig`'s\n * `RigEditState` is one: it holds what a stroke of the weight tool has painted and not yet written. */\nexport interface PaintedSkins {\n /** By node. A new map whenever the weights change. */\n painted(): ReadonlyMap<NodeId, Skin>;\n listen(fn: () => void): () => void;\n}\n\n/** The part of a scene `rigFromNode` reads. */\nexport interface RigNodeSource<TPose> {\n get(id: NodeId): { pose: TPose; data?: unknown } | undefined;\n childrenOf(id: NodeId): readonly NodeId[];\n}\n\n/** The skeleton, bindings, and skins a rig node declares, as `bindRig` takes them. */\nexport interface RigNodeParts {\n skeleton: Skeleton;\n bindings: Record<string, NodeId[]>;\n skins: Record<NodeId, Skin>;\n}\n\ninterface Reading extends RigNodeParts {\n /** Every node, pose, and data object the parts were read from, to tell when they are stale. */\n refs: unknown[];\n}\n\nconst through = (m: Affine | null, x: number, y: number): [number, number] => (m ? applyToPoint(m, x, y) : [x, y]);\n\n/**\n * A skeleton as a rig node's data keeps it, moved to where the node's pose puts it: its root\n * joints are offset from the pose box's top-left corner and turned as the node is. Joints\n * under a root are relative to their parents and are left as they are.\n */\nexport function placeSkeleton<TPose>(\n skeleton: Skeleton,\n pose: TPose,\n descriptor: PoseDescriptor<TPose> = AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>,\n): Skeleton {\n const box = descriptor.getBounds(pose);\n const frame = poseFrameOf(pose);\n const turn = descriptor.getRotation?.(pose) ?? 0;\n return {\n joints: skeleton.joints.map((joint) => {\n if (joint.parent != null) return joint;\n const [x, y] = through(frame, box.x + joint.bind.x, box.y + joint.bind.y);\n return { ...joint, bind: { ...joint.bind, x, y, rotation: joint.bind.rotation + turn } };\n }),\n };\n}\n\n/** The inverse of {@link placeSkeleton}: a skeleton in the scene's space, as the node at\n * `pose` would keep it in its data. */\nexport function unplaceSkeleton<TPose>(\n skeleton: Skeleton,\n pose: TPose,\n descriptor: PoseDescriptor<TPose> = AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>,\n): Skeleton {\n const box = descriptor.getBounds(pose);\n const inverse = poseFrameInverseOf(pose);\n const turn = descriptor.getRotation?.(pose) ?? 0;\n return {\n joints: skeleton.joints.map((joint) => {\n if (joint.parent != null) return joint;\n const [x, y] = through(inverse, joint.bind.x, joint.bind.y);\n return { ...joint, bind: { ...joint.bind, x: x - box.x, y: y - box.y, rotation: joint.bind.rotation - turn } };\n }),\n };\n}\n\nconst UNPAINTED: ReadonlyMap<NodeId, Skin> = new Map();\nconst noWord = (): (() => void) => () => {};\nconst unpainted = (): ReadonlyMap<NodeId, Skin> => UNPAINTED;\n\nfunction read<TPose>(\n scene: RigNodeSource<TPose>,\n id: NodeId,\n descriptor: PoseDescriptor<TPose>,\n painted: ReadonlyMap<NodeId, Skin> = UNPAINTED,\n): Reading {\n const node = scene.get(id);\n if (!node) throw new Error(`rigFromNode: the rig is node \"${id}\", which the scene lacks.`);\n const local = (node.data as RigNodeData | null | undefined)?.skeleton;\n if (!local) throw new Error(`rigFromNode: node \"${id}\" has no skeleton in its data.`);\n\n const skeleton = placeSkeleton(local, node.pose, descriptor);\n\n const out: Reading = { skeleton, bindings: {}, skins: {}, refs: [node.pose, node.data] };\n const walk = (parent: NodeId): void => {\n for (const child of scene.childrenOf(parent)) {\n const n = scene.get(child);\n if (!n) continue;\n const data = n.data as RigNodeData | null | undefined;\n const skin = painted.get(child) ?? data?.skin;\n out.refs.push(child, n.pose, n.data, skin);\n if (skin) out.skins[child] = skin;\n else if (data?.joint !== undefined) (out.bindings[data.joint] ??= []).push(child);\n walk(child);\n }\n };\n walk(id);\n return out;\n}\n\nconst sameRefs = (a: Reading, b: Reading): boolean =>\n a === b || (a.refs.length === b.refs.length && a.refs.every((r, i) => r === b.refs[i]));\n\n/**\n * Read a rig out of the scene: the skeleton in `id`'s `data.skeleton`, placed by that node's\n * pose, and under it every node whose data has a `skin` or names a `joint`.\n */\nexport function rigFromNode<TPose>(\n scene: RigNodeSource<TPose>,\n id: NodeId,\n descriptor: PoseDescriptor<TPose> = AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>,\n): RigNodeParts {\n const { skeleton, bindings, skins } = read(scene, id, descriptor);\n return { skeleton, bindings, skins };\n}\n\n/** Options for `useRigNode`. */\nexport interface UseRigNodeOptions<TPose>\n extends Pick<BindRigOptions<TPose>, 'apply' | 'descriptor' | 'restPath'> {\n scene: RigScene<TPose> & RigNodeSource<TPose> & {\n subscribe(listener: () => void): () => void;\n getVersion(): number;\n };\n /** The node whose data holds the skeleton. */\n node: NodeId;\n /** Weights bound in place of a node's `data.skin` while they are held. Give it the\n * `RigEditState` `@weasel-js/rig`'s weight tool has, and a stroke bends the path as it is painted. */\n painting?: PaintedSkins;\n}\n\n/**\n * A {@link Rig} read from a node, as {@link rigFromNode} reads it. It is bound again when the\n * rig node or a node under it changes its pose or data, or when nodes under it come or go; an\n * edit elsewhere in the scene leaves it alone. A rig bound again keeps its `target`, with the\n * animations running on it, its committed pose, and its placement, unless the skeleton's\n * joints were added, removed, renamed, or reordered. Then it has a new `target`, which takes\n * over the placement and the pose of every joint the skeleton still names. Animations go on\n * running on the target they were started on and show through the new one, so a joint that\n * was renamed stops moving.\n */\nexport function useRigNode<TPose>(options: UseRigNodeOptions<TPose>): Rig {\n const { scene, node, apply, descriptor, restPath, painting } = options;\n const version = useSyncExternalStore(scene.subscribe, scene.getVersion);\n const painted = useSyncExternalStore(painting?.listen ?? noWord, painting?.painted ?? unpainted);\n const next = useMemo(\n () => read(scene, node, descriptor ?? (AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>), painted),\n // `version` is what says the scene may answer differently.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [scene, node, descriptor, version, painted],\n );\n // Each read makes new objects; the rig must outlive a read that found nothing changed.\n const [held, setHeld] = useState(next);\n const parts = sameRefs(held, next) ? held : next;\n if (parts !== held) setHeld(next);\n const joints = JSON.stringify(parts.skeleton.joints.map((joint) => joint.name));\n // A pose target's axes are its joints' names in order, so the seat holds while those do,\n // and the next one takes over from it.\n const [seated, setSeated] = useState(() => ({ joints, seat: createRigSeat(parts.skeleton) }));\n let { seat } = seated;\n if (seated.joints !== joints) {\n seat = createRigSeat(parts.skeleton, seated.seat);\n setSeated({ joints, seat });\n }\n return useSeatedRig({\n scene, skeleton: parts.skeleton, bindings: parts.bindings, skins: parts.skins, apply, descriptor, restPath,\n }, seat);\n}\n"],"mappings":"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"}
|
package/dist/barrel/input.js
CHANGED
|
@@ -10,10 +10,11 @@ import { renderNeeds } from "../canvas/renderNeeds.js";
|
|
|
10
10
|
import { createRasterSession, planPixelRender, renderSceneToPixels, warmRender } from "../canvas/renderSceneToPixels.js";
|
|
11
11
|
import { debugSnapshotArgs, renderDebugSnapshot } from "../canvas/renderDebugSnapshot.js";
|
|
12
12
|
import { rasterToPng } from "../canvas/rasterToPng.js";
|
|
13
|
+
import { usePublishPointer } from "../input/usePublishPointer.js";
|
|
13
14
|
import { PointerProviderIfRoot } from "../canvas/SceneCanvas/PointerProviderIfRoot.js";
|
|
14
15
|
import { ActionsProviderIfRoot } from "../canvas/SceneCanvas/ActionsProviderIfRoot.js";
|
|
15
16
|
import { DepRegistryProviderIfRoot } from "../canvas/SceneCanvas/DepRegistryProviderIfRoot.js";
|
|
16
17
|
import { LONG_PRESS_MS, activatesFocusedControl, createLongPressStore as createLongPressStore$1, focusedControlOwnsKey, historyKey, isEditableTarget as isEditableTarget$1, matchesKeyBinding } from "@weasel-js/routing";
|
|
17
18
|
import { PointerContextProvider as PointerContextProvider$1, createPointerStore, usePointerContext as usePointerContext$1, usePointerPosition } from "@weasel-js/routing/react";
|
|
18
19
|
|
|
19
|
-
export { ActionsProviderIfRoot, DepRegistryProviderIfRoot, LONG_PRESS_FEEDBACK_ID, LONG_PRESS_MS, PointerContextProvider$1 as PointerContextProvider, PointerProviderIfRoot, activatesFocusedControl, buildSceneViewCommands, createKeyState, createLongPressFeedbackContribution, createLongPressStore$1 as createLongPressStore, createPointerStore, createRasterSession, debugSnapshotArgs, focusedControlOwnsKey, forEachCoalesced, getStylusData, historyKey, isEditableTarget$1 as isEditableTarget, matchesKeyBinding, planPixelRender, pressureToWidth, rasterToPng, releaseCanvasRenderer, renderDebugSnapshot, renderNeeds, renderSceneToCanvas, renderSceneToPixels, useKeyModifiers, useKeyState, usePointerContext$1 as usePointerContext, usePointerPosition, usePointerStylus, warmRender };
|
|
20
|
+
export { ActionsProviderIfRoot, DepRegistryProviderIfRoot, LONG_PRESS_FEEDBACK_ID, LONG_PRESS_MS, PointerContextProvider$1 as PointerContextProvider, PointerProviderIfRoot, activatesFocusedControl, buildSceneViewCommands, createKeyState, createLongPressFeedbackContribution, createLongPressStore$1 as createLongPressStore, createPointerStore, createRasterSession, debugSnapshotArgs, focusedControlOwnsKey, forEachCoalesced, getStylusData, historyKey, isEditableTarget$1 as isEditableTarget, matchesKeyBinding, planPixelRender, pressureToWidth, rasterToPng, releaseCanvasRenderer, renderDebugSnapshot, renderNeeds, renderSceneToCanvas, renderSceneToPixels, useKeyModifiers, useKeyState, usePointerContext$1 as usePointerContext, usePointerPosition, usePointerStylus, usePublishPointer, warmRender };
|
package/dist/barrel/paint.js
CHANGED
|
@@ -5,8 +5,8 @@ import { DEFAULT_FILL_COLOR, DEFAULT_PALETTE, DEFAULT_STROKE_COLOR, GHOST_STROKE
|
|
|
5
5
|
import { asPaint } from "../core/paint/paintKindEntry.js";
|
|
6
6
|
import { useGradientKinds, usePaintKind, usePaintKinds } from "../core/usePaintKinds.js";
|
|
7
7
|
import { commandFill as commandFill$1, commandPaint as commandPaint$1, commandStroke as commandStroke$1, createTilePattern, fillInPoseFrame, fillToBoundsFrame, isPaintKindKnown as isPaintKindKnown$1, resolveFillPattern, resolvePatternSpec, warmPaintKinds as warmPaintKinds$1 } from "@weasel-js/renderer";
|
|
8
|
-
import { INHERIT as INHERIT$1, STROKE_DASH_RATIOS, TILE_KINDS, alignedStrokeRect as alignedStrokeRect$1, composePatternTransform, contrastLineColor, contrastRatio, dashForStrokeStyle as dashForStrokeStyle$1, decomposePatternTransform, gradientForBounds as gradientForBounds$1, gradientGeometry, hexToRgbBytes, hexToRgba, isGradientPaint as isGradientPaint$1, isImagePatternPaint, isPatternPaint, isSolidPaint, isTileKind, isTilePaint as isTilePaint$1, lerpFill as lerpFill$1, lerpPaint, lerpStroke as lerpStroke$1, normalizeHex, parseColor, parseColorToRgba255, parseHexBytes, patternPlacementOf, resolveColor, resolveFill as resolveFill$1, resolveScreenLength as resolveScreenLength$1, resolveStroke as resolveStroke$1, rgbaToHex, sampleGradientStops, scaleVertexWidth as scaleVertexWidth$1, strokeDashStyleOf as strokeDashStyleOf$1, tileSpecOf, vertexHalfWidths as vertexHalfWidths$1, vertexWidthFromSides as vertexWidthFromSides$1, wcagLevels, widestHalfWidth, withGradientKind as withGradientKind$1 } from "@weasel-js/paint";
|
|
8
|
+
import { INHERIT as INHERIT$1, STROKE_DASH_RATIOS, TILE_KINDS, alignedStrokeRect as alignedStrokeRect$1, composePatternTransform, contrastLineColor, contrastRatio, dashForStrokeStyle as dashForStrokeStyle$1, decomposePatternTransform, gradientForBounds as gradientForBounds$1, gradientGeometry, hexToRgbBytes, hexToRgba, isGradientPaint as isGradientPaint$1, isImagePatternPaint, isPatternPaint, isSolidPaint, isTileKind, isTilePaint as isTilePaint$1, lerpFill as lerpFill$1, lerpPaint, lerpStroke as lerpStroke$1, luminanceContrast, normalizeHex, parseColor, parseColorToRgba255, parseHexBytes, patternPlacementOf, relativeLuminance, resolveColor, resolveFill as resolveFill$1, resolveScreenLength as resolveScreenLength$1, resolveStroke as resolveStroke$1, rgbaToHex, sampleGradientStops, scaleVertexWidth as scaleVertexWidth$1, strokeDashStyleOf as strokeDashStyleOf$1, tileSpecOf, vertexHalfWidths as vertexHalfWidths$1, vertexWidthFromSides as vertexWidthFromSides$1, wcagLevels, widestHalfWidth, withGradientKind as withGradientKind$1 } from "@weasel-js/paint";
|
|
9
9
|
import { BUILTIN_MARKERS, getMarker, listMarkers as listMarkers$1, markerInset, markerKeyOf, markerRegistry, markerSites, registerMarker, resolveMarkerSize, resolveStrokeWidth as resolveStrokeWidth$1, strokeInsets as strokeInsets$1 } from "@weasel-js/stroke";
|
|
10
10
|
import { trimPolyline } from "@weasel-js/geom/tessellate";
|
|
11
11
|
|
|
12
|
-
export { BUILTIN_MARKERS, DEFAULT_FILL_COLOR, DEFAULT_PALETTE, DEFAULT_STROKE_COLOR, GHOST_STROKE, INHERIT$1 as INHERIT, STROKE_DASH_RATIOS, TILE_KINDS, alignedStrokeRect$1 as alignedStrokeRect, asPaint, commandFill$1 as commandFill, commandPaint$1 as commandPaint, commandStroke$1 as commandStroke, composePatternTransform, contrastLineColor, contrastRatio, createTilePattern, cycleFill, dashForStrokeStyle$1 as dashForStrokeStyle, decomposePatternTransform, fillInPoseFrame, fillToBoundsFrame, getAlpha01, getMarker, getPaintKind, gradientForBounds$1 as gradientForBounds, gradientGeometry, hexToRgbBytes, hexToRgba, isGradientPaint$1 as isGradientPaint, isImagePatternPaint, isPaintKindKnown$1 as isPaintKindKnown, isPatternPaint, isSolidPaint, isTileKind, isTilePaint$1 as isTilePaint, lerpFill$1 as lerpFill, lerpPaint, lerpStroke$1 as lerpStroke, listGradientKinds, listMarkers$1 as listMarkers, listPaintKinds, markerInset, markerKeyOf, markerRegistry, markerSites, mergeAlphaFromPrev, normalizeHex, paintAlpha, paintKindOf, paintKindRegistry, paintWithAlpha, paintWithColor, parseColor, parseColorToRgba255, parseHexBytes, pathPaintOf, patternFamily, patternPlacementOf, registerMarker, registerPaintKind, registerPaintKindLoader, resolveColor, resolveFill$1 as resolveFill, resolveFillPattern, resolveMarkerSize, resolvePatternSpec, resolveScreenLength$1 as resolveScreenLength, resolveStroke$1 as resolveStroke, resolveStrokeWidth$1 as resolveStrokeWidth, rgbaToHex, sampleGradientStops, scaleVertexWidth$1 as scaleVertexWidth, seedTile, solid, strokeDashStyleOf$1 as strokeDashStyleOf, strokeInsets$1 as strokeInsets, strokeOf, strokeWith, switchGradientKind, tileSpecOf, toHex8, trimPolyline, useGradientKinds, usePaintKind, usePaintKinds, vertexHalfWidths$1 as vertexHalfWidths, vertexWidthFromSides$1 as vertexWidthFromSides, warmPaintKinds$1 as warmPaintKinds, wcagLevels, widestHalfWidth, withAlpha01, withGradientKind$1 as withGradientKind };
|
|
12
|
+
export { BUILTIN_MARKERS, DEFAULT_FILL_COLOR, DEFAULT_PALETTE, DEFAULT_STROKE_COLOR, GHOST_STROKE, INHERIT$1 as INHERIT, STROKE_DASH_RATIOS, TILE_KINDS, alignedStrokeRect$1 as alignedStrokeRect, asPaint, commandFill$1 as commandFill, commandPaint$1 as commandPaint, commandStroke$1 as commandStroke, composePatternTransform, contrastLineColor, contrastRatio, createTilePattern, cycleFill, dashForStrokeStyle$1 as dashForStrokeStyle, decomposePatternTransform, fillInPoseFrame, fillToBoundsFrame, getAlpha01, getMarker, getPaintKind, gradientForBounds$1 as gradientForBounds, gradientGeometry, hexToRgbBytes, hexToRgba, isGradientPaint$1 as isGradientPaint, isImagePatternPaint, isPaintKindKnown$1 as isPaintKindKnown, isPatternPaint, isSolidPaint, isTileKind, isTilePaint$1 as isTilePaint, lerpFill$1 as lerpFill, lerpPaint, lerpStroke$1 as lerpStroke, listGradientKinds, listMarkers$1 as listMarkers, listPaintKinds, luminanceContrast, markerInset, markerKeyOf, markerRegistry, markerSites, mergeAlphaFromPrev, normalizeHex, paintAlpha, paintKindOf, paintKindRegistry, paintWithAlpha, paintWithColor, parseColor, parseColorToRgba255, parseHexBytes, pathPaintOf, patternFamily, patternPlacementOf, registerMarker, registerPaintKind, registerPaintKindLoader, relativeLuminance, resolveColor, resolveFill$1 as resolveFill, resolveFillPattern, resolveMarkerSize, resolvePatternSpec, resolveScreenLength$1 as resolveScreenLength, resolveStroke$1 as resolveStroke, resolveStrokeWidth$1 as resolveStrokeWidth, rgbaToHex, sampleGradientStops, scaleVertexWidth$1 as scaleVertexWidth, seedTile, solid, strokeDashStyleOf$1 as strokeDashStyleOf, strokeInsets$1 as strokeInsets, strokeOf, strokeWith, switchGradientKind, tileSpecOf, toHex8, trimPolyline, useGradientKinds, usePaintKind, usePaintKinds, vertexHalfWidths$1 as vertexHalfWidths, vertexWidthFromSides$1 as vertexWidthFromSides, warmPaintKinds$1 as warmPaintKinds, wcagLevels, widestHalfWidth, withAlpha01, withGradientKind$1 as withGradientKind };
|
package/dist/barrel/viewport.js
CHANGED
|
@@ -22,11 +22,11 @@ import { COARSE_TARGET_SCALE, DEFAULT_DEVICE_PROFILE as DEFAULT_DEVICE_PROFILE$1
|
|
|
22
22
|
import { fitToBounds, fitZoom } from "../core/viewport/fitToBounds.js";
|
|
23
23
|
import { adjacentPage, layoutPages, pageAt, pageHolding, pageName, pageOfBounds, pagesBounds } from "../core/pages/pages.js";
|
|
24
24
|
import { pagesLayer } from "../core/pages/pagesLayer.js";
|
|
25
|
+
import { viewportWorldRect } from "../core/viewport/viewportWorldRect.js";
|
|
25
26
|
import { fitViewToPage, pageInView } from "../core/pages/fitViewToPage.js";
|
|
26
27
|
import { PAGES_DOC_KEY, addPage, movePage, nextPageId, pageMembers, pagesOf, placedPagesOf, removePage, renamePage, resizePage, setPages } from "../core/pages/pageEdits.js";
|
|
27
28
|
import { axisZoomFactor, zoomAt } from "../core/viewport/zoomAt.js";
|
|
28
29
|
import { clampView } from "../core/viewport/clampView.js";
|
|
29
|
-
import { viewportWorldRect } from "../core/viewport/viewportWorldRect.js";
|
|
30
30
|
import { sceneNodeClientRect } from "../core/viewport/sceneNodeClientRect.js";
|
|
31
31
|
import { computeFitViewport, useAutoCenter } from "../core/viewport/useAutoCenter.js";
|
|
32
32
|
import { WHEEL_ZOOM_STEP, computeWheelAction, wheelPan, wheelZoom, wheelZoomFactor } from "../core/viewport/wheelHandler.js";
|
|
@@ -43,10 +43,10 @@ import { SceneViewCanvas } from "../canvas/SceneViewCanvas.js";
|
|
|
43
43
|
import { DrawCanvas } from "../canvas/DrawCanvas.js";
|
|
44
44
|
import { FALLBACK_ACCENT, accentColorOf } from "../canvas/accentColor.js";
|
|
45
45
|
import { gateLayer } from "../core/layers/gateLayer.js";
|
|
46
|
-
import { meanScale as meanScale$1, pxExtent as pxExtent$1, scaleDelta as scaleDelta$1, screenAngleOf as screenAngleOf$1, standoff as standoff$1, withinPxBox, withinPxRadius as withinPxRadius$1 } from "@weasel-js/geom";
|
|
46
|
+
import { meanScale as meanScale$1, pxExtent as pxExtent$1, scaleDelta as scaleDelta$1, screenAngleOf as screenAngleOf$1, standoff as standoff$1, viewRectToScreen as viewRectToScreen$1, viewScreenToWorld as viewScreenToWorld$1, viewWorldToScreen as viewWorldToScreen$1, withinPxBox, withinPxRadius as withinPxRadius$1 } from "@weasel-js/geom";
|
|
47
47
|
import { DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_VELOCITY_DECAY, createSimulation } from "@weasel-js/simulation";
|
|
48
48
|
import { useSimulation } from "@weasel-js/simulation/react";
|
|
49
49
|
import { useCanvasFocus, useVisibleRaf as useVisibleRaf$1 } from "@weasel-js/react";
|
|
50
50
|
import { clientToCanvas as clientToCanvas$1 } from "@weasel-js/routing";
|
|
51
51
|
|
|
52
|
-
export { ANCHOR_HIT_BASE_PX, COARSE_TARGET_SCALE, CanvasView, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEVICE_PROFILE$1 as DEFAULT_DEVICE_PROFILE, DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM, DEFAULT_VELOCITY_DECAY, DeviceProfileProvider, DrawCanvas, FALLBACK_ACCENT, HANDLE_BASE_PX, PAGES_DOC_KEY, ROTATION_HANDLE_BASE_PX, SceneViewCanvas, VERSION, VIEW_ANIMATION_KEY, VIEW_MARKER_DEFAULT_STYLE, VIEW_MARKER_DRAG, VIEW_MARKER_HIT_SLOP_PX, ViewMarker, WHEEL_ZOOM_STEP, ZOOM_FLOOR, accentColorOf, addPage, adjacentPage, aimSpace2d, axisZoomFactor, clampView, clientToCanvas$1 as clientToCanvas, computeFitViewport, computeWheelAction, createSimulation, createVelocityTracker, createViewMarkerContribution, createViewMarkerLayer, createViewResolver, createViewportLayer, deriveParallaxView, fitToBounds, fitViewToBounds, fitViewToPage, fitZoom, fromPlane, gateLayer, grabViewMarker, interpolateView, isPlainObject, isRectAim, layoutPages, meanScale$1 as meanScale, movePage, nextPageId, normalizeView, normalizeZoom$1 as normalizeZoom, pageAt, pageHolding, pageInView, pageMembers, pageName, pageOfBounds, pagesBounds, pagesLayer, pagesOf, placedPagesOf, planeMap, planeMatrix, planeToPlane, pxExtent$1 as pxExtent, rectAim, rectFromPlane, rectToPlane, removePage, renamePage, resizePage, resolveDeviceProfile$1 as resolveDeviceProfile, scaleDelta$1 as scaleDelta, sceneNodeClientRect, screenAngleOf$1 as screenAngleOf, screenToWorld, setPages, stableStringify, standoff$1 as standoff, startThresholdDrag, toPlane, useAutoCenter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDragHandle, useDropZone, useElementSize, useHandleDrag, useNodeOverlayFrame, usePoseRun, useSimulation, useVelocityTracker, useViewAnimation, useViewAnimationOn, useVisibleRaf$1 as useVisibleRaf, viewAxes, viewForAim, viewMarkerDragAction, viewToTransform, viewZoom, viewportWorldRect, watchDevicePixelRatio, wheelPan, wheelZoom, wheelZoomFactor, withinPxBox, withinPxRadius$1 as withinPxRadius, worldToScreen, zoomAt };
|
|
52
|
+
export { ANCHOR_HIT_BASE_PX, COARSE_TARGET_SCALE, CanvasView, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEVICE_PROFILE$1 as DEFAULT_DEVICE_PROFILE, DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM, DEFAULT_VELOCITY_DECAY, DeviceProfileProvider, DrawCanvas, FALLBACK_ACCENT, HANDLE_BASE_PX, PAGES_DOC_KEY, ROTATION_HANDLE_BASE_PX, SceneViewCanvas, VERSION, VIEW_ANIMATION_KEY, VIEW_MARKER_DEFAULT_STYLE, VIEW_MARKER_DRAG, VIEW_MARKER_HIT_SLOP_PX, ViewMarker, WHEEL_ZOOM_STEP, ZOOM_FLOOR, accentColorOf, addPage, adjacentPage, aimSpace2d, axisZoomFactor, clampView, clientToCanvas$1 as clientToCanvas, computeFitViewport, computeWheelAction, createSimulation, createVelocityTracker, createViewMarkerContribution, createViewMarkerLayer, createViewResolver, createViewportLayer, deriveParallaxView, fitToBounds, fitViewToBounds, fitViewToPage, fitZoom, fromPlane, gateLayer, grabViewMarker, interpolateView, isPlainObject, isRectAim, layoutPages, meanScale$1 as meanScale, movePage, nextPageId, normalizeView, normalizeZoom$1 as normalizeZoom, pageAt, pageHolding, pageInView, pageMembers, pageName, pageOfBounds, pagesBounds, pagesLayer, pagesOf, placedPagesOf, planeMap, planeMatrix, planeToPlane, pxExtent$1 as pxExtent, rectAim, rectFromPlane, rectToPlane, removePage, renamePage, resizePage, resolveDeviceProfile$1 as resolveDeviceProfile, scaleDelta$1 as scaleDelta, sceneNodeClientRect, screenAngleOf$1 as screenAngleOf, screenToWorld, setPages, stableStringify, standoff$1 as standoff, startThresholdDrag, toPlane, useAutoCenter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDragHandle, useDropZone, useElementSize, useHandleDrag, useNodeOverlayFrame, usePoseRun, useSimulation, useVelocityTracker, useViewAnimation, useViewAnimationOn, useVisibleRaf$1 as useVisibleRaf, viewAxes, viewForAim, viewMarkerDragAction, viewRectToScreen$1 as viewRectToScreen, viewScreenToWorld$1 as viewScreenToWorld, viewToTransform, viewWorldToScreen$1 as viewWorldToScreen, viewZoom, viewportWorldRect, watchDevicePixelRatio, wheelPan, wheelZoom, wheelZoomFactor, withinPxBox, withinPxRadius$1 as withinPxRadius, worldToScreen, zoomAt };
|
|
@@ -1,83 +1,36 @@
|
|
|
1
1
|
import { useOptionalViewRegistry } from "../viewRegistry.js";
|
|
2
2
|
import { worldPointAt } from "./worldPointAt.js";
|
|
3
|
-
import {
|
|
4
|
-
import { heldPointers } from "@weasel-js/gestures";
|
|
3
|
+
import { usePublishPointer } from "../../input/usePublishPointer.js";
|
|
5
4
|
import { Fragment, jsx } from "react/jsx-runtime";
|
|
6
5
|
import { PointerContextProvider, usePointerContext } from "@weasel-js/routing/react";
|
|
7
6
|
|
|
8
7
|
// src/canvas/SceneCanvas/PointerProviderIfRoot.tsx
|
|
9
|
-
/**
|
|
10
|
-
* Conditional `<PointerContextProvider>` wrapper. Mounts a provider only
|
|
11
|
-
* when no parent provider is in scope — otherwise renders children unwrapped
|
|
12
|
-
* so SceneCanvas defers to the host's existing scope.
|
|
13
|
-
*
|
|
14
|
-
* Pairs with `usePointerPublisher`, which attaches native pointermove/leave
|
|
15
|
-
* listeners to the canvas element and writes world-space coords into the
|
|
16
|
-
* surrounding context's `pointerRef`.
|
|
17
|
-
*/
|
|
18
8
|
/** Mount a `<PointerContextProvider>` only when none is already in scope. */
|
|
19
9
|
function PointerProviderIfRoot({ children }) {
|
|
20
10
|
if (usePointerContext()) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
21
11
|
return /* @__PURE__ */ jsx(PointerContextProvider, { children });
|
|
22
12
|
}
|
|
23
13
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* Called inside the provider scope so it sees both consumer-mounted providers
|
|
30
|
-
* and SceneCanvas's auto-mounted one.
|
|
14
|
+
* Publishes the canvas's pointer into the surrounding `<PointerContextProvider>`
|
|
15
|
+
* through {@link usePublishPointer}, placing it with the surface's view
|
|
16
|
+
* resolver so paste-at-pointer lands in the panel the cursor is over. With no
|
|
17
|
+
* views declared it answers the canvas's own camera and rect.
|
|
31
18
|
*/
|
|
32
19
|
function usePointerPublisher(canvasRef, canvasApiRef) {
|
|
33
|
-
const ctx = usePointerContext();
|
|
34
20
|
const registry = useOptionalViewRegistry();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
x: worldX,
|
|
47
|
-
y: worldY
|
|
48
|
-
},
|
|
49
|
-
viewId: target?.id ?? null
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
const onMove = (e) => publish(e.pointerId, e.clientX, e.clientY);
|
|
53
|
-
const held = heldPointers(el);
|
|
54
|
-
const onLeave = (e) => {
|
|
55
|
-
if (held.count() > 0) {
|
|
56
|
-
publish(e.pointerId, e.clientX, e.clientY);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
ctx.set(null);
|
|
60
|
-
};
|
|
61
|
-
const onDocMove = (e) => {
|
|
62
|
-
if (e.target === el) return;
|
|
63
|
-
if (held.count() === 0) return;
|
|
64
|
-
publish(e.pointerId, e.clientX, e.clientY);
|
|
65
|
-
};
|
|
66
|
-
el.addEventListener("pointermove", onMove);
|
|
67
|
-
el.addEventListener("pointerleave", onLeave);
|
|
68
|
-
document.addEventListener("pointermove", onDocMove);
|
|
69
|
-
return () => {
|
|
70
|
-
el.removeEventListener("pointermove", onMove);
|
|
71
|
-
el.removeEventListener("pointerleave", onLeave);
|
|
72
|
-
document.removeEventListener("pointermove", onDocMove);
|
|
73
|
-
held.release();
|
|
21
|
+
usePublishPointer(canvasRef, (pointerId, clientX, clientY) => {
|
|
22
|
+
const api = canvasApiRef.current;
|
|
23
|
+
if (!api) return null;
|
|
24
|
+
const target = registry?.resolver.at(pointerId, clientX, clientY);
|
|
25
|
+
const [x, y] = worldPointAt(api, target, clientX, clientY);
|
|
26
|
+
return {
|
|
27
|
+
world: {
|
|
28
|
+
x,
|
|
29
|
+
y
|
|
30
|
+
},
|
|
31
|
+
viewId: target?.id ?? null
|
|
74
32
|
};
|
|
75
|
-
}
|
|
76
|
-
ctx,
|
|
77
|
-
canvasRef,
|
|
78
|
-
canvasApiRef,
|
|
79
|
-
registry
|
|
80
|
-
]);
|
|
33
|
+
});
|
|
81
34
|
}
|
|
82
35
|
|
|
83
36
|
export { PointerProviderIfRoot, usePointerPublisher };
|