@weasel-js/core 1.12.0 → 1.13.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/dist/animation/poseTarget.js +46 -15
- package/dist/animation/poseTarget.js.map +1 -1
- package/dist/animation/poseTransition.js +3 -3
- package/dist/animation/poseTransition.js.map +1 -1
- package/dist/animation/reflow.js +3 -6
- package/dist/animation/reflow.js.map +1 -1
- package/dist/animation/rig/bindRig.js +2 -2
- package/dist/animation/rig/bindRig.js.map +1 -1
- package/dist/animation/rig/skinBinding.js +2 -2
- package/dist/animation/rig/skinBinding.js.map +1 -1
- package/dist/barrel/canvas.js +5 -3
- package/dist/barrel/chrome.js +3 -1
- package/dist/barrel/paths.js +3 -2
- package/dist/barrel/routing.js +2 -2
- package/dist/barrel/scene.js +6 -1
- package/dist/canvas/NodeShape.js +1 -0
- package/dist/canvas/NodeShape.js.map +1 -1
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js +2 -0
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
- package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -1
- package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
- package/dist/canvas/SceneCanvas/defaultNodeRouting.js +2 -2
- package/dist/canvas/SceneCanvas/defaultNodeRouting.js.map +1 -1
- package/dist/canvas/SceneCanvas/scenePointQuery.js +20 -2
- package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
- package/dist/canvas/SceneCanvas/useCanvasTools.js +3 -0
- package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js +25 -19
- package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +7 -0
- package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -0
- package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useScenePicking.js +31 -6
- package/dist/canvas/SceneCanvas/useScenePicking.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js +4 -5
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js +5 -3
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +19 -2
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/accentColor.js +26 -3
- package/dist/canvas/accentColor.js.map +1 -1
- package/dist/canvas/buildSceneTree.js +14 -20
- package/dist/canvas/buildSceneTree.js.map +1 -1
- package/dist/canvas/copiesAbove.js +1 -1
- package/dist/canvas/copiesAbove.js.map +1 -1
- package/dist/canvas/defaultDrawOne.js +10 -7
- package/dist/canvas/defaultDrawOne.js.map +1 -1
- package/dist/canvas/deps/areaSelect.js +5 -2
- package/dist/canvas/deps/areaSelect.js.map +1 -1
- package/dist/canvas/deps/booleans.js +1 -1
- package/dist/canvas/deps/booleans.js.map +1 -1
- package/dist/canvas/deps/enteredGroup.js +81 -0
- package/dist/canvas/deps/enteredGroup.js.map +1 -0
- package/dist/canvas/deps/hitTestArea.js +27 -15
- package/dist/canvas/deps/hitTestArea.js.map +1 -1
- package/dist/canvas/deps/insert.js +4 -36
- package/dist/canvas/deps/insert.js.map +1 -1
- package/dist/canvas/deps/insertSite.js +39 -0
- package/dist/canvas/deps/insertSite.js.map +1 -0
- package/dist/canvas/deps/lassoSelect.js +6 -3
- package/dist/canvas/deps/lassoSelect.js.map +1 -1
- package/dist/canvas/deps/pathLeaf.js +1 -1
- package/dist/canvas/deps/pathLeaf.js.map +1 -1
- package/dist/canvas/deps/slice.js +2 -1
- package/dist/canvas/deps/slice.js.map +1 -1
- package/dist/canvas/derivedPath.js +14 -6
- package/dist/canvas/derivedPath.js.map +1 -1
- package/dist/canvas/doubleClickTarget.js +9 -11
- package/dist/canvas/doubleClickTarget.js.map +1 -1
- package/dist/canvas/drawnBounds.js +16 -7
- package/dist/canvas/drawnBounds.js.map +1 -1
- package/dist/canvas/inheritedPaints.js +22 -11
- package/dist/canvas/inheritedPaints.js.map +1 -1
- package/dist/canvas/nodeShapes/nodePaint.js +1 -7
- package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
- package/dist/canvas/nodeShapes/pathPainter.js +2 -3
- package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
- package/dist/canvas/nodeShapes/rectFallbackPainter.js +1 -1
- package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
- package/dist/canvas/nodeShapes/shapePainter.js +2 -3
- package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
- package/dist/canvas/ownClip.js +24 -0
- package/dist/canvas/ownClip.js.map +1 -0
- package/dist/canvas/paintCull.js.map +1 -1
- package/dist/canvas/pickIndex/nodeBroadBox.js +86 -0
- package/dist/canvas/pickIndex/nodeBroadBox.js.map +1 -0
- package/dist/canvas/pickIndex/scenePickIndex.js +208 -0
- package/dist/canvas/pickIndex/scenePickIndex.js.map +1 -0
- package/dist/canvas/pickWalk.js +14 -90
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/poseCascade.js +1 -1
- package/dist/canvas/poseCascade.js.map +1 -1
- package/dist/canvas/publishPreviews.js +97 -0
- package/dist/canvas/publishPreviews.js.map +1 -0
- package/dist/canvas/sceneAdapter.js +7 -5
- package/dist/canvas/sceneAdapter.js.map +1 -1
- package/dist/canvas/sceneClipboard.js +17 -10
- package/dist/canvas/sceneClipboard.js.map +1 -1
- package/dist/canvas/scenePickReads.js +37 -0
- package/dist/canvas/scenePickReads.js.map +1 -0
- package/dist/canvas/scenePickSource.js +74 -0
- package/dist/canvas/scenePickSource.js.map +1 -0
- package/dist/canvas/sceneSlotWiring.js +9 -4
- package/dist/canvas/sceneSlotWiring.js.map +1 -1
- package/dist/canvas/sceneViewRender.js +9 -5
- package/dist/canvas/sceneViewRender.js.map +1 -1
- package/dist/canvas/shownNodes.js +15 -0
- package/dist/canvas/shownNodes.js.map +1 -0
- package/dist/canvas/useSceneAdapter.js +5 -3
- package/dist/canvas/useSceneAdapter.js.map +1 -1
- package/dist/clipboard.d.ts +2 -2
- package/dist/{clone-C3e0OTuF.d.ts → clone-qb2e1F5p.d.ts} +2 -2
- package/dist/clone.d.ts +3 -3
- package/dist/core/isPlainObject.js +2 -2
- package/dist/core/layers/layerGroup.js.map +1 -1
- package/dist/core/scene/NodeRouting.js.map +1 -1
- package/dist/core/scene/arrivalWindow.js +1 -1
- package/dist/core/scene/arrivalWindow.js.map +1 -1
- package/dist/core/scene/clipNothing.js +2 -5
- package/dist/core/scene/clipNothing.js.map +1 -1
- package/dist/core/scene/declaredFieldSetters.js +44 -0
- package/dist/core/scene/declaredFieldSetters.js.map +1 -0
- package/dist/core/scene/depMemo.js +3 -0
- package/dist/core/scene/depMemo.js.map +1 -1
- package/dist/core/scene/dependents.js +3 -1
- package/dist/core/scene/dependents.js.map +1 -1
- package/dist/core/scene/documentOf.js +43 -0
- package/dist/core/scene/documentOf.js.map +1 -0
- package/dist/core/scene/effectivePose.js +37 -13
- package/dist/core/scene/effectivePose.js.map +1 -1
- package/dist/core/scene/fieldEdits.js +17 -14
- package/dist/core/scene/fieldEdits.js.map +1 -1
- package/dist/core/scene/historyForm.js +4 -4
- package/dist/core/scene/historyForm.js.map +1 -1
- package/dist/core/scene/inheritedPaint.js +27 -5
- package/dist/core/scene/inheritedPaint.js.map +1 -1
- package/dist/core/scene/kitOps.js +3 -23
- package/dist/core/scene/kitOps.js.map +1 -1
- package/dist/core/scene/layoutPass.js +1 -2
- package/dist/core/scene/layoutPass.js.map +1 -1
- package/dist/core/scene/nodeAsShown.js +72 -0
- package/dist/core/scene/nodeAsShown.js.map +1 -0
- package/dist/core/scene/nodeFnFields.js +2 -10
- package/dist/core/scene/nodeFnFields.js.map +1 -1
- package/dist/core/scene/nodeMemo.js +6 -1
- package/dist/core/scene/nodeMemo.js.map +1 -1
- package/dist/core/scene/nodeStore.js +2 -4
- package/dist/core/scene/nodeStore.js.map +1 -1
- package/dist/core/scene/pathEffects/derive.js +3 -2
- package/dist/core/scene/pathEffects/derive.js.map +1 -1
- package/dist/core/scene/pathEffects/foundHidden.js +35 -0
- package/dist/core/scene/pathEffects/foundHidden.js.map +1 -0
- package/dist/core/scene/plyStacks.js +89 -0
- package/dist/core/scene/plyStacks.js.map +1 -0
- package/dist/core/scene/poseHold.js +42 -24
- package/dist/core/scene/poseHold.js.map +1 -1
- package/dist/core/scene/poseOverrides.js +35 -38
- package/dist/core/scene/poseOverrides.js.map +1 -1
- package/dist/core/scene/posePlies.js +59 -0
- package/dist/core/scene/posePlies.js.map +1 -0
- package/dist/core/scene/reconcileSpecs.js +6 -15
- package/dist/core/scene/reconcileSpecs.js.map +1 -1
- package/dist/core/scene/renderOrder.js +10 -4
- package/dist/core/scene/renderOrder.js.map +1 -1
- package/dist/core/scene/sameValue.js +17 -0
- package/dist/core/scene/sameValue.js.map +1 -0
- package/dist/core/scene/scene.js +3 -3
- package/dist/core/scene/scene.js.map +1 -1
- package/dist/core/scene/sceneEdits.js +8 -0
- package/dist/core/scene/sceneEdits.js.map +1 -1
- package/dist/core/scene/sceneState.js +1 -2
- package/dist/core/scene/sceneState.js.map +1 -1
- package/dist/core/scene/serialization.js +45 -9
- package/dist/core/scene/serialization.js.map +1 -1
- package/dist/core/scene/staleReplay.js +5 -2
- package/dist/core/scene/staleReplay.js.map +1 -1
- package/dist/core/scene/statedFields.js +4 -2
- package/dist/core/scene/statedFields.js.map +1 -1
- package/dist/core/scene/treeEdits.js +2 -4
- package/dist/core/scene/treeEdits.js.map +1 -1
- package/dist/core/scene/treeOrder.js +3 -1
- package/dist/core/scene/treeOrder.js.map +1 -1
- package/dist/core/scene/treeReads.js +1 -2
- package/dist/core/scene/treeReads.js.map +1 -1
- package/dist/core/scene/treeShape.js +13 -0
- package/dist/core/scene/treeShape.js.map +1 -0
- package/dist/core/scene/types.js.map +1 -1
- package/dist/core/scene/useScene.js +1 -1
- package/dist/core/scene/useScene.js.map +1 -1
- package/dist/{cornerHandles-p8fDIAKt.d.ts → cornerHandles-CNCb7aTY.d.ts} +1 -1
- package/dist/{depSchema-mG19aEdD.d.ts → depSchema-B1RS-IJx.d.ts} +27 -54
- package/dist/devtools/handlers.js +1 -1
- package/dist/devtools/handlers.js.map +1 -1
- package/dist/devtools/protocol.js.map +1 -1
- package/dist/devtools-protocol.d.ts +4 -3
- package/dist/devtools.d.ts +3 -2
- package/dist/features/measure/measureHit.js +5 -3
- package/dist/features/measure/measureHit.js.map +1 -1
- package/dist/features/measure/measurement.js +1 -1
- package/dist/features/measure/measurement.js.map +1 -1
- package/dist/features/modes/modeLayers.js +14 -7
- package/dist/features/modes/modeLayers.js.map +1 -1
- package/dist/features/modes/modeTriggers.js +3 -3
- package/dist/features/modes/modeTriggers.js.map +1 -1
- package/dist/features/pathEffects/effectSources.js +25 -16
- package/dist/features/pathEffects/effectSources.js.map +1 -1
- package/dist/features/pathEffects/readSource.js +1 -1
- package/dist/features/pathEffects/readSource.js.map +1 -1
- package/dist/features/pathEffects/useEffectSourcesLayer.js +15 -19
- package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -1
- package/dist/features/paths/editablePath.js +1 -1
- package/dist/features/paths/editablePath.js.map +1 -1
- package/dist/features/paths/pathSnap/sceneIndex.js +1 -1
- package/dist/features/paths/pathSnap/sceneIndex.js.map +1 -1
- package/dist/features/selection/enteredGroupFrame.js +65 -0
- package/dist/features/selection/enteredGroupFrame.js.map +1 -0
- package/dist/features/selection/enteredGroupScope.js +44 -0
- package/dist/features/selection/enteredGroupScope.js.map +1 -0
- package/dist/features/text/sceneTextDrafts.js +26 -27
- package/dist/features/text/sceneTextDrafts.js.map +1 -1
- package/dist/features/text/textHitAt.js +4 -2
- package/dist/features/text/textHitAt.js.map +1 -1
- package/dist/features/text/useSceneTextEdit.js +3 -3
- package/dist/features/text/useSceneTextEdit.js.map +1 -1
- package/dist/index.d.ts +225 -73
- package/dist/index.js +15 -5
- package/dist/interactions/actions/admitsChild.js +11 -0
- package/dist/interactions/actions/admitsChild.js.map +1 -0
- package/dist/interactions/actions/defaults/contextMenu.js +16 -3
- package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
- package/dist/interactions/actions/defaults/createPaintAction.js +4 -1
- package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
- package/dist/interactions/actions/defaults/enterGroup.js +88 -0
- package/dist/interactions/actions/defaults/enterGroup.js.map +1 -0
- package/dist/interactions/actions/defaults/enterPathEdit.js +1 -1
- package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/enterTextEdit.js +1 -1
- package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/group.js +7 -7
- package/dist/interactions/actions/defaults/group.js.map +1 -1
- package/dist/interactions/actions/defaults/selectAll.js +7 -3
- package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
- package/dist/interactions/actions/defaults/selectDeep.js +35 -0
- package/dist/interactions/actions/defaults/selectDeep.js.map +1 -0
- package/dist/interactions/actions/drawnBoxes.js +2 -1
- package/dist/interactions/actions/drawnBoxes.js.map +1 -1
- package/dist/interactions/actions/insert/action.js +1 -0
- package/dist/interactions/actions/insert/action.js.map +1 -1
- package/dist/interactions/actions/insert/parent.js +7 -5
- package/dist/interactions/actions/insert/parent.js.map +1 -1
- package/dist/interactions/actions/insert/placeAction.js +3 -2
- package/dist/interactions/actions/insert/placeAction.js.map +1 -1
- package/dist/interactions/actions/insert/session.js +2 -2
- package/dist/interactions/actions/insert/session.js.map +1 -1
- package/dist/interactions/actions/move/commit/reparent.js +1 -1
- package/dist/interactions/actions/move/commit/reparent.js.map +1 -1
- package/dist/interactions/actions/move/gestureAdapter.js +6 -7
- package/dist/interactions/actions/move/gestureAdapter.js.map +1 -1
- package/dist/interactions/actions/move/layout/exits.js +1 -1
- package/dist/interactions/actions/move/layout/exits.js.map +1 -1
- package/dist/interactions/actions/move/session.js +4 -4
- package/dist/interactions/actions/move/session.js.map +1 -1
- package/dist/interactions/actions/pathCommands/pathNodes.js +14 -5
- package/dist/interactions/actions/pathCommands/pathNodes.js.map +1 -1
- package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js +14 -2
- package/dist/interactions/gestures/thresholdDrag/thresholdDrag.js.map +1 -1
- package/dist/{math-B56zxVs5.d.ts → math-C_elShP_.d.ts} +2 -5
- package/dist/math.d.ts +1 -1
- package/dist/move.d.ts +4 -4
- package/dist/{options-DBxEryYq.d.ts → options-ClEm3SEl.d.ts} +1 -1
- package/dist/resize.d.ts +4 -4
- package/dist/test-seams.d.ts +6 -6
- package/dist/tools/builtin/pressTarget.js +22 -0
- package/dist/tools/builtin/pressTarget.js.map +1 -0
- package/dist/tools/builtin/select/adapterPointQuery.js +3 -3
- package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
- package/dist/tools/builtin/select/collapseDeferred.js +40 -0
- package/dist/tools/builtin/select/collapseDeferred.js.map +1 -0
- package/dist/tools/builtin/select/groupEntry.js +82 -0
- package/dist/tools/builtin/select/groupEntry.js.map +1 -0
- package/dist/tools/builtin/select/pressDecision.js +1 -1
- package/dist/tools/builtin/select/pressDecision.js.map +1 -1
- package/dist/tools/builtin/select/useSelectTool.js +61 -45
- package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
- package/dist/{types-7puAuVD2.d.ts → types-49uYJN3j.d.ts} +1 -1
- package/dist/{types-CniVctmk.d.ts → types-BEdRIhNc.d.ts} +272 -109
- package/dist/{types-oFeQ5VjV.d.ts → types-p2alu-R1.d.ts} +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.0-rc.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 38d5ad1: Depend on `@msb235/blits` 0.12.0, which has no breaking changes from 0.11.0. Its fixes make a single `fn` voice over many subjects, the shape weasel's animator uses, cheaper per frame.
|
|
8
|
+
- 7edd3be: Canceling an in-place edit of text something derives from, such as the hidden text of a Text Along Path, now leaves no undo entry and keeps the redo stack. Its keystrokes still reach the curved copy as you type, but they are shown through a data override rather than written into the node, so the document holds none of them; a commit records one entry from the text before the edit to the text after it. A write made to the node during the edit, such as a fill change, shows through the draft and is kept, and undoing it after a cancel brings back no draft text.
|
|
9
|
+
- cbb16ca: `kit:shape` paints a container that has a `shape` again, and its interior is grabbable by its fill. The previous release stopped it, so a node that draws itself and has children, such as a skeleton bone with child bones, drew nothing. A container whose `path` is its clip, as an SVG `<g>` imports, still paints nothing of its own.
|
|
10
|
+
- 5f28e9c: A path effect that hides its sources no longer treats a source that was already hidden when the effect was made as one it hid. Release leaves that source hidden, and Expand to Paths, Delete and Cut leave it in the scene; Set Path moving off it leaves it hidden too. The effect records those sources by position in a new `foundHidden` field beside `hideSources`, and a saved effect without one reads as before: it hid them all. `sourcesHiddenBy(scene, id)` in core answers which sources an effect holds hidden, and `sourcesOf(node)` the ids a node names in its `dependsOn`.
|
|
11
|
+
- e70d2cd: A node is a mark or a group, and the two now behave differently:
|
|
12
|
+
|
|
13
|
+
- A group never paints, whatever its data: a `shape` or `path` on a group draws nothing, and only its members draw. A mark with children paints itself and its children. A group is hit anywhere in its box, even with no member under the press or after its last member is deleted.
|
|
14
|
+
- Ungroup dissolves only groups; a mark with children is left whole.
|
|
15
|
+
- The area hit test's `includeContainers` option is now `includeGroups` (breaking for a custom `hitTestArea` dep). Marquee and lasso take a mark and its children, and a group only when asked for.
|
|
16
|
+
- A mark's drawn bounds include its own ink as well as its children's.
|
|
17
|
+
- Any node may clip, repeat (`copies`), lay out and set units for its children, and `'inherit'` fills and strokes read from any parent.
|
|
18
|
+
- A drop or an insert lands in a mark only when the `canDropInto` dep admits it; with no dep, a group admits and a mark does not. An insert asks `canDropInto` with an empty list, since the new node is not minted yet.
|
|
19
|
+
- Diagram bodies, containers, pools and lanes are marks, so they paint again and take drops through `diagramCanDropInto`. A stencil body still takes nothing, and a press inside an unfilled lane hits the lane.
|
|
20
|
+
- In a diagram scene, `kind === 'group'` no longer identifies a body or a region. A region is a node where `diagramContainerOf(node)` is not null. A diagram group box is one where `diagramGroupOf(node)` is not null. A body is a node where `diagramNodeOf(node)` is not null and neither of those is. `isDiagramBody(node)` asks that, and `isBodyOrRegion(node)` asks for either.
|
|
21
|
+
- ed89d42: A node clips its children only when it declares a clip. Breaking: a group no longer clips its children to its outline by default, and nothing else does either. A node clips its children to its `clipFromPose` when it has one, or to its outline when it sets the new `clip: 'outline'` field. Its own paint, under its children or over them, is not clipped by its own clip. That outline is the silhouette of whichever painter claims the node, the rect fallback's box included, read by the new `clipOutline(node, pose)`. `clip` is saved and loaded, copied, pasted and undone like the node's other fields, and `NodeClip` names its type.
|
|
22
|
+
|
|
23
|
+
Scenes saved at version 1 keep their look: a version 1 container that loads as a group, as it does by default, gets `clip: 'outline'` when it has no clip function or a load skips its clip key. One that `classifyV1` makes a mark gets no implicit clip, since the loader cannot know which painter will claim it; set `clip` on it where it should clip. Version 2 scenes saved by builds between the version 2 format and this change are read as written, so a group there that relied on the old default clip draws its children unclipped.
|
|
24
|
+
|
|
25
|
+
- `clipNothing` and symmetry's `SYMMETRY_CLIP_KEY` stay registered so saves naming them load; diagram bodies and symmetry groups no longer set a clip, and `@weasel-js/diagram`'s `BodyNodeSpec` drops its `clipFromPose` field.
|
|
26
|
+
- Baking a symmetry group carries each copied node's pose through the copy's transform, its clip with it, and copies a mark's children and every node's stated fields (`pickable`, `hidden`, `units`, `clip`). A mirrored copy of a clipping group used to clip its content away.
|
|
27
|
+
- `statedFields(node)` and `StatedNodeFields` are exported: the plain optional fields a copy of a node carries.
|
|
28
|
+
- `@weasel-js/svg`: an imported `<g clip-path>` is a group with `clip: 'outline'` (`SvgSceneDraft` carries `clip` on a group). The export writes a clip only for a node that declares one, clipped to the same `clipOutline` the canvas uses; a group holding `data.path` without `clip` exports unclipped. A mark with children now exports as a `<g>` holding its own element and then its children, which were left out before; its declared clip and copies apply to its children only. `SvgKitTreeNode` takes `clip`.
|
|
29
|
+
- `@weasel-js/diagram-formats`: a page export writes a diagram box as its outline followed by its rows, with no special case for diagram nodes.
|
|
30
|
+
- `specsFromSerialized` is exported, with `SpecsFromSerializedOptions`. Its `unknownKeys: 'skip'` loads a node whose registry key is unknown without that function instead of throwing, and `onSkip` hears each key left out as a `SkippedKey`. `sceneFromJSON` takes both options, and `scene.loadState(json, opts)` takes them as a second argument.
|
|
31
|
+
- 04b862d: Scene node kinds are now `mark` and `group`, replacing `leaf` and `container`, and any node may have children. `ContainerNode` and `LeafNode` are now `GroupNode` and `MarkNode`; `AddNodeSpec.kind` is `'mark' | 'group'`, and `isLeaf(node)` and `hasChildren(node)` ask whether a node has children. This breaks code that names the old kinds. Scenes save as version 2, and a version 1 save still loads: its leaves become marks and its containers become groups, unless the `classifyV1` option of `sceneFromJSON(json, { classifyV1 })` says otherwise. `V1Classifier` and `nodeKindOf` are exported from `@weasel-js/core`. `@weasel-js/svg`'s `SvgSceneDraft.kind` changed from `'container' | 'leaf'` to `'group' | 'mark'`.
|
|
32
|
+
- 7e7549c: A group now selects as one item:
|
|
33
|
+
|
|
34
|
+
- A press on a group's member selects the group, and so does a right-click, whose menu is the group's own. A drag from a member moves the group.
|
|
35
|
+
- A double-click on a member enters the group, drawn with a dashed frame; a press then selects among its members, and a further double-click goes one level deeper. Escape leaves one level. Both work under any tool. While a group is entered only what is inside it can be pressed, marqueed, lassoed or selected with Select All. Under the select tool a press outside the entered group's box, with or without modifiers, leaves every group that box is outside of and selects nothing; that leave is its own binding, `group.leave.press`, which `bindingOverrides` can rebind or remove. Under a drawing tool the press draws, and what is drawn or pasted lands in the entered group: core now sources the `insertInto` dep from it, under any source registered after it, and `sceneToAdapter`'s new `pasteInto` option says where a paste lands. A double-click on a member that is not a group still reaches text and path editing, and an open mode takes Escape before the group does.
|
|
36
|
+
- Cmd-click (Ctrl on Windows and Linux) selects the node under the pointer past every group. It no longer extends the selection: Shift-click does that, and Cmd-Shift-click adds the node under the pointer past every group.
|
|
37
|
+
- The entered group is session state beside the selection: undo does not move it. `<SceneCanvas>` publishes it as the optional `enteredGroup` dep, and an absent dep means no group is entered. `<SceneCanvas enteredGroup>` takes an `EnteredGroupApi` made with `useEnteredGroup(scene)`, as `selection` is passed, for an app that reads or drives it; without it the canvas keeps its own. `leaveAll()` leaves every level at once.
|
|
38
|
+
- `createEnteredGroupScope({ entered, scene })` returns a `NodeScope` (from `@weasel-js/modes`, which `ScopingDim` now extends) for the entered group, everything outside it dimmed to `OUT_OF_SCOPE_ALPHA`. Compose its `alphaFor` into `<SceneCanvas alphaFor>` and pass it in `redrawOn` to dim what is outside the group.
|
|
39
|
+
- The entered-group frame takes its color from the `--wzl-entered-group` override hook on the canvas or any element above it, and repaints when a theme, mode or class attribute changes there or the system color scheme flips.
|
|
40
|
+
- A contribution's dep source may read the source it displaces: `ContributionDeps` entries take the `below` argument `DepSource` does. The symmetry contribution's `insertInto` falls through to it when no symmetry group is active.
|
|
41
|
+
- `workspaceTintLayer` takes a `fallback` tint, shown when the mode has none.
|
|
42
|
+
|
|
43
|
+
Breaking: isolation mode is gone. `ISOLATION` is no longer exported from `@weasel-js/modes` and `DEFAULT_MODES` no longer holds it, and `<SceneCanvas isolatedGroup>` is removed: the entered group is the one way of working inside a group. In WeaselDraw a double-click on a group's member now enters the group, dims everything outside it, tints the workspace, and names the group in the breadcrumb. Edits made inside it are each their own undo step, where isolation by default collected them into one entry on leaving, and path and text editing open inside the group without leaving it.
|
|
44
|
+
- d5a9a80: A path effect whose source has an `'inherit'` fill or stroke paints that source in what it inherits from its containers, and follows a container's paint when it changes; it used to read as none. `DerivedDep` gains an optional `inherited`, which every dependency the scene builds carries, so any derivation can read the paint a dependency shows. `diagram:mark` resolves an `'inherit'` fill and stroke too.
|
|
45
|
+
- 3519c96: Two more readers resolve an `'inherit'` fill or stroke against a node's containers. The preview ghost of a gesture paints a member with the paint its containers are taking mid-gesture, so a container's fill or stroke scrubbed in a paint control reaches its inheriting members at once rather than at the commit: the paint actions preview those members with the container, and the ghost reads every previewed container above a member. And culling measures an inherited stroke as the stroke it takes instead of declining to bound the node: `defaultPaintBounds` and `PaintBoundsFn` take an optional fourth `ctx` with `inherited`, which `<SceneCanvas>` and the headless render fill in from the scene; `withInheritedPaintBounds` does the same for a consumer's own cull. A `drawOne` wrapped by `withInheritedPaints` now keeps a `ctx.inherited` its caller passes.
|
|
46
|
+
- 773f9c8: Several writers can now override one node at once. Each entry set on `scene.overrides` is a ply over the ones set before it: for each field the most recently claimed ply that gives it wins, `alpha` multiplies through every ply, and `data` runs each ply's function on what the plies under it show. Releasing a ply shows what lies under it. A ply's `path` is drawn in the box of the `pose` it gives beside it, so a pose laid over it by another writer, such as a move over a skinned node, carries the path along; a path given with no pose is drawn where it is. So a gesture's published preview no longer gives way where another writer holds the node: the data a move previews for a transform follower shows while the move holds the follower's pose, and an edge whose live route is eased shows the drag's reroute beside the route, so what derives from either follows every frame.
|
|
47
|
+
|
|
48
|
+
`has` and `clear` take an optional `entry`, to check or release one writer's ply: `has(id, entry)`, `clear(id, entry)`. `clear(id)` still drops every ply of the node; each keeps its place, so a writer that sets its entry again goes back where it stood. `get(id)` answers the top ply. A hand-written `PoseOverrides` must honor `entry` on both: a writer releasing its own ply must not clear another's. This is a behavior change for a writer that set an entry over another's to replace it, or that checked `get(id) === entry` to know its own was still installed: the earlier entry now stays and shows again when the later one goes, and `get` answers only the top one. A hold's `show` no longer leaves an id another writer holds to that writer; it lays its frame over it. `PoseHold.others()` answers each node's topmost pose that is not the hold's own, where it answered the pose of whichever single entry the node held.
|
|
49
|
+
|
|
50
|
+
`PoseTransitionScene.overrides` now needs `read` where it needed `has`: the undo glide skips only a node something else poses, not one showing a data draft. A container's descendants likewise ride along with its pose animation unless another writer poses them. Rigs and pose targets put their override back when the table drops it, and a pose target retargeted mid-run keeps running when its own write reflows a layout. Labkit's `LensActionApi` gains `unfollow`, which Pinned and Close call to let go of the follow hold alone.
|
|
51
|
+
- 39c72a4: What derives from a node now follows an anchor drag on it every frame, rather than at the release: a Text Along Path laid along a path or a blend from it, and the same for every other action that previews through its handle. `<SceneCanvas>` publishes each in-flight handle's `previewPose` and `previewData` to the scene's overrides through the new `publishPreviews(scene, dispatcher)`, and releases them once the handle's commit has landed, so a cancel leaves the document and its history untouched. A consumer driving a dispatcher of its own, a 3D host among them, can call it too.
|
|
52
|
+
|
|
53
|
+
A pose override can now carry `data(stored)`, the data a node is shown with, worked out from what it stores. Painting, picking, drawn bounds and the effective reading of every derivation read it through the new `nodeAsShown(overrides, node)`; the document reading, `scene.get`, history and export do not. `PoseOverride` and `PoseOverrides` take the scene's data type as a second parameter, and `PoseHold` gains `show(frame)`, which publishes data beside poses and leaves an id another writer holds to that writer; a hold now puts back an entry the scene dropped when its node was removed, once the node is back.
|
|
54
|
+
|
|
55
|
+
Commands and exports read the document through the new `documentOf(scene)`, the scene with its overrides empty: Expand to Paths, Slice, `drawnBoxes` and what it serves (Align, Flip, resize and rotate, snap targets), and the diagram page export no longer bake in an in-flight drag's pose or an open edit's draft, and `documentDepOf` takes the reading node as `derivedDepOf` does. `withPath` keeps returning the same object for the same node and path, so a gesture previewing through it keeps its previews' identity between frames.
|
|
56
|
+
- 8c39126: Picks consult a BVH index kept beside the scene, so a marquee, a lasso or a click visits the nodes near the query instead of every node. `@weasel-js/geom` adds the tree. Three contracts tighten for the index to be sound. `NodeInk.outset` must not depend on the view scale: screen-space reach goes in `outsetPx`. In kernel3d, `world.bounds` must be a function of the node alone. Two picks change behavior: marquee and lasso now test a derived node at its derived path, and the point pick now tests a node carrying an override `path` against that path, which is what the renderer paints, instead of its pose box.
|
|
57
|
+
- 1322310: A node's declared clip can now be changed after it is added: `scene.setClip(id, 'outline')` declares it and `scene.setClip(id, null)` clears it, as one undo step, refused on a locked node like `setUnits`. `reconcileSpecs` writes a changed `clip` in place through it, where before it removed the node and added it again. The two setters are typed together as `SceneDeclaredFieldSetters`, which `Scene` extends. `setUnits` no longer records an entry when given a scope equal to the one declared. This adds API.
|
|
58
|
+
- bbee4df: `startThresholdDrag` takes `captureAt: 'threshold'`, which leaves the pointer uncaptured until the press is a drag, so a press released short of the threshold still clicks what it was on; the capture it takes is released when the drag ends or is canceled. `modifierStateOf`, which reads an event's modifier keys as a `ModifierState`, is exported from routing and core, for `keysHeld` to read a press.
|
|
59
|
+
- Updated dependencies [7e5f9e3]
|
|
60
|
+
- Updated dependencies [38d5ad1]
|
|
61
|
+
- Updated dependencies [04b862d]
|
|
62
|
+
- Updated dependencies [7e7549c]
|
|
63
|
+
- Updated dependencies [c7516cd]
|
|
64
|
+
- Updated dependencies [1f2a0b3]
|
|
65
|
+
- Updated dependencies [8c39126]
|
|
66
|
+
- Updated dependencies [3eaceb1]
|
|
67
|
+
- Updated dependencies [86f67ad]
|
|
68
|
+
- Updated dependencies [769eef8]
|
|
69
|
+
- Updated dependencies [352502d]
|
|
70
|
+
- Updated dependencies [bbee4df]
|
|
71
|
+
- Updated dependencies [aba2016]
|
|
72
|
+
- Updated dependencies [4c113da]
|
|
73
|
+
- @weasel-js/geom@1.13.0-rc.0
|
|
74
|
+
- @weasel-js/animator@1.13.0-rc.0
|
|
75
|
+
- @weasel-js/quantity@1.13.0-rc.0
|
|
76
|
+
- @weasel-js/modes@1.13.0-rc.0
|
|
77
|
+
- @weasel-js/routing@1.13.0-rc.0
|
|
78
|
+
- @weasel-js/prefs@1.13.0-rc.0
|
|
79
|
+
- @weasel-js/text-dom@1.13.0-rc.0
|
|
80
|
+
- @weasel-js/renderer@1.13.0-rc.0
|
|
81
|
+
- @weasel-js/stroke@1.13.0-rc.0
|
|
82
|
+
- @weasel-js/text@1.13.0-rc.0
|
|
83
|
+
- @weasel-js/gestures@1.13.0-rc.0
|
|
84
|
+
- @weasel-js/cursor@1.13.0-rc.0
|
|
85
|
+
- @weasel-js/font@1.13.0-rc.0
|
|
86
|
+
- @weasel-js/history@1.13.0-rc.0
|
|
87
|
+
- @weasel-js/icons@1.13.0-rc.0
|
|
88
|
+
- @weasel-js/paint@1.13.0-rc.0
|
|
89
|
+
- @weasel-js/react@1.13.0-rc.0
|
|
90
|
+
- @weasel-js/registry@1.13.0-rc.0
|
|
91
|
+
- @weasel-js/select@1.13.0-rc.0
|
|
92
|
+
- @weasel-js/simulation@1.13.0-rc.0
|
|
93
|
+
|
|
3
94
|
## 1.12.0
|
|
4
95
|
|
|
5
96
|
### Minor Changes
|
|
@@ -57,7 +57,7 @@ function liveAxes(descriptor, get) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
/** Shows a moving container's descendants through overrides of their own, and takes them back
|
|
60
|
-
* as it rests. A descendant something else already
|
|
60
|
+
* as it rests. A descendant something else already poses is left to it. */
|
|
61
61
|
function riders(overrides, cascade) {
|
|
62
62
|
const riding = /* @__PURE__ */ new Map();
|
|
63
63
|
return (pose) => {
|
|
@@ -65,7 +65,7 @@ function riders(overrides, cascade) {
|
|
|
65
65
|
for (const [id, beside] of pose === null ? [] : cascade.beside(pose)) {
|
|
66
66
|
let entry = riding.get(id);
|
|
67
67
|
if (!entry) {
|
|
68
|
-
if (overrides.
|
|
68
|
+
if (overrides.read(id)?.pose !== void 0) continue;
|
|
69
69
|
riding.set(id, entry = {});
|
|
70
70
|
overrides.set(id, entry);
|
|
71
71
|
}
|
|
@@ -74,7 +74,7 @@ function riders(overrides, cascade) {
|
|
|
74
74
|
}
|
|
75
75
|
for (const [id, entry] of riding) {
|
|
76
76
|
if (seen.has(id)) continue;
|
|
77
|
-
|
|
77
|
+
overrides.clear(id, entry);
|
|
78
78
|
riding.delete(id);
|
|
79
79
|
}
|
|
80
80
|
};
|
|
@@ -86,23 +86,37 @@ function build(opts, onRest, overwritten, ride) {
|
|
|
86
86
|
if (!axes) return null;
|
|
87
87
|
const entry = {};
|
|
88
88
|
let installed = false;
|
|
89
|
+
/** While installed, stops the run as soon as the table changes after something else wrote
|
|
90
|
+
* the pose: a drag laid over the run commits and then lets go, which would show this run's
|
|
91
|
+
* last frame until its next one. */
|
|
92
|
+
let unwatch = null;
|
|
93
|
+
const rest = () => {
|
|
94
|
+
installed = false;
|
|
95
|
+
unwatch?.();
|
|
96
|
+
unwatch = null;
|
|
97
|
+
overrides.clear(id, entry);
|
|
98
|
+
ride?.(null);
|
|
99
|
+
onRest?.();
|
|
100
|
+
};
|
|
89
101
|
const target = createTarget({
|
|
90
102
|
get: opts.get,
|
|
91
103
|
set: opts.set,
|
|
92
104
|
axes,
|
|
93
105
|
show(pose) {
|
|
94
106
|
if (target.active && overwritten?.()) target.stop({ commit: false });
|
|
95
|
-
if (target.active) {
|
|
96
|
-
|
|
97
|
-
if (!installed) overrides.set(id, entry);
|
|
98
|
-
installed = true;
|
|
99
|
-
ride?.(pose);
|
|
107
|
+
if (!target.active) {
|
|
108
|
+
rest();
|
|
100
109
|
return;
|
|
101
110
|
}
|
|
102
|
-
|
|
103
|
-
if (overrides.
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
entry.pose = pose;
|
|
112
|
+
if (!overrides.has(id, entry)) overrides.set(id, entry);
|
|
113
|
+
installed = true;
|
|
114
|
+
if (overwritten && unwatch === null) unwatch = overrides.subscribe(() => {
|
|
115
|
+
if (!target.active || !overwritten()) return;
|
|
116
|
+
target.stop({ commit: false });
|
|
117
|
+
if (installed) rest();
|
|
118
|
+
});
|
|
119
|
+
ride?.(pose);
|
|
106
120
|
},
|
|
107
121
|
publish: publisherOf(overrides)
|
|
108
122
|
});
|
|
@@ -122,11 +136,20 @@ function createPoseTarget(opts) {
|
|
|
122
136
|
return target;
|
|
123
137
|
}
|
|
124
138
|
var sceneTargets = /* @__PURE__ */ new WeakMap();
|
|
139
|
+
/** Whether two poses hold the same fields, compared without allocating: a running target asks on every publish. */
|
|
125
140
|
function samePose(a, b) {
|
|
126
141
|
if (a === b) return true;
|
|
127
142
|
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
128
|
-
const
|
|
129
|
-
|
|
143
|
+
const ra = a;
|
|
144
|
+
const rb = b;
|
|
145
|
+
let count = 0;
|
|
146
|
+
for (const k in ra) {
|
|
147
|
+
if (!Object.hasOwn(ra, k)) continue;
|
|
148
|
+
if (!Object.hasOwn(rb, k) || ra[k] !== rb[k]) return false;
|
|
149
|
+
count++;
|
|
150
|
+
}
|
|
151
|
+
for (const k in rb) if (Object.hasOwn(rb, k)) count--;
|
|
152
|
+
return count === 0;
|
|
130
153
|
}
|
|
131
154
|
/**
|
|
132
155
|
* The pose target of node `id` in `scene`: the same one on every call, so everything animating
|
|
@@ -154,6 +177,8 @@ function scenePoseTarget(scene, id, descriptor, cascade) {
|
|
|
154
177
|
let last = documentPose(scene, node);
|
|
155
178
|
/** The node's pose as an animation last left it, or as it was when the first one started. */
|
|
156
179
|
let mine;
|
|
180
|
+
/** While this target's own commit is being written. */
|
|
181
|
+
let writing = false;
|
|
157
182
|
const cache = made;
|
|
158
183
|
let target = null;
|
|
159
184
|
target = build({
|
|
@@ -169,12 +194,18 @@ function scenePoseTarget(scene, id, descriptor, cascade) {
|
|
|
169
194
|
set(pose) {
|
|
170
195
|
const n = scene.get(nid);
|
|
171
196
|
if (!n) return;
|
|
172
|
-
|
|
197
|
+
writing = true;
|
|
198
|
+
try {
|
|
199
|
+
if (!samePose(n.pose, pose)) scene.untracked(() => cascade ? cascade.write(pose) : scene.setPose(nid, pose));
|
|
200
|
+
} finally {
|
|
201
|
+
writing = false;
|
|
202
|
+
}
|
|
173
203
|
mine = scene.get(nid)?.pose;
|
|
174
204
|
}
|
|
175
205
|
}, () => {
|
|
176
206
|
if (!scene.get(nid)) cache.delete(id);
|
|
177
207
|
}, () => {
|
|
208
|
+
if (writing) return false;
|
|
178
209
|
const n = scene.get(nid);
|
|
179
210
|
return !!n && mine !== void 0 && !samePose(n.pose, mine);
|
|
180
211
|
}, cascade && riders(scene.overrides, cascade));
|
|
@@ -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\n/** How a container's pose target carries its descendants, for {@link scenePoseTarget}. */\nexport interface PoseCascade<TPose> {\n /** Commits `pose` to the container, moving its descendants' poses with it. */\n write(pose: TPose): void;\n /** The descendants, each at the pose to show it at while the container shows at `pose`. */\n beside(pose: TPose): Iterable<readonly [NodeId, TPose]>;\n}\n\n/** Shows a moving container's descendants through overrides of their own, and takes them back\n * as it rests. A descendant something else already overrides is left to it. */\nfunction riders<TPose>(overrides: PoseOverrides<TPose>, cascade: PoseCascade<TPose>): (pose: TPose | null) => void {\n const riding = new Map<NodeId, PoseOverride<TPose>>();\n return (pose) => {\n const seen = new Set<NodeId>();\n for (const [id, beside] of pose === null ? [] : cascade.beside(pose)) {\n let entry = riding.get(id);\n if (!entry) {\n if (overrides.has(id)) continue;\n riding.set(id, entry = {});\n overrides.set(id, entry);\n }\n entry.pose = beside;\n seen.add(id);\n }\n for (const [id, entry] of riding) {\n if (seen.has(id)) continue;\n if (overrides.get(id) === entry) overrides.clear(id);\n riding.delete(id);\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 /** Told the pose on screen each time it is shown, and null as the node comes to rest. */\n ride?: (pose: TPose | null) => void,\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 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 entry.pose = pose;\n // Installed once per run: an override laid over it since, a drag's, is not taken back.\n if (!installed) overrides.set(id, entry);\n installed = true;\n ride?.(pose);\n return;\n }\n installed = false;\n if (overrides.get(id) === entry) overrides.clear(id);\n ride?.(null);\n onRest?.();\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\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 ka = Object.keys(a);\n return ka.length === Object.keys(b).length\n && ka.every((k) => (a as Record<string, unknown>)[k] === (b as Record<string, unknown>)[k]);\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`. With `cascade`, for a container whose descendants hold\n * poses of their own, a commit goes through `cascade.write` and the descendants are shown\n * traveling beside the container. Both are 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 cascade?: PoseCascade<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 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 if (!samePose(n.pose, pose)) scene.untracked(() => (cascade ? cascade.write(pose) : scene.setPose(nid, pose)));\n mine = scene.get(nid)?.pose;\n },\n }, () => {\n if (!scene.get(nid)) cache.delete(id);\n }, () => {\n const n = scene.get(nid);\n return !!n && mine !== undefined && !samePose(n.pose, mine);\n }, cascade && riders(scene.overrides, cascade));\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\n/** How a container's pose target carries its descendants, for {@link scenePoseTarget}. */\nexport interface PoseCascade<TPose> {\n /** Commits `pose` to the container, moving its descendants' poses with it. */\n write(pose: TPose): void;\n /** The descendants, each at the pose to show it at while the container shows at `pose`. */\n beside(pose: TPose): Iterable<readonly [NodeId, TPose]>;\n}\n\n/** Shows a moving container's descendants through overrides of their own, and takes them back\n * as it rests. A descendant something else already poses is left to it. */\nfunction riders<TPose>(overrides: PoseOverrides<TPose>, cascade: PoseCascade<TPose>): (pose: TPose | null) => void {\n const riding = new Map<NodeId, PoseOverride<TPose>>();\n return (pose) => {\n const seen = new Set<NodeId>();\n for (const [id, beside] of pose === null ? [] : cascade.beside(pose)) {\n let entry = riding.get(id);\n if (!entry) {\n if (overrides.read(id)?.pose !== undefined) continue;\n riding.set(id, entry = {});\n overrides.set(id, entry);\n }\n entry.pose = beside;\n seen.add(id);\n }\n for (const [id, entry] of riding) {\n if (seen.has(id)) continue;\n overrides.clear(id, entry);\n riding.delete(id);\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 /** Told the pose on screen each time it is shown, and null as the node comes to rest. */\n ride?: (pose: TPose | null) => void,\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 ride?.(null);\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 ride?.(pose);\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`. With `cascade`, for a container whose descendants hold\n * poses of their own, a commit goes through `cascade.write` and the descendants are shown\n * traveling beside the container. Both are 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 cascade?: PoseCascade<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(() => (cascade ? cascade.write(pose) : 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 }, cascade && riders(scene.overrides, cascade));\n if (!target) return undefined;\n made.set(id, target as Target<unknown>);\n return target;\n}\n"],"mappings":"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"}
|
|
@@ -15,8 +15,8 @@ function glidesByDefault(write) {
|
|
|
15
15
|
}
|
|
16
16
|
var exitsByDefault = (removal) => removal.origin !== "untracked";
|
|
17
17
|
/** Hand `scene`'s pose writes to the seat's transition, each gliding from the
|
|
18
|
-
* pose it was shown at. A node something else
|
|
19
|
-
*
|
|
18
|
+
* pose it was shown at. A node something else poses through an override — a
|
|
19
|
+
* drag's preview, another animation — is left to it. With `departures`,
|
|
20
20
|
* removals leave as ghosts there, and a node that comes back while its ghost
|
|
21
21
|
* is leaving returns from where the ghost was. */
|
|
22
22
|
function bindPoseTransition(scene, seat, departures) {
|
|
@@ -37,7 +37,7 @@ function bindPoseTransition(scene, seat, departures) {
|
|
|
37
37
|
t.settle(id);
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
40
|
-
if (scene.overrides.
|
|
40
|
+
if (scene.overrides.read(w.id)?.pose !== void 0) continue;
|
|
41
41
|
const from = w.from ?? back ?? s.enter?.(w.to);
|
|
42
42
|
if (from !== void 0) t.settle(id, { from });
|
|
43
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poseTransition.js","names":[],"sources":["../../src/animation/poseTransition.ts"],"sourcesContent":["import { useEffect, useMemo } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { PoseRemoval, PoseWrite, ScenePoseReports } from '../core/scene/poseWrites';\nimport type { NodeId, PoseOverrides } from '../core/scene/types';\nimport type { ReflowTransition } from '../layout/types';\nimport { createDepartures, type Departures } from './departures';\n\n/**\n * Which pose writes a canvas glides, and through what: `<SceneCanvas\n * poseTransition>`. The transition is the kind `reflowTransition` takes, and\n * one object can serve both.\n */\nexport interface PoseTransitionSeat<TPose> {\n /** Shows each glide through a pose override; writes nothing. */\n transition: ReflowTransition<TPose>;\n /** Whether `write` glides. Default {@link glidesByDefault}. */\n glides?: (write: PoseWrite<TPose>) => boolean;\n /** Where a node the scene adds is first shown, given the pose it was added\n * at. Absent, an added node appears in place. */\n enter?: (pose: TPose) => TPose;\n /** Where a node the scene removes is seen off to, given the pose it was\n * shown at: a ghost of it glides there fading out, drawn but never picked,\n * selected or recorded. Absent, a removed node vanishes. Needs a\n * transition from `useAnimatedReflow` or `createReflowTransition`. */\n exit?: (pose: TPose) => TPose;\n /** Whether a removal is seen off through `exit`. Default: every one but\n * an untracked removal. */\n exits?: (removal: PoseRemoval<TPose>) => boolean;\n}\n\n/**\n * The writes a {@link PoseTransitionSeat} glides unless told otherwise: a\n * direct scene call, an undo, and a redo. An action's commit (`'ops'`) is\n * left out because most show their own result — a drag's release is already\n * where it is written — and `tweenPose` records its op before it animates.\n * Layout writes belong to `reflowTransition`.\n */\nexport function glidesByDefault(write: PoseWrite<unknown>): boolean {\n return write.origin === 'call' || write.origin === 'undo' || write.origin === 'redo';\n}\n\nconst exitsByDefault = (removal: PoseRemoval<unknown>): boolean => removal.origin !== 'untracked';\n\n/** The part of a scene a pose transition hears and reads. */\nexport interface PoseTransitionScene<TPose, TNode = unknown> extends Pick<ScenePoseReports<TPose, TNode>, 'onPoseWrite'> {\n readonly overrides: Pick<PoseOverrides<TPose>, '
|
|
1
|
+
{"version":3,"file":"poseTransition.js","names":[],"sources":["../../src/animation/poseTransition.ts"],"sourcesContent":["import { useEffect, useMemo } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { PoseRemoval, PoseWrite, ScenePoseReports } from '../core/scene/poseWrites';\nimport type { NodeId, PoseOverrides } from '../core/scene/types';\nimport type { ReflowTransition } from '../layout/types';\nimport { createDepartures, type Departures } from './departures';\n\n/**\n * Which pose writes a canvas glides, and through what: `<SceneCanvas\n * poseTransition>`. The transition is the kind `reflowTransition` takes, and\n * one object can serve both.\n */\nexport interface PoseTransitionSeat<TPose> {\n /** Shows each glide through a pose override; writes nothing. */\n transition: ReflowTransition<TPose>;\n /** Whether `write` glides. Default {@link glidesByDefault}. */\n glides?: (write: PoseWrite<TPose>) => boolean;\n /** Where a node the scene adds is first shown, given the pose it was added\n * at. Absent, an added node appears in place. */\n enter?: (pose: TPose) => TPose;\n /** Where a node the scene removes is seen off to, given the pose it was\n * shown at: a ghost of it glides there fading out, drawn but never picked,\n * selected or recorded. Absent, a removed node vanishes. Needs a\n * transition from `useAnimatedReflow` or `createReflowTransition`. */\n exit?: (pose: TPose) => TPose;\n /** Whether a removal is seen off through `exit`. Default: every one but\n * an untracked removal. */\n exits?: (removal: PoseRemoval<TPose>) => boolean;\n}\n\n/**\n * The writes a {@link PoseTransitionSeat} glides unless told otherwise: a\n * direct scene call, an undo, and a redo. An action's commit (`'ops'`) is\n * left out because most show their own result — a drag's release is already\n * where it is written — and `tweenPose` records its op before it animates.\n * Layout writes belong to `reflowTransition`.\n */\nexport function glidesByDefault(write: PoseWrite<unknown>): boolean {\n return write.origin === 'call' || write.origin === 'undo' || write.origin === 'redo';\n}\n\nconst exitsByDefault = (removal: PoseRemoval<unknown>): boolean => removal.origin !== 'untracked';\n\n/** The part of a scene a pose transition hears and reads. */\nexport interface PoseTransitionScene<TPose, TNode = unknown> extends Pick<ScenePoseReports<TPose, TNode>, 'onPoseWrite'> {\n readonly overrides: Pick<PoseOverrides<TPose>, 'read'>;\n}\n\n/** Hand `scene`'s pose writes to the seat's transition, each gliding from the\n * pose it was shown at. A node something else poses through an override — a\n * drag's preview, another animation — is left to it. With `departures`,\n * removals leave as ghosts there, and a node that comes back while its ghost\n * is leaving returns from where the ghost was. */\nexport function bindPoseTransition<TPose, TNode = unknown>(\n scene: PoseTransitionScene<TPose, TNode>,\n seat: () => PoseTransitionSeat<TPose> | null | undefined,\n departures?: Departures<TPose, TNode>,\n): () => void {\n return scene.onPoseWrite((writes, removed) => {\n const s = seat();\n if (!s) return;\n const glides = s.glides ?? glidesByDefault;\n const t = s.transition;\n for (const r of removed) {\n t.stop(r.id as string);\n if (s.exit && departures && (s.exits ?? exitsByDefault)(r)) departures.leave(r, s.exit, t);\n }\n for (const w of writes) {\n const id = w.id as string;\n const back = w.from === undefined ? departures?.recall(id) : undefined;\n if (!glides(w)) continue;\n // One already gliding turns toward the new pose from where it is shown.\n if (t.poseOf(id) !== undefined) {\n t.settle(id);\n continue;\n }\n if (scene.overrides.read(w.id as NodeId)?.pose !== undefined) continue;\n const from = w.from ?? back ?? s.enter?.(w.to);\n if (from !== undefined) t.settle(id, { from });\n }\n });\n}\n\n/** {@link bindPoseTransition} for a component's life, following the latest\n * seat. Subscribes only while there is one. Answers the ghosts it shows. */\nexport function usePoseTransition<TPose, TNode = unknown>(\n scene: PoseTransitionScene<TPose, TNode>,\n seat: PoseTransitionSeat<TPose> | null | undefined,\n): Departures<TPose, TNode> {\n const latest = useLatest(seat);\n const on = !!seat;\n const departures = useMemo(() => createDepartures<TPose, TNode>(), []);\n useEffect(() => () => departures.dispose(), [departures]);\n useEffect(\n () => (on ? bindPoseTransition(scene, () => latest.current, departures) : undefined),\n [on, scene, latest, departures],\n );\n return departures;\n}\n"],"mappings":";;;;;;;;;;;;AAqCA,SAAgB,gBAAgB,OAAoC;CAClE,OAAO,MAAM,WAAW,UAAU,MAAM,WAAW,UAAU,MAAM,WAAW;AAChF;AAEA,IAAM,kBAAkB,YAA2C,QAAQ,WAAW;;;;;;AAYtF,SAAgB,mBACd,OACA,MACA,YACY;CACZ,OAAO,MAAM,aAAa,QAAQ,YAAY;EAC5C,MAAM,IAAI,KAAK;EACf,IAAI,CAAC,GAAG;EACR,MAAM,SAAS,EAAE,UAAU;EAC3B,MAAM,IAAI,EAAE;EACZ,KAAK,MAAM,KAAK,SAAS;GACvB,EAAE,KAAK,EAAE,EAAY;GACrB,IAAI,EAAE,QAAQ,eAAe,EAAE,SAAS,eAAA,CAAgB,CAAC,GAAG,WAAW,MAAM,GAAG,EAAE,MAAM,CAAC;EAC3F;EACA,KAAK,MAAM,KAAK,QAAQ;GACtB,MAAM,KAAK,EAAE;GACb,MAAM,OAAO,EAAE,SAAS,SAAY,YAAY,OAAO,EAAE,IAAI;GAC7D,IAAI,CAAC,OAAO,CAAC,GAAG;GAEhB,IAAI,EAAE,OAAO,EAAE,MAAM,QAAW;IAC9B,EAAE,OAAO,EAAE;IACX;GACF;GACA,IAAI,MAAM,UAAU,KAAK,EAAE,EAAY,CAAC,EAAE,SAAS,QAAW;GAC9D,MAAM,OAAO,EAAE,QAAQ,QAAQ,EAAE,QAAQ,EAAE,EAAE;GAC7C,IAAI,SAAS,QAAW,EAAE,OAAO,IAAI,EAAE,KAAK,CAAC;EAC/C;CACF,CAAC;AACH;;;AAIA,SAAgB,kBACd,OACA,MAC0B;CAC1B,MAAM,SAAS,UAAU,IAAI;CAC7B,MAAM,KAAK,CAAC,CAAC;CACb,MAAM,aAAa,cAAc,iBAA+B,GAAG,CAAC,CAAC;CACrE,sBAAsB,WAAW,QAAQ,GAAG,CAAC,UAAU,CAAC;CACxD,gBACS,KAAK,mBAAmB,aAAa,OAAO,SAAS,UAAU,IAAI,QAC1E;EAAC;EAAI;EAAO;EAAQ;CAAU,CAChC;CACA,OAAO;AACT"}
|
package/dist/animation/reflow.js
CHANGED
|
@@ -49,7 +49,7 @@ function makeGlides(scene, animator, read) {
|
|
|
49
49
|
const release = (id, g) => {
|
|
50
50
|
if (glides.get(id) === g) glides.delete(id);
|
|
51
51
|
const nid = id;
|
|
52
|
-
|
|
52
|
+
scene.overrides.clear(nid, g.entry);
|
|
53
53
|
dirty = true;
|
|
54
54
|
};
|
|
55
55
|
const run = (id, g, to, settling) => {
|
|
@@ -131,10 +131,7 @@ function makeGlides(scene, animator, read) {
|
|
|
131
131
|
glides.delete(id);
|
|
132
132
|
animator.cancelKey(keyFor(id));
|
|
133
133
|
const nid = id;
|
|
134
|
-
|
|
135
|
-
scene.overrides.clear(nid);
|
|
136
|
-
scene.overrides.commit();
|
|
137
|
-
}
|
|
134
|
+
scene.overrides.clear(nid, g.entry);
|
|
138
135
|
unsubscribeIfIdle();
|
|
139
136
|
};
|
|
140
137
|
const start = (id, pose) => {
|
|
@@ -167,7 +164,7 @@ function makeGlides(scene, animator, read) {
|
|
|
167
164
|
g.slot = from;
|
|
168
165
|
g.entry.pose = from;
|
|
169
166
|
}
|
|
170
|
-
if (scene.overrides.
|
|
167
|
+
if (!scene.overrides.has(id, g.entry)) scene.overrides.set(id, g.entry);
|
|
171
168
|
return g;
|
|
172
169
|
};
|
|
173
170
|
return {
|
|
@@ -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 if (scene.overrides.get(nid) === g.entry) scene.overrides.clear(nid);\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 if (scene.overrides.get(nid) === g.entry) {\n scene.overrides.clear(nid);\n scene.overrides.commit();\n }\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 published over whatever override is showing the node.\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.get(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 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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"}
|
|
@@ -84,7 +84,7 @@ function bindRigOn(options, seat) {
|
|
|
84
84
|
const drop = () => {
|
|
85
85
|
if (!installed) return;
|
|
86
86
|
installed = false;
|
|
87
|
-
for (const b of bound) scene.overrides.clear(b.node);
|
|
87
|
+
for (const b of bound) scene.overrides.clear(b.node, b.entry);
|
|
88
88
|
skins.release();
|
|
89
89
|
};
|
|
90
90
|
const release = () => {
|
|
@@ -114,7 +114,7 @@ function bindRigOn(options, seat) {
|
|
|
114
114
|
restWorld: b.restWorld,
|
|
115
115
|
fromRest: motionOf(b.joint)
|
|
116
116
|
});
|
|
117
|
-
if (!
|
|
117
|
+
if (!scene.overrides.has(b.node, b.entry)) scene.overrides.set(b.node, b.entry);
|
|
118
118
|
}
|
|
119
119
|
skins.joints.forEach(motionOf);
|
|
120
120
|
skins.show(fromRest);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindRig.js","names":[],"sources":["../../../src/animation/rig/bindRig.ts"],"sourcesContent":["import type { PoseDescriptor } from '../../core/geometry/poseDescriptor';\nimport type { NodeId, PoseOverride } from '../../core/scene/types';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport { mat3, type GlMat3 } from '@weasel-js/geom';\nimport type { Target } from '@weasel-js/animator';\nimport { resolveSkeleton } from '@weasel-js/animator';\nimport type { Pose, Skeleton } from '@weasel-js/animator';\nimport type { Skin } from '@weasel-js/animator';\nimport { createRigSeat, type RigBinding, type RigSeat } from './rigSeat';\nimport { bindSkins, dataRestPath, type RestPath, type SkinScene } from './skinBinding';\n\n/** The part of a scene a rig writes to. */\nexport interface RigScene<TPose> extends SkinScene<TPose> {\n batch<T>(label: string, fn: () => T): T;\n}\n\n/** What a `RigApply` knows about the node it is posing. */\nexport interface RigApplyContext<TPose> {\n /** The joint the node rides. */\n joint: string;\n node: NodeId;\n /** The node's document pose when the rig was bound — where it sits at the\n * bind pose. */\n rest: TPose;\n /** The joint's world transform at the bind pose. */\n restWorld: GlMat3;\n /** `world * inverse(restWorld)`: the motion since the bind pose, in world\n * space. Carrying `rest` by it is what welding the node to the joint means. */\n fromRest: GlMat3;\n}\n\n/** Turns a joint's world transform into a pose for one bound node. The only\n * part of a rig that knows the scene's pose shape. */\nexport type RigApply<TPose> = (world: GlMat3, ctx: RigApplyContext<TPose>) => TPose;\n\n/** Options for `bindRig`. */\nexport interface BindRigOptions<TPose> {\n scene: RigScene<TPose>;\n skeleton: Skeleton;\n /** Joint name → the node or nodes that ride it. Each node keeps the offset\n * from its joint that it has at the bind pose. */\n bindings?: Readonly<Record<string, NodeId | readonly NodeId[]>>;\n /** Node id → the weights its path bends by. Where a bound node moves whole with one joint,\n * a skinned node's points each follow their own joints, and the rig shows the bent path\n * through the node's pose override, posed at the path's bounds. */\n skins?: Readonly<Record<NodeId, Skin>>;\n /** Where a skinned node's path is drawn when the rig is bound. Default\n * {@link dataRestPath}: the node's `data.path`, through its pose. */\n restPath?: RestPath<TPose>;\n /** Default: {@link rigidRigApply} over `descriptor`. */\n apply?: RigApply<TPose>;\n /** The descriptor the default `apply` reads and writes poses through.\n * Default `AUTO_POSE_DESCRIPTOR`. */\n descriptor?: PoseDescriptor<TPose>;\n}\n\n/** A skeleton bound to scene nodes, as `bindRig` returns it. */\nexport interface Rig {\n readonly skeleton: Skeleton;\n /**\n * The rig's pose as a value an animator moves. Its committed value starts as `{}`, the bind\n * pose, and each frame the rig is posed with what the target shows. A clip is a\n * `SampledTrack<Pose>` given this as its `target`: clips on one rig crossfade by their\n * `weight`s, weights short of 1 leave the rest to the committed pose, and a `spring` or\n * `tween` from a pose away from the committed one adds on top. `animator.hold` shows a pose made elsewhere the same way, an\n * IK solve included.\n */\n readonly target: Target<Pose>;\n /**\n * Commit `pose` and move every bound node to what the target now shows, which is `pose`\n * unless something is animating the target; a hold on it weighs the new pose in when it is\n * next read, at the animator's next frame or the hold's `touch()`. Writes go through the scene's pose overrides —\n * no history entry, no document write, no version bump — and are committed. `root`, where\n * given, replaces the placement, as {@link Rig.place}.\n */\n pose(pose: Pose, root?: GlMat3): void;\n /** Place the rig in the world by `root`, which may mirror it, and pose it there. It stays\n * until placed again; with no `root`, the rig goes back to where its skeleton puts it. */\n place(root?: GlMat3): void;\n /** The joint world transforms as the rig was last posed, root applied. */\n world(): ReadonlyMap<string, GlMat3>;\n /** Write the current frame into the bound nodes' document poses as one\n * undo entry, and drop the overrides. A skinned node's bent path goes into\n * its `data.path`. Later frames stay relative to the bind-time rest, so\n * baking does not compound. */\n bake(label?: string): void;\n /** Drop the overrides; the nodes show their document poses again until the\n * rig is next posed. */\n release(): void;\n}\n\n/**\n * Carries a node rigidly with its joint: the rest pose's center moves by\n * `fromRest`, and its rotation turns with the rest pose's x-axis. Scale in the\n * joint chain moves positions but not sizes — the pose model holds no shear,\n * and a sized pose under anisotropic scale would need one. Write an `apply`\n * that reads `world` to size nodes from joint scale.\n */\nexport function rigidRigApply<TPose>(descriptor: PoseDescriptor<TPose>): RigApply<TPose> {\n return (_world, { rest, fromRest }) => {\n const b = descriptor.getBounds(rest);\n const cx = b.x + b.width / 2;\n const cy = b.y + b.height / 2;\n const [nx, ny] = mat3.apply(fromRest, cx, cy);\n const moved = descriptor.translate\n ? descriptor.translate(rest, nx - cx, ny - cy)\n : descriptor.remapBounds(rest, b, { ...b, x: b.x + nx - cx, y: b.y + ny - cy });\n if (!descriptor.withRotation) return moved;\n const r0 = descriptor.getRotation?.(rest) ?? 0;\n const ux = Math.cos(r0);\n const uy = Math.sin(r0);\n return descriptor.withRotation(\n moved,\n Math.atan2(fromRest[1] * ux + fromRest[4] * uy, fromRest[0] * ux + fromRest[3] * uy),\n );\n };\n}\n\ninterface Bound<TPose> {\n node: NodeId;\n joint: string;\n rest: TPose;\n restWorld: GlMat3;\n entry: PoseOverride<TPose>;\n}\n\n/**\n * Bind a skeleton's joints to scene nodes. Nodes are written in world space\n * and the rig resolves its own hierarchy, so bound nodes are best left\n * unparented, or under containers whose pose does not compose into theirs;\n * the scene's tree stays free to order and group them for drawing.\n */\nexport function bindRig<TPose>(options: BindRigOptions<TPose>): Rig {\n const seat = createRigSeat(options.skeleton);\n const { rig, binding } = bindRigOn(options, seat);\n seat.binding = binding;\n return rig;\n}\n\n/** {@link bindRig} on a seat the caller keeps. The rig shows the seat's target once its\n * `binding` is the seat's. */\nexport function bindRigOn<TPose>(\n options: BindRigOptions<TPose>,\n seat: RigSeat,\n): { rig: Rig; binding: RigBinding } {\n const { scene, skeleton } = options;\n const descriptor = options.descriptor ?? (AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>);\n const apply = options.apply ?? rigidRigApply(descriptor);\n const restWorlds = resolveSkeleton(skeleton, {});\n const restInverses = new Map<string, GlMat3>();\n const restInverseOf = (joint: string): GlMat3 => {\n let inverse = restInverses.get(joint);\n if (inverse) return inverse;\n const restWorld = restWorlds.get(joint);\n if (!restWorld) throw new Error(`bindRig: the skeleton has no joint \"${joint}\".`);\n inverse = mat3.invert(restWorld) ?? undefined;\n if (!inverse) throw new Error(`bindRig: joint \"${joint}\" is singular at the bind pose.`);\n restInverses.set(joint, inverse);\n return inverse;\n };\n\n const skins = bindSkins(scene, options.skins ?? {}, options.restPath ?? dataRestPath, descriptor);\n skins.joints.forEach(restInverseOf);\n\n const bound: Bound<TPose>[] = [];\n for (const [joint, ids] of Object.entries(options.bindings ?? {})) {\n restInverseOf(joint);\n const restWorld = restWorlds.get(joint)!;\n for (const node of typeof ids === 'string' ? [ids] : (ids as readonly NodeId[])) {\n if (options.skins && node in options.skins) {\n throw new Error(`bindRig: node \"${node}\" is both bound to joint \"${joint}\" and skinned.`);\n }\n const n = scene.get(node);\n if (!n) throw new Error(`bindRig: joint \"${joint}\" is bound to node \"${node}\", which the scene lacks.`);\n bound.push({ node, joint, rest: n.pose, restWorld, entry: { pose: n.pose } });\n }\n }\n\n let world: ReadonlyMap<string, GlMat3> = restWorlds;\n let installed = false;\n\n const drop = () => {\n if (!installed) return;\n installed = false;\n for (const b of bound) scene.overrides.clear(b.node);\n skins.release();\n };\n const release = () => {\n seat.shown = false;\n drop();\n };\n\n const show = (pose: Pose): void => {\n const local = resolveSkeleton(skeleton, pose);\n const { root } = seat;\n if (root) for (const [name, m] of local) local.set(name, mat3.multiply(root, m));\n world = local;\n const fromRest = new Map<string, GlMat3>();\n const motionOf = (joint: string): GlMat3 => {\n let delta = fromRest.get(joint);\n if (!delta) {\n delta = mat3.multiply(local.get(joint)!, restInverseOf(joint));\n fromRest.set(joint, delta);\n }\n return delta;\n };\n for (const b of bound) {\n const w = local.get(b.joint)!;\n b.entry.pose = apply(w, { joint: b.joint, node: b.node, rest: b.rest, restWorld: b.restWorld, fromRest: motionOf(b.joint) });\n if (!installed) scene.overrides.set(b.node, b.entry);\n }\n skins.joints.forEach(motionOf);\n skins.show(fromRest);\n installed = true;\n seat.shown = true;\n scene.overrides.commit();\n };\n\n const { target } = seat;\n const rig: Rig = {\n skeleton,\n target,\n pose(pose, at) {\n seat.committed = pose;\n if (at) seat.root = at;\n show(target.read());\n },\n place(at) {\n seat.root = at;\n show(target.read());\n },\n world: () => world,\n bake(label = 'Bake rig pose') {\n if (!installed) return;\n scene.batch(label, () => {\n for (const b of bound) if (b.entry.pose !== undefined) scene.setPose(b.node, b.entry.pose);\n skins.write();\n });\n release();\n },\n release,\n };\n return { rig, binding: { show, drop } };\n}\n"],"mappings":";;;;;;;;;;;;;;AAkGA,SAAgB,cAAqB,YAAoD;CACvF,QAAQ,QAAQ,EAAE,MAAM,eAAe;EACrC,MAAM,IAAI,WAAW,UAAU,IAAI;EACnC,MAAM,KAAK,EAAE,IAAI,EAAE,QAAQ;EAC3B,MAAM,KAAK,EAAE,IAAI,EAAE,SAAS;EAC5B,MAAM,CAAC,IAAI,MAAM,KAAK,MAAM,UAAU,IAAI,EAAE;EAC5C,MAAM,QAAQ,WAAW,YACrB,WAAW,UAAU,MAAM,KAAK,IAAI,KAAK,EAAE,IAC3C,WAAW,YAAY,MAAM,GAAG;GAAE,GAAG;GAAG,GAAG,EAAE,IAAI,KAAK;GAAI,GAAG,EAAE,IAAI,KAAK;EAAG,CAAC;EAChF,IAAI,CAAC,WAAW,cAAc,OAAO;EACrC,MAAM,KAAK,WAAW,cAAc,IAAI,KAAK;EAC7C,MAAM,KAAK,KAAK,IAAI,EAAE;EACtB,MAAM,KAAK,KAAK,IAAI,EAAE;EACtB,OAAO,WAAW,aAChB,OACA,KAAK,MAAM,SAAS,KAAK,KAAK,SAAS,KAAK,IAAI,SAAS,KAAK,KAAK,SAAS,KAAK,EAAE,CACrF;CACF;AACF;;;;;;;AAgBA,SAAgB,QAAe,SAAqC;CAClE,MAAM,OAAO,cAAc,QAAQ,QAAQ;CAC3C,MAAM,EAAE,KAAK,YAAY,UAAU,SAAS,IAAI;CAChD,KAAK,UAAU;CACf,OAAO;AACT;;;AAIA,SAAgB,UACd,SACA,MACmC;CACnC,MAAM,EAAE,OAAO,aAAa;CAC5B,MAAM,aAAa,QAAQ,cAAe;CAC1C,MAAM,QAAQ,QAAQ,SAAS,cAAc,UAAU;CACvD,MAAM,aAAa,gBAAgB,UAAU,CAAC,CAAC;CAC/C,MAAM,+BAAe,IAAI,IAAoB;CAC7C,MAAM,iBAAiB,UAA0B;EAC/C,IAAI,UAAU,aAAa,IAAI,KAAK;EACpC,IAAI,SAAS,OAAO;EACpB,MAAM,YAAY,WAAW,IAAI,KAAK;EACtC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,uCAAuC,MAAM,GAAG;EAChF,UAAU,KAAK,OAAO,SAAS,KAAK;EACpC,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,mBAAmB,MAAM,gCAAgC;EACvF,aAAa,IAAI,OAAO,OAAO;EAC/B,OAAO;CACT;CAEA,MAAM,QAAQ,UAAU,OAAO,QAAQ,SAAS,CAAC,GAAG,QAAQ,YAAY,cAAc,UAAU;CAChG,MAAM,OAAO,QAAQ,aAAa;CAElC,MAAM,QAAwB,CAAC;CAC/B,KAAK,MAAM,CAAC,OAAO,QAAQ,OAAO,QAAQ,QAAQ,YAAY,CAAC,CAAC,GAAG;EACjE,cAAc,KAAK;EACnB,MAAM,YAAY,WAAW,IAAI,KAAK;EACtC,KAAK,MAAM,QAAQ,OAAO,QAAQ,WAAW,CAAC,GAAG,IAAK,KAA2B;GAC/E,IAAI,QAAQ,SAAS,QAAQ,QAAQ,OACnC,MAAM,IAAI,MAAM,kBAAkB,KAAK,4BAA4B,MAAM,eAAe;GAE1F,MAAM,IAAI,MAAM,IAAI,IAAI;GACxB,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,mBAAmB,MAAM,sBAAsB,KAAK,0BAA0B;GACtG,MAAM,KAAK;IAAE;IAAM;IAAO,MAAM,EAAE;IAAM;IAAW,OAAO,EAAE,MAAM,EAAE,KAAK;GAAE,CAAC;EAC9E;CACF;CAEA,IAAI,QAAqC;CACzC,IAAI,YAAY;CAEhB,MAAM,aAAa;EACjB,IAAI,CAAC,WAAW;EAChB,YAAY;EACZ,KAAK,MAAM,KAAK,OAAO,MAAM,UAAU,MAAM,EAAE,IAAI;EACnD,MAAM,QAAQ;CAChB;CACA,MAAM,gBAAgB;EACpB,KAAK,QAAQ;EACb,KAAK;CACP;CAEA,MAAM,QAAQ,SAAqB;EACjC,MAAM,QAAQ,gBAAgB,UAAU,IAAI;EAC5C,MAAM,EAAE,SAAS;EACjB,IAAI,MAAM,KAAK,MAAM,CAAC,MAAM,MAAM,OAAO,MAAM,IAAI,MAAM,KAAK,SAAS,MAAM,CAAC,CAAC;EAC/E,QAAQ;EACR,MAAM,2BAAW,IAAI,IAAoB;EACzC,MAAM,YAAY,UAA0B;GAC1C,IAAI,QAAQ,SAAS,IAAI,KAAK;GAC9B,IAAI,CAAC,OAAO;IACV,QAAQ,KAAK,SAAS,MAAM,IAAI,KAAK,GAAI,cAAc,KAAK,CAAC;IAC7D,SAAS,IAAI,OAAO,KAAK;GAC3B;GACA,OAAO;EACT;EACA,KAAK,MAAM,KAAK,OAAO;GACrB,MAAM,IAAI,MAAM,IAAI,EAAE,KAAK;GAC3B,EAAE,MAAM,OAAO,MAAM,GAAG;IAAE,OAAO,EAAE;IAAO,MAAM,EAAE;IAAM,MAAM,EAAE;IAAM,WAAW,EAAE;IAAW,UAAU,SAAS,EAAE,KAAK;GAAE,CAAC;GAC3H,IAAI,CAAC,WAAW,MAAM,UAAU,IAAI,EAAE,MAAM,EAAE,KAAK;EACrD;EACA,MAAM,OAAO,QAAQ,QAAQ;EAC7B,MAAM,KAAK,QAAQ;EACnB,YAAY;EACZ,KAAK,QAAQ;EACb,MAAM,UAAU,OAAO;CACzB;CAEA,MAAM,EAAE,WAAW;CAwBnB,OAAO;EAAE;GAtBP;GACA;GACA,KAAK,MAAM,IAAI;IACb,KAAK,YAAY;IACjB,IAAI,IAAI,KAAK,OAAO;IACpB,KAAK,OAAO,KAAK,CAAC;GACpB;GACA,MAAM,IAAI;IACR,KAAK,OAAO;IACZ,KAAK,OAAO,KAAK,CAAC;GACpB;GACA,aAAa;GACb,KAAK,QAAQ,iBAAiB;IAC5B,IAAI,CAAC,WAAW;IAChB,MAAM,MAAM,aAAa;KACvB,KAAK,MAAM,KAAK,OAAO,IAAI,EAAE,MAAM,SAAS,QAAW,MAAM,QAAQ,EAAE,MAAM,EAAE,MAAM,IAAI;KACzF,MAAM,MAAM;IACd,CAAC;IACD,QAAQ;GACV;GACA;EAEO;EAAK,SAAS;GAAE;GAAM;EAAK;CAAE;AACxC"}
|
|
1
|
+
{"version":3,"file":"bindRig.js","names":[],"sources":["../../../src/animation/rig/bindRig.ts"],"sourcesContent":["import type { PoseDescriptor } from '../../core/geometry/poseDescriptor';\nimport type { NodeId, PoseOverride } from '../../core/scene/types';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport { mat3, type GlMat3 } from '@weasel-js/geom';\nimport type { Target } from '@weasel-js/animator';\nimport { resolveSkeleton } from '@weasel-js/animator';\nimport type { Pose, Skeleton } from '@weasel-js/animator';\nimport type { Skin } from '@weasel-js/animator';\nimport { createRigSeat, type RigBinding, type RigSeat } from './rigSeat';\nimport { bindSkins, dataRestPath, type RestPath, type SkinScene } from './skinBinding';\n\n/** The part of a scene a rig writes to. */\nexport interface RigScene<TPose> extends SkinScene<TPose> {\n batch<T>(label: string, fn: () => T): T;\n}\n\n/** What a `RigApply` knows about the node it is posing. */\nexport interface RigApplyContext<TPose> {\n /** The joint the node rides. */\n joint: string;\n node: NodeId;\n /** The node's document pose when the rig was bound — where it sits at the\n * bind pose. */\n rest: TPose;\n /** The joint's world transform at the bind pose. */\n restWorld: GlMat3;\n /** `world * inverse(restWorld)`: the motion since the bind pose, in world\n * space. Carrying `rest` by it is what welding the node to the joint means. */\n fromRest: GlMat3;\n}\n\n/** Turns a joint's world transform into a pose for one bound node. The only\n * part of a rig that knows the scene's pose shape. */\nexport type RigApply<TPose> = (world: GlMat3, ctx: RigApplyContext<TPose>) => TPose;\n\n/** Options for `bindRig`. */\nexport interface BindRigOptions<TPose> {\n scene: RigScene<TPose>;\n skeleton: Skeleton;\n /** Joint name → the node or nodes that ride it. Each node keeps the offset\n * from its joint that it has at the bind pose. */\n bindings?: Readonly<Record<string, NodeId | readonly NodeId[]>>;\n /** Node id → the weights its path bends by. Where a bound node moves whole with one joint,\n * a skinned node's points each follow their own joints, and the rig shows the bent path\n * through the node's pose override, posed at the path's bounds. */\n skins?: Readonly<Record<NodeId, Skin>>;\n /** Where a skinned node's path is drawn when the rig is bound. Default\n * {@link dataRestPath}: the node's `data.path`, through its pose. */\n restPath?: RestPath<TPose>;\n /** Default: {@link rigidRigApply} over `descriptor`. */\n apply?: RigApply<TPose>;\n /** The descriptor the default `apply` reads and writes poses through.\n * Default `AUTO_POSE_DESCRIPTOR`. */\n descriptor?: PoseDescriptor<TPose>;\n}\n\n/** A skeleton bound to scene nodes, as `bindRig` returns it. */\nexport interface Rig {\n readonly skeleton: Skeleton;\n /**\n * The rig's pose as a value an animator moves. Its committed value starts as `{}`, the bind\n * pose, and each frame the rig is posed with what the target shows. A clip is a\n * `SampledTrack<Pose>` given this as its `target`: clips on one rig crossfade by their\n * `weight`s, weights short of 1 leave the rest to the committed pose, and a `spring` or\n * `tween` from a pose away from the committed one adds on top. `animator.hold` shows a pose made elsewhere the same way, an\n * IK solve included.\n */\n readonly target: Target<Pose>;\n /**\n * Commit `pose` and move every bound node to what the target now shows, which is `pose`\n * unless something is animating the target; a hold on it weighs the new pose in when it is\n * next read, at the animator's next frame or the hold's `touch()`. Writes go through the scene's pose overrides —\n * no history entry, no document write, no version bump — and are committed. `root`, where\n * given, replaces the placement, as {@link Rig.place}.\n */\n pose(pose: Pose, root?: GlMat3): void;\n /** Place the rig in the world by `root`, which may mirror it, and pose it there. It stays\n * until placed again; with no `root`, the rig goes back to where its skeleton puts it. */\n place(root?: GlMat3): void;\n /** The joint world transforms as the rig was last posed, root applied. */\n world(): ReadonlyMap<string, GlMat3>;\n /** Write the current frame into the bound nodes' document poses as one\n * undo entry, and drop the overrides. A skinned node's bent path goes into\n * its `data.path`. Later frames stay relative to the bind-time rest, so\n * baking does not compound. */\n bake(label?: string): void;\n /** Drop the overrides; the nodes show their document poses again until the\n * rig is next posed. */\n release(): void;\n}\n\n/**\n * Carries a node rigidly with its joint: the rest pose's center moves by\n * `fromRest`, and its rotation turns with the rest pose's x-axis. Scale in the\n * joint chain moves positions but not sizes — the pose model holds no shear,\n * and a sized pose under anisotropic scale would need one. Write an `apply`\n * that reads `world` to size nodes from joint scale.\n */\nexport function rigidRigApply<TPose>(descriptor: PoseDescriptor<TPose>): RigApply<TPose> {\n return (_world, { rest, fromRest }) => {\n const b = descriptor.getBounds(rest);\n const cx = b.x + b.width / 2;\n const cy = b.y + b.height / 2;\n const [nx, ny] = mat3.apply(fromRest, cx, cy);\n const moved = descriptor.translate\n ? descriptor.translate(rest, nx - cx, ny - cy)\n : descriptor.remapBounds(rest, b, { ...b, x: b.x + nx - cx, y: b.y + ny - cy });\n if (!descriptor.withRotation) return moved;\n const r0 = descriptor.getRotation?.(rest) ?? 0;\n const ux = Math.cos(r0);\n const uy = Math.sin(r0);\n return descriptor.withRotation(\n moved,\n Math.atan2(fromRest[1] * ux + fromRest[4] * uy, fromRest[0] * ux + fromRest[3] * uy),\n );\n };\n}\n\ninterface Bound<TPose> {\n node: NodeId;\n joint: string;\n rest: TPose;\n restWorld: GlMat3;\n entry: PoseOverride<TPose>;\n}\n\n/**\n * Bind a skeleton's joints to scene nodes. Nodes are written in world space\n * and the rig resolves its own hierarchy, so bound nodes are best left\n * unparented, or under containers whose pose does not compose into theirs;\n * the scene's tree stays free to order and group them for drawing.\n */\nexport function bindRig<TPose>(options: BindRigOptions<TPose>): Rig {\n const seat = createRigSeat(options.skeleton);\n const { rig, binding } = bindRigOn(options, seat);\n seat.binding = binding;\n return rig;\n}\n\n/** {@link bindRig} on a seat the caller keeps. The rig shows the seat's target once its\n * `binding` is the seat's. */\nexport function bindRigOn<TPose>(\n options: BindRigOptions<TPose>,\n seat: RigSeat,\n): { rig: Rig; binding: RigBinding } {\n const { scene, skeleton } = options;\n const descriptor = options.descriptor ?? (AUTO_POSE_DESCRIPTOR as PoseDescriptor<TPose>);\n const apply = options.apply ?? rigidRigApply(descriptor);\n const restWorlds = resolveSkeleton(skeleton, {});\n const restInverses = new Map<string, GlMat3>();\n const restInverseOf = (joint: string): GlMat3 => {\n let inverse = restInverses.get(joint);\n if (inverse) return inverse;\n const restWorld = restWorlds.get(joint);\n if (!restWorld) throw new Error(`bindRig: the skeleton has no joint \"${joint}\".`);\n inverse = mat3.invert(restWorld) ?? undefined;\n if (!inverse) throw new Error(`bindRig: joint \"${joint}\" is singular at the bind pose.`);\n restInverses.set(joint, inverse);\n return inverse;\n };\n\n const skins = bindSkins(scene, options.skins ?? {}, options.restPath ?? dataRestPath, descriptor);\n skins.joints.forEach(restInverseOf);\n\n const bound: Bound<TPose>[] = [];\n for (const [joint, ids] of Object.entries(options.bindings ?? {})) {\n restInverseOf(joint);\n const restWorld = restWorlds.get(joint)!;\n for (const node of typeof ids === 'string' ? [ids] : (ids as readonly NodeId[])) {\n if (options.skins && node in options.skins) {\n throw new Error(`bindRig: node \"${node}\" is both bound to joint \"${joint}\" and skinned.`);\n }\n const n = scene.get(node);\n if (!n) throw new Error(`bindRig: joint \"${joint}\" is bound to node \"${node}\", which the scene lacks.`);\n bound.push({ node, joint, rest: n.pose, restWorld, entry: { pose: n.pose } });\n }\n }\n\n let world: ReadonlyMap<string, GlMat3> = restWorlds;\n let installed = false;\n\n const drop = () => {\n if (!installed) return;\n installed = false;\n for (const b of bound) scene.overrides.clear(b.node, b.entry);\n skins.release();\n };\n const release = () => {\n seat.shown = false;\n drop();\n };\n\n const show = (pose: Pose): void => {\n const local = resolveSkeleton(skeleton, pose);\n const { root } = seat;\n if (root) for (const [name, m] of local) local.set(name, mat3.multiply(root, m));\n world = local;\n const fromRest = new Map<string, GlMat3>();\n const motionOf = (joint: string): GlMat3 => {\n let delta = fromRest.get(joint);\n if (!delta) {\n delta = mat3.multiply(local.get(joint)!, restInverseOf(joint));\n fromRest.set(joint, delta);\n }\n return delta;\n };\n for (const b of bound) {\n const w = local.get(b.joint)!;\n b.entry.pose = apply(w, { joint: b.joint, node: b.node, rest: b.rest, restWorld: b.restWorld, fromRest: motionOf(b.joint) });\n // Checked every frame: the table drops a node's overrides when the node goes.\n if (!scene.overrides.has(b.node, b.entry)) scene.overrides.set(b.node, b.entry);\n }\n skins.joints.forEach(motionOf);\n skins.show(fromRest);\n installed = true;\n seat.shown = true;\n scene.overrides.commit();\n };\n\n const { target } = seat;\n const rig: Rig = {\n skeleton,\n target,\n pose(pose, at) {\n seat.committed = pose;\n if (at) seat.root = at;\n show(target.read());\n },\n place(at) {\n seat.root = at;\n show(target.read());\n },\n world: () => world,\n bake(label = 'Bake rig pose') {\n if (!installed) return;\n scene.batch(label, () => {\n for (const b of bound) if (b.entry.pose !== undefined) scene.setPose(b.node, b.entry.pose);\n skins.write();\n });\n release();\n },\n release,\n };\n return { rig, binding: { show, drop } };\n}\n"],"mappings":"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"}
|
|
@@ -47,7 +47,7 @@ function bindSkins(scene, skins, restPath, descriptor) {
|
|
|
47
47
|
const path = skinPath(s.rest, s.skin, motions);
|
|
48
48
|
s.entry.path = path;
|
|
49
49
|
s.entry.pose = descriptor.fromBounds(boundsOfPath(path), s.restPose);
|
|
50
|
-
if (!
|
|
50
|
+
if (!scene.overrides.has(s.node, s.entry)) scene.overrides.set(s.node, s.entry);
|
|
51
51
|
}
|
|
52
52
|
installed = true;
|
|
53
53
|
},
|
|
@@ -66,7 +66,7 @@ function bindSkins(scene, skins, restPath, descriptor) {
|
|
|
66
66
|
release() {
|
|
67
67
|
if (!installed) return;
|
|
68
68
|
installed = false;
|
|
69
|
-
for (const s of skinned) scene.overrides.clear(s.node);
|
|
69
|
+
for (const s of skinned) scene.overrides.clear(s.node, s.entry);
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skinBinding.js","names":[],"sources":["../../../src/animation/rig/skinBinding.ts"],"sourcesContent":["import {\n boundsOfPath, pathInWorld, remapPointValues,\n type GlMat3, type Path, type PathInWorldPose, type PointOrigin,\n} from '@weasel-js/geom';\nimport type { PoseDescriptor } from '../../core/geometry/poseDescriptor';\nimport type { NodeId, PoseOverride, PoseOverrides } from '../../core/scene/types';\nimport { skinPath, type Skin } from '@weasel-js/animator';\n\n/** The part of a scene a rig's skins read and write. */\nexport interface SkinScene<TPose> {\n get(id: NodeId): { pose: TPose; data?: unknown } | undefined;\n readonly overrides: PoseOverrides<TPose>;\n setPose(id: NodeId, pose: TPose): void;\n update(id: NodeId, patch: { data: never }): void;\n}\n\n/** Where a node's path is drawn, for a skin to bend: `null` for a node with none. */\nexport type RestPath<TPose> = (node: { pose: TPose; data?: unknown }) => Path | null;\n\n/** The path in a node's `data.path`, carried through its pose as `kit:path` paints it. */\nexport function dataRestPath<TPose>(node: { pose: TPose; data?: unknown }): Path | null {\n const path = (node.data as { path?: Path } | null | undefined)?.path;\n return path ? pathInWorld(path, node.pose as PathInWorldPose) : null;\n}\n\n/**\n * `skin` after an edit to the points of the path it weighs: `origins` says where each point\n * of the new path came from, as `editAnchorSetTracked` answers. A point the edit made takes\n * weights between those of the points it lies between, and a joint left weighing nothing is\n * dropped.\n */\nexport function remapSkin(skin: Skin, origins: readonly (PointOrigin | null)[]): Skin {\n const next: Record<string, number[]> = {};\n for (const joint in skin) {\n const weights = remapPointValues(skin[joint], origins);\n if (weights.some((w) => w > 0)) next[joint] = weights;\n }\n return next;\n}\n\ninterface Skinned<TPose> {\n node: NodeId;\n skin: Skin;\n rest: Path;\n restPose: TPose;\n entry: PoseOverride<TPose>;\n}\n\n/** A rig's skinned nodes, as `bindSkins` returns them. */\nexport interface SkinBinding {\n /** Every joint a skin weighs. */\n readonly joints: readonly string[];\n /** Bend each skinned path by `motions`, which holds every one of `joints`, into its node's\n * pose override. The caller commits the overrides. */\n show(motions: ReadonlyMap<string, GlMat3>): void;\n /** Write each bent path and its bounds into the document. */\n write(): void;\n release(): void;\n}\n\n/** Read each skinned node's rest path and ready an override for it. */\nexport function bindSkins<TPose>(\n scene: SkinScene<TPose>,\n skins: Readonly<Record<NodeId, Skin>>,\n restPath: RestPath<TPose>,\n descriptor: PoseDescriptor<TPose>,\n): SkinBinding {\n const skinned: Skinned<TPose>[] = [];\n const joints = new Set<string>();\n for (const [id, skin] of Object.entries(skins) as [NodeId, Skin][]) {\n const node = scene.get(id);\n if (!node) throw new Error(`bindRig: a skin names node \"${id}\", which the scene lacks.`);\n const rest = restPath(node);\n if (!rest) throw new Error(`bindRig: node \"${id}\" has no path to skin.`);\n for (const joint in skin) joints.add(joint);\n skinned.push({ node: id, skin, rest, restPose: node.pose, entry: {} });\n }\n let installed = false;\n return {\n joints: [...joints],\n show(motions) {\n for (const s of skinned) {\n const path = skinPath(s.rest, s.skin, motions);\n s.entry.path = path;\n s.entry.pose = descriptor.fromBounds(boundsOfPath(path), s.restPose);\n if (!
|
|
1
|
+
{"version":3,"file":"skinBinding.js","names":[],"sources":["../../../src/animation/rig/skinBinding.ts"],"sourcesContent":["import {\n boundsOfPath, pathInWorld, remapPointValues,\n type GlMat3, type Path, type PathInWorldPose, type PointOrigin,\n} from '@weasel-js/geom';\nimport type { PoseDescriptor } from '../../core/geometry/poseDescriptor';\nimport type { NodeId, PoseOverride, PoseOverrides } from '../../core/scene/types';\nimport { skinPath, type Skin } from '@weasel-js/animator';\n\n/** The part of a scene a rig's skins read and write. */\nexport interface SkinScene<TPose> {\n get(id: NodeId): { pose: TPose; data?: unknown } | undefined;\n readonly overrides: PoseOverrides<TPose>;\n setPose(id: NodeId, pose: TPose): void;\n update(id: NodeId, patch: { data: never }): void;\n}\n\n/** Where a node's path is drawn, for a skin to bend: `null` for a node with none. */\nexport type RestPath<TPose> = (node: { pose: TPose; data?: unknown }) => Path | null;\n\n/** The path in a node's `data.path`, carried through its pose as `kit:path` paints it. */\nexport function dataRestPath<TPose>(node: { pose: TPose; data?: unknown }): Path | null {\n const path = (node.data as { path?: Path } | null | undefined)?.path;\n return path ? pathInWorld(path, node.pose as PathInWorldPose) : null;\n}\n\n/**\n * `skin` after an edit to the points of the path it weighs: `origins` says where each point\n * of the new path came from, as `editAnchorSetTracked` answers. A point the edit made takes\n * weights between those of the points it lies between, and a joint left weighing nothing is\n * dropped.\n */\nexport function remapSkin(skin: Skin, origins: readonly (PointOrigin | null)[]): Skin {\n const next: Record<string, number[]> = {};\n for (const joint in skin) {\n const weights = remapPointValues(skin[joint], origins);\n if (weights.some((w) => w > 0)) next[joint] = weights;\n }\n return next;\n}\n\ninterface Skinned<TPose> {\n node: NodeId;\n skin: Skin;\n rest: Path;\n restPose: TPose;\n entry: PoseOverride<TPose>;\n}\n\n/** A rig's skinned nodes, as `bindSkins` returns them. */\nexport interface SkinBinding {\n /** Every joint a skin weighs. */\n readonly joints: readonly string[];\n /** Bend each skinned path by `motions`, which holds every one of `joints`, into its node's\n * pose override. The caller commits the overrides. */\n show(motions: ReadonlyMap<string, GlMat3>): void;\n /** Write each bent path and its bounds into the document. */\n write(): void;\n release(): void;\n}\n\n/** Read each skinned node's rest path and ready an override for it. */\nexport function bindSkins<TPose>(\n scene: SkinScene<TPose>,\n skins: Readonly<Record<NodeId, Skin>>,\n restPath: RestPath<TPose>,\n descriptor: PoseDescriptor<TPose>,\n): SkinBinding {\n const skinned: Skinned<TPose>[] = [];\n const joints = new Set<string>();\n for (const [id, skin] of Object.entries(skins) as [NodeId, Skin][]) {\n const node = scene.get(id);\n if (!node) throw new Error(`bindRig: a skin names node \"${id}\", which the scene lacks.`);\n const rest = restPath(node);\n if (!rest) throw new Error(`bindRig: node \"${id}\" has no path to skin.`);\n for (const joint in skin) joints.add(joint);\n skinned.push({ node: id, skin, rest, restPose: node.pose, entry: {} });\n }\n let installed = false;\n return {\n joints: [...joints],\n show(motions) {\n for (const s of skinned) {\n const path = skinPath(s.rest, s.skin, motions);\n s.entry.path = path;\n s.entry.pose = descriptor.fromBounds(boundsOfPath(path), s.restPose);\n if (!scene.overrides.has(s.node, s.entry)) scene.overrides.set(s.node, s.entry);\n }\n installed = true;\n },\n write() {\n for (const s of skinned) {\n const { path, pose } = s.entry;\n if (path === undefined || pose === undefined) continue;\n const data = scene.get(s.node)?.data;\n scene.update(s.node, { data: { ...(data as object), path } as never });\n scene.setPose(s.node, pose);\n }\n },\n release() {\n if (!installed) return;\n installed = false;\n for (const s of skinned) scene.overrides.clear(s.node, s.entry);\n },\n };\n}\n"],"mappings":";;;;;AAoBA,SAAgB,aAAoB,MAAoD;CACtF,MAAM,OAAQ,KAAK,MAA6C;CAChE,OAAO,OAAO,YAAY,MAAM,KAAK,IAAuB,IAAI;AAClE;;;;;;;AAQA,SAAgB,UAAU,MAAY,SAAgD;CACpF,MAAM,OAAiC,CAAC;CACxC,KAAK,MAAM,SAAS,MAAM;EACxB,MAAM,UAAU,iBAAiB,KAAK,QAAQ,OAAO;EACrD,IAAI,QAAQ,MAAM,MAAM,IAAI,CAAC,GAAG,KAAK,SAAS;CAChD;CACA,OAAO;AACT;;AAuBA,SAAgB,UACd,OACA,OACA,UACA,YACa;CACb,MAAM,UAA4B,CAAC;CACnC,MAAM,yBAAS,IAAI,IAAY;CAC/B,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,KAAK,GAAuB;EAClE,MAAM,OAAO,MAAM,IAAI,EAAE;EACzB,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,+BAA+B,GAAG,0BAA0B;EACvF,MAAM,OAAO,SAAS,IAAI;EAC1B,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,kBAAkB,GAAG,uBAAuB;EACvE,KAAK,MAAM,SAAS,MAAM,OAAO,IAAI,KAAK;EAC1C,QAAQ,KAAK;GAAE,MAAM;GAAI;GAAM;GAAM,UAAU,KAAK;GAAM,OAAO,CAAC;EAAE,CAAC;CACvE;CACA,IAAI,YAAY;CAChB,OAAO;EACL,QAAQ,CAAC,GAAG,MAAM;EAClB,KAAK,SAAS;GACZ,KAAK,MAAM,KAAK,SAAS;IACvB,MAAM,OAAO,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO;IAC7C,EAAE,MAAM,OAAO;IACf,EAAE,MAAM,OAAO,WAAW,WAAW,aAAa,IAAI,GAAG,EAAE,QAAQ;IACnE,IAAI,CAAC,MAAM,UAAU,IAAI,EAAE,MAAM,EAAE,KAAK,GAAG,MAAM,UAAU,IAAI,EAAE,MAAM,EAAE,KAAK;GAChF;GACA,YAAY;EACd;EACA,QAAQ;GACN,KAAK,MAAM,KAAK,SAAS;IACvB,MAAM,EAAE,MAAM,SAAS,EAAE;IACzB,IAAI,SAAS,UAAa,SAAS,QAAW;IAC9C,MAAM,OAAO,MAAM,IAAI,EAAE,IAAI,CAAC,EAAE;IAChC,MAAM,OAAO,EAAE,MAAM,EAAE,MAAM;KAAE,GAAI;KAAiB;IAAK,EAAW,CAAC;IACrE,MAAM,QAAQ,EAAE,MAAM,IAAI;GAC5B;EACF;EACA,UAAU;GACR,IAAI,CAAC,WAAW;GAChB,YAAY;GACZ,KAAK,MAAM,KAAK,SAAS,MAAM,UAAU,MAAM,EAAE,MAAM,EAAE,KAAK;EAChE;CACF;AACF"}
|