@weasel-js/core 1.10.0 → 1.10.1
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 +168 -0
- package/README.md +33 -0
- package/dist/affordances/composeAffordanceLayer.js +21 -21
- package/dist/affordances/composeAffordanceLayer.js.map +1 -1
- package/dist/affordances/hitAffordanceRegions.js +48 -32
- package/dist/affordances/hitAffordanceRegions.js.map +1 -1
- package/dist/affordances/pathAnchors.js +50 -28
- package/dist/affordances/pathAnchors.js.map +1 -1
- package/dist/affordances/rotateCursor.js +12 -0
- package/dist/affordances/rotateCursor.js.map +1 -0
- package/dist/affordances/rotationHandle.js +40 -43
- package/dist/affordances/rotationHandle.js.map +1 -1
- package/dist/animation/rig/rigNode.js +9 -3
- package/dist/animation/rig/rigNode.js.map +1 -1
- package/dist/animation/rig/rigSeat.js +46 -13
- package/dist/animation/rig/rigSeat.js.map +1 -1
- package/dist/animation/rig/skinBinding.js +22 -14
- package/dist/animation/rig/skinBinding.js.map +1 -1
- package/dist/barrel/canvas.js +29 -28
- package/dist/barrel/interactions.js +33 -32
- package/dist/barrel/paths.js +11 -10
- package/dist/barrel/routing.js +49 -48
- package/dist/barrel/scene.js +10 -10
- package/dist/barrel/viewport.js +24 -21
- package/dist/canvas/Canvas.js +6 -17
- package/dist/canvas/Canvas.js.map +1 -1
- package/dist/canvas/CanvasView.js +34 -37
- package/dist/canvas/CanvasView.js.map +1 -1
- package/dist/canvas/CursorCoordsHud.js +3 -3
- package/dist/canvas/CursorCoordsHud.js.map +1 -1
- package/dist/canvas/DrawCanvas.js +3 -3
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +24 -23
- package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js.map +1 -1
- package/dist/canvas/SceneCanvas/StandardActionsRegistrar.js +40 -49
- package/dist/canvas/SceneCanvas/StandardActionsRegistrar.js.map +1 -1
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js +297 -259
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js.map +1 -1
- package/dist/canvas/SceneCanvas/features.js +3 -24
- package/dist/canvas/SceneCanvas/features.js.map +1 -1
- package/dist/canvas/SceneCanvas/insertPreviewCommands.js +97 -0
- package/dist/canvas/SceneCanvas/insertPreviewCommands.js.map +1 -0
- package/dist/canvas/SceneCanvas/poseGeometry.js.map +1 -1
- package/dist/canvas/SceneCanvas/props.js.map +1 -1
- package/dist/canvas/SceneCanvas/scenePointQuery.js +50 -0
- package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -0
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js +52 -0
- package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js.map +1 -0
- package/dist/canvas/SceneCanvas/useActionTunings.js +17 -0
- package/dist/canvas/SceneCanvas/useActionTunings.js.map +1 -0
- package/dist/canvas/SceneCanvas/useCanvasCamera.js +37 -29
- package/dist/canvas/SceneCanvas/useCanvasCamera.js.map +1 -1
- package/dist/canvas/SceneCanvas/useCanvasDispatcher.js +11 -11
- package/dist/canvas/SceneCanvas/useCanvasDispatcher.js.map +1 -1
- package/dist/canvas/SceneCanvas/useDispatcherOverlayLayer.js +45 -109
- package/dist/canvas/SceneCanvas/useDispatcherOverlayLayer.js.map +1 -1
- package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js +49 -41
- package/dist/canvas/SceneCanvas/usePreviewGhostLayer.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js +64 -96
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
- package/dist/canvas/SceneCanvas/worldPointAt.js +3 -4
- package/dist/canvas/SceneCanvas/worldPointAt.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +77 -80
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +3 -3
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/SurfaceCanvas.js +40 -0
- package/dist/canvas/SurfaceCanvas.js.map +1 -0
- package/dist/canvas/adapterDerivedPath.js +13 -13
- package/dist/canvas/adapterDerivedPath.js.map +1 -1
- package/dist/canvas/adapterPickSource.js +34 -0
- package/dist/canvas/adapterPickSource.js.map +1 -0
- package/dist/canvas/affordanceAt.js +1 -1
- package/dist/canvas/affordanceAt.js.map +1 -1
- package/dist/canvas/buildSceneLayer.js.map +1 -1
- package/dist/canvas/buildSceneTree.js +56 -34
- package/dist/canvas/buildSceneTree.js.map +1 -1
- package/dist/canvas/copiesAbove.js +27 -0
- package/dist/canvas/copiesAbove.js.map +1 -0
- package/dist/canvas/defaultDrawOne.js +24 -23
- package/dist/canvas/defaultDrawOne.js.map +1 -1
- package/dist/canvas/deps/editAnchors.js +45 -50
- package/dist/canvas/deps/editAnchors.js.map +1 -1
- package/dist/canvas/deps/hitTestArea.js +30 -15
- package/dist/canvas/deps/hitTestArea.js.map +1 -1
- package/dist/canvas/deps/insert.js +134 -100
- package/dist/canvas/deps/insert.js.map +1 -1
- package/dist/canvas/deps/layout.js +9 -15
- package/dist/canvas/deps/layout.js.map +1 -1
- package/dist/canvas/derivedPath.js +53 -29
- package/dist/canvas/derivedPath.js.map +1 -1
- package/dist/canvas/insertPreviewExtent.js +33 -31
- package/dist/canvas/insertPreviewExtent.js.map +1 -1
- package/dist/canvas/overChildren.js +29 -0
- package/dist/canvas/overChildren.js.map +1 -0
- package/dist/canvas/paintedCursorLayer.js.map +1 -1
- package/dist/canvas/pickReach.js +23 -0
- package/dist/canvas/pickReach.js.map +1 -0
- package/dist/canvas/pickWalk.js +51 -80
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/sceneAdapter.js +14 -35
- package/dist/canvas/sceneAdapter.js.map +1 -1
- package/dist/canvas/sceneSlotWiring.js +11 -10
- package/dist/canvas/sceneSlotWiring.js.map +1 -1
- package/dist/canvas/sceneViewRender.js +38 -44
- package/dist/canvas/sceneViewRender.js.map +1 -1
- package/dist/canvas/useCanvasView.js +42 -44
- package/dist/canvas/useCanvasView.js.map +1 -1
- package/dist/canvas/useSceneAdapter.js +22 -0
- package/dist/canvas/useSceneAdapter.js.map +1 -0
- package/dist/canvas/useSurfaceInput.js +13 -16
- package/dist/canvas/useSurfaceInput.js.map +1 -1
- package/dist/canvas/viewRegistry.js +31 -23
- package/dist/canvas/viewRegistry.js.map +1 -1
- package/dist/canvas/wrapNodeOutput.js +7 -2
- package/dist/canvas/wrapNodeOutput.js.map +1 -1
- package/dist/clipboard.d.ts +1 -1
- package/dist/{clone-2a5hVbBd.d.ts → clone-C0eADtS0.d.ts} +13 -13
- package/dist/clone.d.ts +2 -2
- package/dist/core/geometry/poseDescriptor.js +49 -23
- package/dist/core/geometry/poseDescriptor.js.map +1 -1
- package/dist/core/geometry/regionHit.js +2 -2
- package/dist/core/geometry/regionHit.js.map +1 -1
- package/dist/core/geometry/targetFrame.js.map +1 -1
- package/dist/core/layers/children.js +1 -3
- package/dist/core/layers/children.js.map +1 -1
- package/dist/core/ops/create.js +2 -2
- package/dist/core/ops/create.js.map +1 -1
- package/dist/core/ops/delete.js +19 -23
- package/dist/core/ops/delete.js.map +1 -1
- package/dist/core/ops/reorder/index.js +3 -7
- package/dist/core/ops/reorder/index.js.map +1 -1
- package/dist/core/ops/reorder/setChildOrder.js +2 -4
- package/dist/core/ops/reorder/setChildOrder.js.map +1 -1
- package/dist/core/ops/reparent.js +0 -1
- package/dist/core/ops/reparent.js.map +1 -1
- package/dist/core/ops/slot.js +3 -6
- package/dist/core/ops/slot.js.map +1 -1
- package/dist/core/pages/fitViewToPage.js +15 -0
- package/dist/core/pages/fitViewToPage.js.map +1 -0
- package/dist/core/pages/pages.js +65 -0
- package/dist/core/pages/pages.js.map +1 -0
- package/dist/core/pages/pagesLayer.js +27 -0
- package/dist/core/pages/pagesLayer.js.map +1 -0
- package/dist/core/paint/meshKindLazy.js +2 -2
- package/dist/core/paint/meshKindLazy.js.map +1 -1
- package/dist/core/scene/NodeProperties.js.map +1 -1
- package/dist/core/scene/composeFrame.js.map +1 -1
- package/dist/core/scene/composePose.js +58 -20
- package/dist/core/scene/composePose.js.map +1 -1
- package/dist/core/scene/effectivePose.js +5 -1
- package/dist/core/scene/effectivePose.js.map +1 -1
- package/dist/core/scene/historyFacade.js +19 -13
- package/dist/core/scene/historyFacade.js.map +1 -1
- package/dist/core/scene/nodeFnFields.js +7 -0
- package/dist/core/scene/nodeFnFields.js.map +1 -1
- package/dist/core/scene/outerOps.js +2 -1
- package/dist/core/scene/outerOps.js.map +1 -1
- package/dist/core/scene/reconcileSpecs.js +1 -1
- package/dist/core/scene/reconcileSpecs.js.map +1 -1
- package/dist/core/scene/sceneRecording.js +2 -1
- package/dist/core/scene/sceneRecording.js.map +1 -1
- package/dist/core/scene/types.js.map +1 -1
- package/dist/core/viewport/screenSlop.js.map +1 -1
- package/dist/core/viewport/useDecayLoop.js +28 -30
- package/dist/core/viewport/useDecayLoop.js.map +1 -1
- package/dist/core/viewport/useNodeOverlayFrame.js.map +1 -1
- package/dist/core/viewport/useViewAnimation.js +70 -69
- package/dist/core/viewport/useViewAnimation.js.map +1 -1
- package/dist/core/viewport/view.js +8 -5
- package/dist/core/viewport/view.js.map +1 -1
- package/dist/core/viewport/zoomAt.js +11 -2
- package/dist/core/viewport/zoomAt.js.map +1 -1
- package/dist/{cornerHandles-BEshc4z3.d.ts → cornerHandles-BSKbyuvQ.d.ts} +1 -1
- package/dist/features/paths/editablePath.js +24 -8
- package/dist/features/paths/editablePath.js.map +1 -1
- package/dist/features/paths/index.js +4 -4
- package/dist/features/paths/shapeGenerator.js +89 -0
- package/dist/features/paths/shapeGenerator.js.map +1 -0
- package/dist/features/paths/usePathEdit.js +13 -13
- package/dist/features/poseRun/usePoseRun.js +25 -25
- package/dist/features/poseRun/usePoseRun.js.map +1 -1
- package/dist/features/text/useSceneTextEdit.js +82 -64
- package/dist/features/text/useSceneTextEdit.js.map +1 -1
- package/dist/features/viewports/viewResolver.js +28 -24
- package/dist/features/viewports/viewResolver.js.map +1 -1
- package/dist/icons/SliceIcon.js +24 -0
- package/dist/icons/SliceIcon.js.map +1 -0
- package/dist/index.d.ts +956 -581
- package/dist/index.js +260 -251
- package/dist/interactions/actions/align/align.js +9 -9
- package/dist/interactions/actions/align/align.js.map +1 -1
- package/dist/interactions/actions/booleans/booleans.js +18 -18
- package/dist/interactions/actions/booleans/booleans.js.map +1 -1
- package/dist/interactions/actions/booleans/operands.js.map +1 -1
- package/dist/interactions/actions/clone/action.js +35 -30
- package/dist/interactions/actions/clone/action.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js +56 -59
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorHandles.js +42 -0
- package/dist/interactions/actions/defaults/anchorHandles.js.map +1 -0
- package/dist/interactions/actions/defaults/anchorJoin.js +92 -0
- package/dist/interactions/actions/defaults/anchorJoin.js.map +1 -0
- package/dist/interactions/actions/defaults/bendSegment.js +46 -0
- package/dist/interactions/actions/defaults/bendSegment.js.map +1 -0
- package/dist/interactions/actions/defaults/copySubtree.js +1 -1
- package/dist/interactions/actions/defaults/copySubtree.js.map +1 -1
- package/dist/interactions/actions/defaults/duplicate.js +32 -17
- package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
- package/dist/interactions/actions/defaults/enterPathEdit.js +19 -7
- package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/icons/anchorIcons.js +75 -0
- package/dist/interactions/actions/defaults/icons/anchorIcons.js.map +1 -0
- package/dist/interactions/actions/defaults/icons/editIcons.js +35 -8
- package/dist/interactions/actions/defaults/icons/editIcons.js.map +1 -1
- package/dist/interactions/actions/defaults/insertPathAnchor.js +16 -31
- package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
- package/dist/interactions/actions/defaults/nudge.js +28 -23
- package/dist/interactions/actions/defaults/nudge.js.map +1 -1
- package/dist/interactions/actions/defaults/pinchZoom.js +29 -29
- package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
- package/dist/interactions/actions/defaults/segmentUnder.js +22 -0
- package/dist/interactions/actions/defaults/segmentUnder.js.map +1 -0
- package/dist/interactions/actions/defaults/viewportZoom.js +82 -79
- package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
- package/dist/interactions/actions/distribute/distribute.js +8 -8
- package/dist/interactions/actions/distribute/distribute.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/action.js +58 -55
- package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/preview.js +26 -0
- package/dist/interactions/actions/edit-anchors/preview.js.map +1 -0
- package/dist/interactions/actions/edit-anchors/symmetry.js +11 -0
- package/dist/interactions/actions/edit-anchors/symmetry.js.map +1 -0
- package/dist/interactions/actions/flip/action.js.map +1 -1
- package/dist/interactions/actions/geometryProjection.js.map +1 -1
- package/dist/interactions/actions/insert/action.js +37 -30
- package/dist/interactions/actions/insert/action.js.map +1 -1
- package/dist/interactions/actions/insert/frame.js +9 -2
- package/dist/interactions/actions/insert/frame.js.map +1 -1
- package/dist/interactions/actions/insert/repositionAction.js +31 -0
- package/dist/interactions/actions/insert/repositionAction.js.map +1 -0
- package/dist/interactions/actions/lasso-select/action.js +32 -32
- package/dist/interactions/actions/move/action.js +29 -25
- package/dist/interactions/actions/move/action.js.map +1 -1
- package/dist/interactions/actions/move/behaviors/snapToContainer.js +12 -6
- package/dist/interactions/actions/move/behaviors/snapToContainer.js.map +1 -1
- package/dist/interactions/actions/move/capture.js +32 -23
- package/dist/interactions/actions/move/capture.js.map +1 -1
- package/dist/interactions/actions/move/frame.js +2 -14
- package/dist/interactions/actions/move/frame.js.map +1 -1
- package/dist/interactions/actions/move/session.js +1 -1
- package/dist/interactions/actions/move/session.js.map +1 -1
- package/dist/interactions/actions/move/translate.js.map +1 -1
- package/dist/interactions/actions/outlines/createOutlines.js.map +1 -1
- package/dist/interactions/actions/outlines/replaceWithPaths.js +13 -13
- package/dist/interactions/actions/outlines/replaceWithPaths.js.map +1 -1
- package/dist/interactions/actions/outlines/strokeRegion.js.map +1 -1
- package/dist/interactions/actions/pathCommands/cutJoin.js +102 -0
- package/dist/interactions/actions/pathCommands/cutJoin.js.map +1 -0
- package/dist/interactions/actions/pathCommands/livePathEdit.js +64 -0
- package/dist/interactions/actions/pathCommands/livePathEdit.js.map +1 -0
- package/dist/interactions/actions/pathCommands/pathNodes.js +99 -0
- package/dist/interactions/actions/pathCommands/pathNodes.js.map +1 -0
- package/dist/interactions/actions/pathCommands/simplifyOffset.js +42 -0
- package/dist/interactions/actions/pathCommands/simplifyOffset.js.map +1 -0
- package/dist/interactions/actions/resize/action.js +31 -20
- package/dist/interactions/actions/resize/action.js.map +1 -1
- package/dist/interactions/actions/resize/commit.js.map +1 -1
- package/dist/interactions/actions/resize/frame.js +29 -15
- package/dist/interactions/actions/resize/frame.js.map +1 -1
- package/dist/interactions/actions/resize/replay.js +47 -0
- package/dist/interactions/actions/resize/replay.js.map +1 -0
- package/dist/interactions/actions/resize/session.js +49 -35
- package/dist/interactions/actions/resize/session.js.map +1 -1
- package/dist/interactions/actions/resolveOverlays.js +2 -1
- package/dist/interactions/actions/resolveOverlays.js.map +1 -1
- package/dist/interactions/actions/rotate/action.js +40 -25
- package/dist/interactions/actions/rotate/action.js.map +1 -1
- package/dist/interactions/actions/rotate/frame.js +1 -1
- package/dist/interactions/actions/rotate/frame.js.map +1 -1
- package/dist/interactions/actions/rotate/replay.js +22 -0
- package/dist/interactions/actions/rotate/replay.js.map +1 -0
- package/dist/interactions/actions/rotate/session.js +42 -32
- package/dist/interactions/actions/rotate/session.js.map +1 -1
- package/dist/interactions/actions/transform-again/action.js +63 -0
- package/dist/interactions/actions/transform-again/action.js.map +1 -0
- package/dist/interactions/actions/transform-again/memory.js +35 -0
- package/dist/interactions/actions/transform-again/memory.js.map +1 -0
- package/dist/interactions/actions/useStandardActions.js +101 -83
- package/dist/interactions/actions/useStandardActions.js.map +1 -1
- package/dist/move.d.ts +4 -4
- package/dist/{originProjection-PPT88jZv.d.ts → originProjection-CoNVpITV.d.ts} +1 -1
- package/dist/resize.d.ts +3 -3
- package/dist/style.css +1 -1
- package/dist/tools/builtin/ellipse/useEllipseTool.js +16 -15
- package/dist/tools/builtin/ellipse/useEllipseTool.js.map +1 -1
- package/dist/tools/builtin/line/useLineTool.js +16 -15
- package/dist/tools/builtin/line/useLineTool.js.map +1 -1
- package/dist/tools/builtin/pen/usePenTool.js +111 -111
- package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
- package/dist/tools/builtin/pickTopMostHit.js +11 -14
- package/dist/tools/builtin/pickTopMostHit.js.map +1 -1
- package/dist/tools/builtin/polygon/usePolygonTool.js +17 -15
- package/dist/tools/builtin/polygon/usePolygonTool.js.map +1 -1
- package/dist/tools/builtin/rect/useRectTool.js +15 -14
- package/dist/tools/builtin/rect/useRectTool.js.map +1 -1
- package/dist/tools/builtin/select/adapterPointQuery.js +22 -18
- package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
- package/dist/tools/builtin/select/selectionContributions.js +1 -1
- package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
- package/dist/tools/builtin/select/useSelectTool.js +2 -1
- package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
- package/dist/tools/builtin/slice/useSliceTool.js +42 -40
- package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
- package/dist/tools/builtin/star/useStarTool.js +18 -16
- package/dist/tools/builtin/star/useStarTool.js.map +1 -1
- package/dist/tools/builtin/text/useTextTool.js +2 -2
- package/dist/tools/builtin/text/useTextTool.js.map +1 -1
- package/dist/{types-CIKDwlsP.d.ts → types-DBbbCy0i.d.ts} +22 -15
- package/dist/version.js +1 -1
- package/package.json +22 -22
- package/dist/canvas/surface.module.js +0 -6
- package/dist/canvas/surface.module.js.map +0 -1
- package/dist/canvas/surfaceBox.js +0 -16
- package/dist/canvas/surfaceBox.js.map +0 -1
- package/dist/layout/arrivals.js +0 -33
- package/dist/layout/arrivals.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["asNodeId","s"],"sources":["../../../src/core/scene/types.ts"],"sourcesContent":["import type { Path } from '@weasel-js/geom';\nimport type { History, SerializedHistory } from '@weasel-js/history';\nimport type { SerializedSceneHistory } from './historyForm';\nimport type { ParallaxOpts } from '../viewport/parallax';\nimport type { UnitScope } from '@weasel-js/quantity';\nimport type { LayoutStrategy } from '../../layout/types';\nimport type { LayoutFrame, LayoutMove } from './layoutTypes';\nimport type { PoseComposition } from './composeFrame';\nimport type { PoseLinear } from '@weasel-js/geom';\nimport type { PoseHold, PoseOverride, PoseOverrides } from './poseOverrideTypes';\nimport type { SerializedLayer, SerializedNode, SerializedScene } from './serializedTypes';\n\nexport type { LayoutFrame, LayoutMove, PoseHold, PoseOverride, PoseOverrides, SerializedLayer, SerializedNode, SerializedScene };\n\n/**\n * Axis-aligned rectangle pose with optional rotation. Rotation is in\n * radians, pivoted on the unrotated AABB center; absent === 0.\n */\nexport interface RectPose {\n x: number;\n y: number;\n width: number;\n height: number;\n /** Rotation in radians around the unrotated AABB center. Absent === 0. */\n rotation?: number;\n /** A skew or per-axis scale the box and rotation cannot hold; see\n * {@link PoseLinear}. Absent is the identity. */\n transform?: PoseLinear;\n}\n\n/**\n * # SceneNode — the thing in the scene\n *\n * Everything the user sees on the canvas — a shape, a group, an annotation,\n * a tile — is a node. Containers and leaves are both nodes; the kit has no\n * other concept of \"scene element.\"\n *\n * Every node carries three independent slots, plus its tree position:\n *\n * - **`data: TData`** — app-defined payload. The kit never inspects it.\n * - **`pose: TPose`** — local transform, relative to the node's direct\n * parent (or world, for root nodes).\n * - **`layer: TLayer`** — a string tag associating the node with a\n * visual `RenderLayer` at draw time. Separate from `LayerRecord` (the\n * per-layer visible/locked metadata held by the `Scene`).\n *\n * Nodes are addressed by `NodeId` everywhere outside the scene tree. Don't\n * hold references to a `Node` object across scene updates; look up by id\n * when you need the latest.\n *\n * The kit-internal name is `Node`; the public re-export is `SceneNode`\n * (avoids collision with DOM `Node` at call sites).\n */\n\n/** Opaque branded id. Treat as opaque outside the kit. */\nimport type { NodeId } from '@weasel-js/routing';\nexport type { NodeId };\n\n/** Brand a string as a NodeId. */\nexport const asNodeId = (s: string): NodeId => s as NodeId;\n\n/**\n * One dependency, as a derivation reads it: the node itself and the pose it is\n * painted at.\n *\n * The node comes along because a connector legitimately reads more than a box\n * — an edge that thickens with its endpoint's weight, or routes only to nodes\n * on a given layer, is answering off `data` and `layer`.\n */\nexport interface DerivedDep<TPose, TData = unknown, TLayer extends string = string> {\n node: Node<TData, TLayer, TPose>;\n /** Its override when it has one, else its own derived pose, else the pose\n * the document stores — `effectivePose`, the same answer the renderer uses. */\n pose: TPose;\n /** The path it derives, or `null` when it derives none. Read it to place\n * something *along* a dependency rather than beside it — a label on a\n * routed edge, a tick on a curve. Resolved on first read and memoized on\n * the dependency. */\n readonly path: Path | null;\n}\n\n/** `F` with its parameters checked bivariantly, as a method's are, so that a\n * `Scene<D, L, MyPose>` is a `Scene<unknown, string, unknown>`. A callback for\n * an unrelated pose is still rejected; one for a narrower pose is not. */\nexport type Bivariant<F extends (...args: never[]) => unknown> = {\n method(...args: Parameters<F>): ReturnType<F>;\n}['method'];\n\n/** Computes a node's path from its dependencies: the type of a node's\n * `derivePath` and of each entry in `SceneRegistry.derivePath`. */\nexport type DerivePathFn<TPose, TData = unknown, TLayer extends string = string> = Bivariant<(\n node: Node<TData, TLayer, TPose>,\n deps: readonly (DerivedDep<TPose, TData, TLayer> | undefined)[],\n) => Path | null>;\n\n/** Computes a node's pose from its dependencies: the type of a node's\n * `derivePose` and of each entry in `SceneRegistry.derivePose`. */\nexport type DerivePoseFn<TPose, TData = unknown, TLayer extends string = string> = Bivariant<(\n node: Node<TData, TLayer, TPose>,\n deps: readonly (DerivedDep<TPose, TData, TLayer> | undefined)[],\n) => TPose | null>;\n\n/** A container's clip, from its pose: the type of `ContainerNode.clipFromPose`\n * and of each entry in `SceneRegistry.clipFromPose`. */\nexport type ClipFromPoseFn<TPose> = Bivariant<(pose: TPose) => Path | null>;\n\ninterface NodeBase<TData, TLayer extends string, TPose> {\n id: NodeId;\n layer: TLayer;\n pose: TPose;\n data: TData;\n parent: NodeId | null;\n /** Nodes whose poses this node's geometry is computed from. Fixed at add\n * time. Absent or empty means the node's geometry is authored, which is the\n * normal case.\n *\n * `'children'` means \"my own children, in child order\" — a container that\n * hugs its contents, which a fixed id list cannot express because\n * reparenting would have to maintain it. The two forms differ in lifetime\n * as well as in membership: deleting a node deletes everything that names\n * it in `dependsOn`, but a container outlives the children it derives\n * from — an emptied group is still a group. */\n dependsOn?: readonly NodeId[] | 'children';\n /** Whether the hit-test walk can return this node. Default `true`; `false`\n * makes it transparent to picking, so a click on it lands on whatever is\n * behind — normally its own container.\n *\n * For content a container owns rather than content the user selects on its\n * own: the label inside a box, a body's rows. It does not hide the node\n * from anything else — it still paints, still clips, and a consumer can\n * still select it by id. */\n pickable?: boolean;\n /** Computes this node's path from its dependencies, in `dependsOn` order —\n * each one the node and the pose it is painted at.\n * A dependency that has been removed arrives as `undefined`.\n * Returning `null` means \"nothing to draw right now\". Re-evaluated when a\n * dependency's world pose changes, never authored. */\n derivePath?: DerivePathFn<TPose, TData, TLayer>;\n /** Computes this node's pose from its dependencies' poses, the same way\n * `derivePath` computes its path — same `dependsOn` list, same\n * registry-keyed serialization.\n *\n * Where a derived path is resolved at paint time and reaches only the\n * painter, a derived pose is what the node *is* at: it feeds bounds,\n * hit-testing, selection chrome, snapping and layout, and every reader\n * gets it through `effectivePose`. A pose override still wins over it.\n *\n * Returning `null` means \"I have nothing to derive from right now\", and\n * the node falls back to its authored `pose`. `setPose` on a derived node\n * still writes that authored pose; it is simply not what anything reads. */\n derivePose?: DerivePoseFn<TPose, TData, TLayer>;\n}\n\n/** A node with no children — a shape, a label, an image. */\nexport interface LeafNode<TData, TLayer extends string, TPose = RectPose>\n extends NodeBase<TData, TLayer, TPose> {\n kind: 'leaf';\n}\n\n/** A node with an ordered list of children. This is the real group: what\n * Cmd+G creates, what SVG `<g>` round-trips to. A container has its own pose,\n * which its children's poses are relative to, and may optionally clip them. */\nexport interface ContainerNode<TData, TLayer extends string, TPose = RectPose>\n extends NodeBase<TData, TLayer, TPose> {\n kind: 'container';\n children: NodeId[];\n /** Optional clip-path source. Re-evaluated each render. Returning `null`\n * means \"no clip for this container right now\"; an empty / zero-area path\n * means \"clip everything out\" (children render nowhere). */\n clipFromPose?: ClipFromPoseFn<TPose>;\n /** How this container arranges its children. Whenever the container's\n * child set or order changes, or its bounds change size, the scene hands\n * the change to the strategy — `depart` for children that left it,\n * `arrive` for children that joined it, `childPoses` otherwise — and records the poses it writes in the same\n * undo entry as the change. A drag reads it too: it is the layout `move`\n * reflows and drops against. */\n layout?: LayoutStrategy<TPose>;\n /** The unit system this container's contents are written in, and what one\n * of its base units is worth in the enclosing scope's. It covers the\n * children, not the container; `sceneUnitScopes` reads the chain. */\n units?: UnitScope;\n}\n\n/** A node in the scene tree: either a leaf or a container. Re-exported\n * publicly as `SceneNode`, to avoid colliding with the DOM's `Node`. */\nexport type Node<TData, TLayer extends string, TPose = RectPose> =\n | LeafNode<TData, TLayer, TPose>\n | ContainerNode<TData, TLayer, TPose>;\n\ninterface LayerRecordBase<TLayer extends string> {\n id: TLayer;\n visible: boolean;\n locked: boolean;\n /** Draw this layer as a parallax plane: through a view derived from the\n * camera's (see `deriveParallaxView`), and picked through the same one.\n * Absent means the layer moves with the camera. */\n parallax?: ParallaxOpts;\n}\n\n/** A layer declared when the scene was created. Fixed set, no display name —\n * these are the kit's own render bands, not something a user manages. */\nexport interface SystemLayerRecord<TLayer extends string>\n extends LayerRecordBase<TLayer> {\n kind: 'system';\n}\n\n/** A layer the user created and can rename, reorder or delete. */\nexport interface UserLayerRecord<TLayer extends string>\n extends LayerRecordBase<TLayer> {\n kind: 'user';\n name: string;\n}\n\n/** Per-layer metadata held by the scene: whether it is visible and locked,\n * and where it sits in the render stack. Distinct from a node's `layer` tag,\n * which merely names one of these. */\nexport type LayerRecord<TLayer extends string> =\n | SystemLayerRecord<TLayer>\n | UserLayerRecord<TLayer>;\n\n/** What `Scene.add` needs to mint a node. Everything except the id is\n * required; the id is generated unless one is supplied. */\nexport interface AddNodeSpec<TData, TLayer extends string, TPose = RectPose> {\n kind: 'leaf' | 'container';\n layer: TLayer;\n pose: TPose;\n data: TData;\n parent?: NodeId | null;\n index?: number;\n /** Explicit id wins over the Scene's `generateId` and the kit default. */\n id?: NodeId;\n /** Mirrors `SceneNode.pickable`. Omit for the default, which is pickable. */\n pickable?: boolean;\n /** Only meaningful when `kind === 'container'`. Attach a clip-path function\n * to the node; ignored for leaves. Mirrors `ContainerNode.clipFromPose`. */\n clipFromPose?: ClipFromPoseFn<TPose>;\n /** Mirrors `SceneNode.dependsOn`. */\n dependsOn?: readonly NodeId[] | 'children';\n /** Mirrors `SceneNode.derivePath`. Taken as a live function; its registry key is\n * looked up from it, never passed in. */\n derivePath?: DerivePathFn<TPose, TData, TLayer>;\n /** Mirrors `SceneNode.derivePose`, on the same terms as `derivePath`. */\n derivePose?: DerivePoseFn<TPose, TData, TLayer>;\n /** Mirrors `ContainerNode.layout`; ignored for leaves. */\n layout?: LayoutStrategy<TPose>;\n /** Mirrors `ContainerNode.units`; ignored for leaves. */\n units?: UnitScope;\n}\n\n/** A custom scene mutation registered with `Scene.registerOp`: how to apply\n * it and how to undo it. The pair is what makes it participate in history. */\nexport interface RegisteredOp<P> {\n apply: (payload: P) => void;\n revert: (payload: P) => void;\n /** Opts this kind into coalescing under `UseSceneOptions.coalesceWindowMs`:\n * two entries of this kind that return the same key inside the window\n * merge, and undoing the merged entry calls `revert` with the first\n * payload only. Declare it only when `revert` restores an absolute state\n * (a stored \"from\" value), never for a relative payload such as a delta.\n * Omitted, or returning `undefined`, the op is always its own entry. */\n coalesceKey?: (payload: P) => string | undefined;\n}\n\n/** One of the layers a scene is created with. */\nexport interface SystemLayerSpec<TLayer extends string> {\n id: TLayer;\n visible?: boolean;\n locked?: boolean;\n /** See `LayerRecord.parallax`. */\n parallax?: ParallaxOpts;\n}\n\n/** Argument to `Scene.addLayer`. Always produces a `UserLayerRecord`\n * (`kind: 'user'`). */\nexport interface AddLayerSpec<TLayer extends string> {\n id: TLayer;\n name: string;\n /** Default `true`. */\n visible?: boolean;\n /** Default `false`. */\n locked?: boolean;\n /** See `LayerRecord.parallax`. */\n parallax?: ParallaxOpts;\n /** Render-stack position. Default: top of stack (highest render index). */\n index?: number;\n}\n\n/** Per-scene registry mapping string keys to live function references.\n * Passed to `createScene({ ..., registry })` and `sceneFromJSON(json, { registry })`.\n * Each function-field type has its own keyed map. */\nexport interface SceneRegistry<TPose, TData = unknown, TLayer extends string = string> {\n /** Maps registry keys to `clipFromPose` factory functions for container nodes. */\n clipFromPose?: Readonly<Record<string, ClipFromPoseFn<TPose>>>;\n /** Maps registry keys to `derivePath` functions for nodes with `dependsOn`. */\n derivePath?: Readonly<Record<string, DerivePathFn<TPose, TData, TLayer>>>;\n /** Maps registry keys to `derivePose` functions for nodes with `dependsOn`. */\n derivePose?: Readonly<Record<string, DerivePoseFn<TPose, TData, TLayer>>>;\n /** Maps registry keys to container layout strategies. */\n layout?: Readonly<Record<string, LayoutStrategy<TPose>>>;\n // A new function field is a row in `NODE_FN_FIELDS` (core/scene/nodeFnFields.ts)\n // plus its entry here; the field name is the registry key by construction.\n}\n\n/**\n * Decides what one edit's changes to containers do to their children.\n * `arrivals` maps each container to the ids that joined it — by an `add`\n * under it or a `move` into it, whatever op or call made them — in the order\n * they arrived. `changed` names every container whose child set or order\n * changed, or whose pose changed size, arrivals included. Both are read once\n * the edit's other mutations have landed.\n *\n * Return the poses to write, in each node's stored frame; they become part of\n * the same edit, and so of the same undo step. An empty map writes nothing.\n * Return `null` to refuse: the whole edit is reverted (see\n * `SceneArrivalRefused`).\n *\n * Runs only for live edits. Undo, redo and a restored history replay the\n * arrangement that was decided the first time. With none installed, a scene\n * holding declared layouts (`ContainerNode.layout`) runs its own.\n */\nexport type SceneArrivalHandler<TPose> = (\n arrivals: ReadonlyMap<NodeId, readonly NodeId[]>,\n changed: ReadonlySet<NodeId>,\n /** The nodes that left each container in the edit, by the container they\n * left — removed, or moved out. */\n departures: ReadonlyMap<NodeId, readonly NodeId[]>,\n) => ReadonlyMap<NodeId, TPose> | null;\n\n/** Options for `useScene` — the layers the scene has, what it starts out\n * holding, and how its history behaves. */\nexport interface UseSceneOptions<TData, TLayer extends string, TPose = RectPose> {\n systemLayers: readonly SystemLayerSpec<TLayer>[];\n initial?: readonly AddNodeSpec<TData, TLayer, TPose>[];\n ops?: Readonly<Record<string, RegisteredOp<unknown>>>;\n historyLimit?: number;\n /** Window (ms) within which consecutive same-shaped mutations merge into\n * the previous undo entry (matching per-op coalesce keys — e.g. repeated\n * `setPose` on the same node, or repeated `applyBatch` calls whose ops\n * carry matching `coalesceKey` multisets). `0` (default) disables\n * coalescing: every mutation is a discrete undo entry. Undo of a\n * coalesced entry returns to the state before the first merged mutation;\n * redo restores the latest. `scene.batch` entries never coalesce. */\n coalesceWindowMs?: number;\n generateId?: () => NodeId;\n /** Per-scene registry for non-serializable function fields (clipFromPose, etc.).\n * Required only when serializing/deserializing scenes that use function fields. */\n registry?: SceneRegistry<TPose, TData, TLayer>;\n /** How a child's pose folds into its container's. Default: it does not, and every pose is world. */\n composition?: PoseComposition<TPose>;\n /** How laid-out containers are measured. See {@link LayoutFrame}. */\n layoutFrame?: LayoutFrame<TPose>;\n /** When supplied, `scene.applyBatch` consults this on every call. If it\n * returns a non-null `Journal`, ops are routed to the journal's\n * `applyBatch` instead of recording a new parent-history entry. The scene\n * state changes as normal; only the history tracking differs.\n *\n * When the accessor isn't known at scene-construction time, wire it after\n * construction via `scene.setActiveJournalAccessor(fn)`. */\n getActiveJournal?: () => import('@weasel-js/history').Journal | null;\n /** Re-render the host on every scene mutation. Default `true`. Set `false`\n * when the scene is read by a frame loop rather than by a render — a game\n * loop, a simulation — and nothing in the host's DOM derives from it.\n * Read by `useScene`; `createScene` ignores it. */\n subscribe?: boolean;\n}\n\n/**\n * The kit-owned scene tree: nodes, layers, and the undo history over both.\n *\n * A scene is logical, not visual — it says what exists and where, and nothing\n * about how it is painted. Every mutating method is undoable, and reads are\n * snapshots rather than live views. Nodes are addressed by `NodeId`; hold ids\n * across mutations, not node objects.\n *\n * Three type parameters keep it domain-agnostic: `TData` is the app's payload,\n * which the kit never inspects; `TPose` is the transform shape, `RectPose` by\n * default; `TLayer` is the union of layer names.\n */\nexport interface Scene<TData, TLayer extends string, TPose = RectPose> {\n // Reads\n readonly nodes: ReadonlyMap<NodeId, Node<TData, TLayer, TPose>>;\n readonly roots: readonly NodeId[];\n readonly layers: readonly LayerRecord<TLayer>[];\n /** The registry this scene resolves node functions against — the consumer's\n * entries over the kit's. */\n readonly registry: SceneRegistry<TPose, TData, TLayer>;\n /** See {@link UseSceneOptions.composition}. */\n readonly composition: PoseComposition<TPose>;\n get(id: NodeId): Node<TData, TLayer, TPose> | undefined;\n /** A token for the node `id` names right now, for telling one node from a\n * later one that reuses its id. It holds for as long as the node stays in\n * the scene, whatever edits it takes, and is different every time the id\n * enters the scene — by `add`, by an undo or redo that brings it back, or\n * by `loadState`. `undefined` when `id` is not in the scene. Compare tokens\n * for equality only; their order means nothing. */\n incarnation(id: NodeId): number | undefined;\n childrenOf(id: NodeId): readonly NodeId[];\n ancestorsOf(id: NodeId): readonly NodeId[];\n renderOrder(): Iterable<NodeId>;\n /** The same layer-major sequence as {@link Scene.renderOrder}, as the nodes\n * themselves. Prefer this wherever the ids are only going to be resolved\n * back to nodes. Cached until a structural edit, so repeat calls hand back\n * the same array — a snapshot, not a live view, and not yours to mutate. */\n renderOrderNodes(): readonly Node<TData, TLayer, TPose>[];\n /** The nodes on one layer, in render order. Cached alongside\n * {@link Scene.renderOrderNodes} until a structural edit. A snapshot, not\n * a live view, and not yours to mutate. An unknown layer id yields an\n * empty array. */\n nodesOnLayer(layer: TLayer): readonly Node<TData, TLayer, TPose>[];\n\n /** A fresh id from this scene's `generateId`, or the kit default: the id\n * `add` would give a spec with none. For code that has to name a node\n * before it exists, such as one building an insert op. */\n mintId(): NodeId;\n\n // Mutations (all auto-undoable)\n add(spec: AddNodeSpec<TData, TLayer, TPose>): NodeId;\n /** Delete `id`, its **entire subtree**, and **everything that derives from**\n * any of those nodes — a node listing one of them in `dependsOn` goes too,\n * along with its own subtree, transitively. A dependent can live anywhere in\n * the tree, so this deletes nodes the caller never named and may unlink\n * several disjoint subtrees at once. Recorded as one undoable step; `undo()`\n * restores every one of them where it was, child order intact. */\n remove(id: NodeId): void;\n /** {@link remove} over several roots at once, as a **single** undoable step.\n * Ids resolve against the tree as it stands at the call, so an id that\n * another one would cascade away is absorbed rather than removed twice —\n * which is what makes it safe to pass a whole selection. Throws if any id is\n * not in the scene; an empty list does nothing and records no step. */\n removeMany(ids: readonly NodeId[]): void;\n /** Every node {@link removeMany} would take if given `ids`: each id, its\n * subtree, everything deriving from any of those, and those nodes' subtrees\n * in turn. Ids not in the scene come back unchanged.\n *\n * A set, not a restore order: a dependent can be reached before the parent\n * it sits under, which is also in the closure. */\n removalClosure(ids: readonly NodeId[]): readonly NodeId[];\n update(id: NodeId, patch: { data: TData }): void;\n setPose(id: NodeId, pose: TPose): void;\n /** Retag `id` to `layer`. On a **container this cascades**: every descendant\n * is moved to the same layer, recorded as a **single** undo step.\n *\n * Invariants:\n * - **Layer floor** — a child may not render below its parent, so retagging\n * to a layer *below* the node's parent throws. Retagging to the parent's\n * layer or any higher one is allowed; a node with no parent is\n * unconstrained.\n * - **No-op elision** — setting the layer a node already has does nothing\n * and pushes **no** history entry. */\n setLayer(id: NodeId, layer: TLayer): void;\n /** Point `id` at a different set of dependencies — retargeting an edge's end\n * onto another node, or switching a container between an id list and\n * `'children'`. Recorded as one undoable step.\n *\n * Order is significant: a derivation reads its dependencies positionally,\n * so `[b, a]` is not `[a, b]`. Ids need not be in the scene — a declared\n * dependency that appears later invalidates the node when it does, the same\n * as one declared at `add`. Passing `undefined` drops the declaration, after\n * which nothing cascades the node away.\n *\n * **No-op elision** — declaring what the node already declares does nothing\n * and pushes no history entry. */\n setDependsOn(id: NodeId, dependsOn: readonly NodeId[] | 'children' | undefined): void;\n /** Give container `id` a different layout — a strategy, its key in\n * `SceneRegistry.layout`, or `null` for none — and re-arrange its children\n * under it, as one undoable step. Throws for a leaf, or a key the registry\n * does not hold. A registered layout is recorded by its key, so a\n * serialized history restores it; an unregistered one lives only as long\n * as the session.\n *\n * **No-op elision** — setting the layout a container already has does\n * nothing and pushes no history entry. */\n setLayout(id: NodeId, layout: LayoutStrategy<TPose> | string | null): void;\n /** Reparent `id` under `parent` (or to a root when `parent` is `null`) at\n * `index` within the new sibling list, appending when `index` is omitted.\n * Siblings are reindexed. Recorded as one undoable step.\n *\n * Rejected (throws) when:\n * - `parent` exists but is a **leaf**, not a container;\n * - the move would form a **cycle** — `parent` is `id` itself or one of\n * `id`'s own descendants;\n * - it would drop `id` **below its new parent's layer** (child may not\n * render below its parent).\n *\n * `move(id, null)` — detaching to a root — is always allowed regardless of\n * layer, since a root has no parent to render beneath. */\n move(id: NodeId, parent: NodeId | null, index?: number): void;\n /** Shift `id` to `index` within its **current** parent's child list. Unlike\n * {@link move}, the parent never changes — only sibling order. */\n reorder(id: NodeId, index: number): void;\n setLayerVisible(layer: TLayer, visible: boolean): void;\n /** Lock or unlock a layer. A locked layer still paints, but its nodes are\n * out of reach: picking and area selection pass over them, the selection\n * drops them, and every node mutation on them throws. Locking is never\n * blocked by the lock, and neither are the other layer operations. */\n setLayerLocked(layer: TLayer, locked: boolean): void;\n /** Set or clear (`undefined`) a layer's parallax — see\n * `LayerRecord.parallax`. Undoable; an animation tweening it writes inside\n * {@link Scene.untracked} so no frame becomes an undo step. */\n setLayerParallax(layer: TLayer, parallax: ParallaxOpts | undefined): void;\n /** Whether `id` sits on a locked layer or under a container that does.\n * A container's lock covers its whole subtree, whatever layers the\n * descendants are tagged to. An id not in the scene is not locked. */\n isLocked(id: NodeId): boolean;\n /** Run `fn` with the lock guard lifted, for a programmatic edit that has to\n * reach a locked node. Undo and redo never need it. */\n unlocked<T>(fn: () => T): T;\n addLayer(spec: AddLayerSpec<TLayer>): void;\n /** Drop a user layer and every node tagged to it, as one undoable step.\n * Removal cascades, so this also deletes nodes **on other layers** that\n * derive from a node on this one. */\n removeLayer(layer: TLayer): void;\n renameLayer(layer: TLayer, name: string): void;\n moveLayer(layer: TLayer, index: number): void;\n\n // Custom op seam\n registerOp<P>(kind: string, handler: RegisteredOp<P>): void;\n recordOp<P>(op: { kind: string; payload: P }): void;\n\n /** Install (or clear with `null`) the active-journal accessor after scene\n * construction, for a journal source built with `scene.history` as a\n * dependency. Overrides any `getActiveJournal` set in `UseSceneOptions`. */\n setActiveJournalAccessor(\n fn: (() => import('@weasel-js/history').Journal | null) | null,\n ): void;\n\n /** Install what the scene does when nodes join a container, replacing any\n * handler installed before; `null` removes it. Returns a function that\n * removes this handler, and does nothing once another has replaced it.\n * See {@link SceneArrivalHandler}. */\n setArrivalHandler(handler: SceneArrivalHandler<TPose> | null): () => void;\n\n /** Apply a batch of ops against `adapter` as one undo entry.\n *\n * The entry holds the scene writes the ops made, as a direct call records\n * them, and beside them any op that wrote no scene state, bound to\n * `adapter`. Undo restores those writes; it does not call an op's\n * `invert()` or the adapter again. **What an op states is not recorded**:\n * one applied over a document that already holds its result, as after an\n * `untracked` preview, records a write from that pose to itself, and undo\n * goes nowhere. Put the document back first.\n *\n * The entry lands in the open `batch` when there is one, else on the\n * active journal, else on the scene's own history, where it can coalesce\n * under `coalesceWindowMs` with one whose ops' keys match.\n *\n * Returns false when the arrival handler refused the batch: it is reverted\n * and unrecorded. Inside `batch` it is true; that batch may still refuse. */\n applyBatch(ops: import('core/ops/types').Op[], label: string, adapter: unknown): boolean;\n\n // Selection\n\n /** The transient set of active ids — \"operate on these N as a unit\".\n * Shared by every view over this scene unless a view supplies its own\n * (see `CanvasView.selection`). Not document content: it never appears\n * in `toJSON`. It does ride on history entries, so undo and redo put\n * back the selection an edit was made under; changing it is never an\n * undo step of its own. Removing a node deselects it. */\n getSelection(): readonly NodeId[];\n setSelection(ids: readonly NodeId[]): void;\n\n // Ephemeral presentation state\n\n /** Per-node pose / alpha overrides the render and hit-test paths read\n * through. Like {@link Scene.getSelection} this is not document content:\n * writes are never recorded, never serialized, and do not bump\n * {@link Scene.getVersion}. See {@link PoseOverrides}. */\n readonly overrides: PoseOverrides<TPose>;\n\n // History\n /** The scene's undo/redo engine, in the shape `@weasel-js/history` defines.\n * Mutating members route through\n * the scene's own wrappers, so an undo driven through this bumps\n * {@link Scene.getVersion} and notifies subscribers exactly as\n * {@link Scene.undo} does; `getVersion` / `subscribe` on it report the\n * engine's history version, not the scene's. */\n readonly history: History;\n undo(): boolean;\n redo(): boolean;\n canUndo(): boolean;\n canRedo(): boolean;\n batch<T>(label: string, fn: () => T): T;\n /** The layout container `id` declares, or `null` for a leaf or a\n * container that declares none. */\n layoutOf(id: NodeId): LayoutStrategy<TPose> | null;\n /** Declare the unit scope of container `id`, or clear it with `null`. */\n setUnits(id: NodeId, units: UnitScope | null): void;\n /** Called with the poses each live edit's layout pass wrote, after the\n * edit lands — never for an undo or redo, which restore poses rather than\n * arrange them. What a reflow animation glides from. */\n onReflow(listener: (moves: readonly LayoutMove<TPose>[]) => void): () => void;\n /** Run `fn` with every mutation applied but none recorded — for writes\n * that are not edits, such as a simulation stepping poses each frame.\n * Undo and redo move between recorded states and never restore an\n * untracked write; the next recorded change starts from whatever the\n * untracked writes left, so undoing it lands there. A `batch` inside\n * records nothing; inside a `batch`, these writes stay out of its entry.\n * Notifies once, reverts `fn`'s writes if it throws, and throws on\n * `applyBatch` and `history.apply` / `applyOps`, which exist to record. */\n untracked<T>(fn: () => T): T;\n /** Read-only snapshot of every history entry currently reachable from\n * the present state. Oldest applied first, then redoable entries in\n * the order they'd be re-applied. Each entry id is stable. */\n historyEntries(): readonly { id: string; label: string }[];\n /** Index of the \"current state\". Equals the count of applied entries;\n * `0` means \"nothing applied\" (initial). */\n historyIndex(): number;\n /** Jump to the given history index, clamped to [0, total]. Returns true\n * if any movement occurred. */\n jumpToHistoryIndex(index: number): boolean;\n\n // History persistence\n /** Snapshot the undo/redo history in a JSON-serializable form (the\n * engine's `SerializedHistory`, marked with the scene's recorded form).\n * An entry holds the scene's own ops, whichever way the edit was made.\n * An op handed to `applyBatch` that wrote no scene state is held as\n * itself, and an entry with a nameless one is dropped. Payload\n * JSON-safety (e.g. typed arrays inside poses) is the caller's concern.\n * Do not call mid-`batch` — the open batch's ops are not yet recorded. */\n serializeHistory(): SerializedSceneHistory;\n /** Replace the undo/redo history from a `serializeHistory()` snapshot.\n * Call on a scene whose node/layer state already matches the snapshot's\n * head state (i.e. right after `loadState` from the paired scene\n * snapshot); node state is NOT mutated. Ops re-registered via\n * `registerOp` before this call round-trip; unknown kinds become no-op\n * placeholders; external ops rebuild via the global op-factory registry\n * and replay against the `setHistoryAdapter` accessor. Restored entries\n * never coalesce with new ones. Notifies once. A snapshot that is not in\n * the current recorded form (see `SCENE_HISTORY_FORM`) is dropped:\n * the history is left as it was. Do not call mid-`batch`:\n * the stacks are replaced underneath the open batch, whose eventual\n * flush would graft onto (and evict against) the restored stacks. */\n restoreHistory(snapshot: SerializedHistory): void;\n /** Install (or clear with `null`) the accessor for the adapter that\n * RESTORED external ops apply against on undo/redo: the ops handed to\n * `applyBatch` that wrote no scene state, such as a selection change,\n * rebuilt from a `restoreHistory` snapshot. Resolved lazily at each\n * apply, so wiring order relative to `restoreHistory` doesn't matter.\n * Live `applyBatch` entries are unaffected (they bind their call-site\n * adapter). If unset when a restored op applies, the op is a debug-warned\n * no-op. */\n setHistoryAdapter(fn: (() => unknown) | null): void;\n\n // Serialization\n /** Snapshot the current scene state to a JSON-serializable shape.\n * History (undo/redo stacks) is NOT captured. Function fields like\n * `ContainerNode.clipFromPose` are translated to string keys via the\n * scene's registry; throws if any function field has no matching key. */\n toJSON(): SerializedScene<TData, TLayer, TPose>;\n /** Replace this scene's entire node + layer state in place from a snapshot\n * produced by `toJSON()`. Unlike `sceneFromJSON`, the existing Scene\n * instance is preserved — holders such as `<SceneCanvas>` keep their\n * reference. History (undo/redo) is cleared, matching `sceneFromJSON`.\n * Bumps `getVersion()` and notifies subscribers exactly once. Selected\n * ids the snapshot does not hold leave the selection, and every pose\n * override is cleared.\n *\n * Throws inside any other edit — `batch`, `untracked`, `applyBatch`, an\n * undo — which would revert to a state this replaces. Throws on an unsupported version or unknown registry/layer ids; on a\n * malformed snapshot the scene is left empty or partially populated (callers should treat a\n * `loadState` throw as fatal and reload). Snapshots from `toJSON()` are\n * always well-formed. */\n loadState(json: SerializedScene<TData, TLayer, TPose>): void;\n\n // Subscription (used by useScene; also for non-React observers)\n subscribe(listener: () => void): () => void;\n /** Monotonically increasing version. Snapshot for `useSyncExternalStore`. */\n getVersion(): number;\n}\n"],"mappings":";AA2DA,IAAaA,KAAY,MAAsBC"}
|
|
1
|
+
{"version":3,"file":"types.js","names":["asNodeId","s"],"sources":["../../../src/core/scene/types.ts"],"sourcesContent":["import type { History, SerializedHistory } from '@weasel-js/history';\nimport type { SerializedSceneHistory } from './historyForm';\nimport type { ParallaxOpts } from '../viewport/parallax';\nimport type { UnitScope } from '@weasel-js/quantity';\nimport type { LayoutStrategy } from '../../layout/types';\nimport type { AddLayerSpec, LayerRecord, SystemLayerRecord, SystemLayerSpec, UserLayerRecord } from './layerTypes';\nimport type { LayoutFrame, LayoutMove } from './layoutTypes';\nimport type { PoseComposition } from './composeFrame';\nimport type { PoseLinear } from '@weasel-js/geom';\nimport type { PoseHold, PoseOverride, PoseOverrides } from './poseOverrideTypes';\nimport type { SerializedLayer, SerializedNode, SerializedScene } from './serializedTypes';\n\nexport type { AddLayerSpec, LayerRecord, SystemLayerRecord, SystemLayerSpec, UserLayerRecord };\nexport type { LayoutFrame, LayoutMove, PoseHold, PoseOverride, PoseOverrides, SerializedLayer, SerializedNode, SerializedScene };\n\n/**\n * Axis-aligned rectangle pose with optional rotation. Rotation is in\n * radians, pivoted on the unrotated AABB center; absent === 0.\n */\nexport interface RectPose {\n x: number;\n y: number;\n width: number;\n height: number;\n /** Rotation in radians around the unrotated AABB center. Absent === 0. */\n rotation?: number;\n /** A skew or per-axis scale the box and rotation cannot hold; see\n * {@link PoseLinear}. Absent is the identity. */\n transform?: PoseLinear;\n}\n\n/**\n * # SceneNode — the thing in the scene\n *\n * Everything the user sees on the canvas — a shape, a group, an annotation,\n * a tile — is a node. Containers and leaves are both nodes; the kit has no\n * other concept of \"scene element.\"\n *\n * Every node carries three independent slots, plus its tree position:\n *\n * - **`data: TData`** — app-defined payload. The kit never inspects it.\n * - **`pose: TPose`** — local transform, relative to the node's direct\n * parent (or world, for root nodes).\n * - **`layer: TLayer`** — a string tag associating the node with a\n * visual `RenderLayer` at draw time. Separate from `LayerRecord` (the\n * per-layer visible/locked metadata held by the `Scene`).\n *\n * Nodes are addressed by `NodeId` everywhere outside the scene tree. Don't\n * hold references to a `Node` object across scene updates; look up by id\n * when you need the latest.\n *\n * The kit-internal name is `Node`; the public re-export is `SceneNode`\n * (avoids collision with DOM `Node` at call sites).\n */\n\n/** Opaque branded id. Treat as opaque outside the kit. */\nimport type { NodeId } from '@weasel-js/routing';\nexport type { NodeId };\n\n/** Brand a string as a NodeId. */\nexport const asNodeId = (s: string): NodeId => s as NodeId;\n\nimport type { DerivedDep, Bivariant, DerivePathFn, DerivePoseFn, ClipFromPoseFn, CopiesFromPoseFn } from './nodeFnTypes';\nexport type { DerivedDep, Bivariant, DerivePathFn, DerivePoseFn, ClipFromPoseFn, CopiesFromPoseFn };\n\ninterface NodeBase<TData, TLayer extends string, TPose> {\n id: NodeId;\n layer: TLayer;\n pose: TPose;\n data: TData;\n parent: NodeId | null;\n /** Nodes whose poses this node's geometry is computed from. Fixed at add\n * time. Absent or empty means the node's geometry is authored, which is the\n * normal case.\n *\n * `'children'` means \"my own children, in child order\" — a container that\n * hugs its contents, which a fixed id list cannot express because\n * reparenting would have to maintain it. The two forms differ in lifetime\n * as well as in membership: deleting a node deletes everything that names\n * it in `dependsOn`, but a container outlives the children it derives\n * from — an emptied group is still a group. */\n dependsOn?: readonly NodeId[] | 'children';\n /** Whether the hit-test walk can return this node. Default `true`; `false`\n * makes it transparent to picking, so a click on it lands on whatever is\n * behind — normally its own container.\n *\n * For content a container owns rather than content the user selects on its\n * own: the label inside a box, a body's rows. It does not hide the node\n * from anything else — it still paints, still clips, and a consumer can\n * still select it by id. */\n pickable?: boolean;\n /** Computes this node's path from its dependencies, in `dependsOn` order —\n * each one the node and the pose it is painted at.\n * A dependency that has been removed arrives as `undefined`.\n * Returning `null` means \"nothing to draw right now\". Re-evaluated when a\n * dependency's world pose changes, never authored. */\n derivePath?: DerivePathFn<TPose, TData, TLayer>;\n /** Computes this node's pose from its dependencies' poses, the same way\n * `derivePath` computes its path — same `dependsOn` list, same\n * registry-keyed serialization.\n *\n * Where a derived path is resolved at paint time and reaches only the\n * painter, a derived pose is what the node *is* at: it feeds bounds,\n * hit-testing, selection chrome, snapping and layout, and every reader\n * gets it through `effectivePose`. A pose override still wins over it.\n *\n * Returning `null` means \"I have nothing to derive from right now\", and\n * the node falls back to its authored `pose`. `setPose` on a derived node\n * still writes that authored pose; it is simply not what anything reads. */\n derivePose?: DerivePoseFn<TPose, TData, TLayer>;\n}\n\n/** A node with no children — a shape, a label, an image. */\nexport interface LeafNode<TData, TLayer extends string, TPose = RectPose>\n extends NodeBase<TData, TLayer, TPose> {\n kind: 'leaf';\n}\n\n/** A node with an ordered list of children. This is the real group: what\n * Cmd+G creates, what SVG `<g>` round-trips to. A container has its own pose,\n * which its children's poses are relative to, and may optionally clip them. */\nexport interface ContainerNode<TData, TLayer extends string, TPose = RectPose>\n extends NodeBase<TData, TLayer, TPose> {\n kind: 'container';\n children: NodeId[];\n /** Optional clip-path source. Re-evaluated each render. Returning `null`\n * means \"no clip for this container right now\"; an empty / zero-area path\n * means \"clip everything out\" (children render nowhere). */\n clipFromPose?: ClipFromPoseFn<TPose>;\n /** Draws the whole subtree again under each affine it returns, on top of\n * the original, which still draws once as authored. The copies are paint,\n * not nodes: nothing stores them, so they follow every edit to the\n * children, and a pick that lands on one answers with the child it copies.\n * A copy is a copy of the clipped content — the clip travels with it.\n * Re-evaluated each render; `null` or `[]` draws no copies. */\n copiesFromPose?: CopiesFromPoseFn<TPose, TData, TLayer>;\n /** How this container arranges its children. Whenever the container's\n * child set or order changes, or its bounds change size, the scene hands\n * the change to the strategy — `depart` for children that left it,\n * `arrive` for children that joined it, `childPoses` otherwise — and records the poses it writes in the same\n * undo entry as the change. A drag reads it too: it is the layout `move`\n * reflows and drops against. */\n layout?: LayoutStrategy<TPose>;\n /** The unit system this container's contents are written in, and what one\n * of its base units is worth in the enclosing scope's. It covers the\n * children, not the container; `sceneUnitScopes` reads the chain. */\n units?: UnitScope;\n}\n\n/** A node in the scene tree: either a leaf or a container. Re-exported\n * publicly as `SceneNode`, to avoid colliding with the DOM's `Node`. */\nexport type Node<TData, TLayer extends string, TPose = RectPose> =\n | LeafNode<TData, TLayer, TPose>\n | ContainerNode<TData, TLayer, TPose>;\n\n/** What `Scene.add` needs to mint a node. Everything except the id is\n * required; the id is generated unless one is supplied. */\nexport interface AddNodeSpec<TData, TLayer extends string, TPose = RectPose> {\n kind: 'leaf' | 'container';\n layer: TLayer;\n pose: TPose;\n data: TData;\n parent?: NodeId | null;\n index?: number;\n /** Explicit id wins over the Scene's `generateId` and the kit default. */\n id?: NodeId;\n /** Mirrors `SceneNode.pickable`. Omit for the default, which is pickable. */\n pickable?: boolean;\n /** Only meaningful when `kind === 'container'`. Attach a clip-path function\n * to the node; ignored for leaves. Mirrors `ContainerNode.clipFromPose`. */\n clipFromPose?: ClipFromPoseFn<TPose>;\n /** Mirrors `ContainerNode.copiesFromPose`; ignored for leaves. */\n copiesFromPose?: CopiesFromPoseFn<TPose, TData, TLayer>;\n /** Mirrors `SceneNode.dependsOn`. */\n dependsOn?: readonly NodeId[] | 'children';\n /** Mirrors `SceneNode.derivePath`. Taken as a live function; its registry key is\n * looked up from it, never passed in. */\n derivePath?: DerivePathFn<TPose, TData, TLayer>;\n /** Mirrors `SceneNode.derivePose`, on the same terms as `derivePath`. */\n derivePose?: DerivePoseFn<TPose, TData, TLayer>;\n /** Mirrors `ContainerNode.layout`; ignored for leaves. */\n layout?: LayoutStrategy<TPose>;\n /** Mirrors `ContainerNode.units`; ignored for leaves. */\n units?: UnitScope;\n}\n\n/** A custom scene mutation registered with `Scene.registerOp`: how to apply\n * it and how to undo it. The pair is what makes it participate in history. */\nexport interface RegisteredOp<P> {\n apply: (payload: P) => void;\n revert: (payload: P) => void;\n /** Opts this kind into coalescing under `UseSceneOptions.coalesceWindowMs`:\n * two entries of this kind that return the same key inside the window\n * merge, and undoing the merged entry calls `revert` with the first\n * payload only. Declare it only when `revert` restores an absolute state\n * (a stored \"from\" value), never for a relative payload such as a delta.\n * Omitted, or returning `undefined`, the op is always its own entry. */\n coalesceKey?: (payload: P) => string | undefined;\n}\n\n/** Per-scene registry mapping string keys to live function references.\n * Passed to `createScene({ ..., registry })` and `sceneFromJSON(json, { registry })`.\n * Each function-field type has its own keyed map. */\nexport interface SceneRegistry<TPose, TData = unknown, TLayer extends string = string> {\n /** Maps registry keys to `clipFromPose` factory functions for container nodes. */\n clipFromPose?: Readonly<Record<string, ClipFromPoseFn<TPose>>>;\n /** Maps registry keys to `copiesFromPose` functions for container nodes. */\n copiesFromPose?: Readonly<Record<string, CopiesFromPoseFn<TPose, TData, TLayer>>>;\n /** Maps registry keys to `derivePath` functions for nodes with `dependsOn`. */\n derivePath?: Readonly<Record<string, DerivePathFn<TPose, TData, TLayer>>>;\n /** Maps registry keys to `derivePose` functions for nodes with `dependsOn`. */\n derivePose?: Readonly<Record<string, DerivePoseFn<TPose, TData, TLayer>>>;\n /** Maps registry keys to container layout strategies. */\n layout?: Readonly<Record<string, LayoutStrategy<TPose>>>;\n // A new function field is a row in `NODE_FN_FIELDS` (core/scene/nodeFnFields.ts)\n // plus its entry here; the field name is the registry key by construction.\n}\n\n/**\n * Decides what one edit's changes to containers do to their children.\n * `arrivals` maps each container to the ids that joined it — by an `add`\n * under it or a `move` into it, whatever op or call made them — in the order\n * they arrived. `changed` names every container whose child set or order\n * changed, or whose pose changed size, arrivals included. Both are read once\n * the edit's other mutations have landed.\n *\n * Return the poses to write, in each node's stored frame; they become part of\n * the same edit, and so of the same undo step. An empty map writes nothing.\n * Return `null` to refuse: the whole edit is reverted (see\n * `SceneArrivalRefused`).\n *\n * Runs only for live edits. Undo, redo and a restored history replay the\n * arrangement that was decided the first time. With none installed, a scene\n * holding declared layouts (`ContainerNode.layout`) runs its own.\n */\nexport type SceneArrivalHandler<TPose> = (\n arrivals: ReadonlyMap<NodeId, readonly NodeId[]>,\n changed: ReadonlySet<NodeId>,\n /** The nodes that left each container in the edit, by the container they\n * left — removed, or moved out. */\n departures: ReadonlyMap<NodeId, readonly NodeId[]>,\n) => ReadonlyMap<NodeId, TPose> | null;\n\n/** Options for `useScene` — the layers the scene has, what it starts out\n * holding, and how its history behaves. */\nexport interface UseSceneOptions<TData, TLayer extends string, TPose = RectPose> {\n systemLayers: readonly SystemLayerSpec<TLayer>[];\n initial?: readonly AddNodeSpec<TData, TLayer, TPose>[];\n ops?: Readonly<Record<string, RegisteredOp<unknown>>>;\n historyLimit?: number;\n /** Window (ms) within which consecutive same-shaped mutations merge into\n * the previous undo entry (matching per-op coalesce keys — e.g. repeated\n * `setPose` on the same node, or repeated `applyBatch` calls whose ops\n * carry matching `coalesceKey` multisets). `0` (default) disables\n * coalescing: every mutation is a discrete undo entry. Undo of a\n * coalesced entry returns to the state before the first merged mutation;\n * redo restores the latest. `scene.batch` entries never coalesce. */\n coalesceWindowMs?: number;\n generateId?: () => NodeId;\n /** Per-scene registry for non-serializable function fields (clipFromPose, etc.).\n * Required only when serializing/deserializing scenes that use function fields. */\n registry?: SceneRegistry<TPose, TData, TLayer>;\n /** How a child's pose folds into its container's. Default: it does not, and every pose is world. */\n composition?: PoseComposition<TPose>;\n /** How laid-out containers are measured. See {@link LayoutFrame}. */\n layoutFrame?: LayoutFrame<TPose>;\n /** When supplied, `scene.applyBatch` consults this on every call. If it returns a non-null\n * `Journal`, ops are routed to the journal's `applyBatch` instead of recording a new\n * parent-history entry, and undo and redo step that journal. The scene state changes as\n * normal; only the history tracking differs.\n *\n * When the accessor isn't known at scene-construction time, wire it after\n * construction via `scene.setActiveJournalAccessor(fn)`. */\n getActiveJournal?: () => import('@weasel-js/history').Journal | null;\n /** Re-render the host on every scene mutation. Default `true`. Set `false`\n * when the scene is read by a frame loop rather than by a render — a game\n * loop, a simulation — and nothing in the host's DOM derives from it.\n * Read by `useScene`; `createScene` ignores it. */\n subscribe?: boolean;\n}\n\n/**\n * The kit-owned scene tree: nodes, layers, and the undo history over both.\n *\n * A scene is logical, not visual — it says what exists and where, and nothing\n * about how it is painted. Every mutating method is undoable, and reads are\n * snapshots rather than live views. Nodes are addressed by `NodeId`; hold ids\n * across mutations, not node objects.\n *\n * Three type parameters keep it domain-agnostic: `TData` is the app's payload,\n * which the kit never inspects; `TPose` is the transform shape, `RectPose` by\n * default; `TLayer` is the union of layer names.\n */\nexport interface Scene<TData, TLayer extends string, TPose = RectPose> {\n // Reads\n readonly nodes: ReadonlyMap<NodeId, Node<TData, TLayer, TPose>>;\n readonly roots: readonly NodeId[];\n readonly layers: readonly LayerRecord<TLayer>[];\n /** The registry this scene resolves node functions against — the consumer's\n * entries over the kit's. */\n readonly registry: SceneRegistry<TPose, TData, TLayer>;\n /** See {@link UseSceneOptions.composition}. */\n readonly composition: PoseComposition<TPose>;\n get(id: NodeId): Node<TData, TLayer, TPose> | undefined;\n /** A token for the node `id` names right now, for telling one node from a\n * later one that reuses its id. It holds for as long as the node stays in\n * the scene, whatever edits it takes, and is different every time the id\n * enters the scene — by `add`, by an undo or redo that brings it back, or\n * by `loadState`. `undefined` when `id` is not in the scene. Compare tokens\n * for equality only; their order means nothing. */\n incarnation(id: NodeId): number | undefined;\n childrenOf(id: NodeId): readonly NodeId[];\n ancestorsOf(id: NodeId): readonly NodeId[];\n renderOrder(): Iterable<NodeId>;\n /** The same layer-major sequence as {@link Scene.renderOrder}, as the nodes\n * themselves. Prefer this wherever the ids are only going to be resolved\n * back to nodes. Cached until a structural edit, so repeat calls hand back\n * the same array — a snapshot, not a live view, and not yours to mutate. */\n renderOrderNodes(): readonly Node<TData, TLayer, TPose>[];\n /** The nodes on one layer, in render order. Cached alongside\n * {@link Scene.renderOrderNodes} until a structural edit. A snapshot, not\n * a live view, and not yours to mutate. An unknown layer id yields an\n * empty array. */\n nodesOnLayer(layer: TLayer): readonly Node<TData, TLayer, TPose>[];\n\n /** A fresh id from this scene's `generateId`, or the kit default: the id\n * `add` would give a spec with none. For code that has to name a node\n * before it exists, such as one building an insert op. */\n mintId(): NodeId;\n\n // Mutations (all auto-undoable)\n add(spec: AddNodeSpec<TData, TLayer, TPose>): NodeId;\n /** Delete `id`, its **entire subtree**, and **everything that derives from**\n * any of those nodes — a node listing one of them in `dependsOn` goes too,\n * along with its own subtree, transitively. A dependent can live anywhere in\n * the tree, so this deletes nodes the caller never named and may unlink\n * several disjoint subtrees at once. Recorded as one undoable step; `undo()`\n * restores every one of them where it was, child order intact. */\n remove(id: NodeId): void;\n /** {@link remove} over several roots at once, as a **single** undoable step.\n * Ids resolve against the tree as it stands at the call, so an id that\n * another one would cascade away is absorbed rather than removed twice —\n * which is what makes it safe to pass a whole selection. Throws if any id is\n * not in the scene; an empty list does nothing and records no step. */\n removeMany(ids: readonly NodeId[]): void;\n /** Every node {@link removeMany} would take if given `ids`: each id, its\n * subtree, everything deriving from any of those, and those nodes' subtrees\n * in turn. Ids not in the scene come back unchanged.\n *\n * A set, not a restore order: a dependent can be reached before the parent\n * it sits under, which is also in the closure. */\n removalClosure(ids: readonly NodeId[]): readonly NodeId[];\n update(id: NodeId, patch: { data: TData }): void;\n setPose(id: NodeId, pose: TPose): void;\n /** Retag `id` to `layer`. On a **container this cascades**: every descendant\n * is moved to the same layer, recorded as a **single** undo step.\n *\n * Invariants:\n * - **Layer floor** — a child may not render below its parent, so retagging\n * to a layer *below* the node's parent throws. Retagging to the parent's\n * layer or any higher one is allowed; a node with no parent is\n * unconstrained.\n * - **No-op elision** — setting the layer a node already has does nothing\n * and pushes **no** history entry. */\n setLayer(id: NodeId, layer: TLayer): void;\n /** Point `id` at a different set of dependencies — retargeting an edge's end\n * onto another node, or switching a container between an id list and\n * `'children'`. Recorded as one undoable step.\n *\n * Order is significant: a derivation reads its dependencies positionally,\n * so `[b, a]` is not `[a, b]`. Ids need not be in the scene — a declared\n * dependency that appears later invalidates the node when it does, the same\n * as one declared at `add`. Passing `undefined` drops the declaration, after\n * which nothing cascades the node away.\n *\n * **No-op elision** — declaring what the node already declares does nothing\n * and pushes no history entry. */\n setDependsOn(id: NodeId, dependsOn: readonly NodeId[] | 'children' | undefined): void;\n /** Give container `id` a different layout — a strategy, its key in\n * `SceneRegistry.layout`, or `null` for none — and re-arrange its children\n * under it, as one undoable step. Throws for a leaf, or a key the registry\n * does not hold. A registered layout is recorded by its key, so a\n * serialized history restores it; an unregistered one lives only as long\n * as the session.\n *\n * **No-op elision** — setting the layout a container already has does\n * nothing and pushes no history entry. */\n setLayout(id: NodeId, layout: LayoutStrategy<TPose> | string | null): void;\n /** Reparent `id` under `parent` (or to a root when `parent` is `null`) at\n * `index` within the new sibling list, appending when `index` is omitted.\n * Siblings are reindexed. Recorded as one undoable step.\n *\n * Rejected (throws) when:\n * - `parent` exists but is a **leaf**, not a container;\n * - the move would form a **cycle** — `parent` is `id` itself or one of\n * `id`'s own descendants;\n * - it would drop `id` **below its new parent's layer** (child may not\n * render below its parent).\n *\n * `move(id, null)` — detaching to a root — is always allowed regardless of\n * layer, since a root has no parent to render beneath. */\n move(id: NodeId, parent: NodeId | null, index?: number): void;\n /** Shift `id` to `index` within its **current** parent's child list. Unlike\n * {@link move}, the parent never changes — only sibling order. */\n reorder(id: NodeId, index: number): void;\n setLayerVisible(layer: TLayer, visible: boolean): void;\n /** Lock or unlock a layer. A locked layer still paints, but its nodes are\n * out of reach: picking and area selection pass over them, the selection\n * drops them, and every node mutation on them throws. Locking is never\n * blocked by the lock, and neither are the other layer operations. */\n setLayerLocked(layer: TLayer, locked: boolean): void;\n /** Set or clear (`undefined`) a layer's parallax — see\n * `LayerRecord.parallax`. Undoable; an animation tweening it writes inside\n * {@link Scene.untracked} so no frame becomes an undo step. */\n setLayerParallax(layer: TLayer, parallax: ParallaxOpts | undefined): void;\n /** Whether `id` sits on a locked layer or under a container that does.\n * A container's lock covers its whole subtree, whatever layers the\n * descendants are tagged to. An id not in the scene is not locked. */\n isLocked(id: NodeId): boolean;\n /** Run `fn` with the lock guard lifted, for a programmatic edit that has to\n * reach a locked node. Undo and redo never need it. */\n unlocked<T>(fn: () => T): T;\n addLayer(spec: AddLayerSpec<TLayer>): void;\n /** Drop a user layer and every node tagged to it, as one undoable step.\n * Removal cascades, so this also deletes nodes **on other layers** that\n * derive from a node on this one. */\n removeLayer(layer: TLayer): void;\n renameLayer(layer: TLayer, name: string): void;\n moveLayer(layer: TLayer, index: number): void;\n\n // Custom op seam\n registerOp<P>(kind: string, handler: RegisteredOp<P>): void;\n recordOp<P>(op: { kind: string; payload: P }): void;\n\n /** Install (or clear with `null`) the active-journal accessor after scene\n * construction, for a journal source built with `scene.history` as a\n * dependency. Overrides any `getActiveJournal` set in `UseSceneOptions`. */\n setActiveJournalAccessor(\n fn: (() => import('@weasel-js/history').Journal | null) | null,\n ): void;\n\n /** Install what the scene does when nodes join a container, replacing any\n * handler installed before; `null` removes it. Returns a function that\n * removes this handler, and does nothing once another has replaced it.\n * See {@link SceneArrivalHandler}. */\n setArrivalHandler(handler: SceneArrivalHandler<TPose> | null): () => void;\n\n /** Apply a batch of ops against `adapter` as one undo entry.\n *\n * The entry holds the scene writes the ops made, as a direct call records\n * them, and beside them any op that wrote no scene state, bound to\n * `adapter`. Undo restores those writes; it does not call an op's\n * `invert()` or the adapter again. **What an op states is not recorded**:\n * one applied over a document that already holds its result, as after an\n * `untracked` preview, records a write from that pose to itself, and undo\n * goes nowhere. Put the document back first.\n *\n * The entry lands in the open `batch` when there is one, else on the\n * active journal, else on the scene's own history, where it can coalesce\n * under `coalesceWindowMs` with one whose ops' keys match.\n *\n * Returns false when the arrival handler refused the batch: it is reverted\n * and unrecorded. Inside `batch` it is true; that batch may still refuse. */\n applyBatch(ops: import('core/ops/types').Op[], label: string, adapter: unknown): boolean;\n\n // Selection\n\n /** The transient set of active ids — \"operate on these N as a unit\".\n * Shared by every view over this scene unless a view supplies its own\n * (see `CanvasView.selection`). Not document content: it never appears\n * in `toJSON`. It does ride on history entries, so undo and redo put\n * back the selection an edit was made under; changing it is never an\n * undo step of its own. Removing a node deselects it. */\n getSelection(): readonly NodeId[];\n setSelection(ids: readonly NodeId[]): void;\n\n // Ephemeral presentation state\n\n /** Per-node pose / alpha overrides the render and hit-test paths read\n * through. Like {@link Scene.getSelection} this is not document content:\n * writes are never recorded, never serialized, and do not bump\n * {@link Scene.getVersion}. See {@link PoseOverrides}. */\n readonly overrides: PoseOverrides<TPose>;\n\n // History\n /** The scene's undo/redo engine, in the shape `@weasel-js/history` defines.\n * Mutating members route through\n * the scene's own wrappers, so an undo driven through this bumps\n * {@link Scene.getVersion} and notifies subscribers exactly as\n * {@link Scene.undo} does; `getVersion` / `subscribe` on it report the\n * engine's history version, not the scene's. */\n readonly history: History;\n undo(): boolean;\n redo(): boolean;\n canUndo(): boolean;\n canRedo(): boolean;\n batch<T>(label: string, fn: () => T): T;\n /** The layout container `id` declares, or `null` for a leaf or a\n * container that declares none. */\n layoutOf(id: NodeId): LayoutStrategy<TPose> | null;\n /** Declare the unit scope of container `id`, or clear it with `null`. */\n setUnits(id: NodeId, units: UnitScope | null): void;\n /** Called with the poses each live edit's layout pass wrote, after the\n * edit lands — never for an undo or redo, which restore poses rather than\n * arrange them. What a reflow animation glides from. */\n onReflow(listener: (moves: readonly LayoutMove<TPose>[]) => void): () => void;\n /** Run `fn` with every mutation applied but none recorded — for writes\n * that are not edits, such as a simulation stepping poses each frame.\n * Undo and redo move between recorded states and never restore an\n * untracked write; the next recorded change starts from whatever the\n * untracked writes left, so undoing it lands there. A `batch` inside\n * records nothing; inside a `batch`, these writes stay out of its entry.\n * Notifies once, reverts `fn`'s writes if it throws, and throws on\n * `applyBatch` and `history.apply` / `applyOps`, which exist to record. */\n untracked<T>(fn: () => T): T;\n /** Read-only snapshot of every history entry currently reachable from\n * the present state. Oldest applied first, then redoable entries in\n * the order they'd be re-applied. Each entry id is stable. */\n historyEntries(): readonly { id: string; label: string }[];\n /** Index of the \"current state\". Equals the count of applied entries;\n * `0` means \"nothing applied\" (initial). */\n historyIndex(): number;\n /** Jump to the given history index, clamped to [0, total]. Returns true\n * if any movement occurred. */\n jumpToHistoryIndex(index: number): boolean;\n\n // History persistence\n /** Snapshot the undo/redo history in a JSON-serializable form (the\n * engine's `SerializedHistory`, marked with the scene's recorded form).\n * An entry holds the scene's own ops, whichever way the edit was made.\n * An op handed to `applyBatch` that wrote no scene state is held as\n * itself, and an entry with a nameless one is dropped. Payload\n * JSON-safety (e.g. typed arrays inside poses) is the caller's concern.\n * Do not call mid-`batch` — the open batch's ops are not yet recorded. */\n serializeHistory(): SerializedSceneHistory;\n /** Replace the undo/redo history from a `serializeHistory()` snapshot.\n * Call on a scene whose node/layer state already matches the snapshot's\n * head state (i.e. right after `loadState` from the paired scene\n * snapshot); node state is NOT mutated. Ops re-registered via\n * `registerOp` before this call round-trip; unknown kinds become no-op\n * placeholders; external ops rebuild via the global op-factory registry\n * and replay against the `setHistoryAdapter` accessor. Restored entries\n * never coalesce with new ones. Notifies once. A snapshot that is not in\n * the current recorded form (see `SCENE_HISTORY_FORM`) is dropped:\n * the history is left as it was. Do not call mid-`batch`:\n * the stacks are replaced underneath the open batch, whose eventual\n * flush would graft onto (and evict against) the restored stacks. */\n restoreHistory(snapshot: SerializedHistory): void;\n /** Install (or clear with `null`) the accessor for the adapter that\n * RESTORED external ops apply against on undo/redo: the ops handed to\n * `applyBatch` that wrote no scene state, such as a selection change,\n * rebuilt from a `restoreHistory` snapshot. Resolved lazily at each\n * apply, so wiring order relative to `restoreHistory` doesn't matter.\n * Live `applyBatch` entries are unaffected (they bind their call-site\n * adapter). If unset when a restored op applies, the op is a debug-warned\n * no-op. */\n setHistoryAdapter(fn: (() => unknown) | null): void;\n\n // Serialization\n /** Snapshot the current scene state to a JSON-serializable shape.\n * History (undo/redo stacks) is NOT captured. Function fields like\n * `ContainerNode.clipFromPose` are translated to string keys via the\n * scene's registry; throws if any function field has no matching key. */\n toJSON(): SerializedScene<TData, TLayer, TPose>;\n /** Replace this scene's entire node + layer state in place from a snapshot\n * produced by `toJSON()`. Unlike `sceneFromJSON`, the existing Scene\n * instance is preserved — holders such as `<SceneCanvas>` keep their\n * reference. History (undo/redo) is cleared, matching `sceneFromJSON`.\n * Bumps `getVersion()` and notifies subscribers exactly once. Selected\n * ids the snapshot does not hold leave the selection, and every pose\n * override is cleared.\n *\n * Throws inside any other edit — `batch`, `untracked`, `applyBatch`, an\n * undo — which would revert to a state this replaces. Throws on an unsupported version or unknown registry/layer ids; on a\n * malformed snapshot the scene is left empty or partially populated (callers should treat a\n * `loadState` throw as fatal and reload). Snapshots from `toJSON()` are\n * always well-formed. */\n loadState(json: SerializedScene<TData, TLayer, TPose>): void;\n\n // Subscription (used by useScene; also for non-React observers)\n subscribe(listener: () => void): () => void;\n /** Monotonically increasing version. Snapshot for `useSyncExternalStore`. */\n getVersion(): number;\n}\n"],"mappings":";AA4DA,IAAaA,KAAY,MAAsBC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenSlop.js","names":["scaleMat","slopForStrokeHit","slop","s","scale","frame","multiply","widenSlop","px","hasSlop"],"sources":["../../../src/core/viewport/screenSlop.ts"],"sourcesContent":["/**\n * Pointer forgiveness measured on screen.\n *\n * A pick slop is a screen distance: \"within 4px of the outline\". Converting it\n * to a world distance — per axis or as one scalar — is exact only when the\n * frame the geometry lives in reaches the screen by a similarity. A rotated\n * node under non-uniform zoom does not: a screen circle pulled back into its\n * frame is a tilted ellipse, so the test is run against the geometry's screen\n * image instead (`strokeHitTest`'s `slop`).\n */\nimport { multiply, scale as scaleMat, type
|
|
1
|
+
{"version":3,"file":"screenSlop.js","names":["scaleMat","slopForStrokeHit","slop","s","scale","frame","multiply","widenSlop","px","hasSlop"],"sources":["../../../src/core/viewport/screenSlop.ts"],"sourcesContent":["/**\n * Pointer forgiveness measured on screen.\n *\n * A pick slop is a screen distance: \"within 4px of the outline\". Converting it\n * to a world distance — per axis or as one scalar — is exact only when the\n * frame the geometry lives in reaches the screen by a similarity. A rotated\n * node under non-uniform zoom does not: a screen circle pulled back into its\n * frame is a tilted ellipse, so the test is run against the geometry's screen\n * image instead (`strokeHitTest`'s `slop`).\n */\nimport { multiply, scale as scaleMat, type Affine } from '@weasel-js/geom';\nimport type { Scale2 } from '@weasel-js/geom';\n\n/** `px` screen pixels under a view whose per-axis scale is `scale`. */\nexport interface ScreenSlop {\n px: number;\n scale: Scale2;\n}\n\n/** A slop in world units (a number, the view-less form) or on screen. */\nexport type PickSlop = number | ScreenSlop;\n\n/**\n * How `strokeHitTest` should apply a slop to geometry in a frame that\n * reaches world by `frame` (`poseFrameOf`; only its linear part is read): a\n * world slop adds to the threshold, a screen slop is handed over with the\n * frame's map to the screen.\n */\nexport function slopForStrokeHit(\n slop: PickSlop | undefined,\n frame?: Affine | null,\n): { extra: number; opts: { slop?: { px: number; transform: Affine } } } {\n if (slop === undefined) return { extra: 0, opts: {} };\n if (typeof slop === 'number') return { extra: slop, opts: {} };\n const s = scaleMat(slop.scale.x, slop.scale.y);\n return {\n extra: 0,\n opts: { slop: { px: slop.px, transform: frame ? multiply(s, frame) : s } },\n };\n}\n\n/** `slop` widened by `px` screen pixels — kept on screen when it is measured\n * there, and read at `scale` when it is a world distance. */\nexport function widenSlop(slop: PickSlop | undefined, px: number, scale = 1): PickSlop | undefined {\n if (!(px > 0)) return slop;\n if (typeof slop === 'object') return { ...slop, px: slop.px + px };\n return (slop ?? 0) + px / (scale > 0 ? scale : 1);\n}\n\n/** Is any slop set at all? A zero reach plus a zero slop hits nothing. */\nexport function hasSlop(slop: PickSlop | undefined): boolean {\n if (slop === undefined) return false;\n return typeof slop === 'number' ? slop > 0 : slop.px > 0;\n}\n"],"mappings":";;AA4BA,SAAgBC,EACd,GACA,GACuE;CACvE,IAAIC,MAAS,KAAA,GAAW,OAAO;EAAE,OAAO;EAAG,MAAM,CAAC;CAAE;CACpD,IAAI,OAAOA,KAAS,UAAU,OAAO;EAAE,OAAOA;EAAM,MAAM,CAAC;CAAE;CAC7D,IAAMC,IAAIH,EAASE,EAAK,MAAM,GAAGA,EAAK,MAAM,CAAC;CAC7C,OAAO;EACL,OAAO;EACP,MAAM,EAAE,MAAM;GAAE,IAAIA,EAAK;GAAI,WAAWG,IAAQC,EAASH,GAAGE,CAAK,IAAIF;EAAE,EAAE;CAC3E;AACF;AAIA,SAAgBI,EAAU,GAA4B,GAAY,IAAQ,GAAyB;CAGjG,OAFMC,IAAK,IACP,OAAON,KAAS,WAAiB;EAAE,GAAGA;EAAM,IAAIA,EAAK,KAAKM;CAAG,KACzDN,KAAQ,KAAKM,KAAMJ,IAAQ,IAAIA,IAAQ,KAFzBF;AAGxB;AAGA,SAAgBO,EAAQ,GAAqC;CAE3D,OADIP,MAAS,KAAA,IAAkB,KACxB,OAAOA,KAAS,WAAWA,IAAO,IAAIA,EAAK,KAAK;AACzD"}
|
|
@@ -3,35 +3,33 @@ import { useVisibleRaf as r } from "@weasel-js/react";
|
|
|
3
3
|
//#region src/core/viewport/useDecayLoop.ts
|
|
4
4
|
var i = .5;
|
|
5
5
|
function a() {
|
|
6
|
-
let a = n(null), o =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
o.cancel(), a.current = null;
|
|
12
|
-
}, [o]), c = e((e) => {
|
|
13
|
-
let t = a.current;
|
|
14
|
-
if (!t) return;
|
|
15
|
-
if (t.lastTime === null) {
|
|
16
|
-
t.lastTime = e, o.request();
|
|
6
|
+
let a = n(null), o = e((e, t) => {
|
|
7
|
+
let n = a.current;
|
|
8
|
+
if (!n) return;
|
|
9
|
+
if (n.lastTime === null) {
|
|
10
|
+
n.lastTime = e, t.request();
|
|
17
11
|
return;
|
|
18
12
|
}
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
let
|
|
22
|
-
if (
|
|
23
|
-
a.current = null,
|
|
13
|
+
let r = Math.min(e - n.lastTime, 64);
|
|
14
|
+
n.lastTime = e;
|
|
15
|
+
let o = n.friction ** (r / 16.67);
|
|
16
|
+
if (n.vx *= o, n.vy *= o, Math.sqrt(n.vx * n.vx + n.vy * n.vy) < n.minSpeed) {
|
|
17
|
+
a.current = null, t.cancel(), n.onEnd?.();
|
|
24
18
|
return;
|
|
25
19
|
}
|
|
26
|
-
let s =
|
|
27
|
-
if (
|
|
28
|
-
let { minX: e, maxX:
|
|
29
|
-
|
|
20
|
+
let s = n.vx * r, c = n.vy * r;
|
|
21
|
+
if (n.viewBounds && n.boundary) {
|
|
22
|
+
let { minX: e, maxX: t, minY: r, maxY: a } = n.viewBounds, o = n.posX + s, l = n.posY + c;
|
|
23
|
+
n.boundary === "stop" ? (e !== void 0 && o < e ? (s = e - n.posX, n.vx = 0) : t !== void 0 && o > t && (s = t - n.posX, n.vx = 0), r !== void 0 && l < r ? (c = r - n.posY, n.vy = 0) : a !== void 0 && l > a && (c = a - n.posY, n.vy = 0)) : n.boundary === "spring" ? (e !== void 0 && o < e ? (s = e - n.posX, n.vx = Math.abs(n.vx) * i) : t !== void 0 && o > t && (s = t - n.posX, n.vx = -Math.abs(n.vx) * i), r !== void 0 && l < r ? (c = r - n.posY, n.vy = Math.abs(n.vy) * i) : a !== void 0 && l > a && (c = a - n.posY, n.vy = -Math.abs(n.vy) * i)) : (e !== void 0 && o < e ? (s = e - n.posX, n.vx = Math.abs(n.vx)) : t !== void 0 && o > t && (s = t - n.posX, n.vx = -Math.abs(n.vx)), r !== void 0 && l < r ? (c = r - n.posY, n.vy = Math.abs(n.vy)) : a !== void 0 && l > a && (c = a - n.posY, n.vy = -Math.abs(n.vy))), n.posX += s, n.posY += c;
|
|
30
24
|
}
|
|
31
|
-
|
|
32
|
-
}, [
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
n.onTick(s, c), t.request();
|
|
26
|
+
}, []), s = r(o, { onResume: e(() => {
|
|
27
|
+
a.current && (a.current.lastTime = null);
|
|
28
|
+
}, []) }), c = e(() => {
|
|
29
|
+
s.cancel(), a.current = null;
|
|
30
|
+
}, [s]), l = e((e) => {
|
|
31
|
+
c();
|
|
32
|
+
let { velocity: t, friction: n = .92, minSpeed: r = .01, viewBounds: i, boundary: o, initialPosition: l, onTick: u, onEnd: d } = e;
|
|
35
33
|
if (Math.sqrt(t.vx ** 2 + t.vy ** 2) < r) {
|
|
36
34
|
d?.();
|
|
37
35
|
return;
|
|
@@ -45,16 +43,16 @@ function a() {
|
|
|
45
43
|
posX: l?.x ?? 0,
|
|
46
44
|
posY: l?.y ?? 0,
|
|
47
45
|
viewBounds: i,
|
|
48
|
-
boundary:
|
|
46
|
+
boundary: o,
|
|
49
47
|
onTick: u,
|
|
50
48
|
onEnd: d
|
|
51
|
-
},
|
|
52
|
-
}, [
|
|
49
|
+
}, s.request();
|
|
50
|
+
}, [c, s]);
|
|
53
51
|
return t(() => () => {
|
|
54
|
-
|
|
55
|
-
}, [
|
|
52
|
+
c();
|
|
53
|
+
}, [c]), {
|
|
56
54
|
start: l,
|
|
57
|
-
cancel:
|
|
55
|
+
cancel: c
|
|
58
56
|
};
|
|
59
57
|
}
|
|
60
58
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDecayLoop.js","names":["SPRING_DAMPING","useDecayLoop","stateRef","useRef","
|
|
1
|
+
{"version":3,"file":"useDecayLoop.js","names":["SPRING_DAMPING","useDecayLoop","stateRef","useRef","tick","useCallback","s","now","frameLoop","dt","f","dx","dy","newX","newY","minX","maxX","minY","maxY","useVisibleRaf","cancel","start","config","velocity","minSpeed","initialPosition","useEffect"],"sources":["../../../src/core/viewport/useDecayLoop.ts"],"sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\nimport { useVisibleRaf, type VisibleRaf } from '@weasel-js/react';\n\n/** Per-axis limits on a view's position. Any side may be left open. */\nexport interface PanBounds {\n minX?: number;\n maxX?: number;\n minY?: number;\n maxY?: number;\n}\n\n/** Momentum settings for a pan: how quickly a flung view slows, when it stops,\n * and what happens at the pan limits. The `DecayLoopConfig` fields a caller\n * chooses up front, without the per-gesture `velocity` / `onTick`. */\nexport interface InertiaConfig {\n friction?: number;\n minSpeed?: number;\n /** What to do when inertial pan reaches `bounds`. Default: no clamping. */\n boundary?: 'stop' | 'bounce' | 'spring';\n /** View-coordinate limits for boundary clamping. Requires `boundary` to take effect. */\n bounds?: PanBounds;\n}\n\n/** How a decay should run: its starting velocity, how fast it slows, and what\n * happens if it reaches the pan limits. */\nexport interface DecayLoopConfig {\n velocity: { vx: number; vy: number };\n friction?: number;\n minSpeed?: number;\n /** Bounds for boundary clamping. Requires `boundary` to take effect. */\n viewBounds?: PanBounds;\n /**\n * What to do when the accumulated position hits `viewBounds`. Default: no clamping.\n * - `'stop'`: clamp at boundary, kill velocity component.\n * - `'bounce'`: linear reflection — flip velocity sign, magnitude preserved.\n * - `'spring'`: damped reflection — flip velocity sign and shrink magnitude\n * by `SPRING_DAMPING` per bounce so the motion settles naturally.\n */\n boundary?: 'stop' | 'bounce' | 'spring';\n /** Starting position for internal boundary tracking. Required when `viewBounds` is set. */\n initialPosition?: { x: number; y: number };\n onTick: (dx: number, dy: number) => void;\n onEnd?: () => void;\n}\n\n/**\n * Fraction of velocity preserved per `'spring'` bounce. 0.5 means each bounce\n * loses half its energy, giving a quick visible settle (typically 2–4 bounces\n * before `minSpeed` cuts the loop). Fixed for v1; a future `bounceDamping`\n * config option could expose this if a consumer needs control.\n */\nconst SPRING_DAMPING = 0.5;\n\n/** A rAF loop that coasts a value to a stop under friction, reporting the\n * per-frame delta. What turns a released pan drag into momentum scrolling. */\nexport function useDecayLoop() {\n const stateRef = useRef<{\n vx: number; vy: number;\n friction: number; minSpeed: number;\n lastTime: number | null;\n posX: number; posY: number;\n viewBounds: PanBounds | undefined;\n boundary: 'stop' | 'bounce' | 'spring' | undefined;\n onTick: (dx: number, dy: number) => void;\n onEnd?: () => void;\n } | null>(null);\n\n const tick = useCallback((now: number, frameLoop: VisibleRaf) => {\n const s = stateRef.current;\n if (!s) return;\n if (s.lastTime === null) {\n s.lastTime = now;\n frameLoop.request();\n return;\n }\n const dt = Math.min(now - s.lastTime, 64); // cap at 64ms to avoid huge jumps\n s.lastTime = now;\n // Time-normalize friction to 60fps baseline\n const f = Math.pow(s.friction, dt / 16.67);\n s.vx *= f;\n s.vy *= f;\n const speed = Math.sqrt(s.vx * s.vx + s.vy * s.vy);\n if (speed < s.minSpeed) {\n stateRef.current = null;\n frameLoop.cancel();\n s.onEnd?.();\n return;\n }\n let dx = s.vx * dt;\n let dy = s.vy * dt;\n if (s.viewBounds && s.boundary) {\n const { minX, maxX, minY, maxY } = s.viewBounds;\n const newX = s.posX + dx;\n const newY = s.posY + dy;\n if (s.boundary === 'stop') {\n if (minX !== undefined && newX < minX) { dx = minX - s.posX; s.vx = 0; }\n else if (maxX !== undefined && newX > maxX) { dx = maxX - s.posX; s.vx = 0; }\n if (minY !== undefined && newY < minY) { dy = minY - s.posY; s.vy = 0; }\n else if (maxY !== undefined && newY > maxY) { dy = maxY - s.posY; s.vy = 0; }\n } else if (s.boundary === 'spring') {\n // Damped reflection: flip sign AND scale magnitude by SPRING_DAMPING so\n // each bounce loses energy. Once |v| < minSpeed the outer loop ends.\n if (minX !== undefined && newX < minX) { dx = minX - s.posX; s.vx = Math.abs(s.vx) * SPRING_DAMPING; }\n else if (maxX !== undefined && newX > maxX) { dx = maxX - s.posX; s.vx = -Math.abs(s.vx) * SPRING_DAMPING; }\n if (minY !== undefined && newY < minY) { dy = minY - s.posY; s.vy = Math.abs(s.vy) * SPRING_DAMPING; }\n else if (maxY !== undefined && newY > maxY) { dy = maxY - s.posY; s.vy = -Math.abs(s.vy) * SPRING_DAMPING; }\n } else {\n if (minX !== undefined && newX < minX) { dx = minX - s.posX; s.vx = Math.abs(s.vx); }\n else if (maxX !== undefined && newX > maxX) { dx = maxX - s.posX; s.vx = -Math.abs(s.vx); }\n if (minY !== undefined && newY < minY) { dy = minY - s.posY; s.vy = Math.abs(s.vy); }\n else if (maxY !== undefined && newY > maxY) { dy = maxY - s.posY; s.vy = -Math.abs(s.vy); }\n }\n s.posX += dx;\n s.posY += dy;\n }\n s.onTick(dx, dy);\n frameLoop.request();\n }, []);\n\n const frameLoop = useVisibleRaf(\n tick,\n {\n // Coasting does not continue while suspended: dropping `lastTime` makes\n // the resuming frame seed a fresh interval instead of applying an hour of\n // friction at once, which would end the decay before it was seen.\n onResume: useCallback(() => {\n if (stateRef.current) stateRef.current.lastTime = null;\n }, []),\n },\n );\n\n const cancel = useCallback(() => {\n frameLoop.cancel();\n stateRef.current = null;\n }, [frameLoop]);\n\n const start = useCallback((config: DecayLoopConfig) => {\n cancel();\n const { velocity, friction = 0.92, minSpeed = 0.01, viewBounds, boundary, initialPosition, onTick, onEnd } = config;\n const speed = Math.sqrt(velocity.vx ** 2 + velocity.vy ** 2);\n if (speed < minSpeed) { onEnd?.(); return; }\n stateRef.current = {\n vx: velocity.vx, vy: velocity.vy, friction, minSpeed, lastTime: null,\n posX: initialPosition?.x ?? 0, posY: initialPosition?.y ?? 0,\n viewBounds, boundary,\n onTick, onEnd,\n };\n frameLoop.request();\n }, [cancel, frameLoop]);\n\n useEffect(() => () => { cancel(); }, [cancel]);\n\n return { start, cancel };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useNodeOverlayFrame.js","names":["useNodeOverlayFrame","useCanvasSize","containerRef","options","project","useCallback","box","worldBoxOf","scene","nodeId","compose","frame","poseFrameOf","toView","viewMatrix","resolveView","view","multiply","toScreen","m","p","x","y","applyToPoint","lastInverse","useRef","toLocal","inverse","invert","width","height","useInsertionEffect","nodes","composeWorldPose","effectivePose","id","composeRectPose","sx","sy"],"sources":["../../../src/core/viewport/useNodeOverlayFrame.ts"],"sourcesContent":["/**\n * `useNodeOverlayFrame` — the coordinate frame a DOM overlay pinned to one\n * scene node needs: the node's world box, the projection from that box's\n * frame into overlay pixels, and the overlay's measured size.\n *\n * `toScreen` maps the node's **unrotated world box** frame, which is the\n * frame `fillInPoseFrame` resolves a `units: 'bounds'` paint into and the\n * one `pathInPoseFrame` projects geometry into. The pose's rotation and\n * `transform` live in this hook, not in those: a node's stored geometry is\n * in its box by definition.\n */\nimport { useCallback, useInsertionEffect, useRef, type RefObject } from 'react';\nimport { applyToPoint, invert, multiply, poseFrameOf, type
|
|
1
|
+
{"version":3,"file":"useNodeOverlayFrame.js","names":["useNodeOverlayFrame","useCanvasSize","containerRef","options","project","useCallback","box","worldBoxOf","scene","nodeId","compose","frame","poseFrameOf","toView","viewMatrix","resolveView","view","multiply","toScreen","m","p","x","y","applyToPoint","lastInverse","useRef","toLocal","inverse","invert","width","height","useInsertionEffect","nodes","composeWorldPose","effectivePose","id","composeRectPose","sx","sy"],"sources":["../../../src/core/viewport/useNodeOverlayFrame.ts"],"sourcesContent":["/**\n * `useNodeOverlayFrame` — the coordinate frame a DOM overlay pinned to one\n * scene node needs: the node's world box, the projection from that box's\n * frame into overlay pixels, and the overlay's measured size.\n *\n * `toScreen` maps the node's **unrotated world box** frame, which is the\n * frame `fillInPoseFrame` resolves a `units: 'bounds'` paint into and the\n * one `pathInPoseFrame` projects geometry into. The pose's rotation and\n * `transform` live in this hook, not in those: a node's stored geometry is\n * in its box by definition.\n */\nimport { useCallback, useInsertionEffect, useRef, type RefObject } from 'react';\nimport { applyToPoint, invert, multiply, poseFrameOf, type Affine } from '@weasel-js/geom';\nimport type { Node, RectPose, Scene } from '../scene/types';\nimport { useCanvasSize } from './useCanvasSize';\nimport type { View } from './view';\nimport { composeRectPose, composeWorldPose } from 'core/scene/composePose';\nimport { effectivePose } from 'core/scene/effectivePose';\n\n/** Structural point, matching every other `{ x, y }` the kit passes across a\n * boundary. */\nexport interface OverlayPoint {\n x: number;\n y: number;\n}\n\n/** The overlay frame for one node. `null` from the hook means there is no\n * frame yet — no node, or nothing measured to draw it on. */\nexport interface NodeOverlayFrame {\n /** The node's composed world box, **unrotated**: the frame `toScreen` maps\n * from, and the box to hand `fillInPoseFrame` / `fillToBoundsFrame`. */\n box: { x: number; y: number; width: number; height: number };\n /** Node box frame → overlay pixels: the pose's rotation and `transform`,\n * then the view. */\n toScreen: (p: OverlayPoint) => OverlayPoint;\n /** Overlay pixels → node box frame. Inverts `toScreen`; while a live view\n * flattens an axis, it keeps the last mapping that had an inverse. */\n toLocal: (p: OverlayPoint) => OverlayPoint;\n /** The container's size in CSS pixels — the overlay's own box. */\n width: number;\n height: number;\n}\n\n/** Options for `useNodeOverlayFrame`. */\nexport interface UseNodeOverlayFrameOptions<TPose> {\n /**\n * Current viewport. A thunk is re-read on every projection, which is what\n * an uncontrolled `SceneCanvas` needs — its camera moves without a render,\n * so pass the handle's `getView`. Omit it and world units are handed\n * through as screen pixels.\n */\n view?: View | (() => View);\n /** Fold a child pose into its parent's frame. Default `composeRectPose`. */\n compose?: (parent: TPose, child: TPose) => TPose;\n}\n\n/**\n * Resolve the overlay frame for `nodeId` against `containerRef` — the element\n * the overlay is positioned in, which must be the canvas's own box for the\n * projection to land.\n *\n * Returns `null` when there is no node under `nodeId`, when the container has\n * not been measured, or when the view or the pose's `transform` collapses an\n * axis to zero.\n */\nexport function useNodeOverlayFrame<TData, TLayer extends string, TPose extends RectPose>(\n scene: Scene<TData, TLayer, TPose>,\n containerRef: RefObject<HTMLElement | null>,\n nodeId: string | null | undefined,\n options: UseNodeOverlayFrameOptions<TPose> = {},\n): NodeOverlayFrame | null {\n const { width, height } = useCanvasSize(containerRef);\n const { view, compose } = options;\n\n const project = useCallback((): Affine | null => {\n const box = worldBoxOf(scene, nodeId, compose);\n if (!box) return null;\n const frame = poseFrameOf(box);\n const toView = viewMatrix(resolveView(view));\n return frame ? multiply(toView, frame) : toView;\n }, [scene, nodeId, compose, view]);\n\n const toScreen = useCallback((p: OverlayPoint): OverlayPoint => {\n const m = project();\n if (!m) return p;\n const [x, y] = applyToPoint(m, p.x, p.y);\n return { x, y };\n }, [project]);\n\n const lastInverse = useRef<Affine | null>(null);\n\n const toLocal = useCallback((p: OverlayPoint): OverlayPoint => {\n const m = project();\n const inverse = m && invert(m);\n if (inverse) lastInverse.current = inverse;\n // Published by the commit that handed this toLocal out, so there is one.\n const [x, y] = applyToPoint(inverse ?? lastInverse.current!, p.x, p.y);\n return { x, y };\n }, [project]);\n\n const box = worldBoxOf(scene, nodeId, compose);\n const m = box && width !== 0 && height !== 0 ? project() : null;\n const inverse = m && invert(m);\n useInsertionEffect(() => {\n if (inverse) lastInverse.current = inverse;\n });\n\n if (!box || !inverse) return null;\n return { box: { x: box.x, y: box.y, width: box.width, height: box.height }, toScreen, toLocal, width, height };\n}\n\nfunction resolveView(view: View | (() => View) | undefined): View | undefined {\n return typeof view === 'function' ? view() : view;\n}\n\nfunction worldBoxOf<TData, TLayer extends string, TPose extends RectPose>(\n scene: Scene<TData, TLayer, TPose>,\n nodeId: string | null | undefined,\n compose: ((parent: TPose, child: TPose) => TPose) | undefined,\n): TPose | null {\n if (nodeId == null) return null;\n const nodes = scene.nodes as ReadonlyMap<string, Node<TData, TLayer, TPose>>;\n if (!nodes.has(nodeId)) return null;\n // `effectivePose`, not `node.pose`: an overlay tracks what the renderer\n // draws, and a gesture override or a derived pose is exactly the difference.\n const adapter = {\n getPose: (id: string) => effectivePose(scene, nodes.get(id)!),\n getParent: (id: string) => nodes.get(id)?.parent ?? null,\n };\n return composeWorldPose(adapter, nodeId, compose ?? composeRectPose);\n}\n\n/** World → overlay pixels: `screen = (world − view) × view.scale`. */\nfunction viewMatrix(view: View | undefined): Affine {\n if (!view) return [1, 0, 0, 1, 0, 0];\n const { x: sx, y: sy } = view.scale;\n return [sx, 0, 0, sy, -view.x * sx, -view.y * sy];\n}\n"],"mappings":";;;;;;;AAiEA,SAAgBA,EACd,GACA,GACA,GACA,IAA6C,CAAC,GACrB;CACzB,IAAM,EAAE,UAAO,cAAWC,EAAcC,CAAY,GAC9C,EAAE,SAAM,eAAYC,GAEpBC,IAAUC,QAAiC;EAC/C,IAAMC,IAAMC,EAAWC,GAAOC,GAAQC,CAAO;EAC7C,IAAI,CAACJ,GAAK,OAAO;EACjB,IAAMK,IAAQC,EAAYN,CAAG,GACvBO,IAASC,EAAWC,EAAYC,CAAI,CAAC;EAC3C,OAAOL,IAAQM,EAASJ,GAAQF,CAAK,IAAIE;CAC3C,GAAG;EAACL;EAAOC;EAAQC;EAASM;CAAI,CAAC,GAE3BE,IAAWb,GAAa,MAAkC;EAC9D,IAAMc,IAAIf,EAAQ;EAClB,IAAI,CAACe,GAAG,OAAOC;EACf,IAAM,CAACC,GAAGC,KAAKC,EAAaJ,GAAGC,EAAE,GAAGA,EAAE,CAAC;EACvC,OAAO;GAAE;GAAG;EAAE;CAChB,GAAG,CAAChB,CAAO,CAAC,GAENoB,IAAcC,EAAsB,IAAI,GAExCC,IAAUrB,GAAa,MAAkC;EAC7D,IAAMc,IAAIf,EAAQ,GACZuB,IAAUR,KAAKS,EAAOT,CAAC;EAC7B,AAAIQ,MAAS,EAAY,UAAUA;EAEnC,IAAM,CAACN,GAAGC,KAAKC,EAAaI,KAAWH,EAAY,SAAUJ,EAAE,GAAGA,EAAE,CAAC;EACrE,OAAO;GAAE;GAAG;EAAE;CAChB,GAAG,CAAChB,CAAO,CAAC,GAENE,IAAMC,EAAWC,GAAOC,GAAQC,CAAO,GACvCS,IAAIb,KAAOuB,MAAU,KAAKC,MAAW,IAAI1B,EAAQ,IAAI,MACrDuB,IAAUR,KAAKS,EAAOT,CAAC;CAM7B,OALAY,QAAyB;EACvB,AAAIJ,MAAS,EAAY,UAAUA;CACrC,CAAC,GAEG,CAACrB,KAAO,CAACqB,IAAgB,OACtB;EAAE,KAAK;GAAE,GAAGrB,EAAI;GAAG,GAAGA,EAAI;GAAG,OAAOA,EAAI;GAAO,QAAQA,EAAI;EAAO;EAAG;EAAU;EAAS;EAAO;CAAO;AAC/G;AAEA,SAASS,EAAY,GAAyD;CAC5E,OAAO,OAAOC,KAAS,aAAaA,EAAK,IAAIA;AAC/C;AAEA,SAAST,EACP,GACA,GACA,GACc;CACd,IAAIE,KAAU,MAAM,OAAO;CAC3B,IAAMuB,IAAQxB,EAAM;CAQpB,OAPKwB,EAAM,IAAIvB,CAAM,IAOdwB,EAAiB;EAHtB,UAAU,MAAeC,EAAc1B,GAAOwB,EAAM,IAAIG,CAAE,CAAE;EAC5D,YAAY,MAAeH,EAAM,IAAIG,CAAE,CAAC,EAAE,UAAU;CAE9B,GAAS1B,GAAQC,KAAW0B,CAAe,IAPpC;AAQjC;AAGA,SAAStB,EAAW,GAAgC;CAClD,IAAI,CAACE,GAAM,OAAO;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;CAAC;CACnC,IAAM,EAAE,GAAGqB,GAAI,GAAGC,MAAOtB,EAAK;CAC9B,OAAO;EAACqB;EAAI;EAAG;EAAGC;EAAI,CAACtB,EAAK,IAAIqB;EAAI,CAACrB,EAAK,IAAIsB;CAAE;AAClD"}
|
|
@@ -44,77 +44,78 @@ function p(e, t) {
|
|
|
44
44
|
let n = l();
|
|
45
45
|
return m(e, t ?? n);
|
|
46
46
|
}
|
|
47
|
-
function m(
|
|
48
|
-
let
|
|
49
|
-
return a(() =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
function m(e, t) {
|
|
48
|
+
let n = `${u}:${i()}`, r = s(e), c = s(t), l = o(null), d = o(!1);
|
|
49
|
+
return a(() => h(n, r, c, l, d), [
|
|
50
|
+
n,
|
|
51
|
+
r,
|
|
52
|
+
c
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
function h(i, a, o, s, l) {
|
|
56
|
+
let u = null, p = null, m = () => {
|
|
57
|
+
if (u) return u;
|
|
58
|
+
let e = a.current.get();
|
|
59
|
+
return p ? f.from(f.to(e).map((e, t) => e - p[t])) : e;
|
|
60
|
+
}, h = (t) => {
|
|
61
|
+
l.current = !0;
|
|
62
|
+
try {
|
|
63
|
+
a.current.set(e(t));
|
|
64
|
+
} finally {
|
|
65
|
+
l.current = !1;
|
|
66
|
+
}
|
|
67
|
+
}, g = c({
|
|
68
|
+
axes: f,
|
|
69
|
+
get: m,
|
|
70
|
+
set: (e) => {
|
|
71
|
+
u = e, p = null;
|
|
72
|
+
},
|
|
73
|
+
show: (e) => {
|
|
74
|
+
if (g.active) {
|
|
75
|
+
u || (u = m(), p = null), h(e);
|
|
76
|
+
return;
|
|
60
77
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
let t = u;
|
|
79
|
+
u = p = null, t && h(t);
|
|
80
|
+
}
|
|
81
|
+
}), _ = () => o.current.isActive(i), v = () => _() ? s.current : null, y = () => {
|
|
82
|
+
o.current.cancelKey(i), s.current = null;
|
|
83
|
+
}, b = () => {
|
|
84
|
+
y(), g.active || (u = p = null);
|
|
85
|
+
}, x = (t, a = {}) => {
|
|
86
|
+
let c = e(typeof t == "function" ? t(v() ?? m()) : t);
|
|
87
|
+
s.current = c, o.current.tween({
|
|
88
|
+
target: g,
|
|
89
|
+
to: c,
|
|
90
|
+
ms: a.ms ?? d,
|
|
91
|
+
easing: a.easing ?? r,
|
|
92
|
+
interpolator: a.interpolator ?? n,
|
|
93
|
+
cancelKey: i,
|
|
94
|
+
onDone: () => {
|
|
95
|
+
s.current = null, h(g.read()), g.active || (u = p = null), a.onDone?.();
|
|
74
96
|
}
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
a !== i && x(a, r);
|
|
98
|
-
},
|
|
99
|
-
stop: b,
|
|
100
|
-
isAnimating: u,
|
|
101
|
-
target: p,
|
|
102
|
-
stopIfExternal: () => {
|
|
103
|
-
if (!v.current) {
|
|
104
|
-
if (y(), l.active) {
|
|
105
|
-
let e = f.to(l.read()), t = f.to(o());
|
|
106
|
-
a = e.map((e, n) => e - t[n]);
|
|
107
|
-
} else a = null;
|
|
108
|
-
i = null;
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
viewTarget: l
|
|
112
|
-
};
|
|
113
|
-
}, [
|
|
114
|
-
m,
|
|
115
|
-
h,
|
|
116
|
-
g
|
|
117
|
-
]);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
animate: x,
|
|
101
|
+
animateToBounds: (e, n, r = {}) => {
|
|
102
|
+
let i = a.current.get(), o = t(e, n, i, r);
|
|
103
|
+
o !== i && x(o, r);
|
|
104
|
+
},
|
|
105
|
+
stop: b,
|
|
106
|
+
isAnimating: _,
|
|
107
|
+
target: v,
|
|
108
|
+
stopIfExternal: () => {
|
|
109
|
+
if (!l.current) {
|
|
110
|
+
if (y(), g.active) {
|
|
111
|
+
let e = f.to(g.read()), t = f.to(m());
|
|
112
|
+
p = e.map((e, n) => e - t[n]);
|
|
113
|
+
} else p = null;
|
|
114
|
+
u = null;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
viewTarget: g
|
|
118
|
+
};
|
|
118
119
|
}
|
|
119
120
|
//#endregion
|
|
120
121
|
export { u as VIEW_ANIMATION_KEY, p as useViewAnimation, m as useViewAnimationOn, f as viewAxes };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useViewAnimation.js","names":["VIEW_ANIMATION_KEY","DEFAULT_MS","viewAxes","v","a","d","at","useViewAnimation","own","useAnimator","useViewAnimationOn","view","animator","key","useId","viewRef","useLatest","animatorRef","targetRef","useRef","writingRef","useMemo","committed","offset","base","live","n","i","write","normalizeView","viewTarget","createTarget","landed","isAnimating","target","cancel","stop","animate","resolved","to","opts","easeOutCubic","interpolateView","current","fitted","fitViewToBounds","bounds","dims","shown","under"],"sources":["../../../src/core/viewport/useViewAnimation.ts"],"sourcesContent":["import { useId, useMemo, useRef } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { useAnimator } from '@weasel-js/animator';\nimport { easeOutCubic } from '@weasel-js/geom';\nimport type { Animator, EasingSpec, InterpolatorFactory } from '@weasel-js/animator';\nimport { createTarget, type Target, type TargetAxes } from '@weasel-js/animator';\nimport { interpolateView } from './interpolateView';\nimport { fitViewToBounds } from './fitViewToBounds';\nimport type { Bounds, FitViewToBoundsOptions, ViewportDims } from './fitViewToBounds';\nimport { normalizeView, type View } from './view';\n\n/** Cancel-key prefix. Each hook instance appends its own id, so two runners\n * sharing an animator do not cancel each other. */\nexport const VIEW_ANIMATION_KEY = 'view';\n\nconst DEFAULT_MS = 250;\n\n/** How the camera should move. */\nexport interface ViewAnimationOptions {\n /** Duration in ms. Default 250. */\n ms?: number;\n /** Easing curve. Default `easeOutCubic`. */\n easing?: EasingSpec;\n /** Replace the kit's log-scale / fixed-anchor curve. */\n interpolator?: InterpolatorFactory<View>;\n /** Fires when the target is reached. Not called on cancel. */\n onDone?: () => void;\n}\n\n/** Options accepted by {@link ViewAnimationApi.animateToBounds}. */\nexport interface AnimateToBoundsOptions extends FitViewToBoundsOptions, ViewAnimationOptions {}\n\n/** What the runner reads and writes. On `<SceneCanvas>` this is the same\n * channel `view.set` uses, so a camera animation on an uncontrolled canvas\n * costs no React render. */\nexport interface ViewChannel {\n get(): View;\n set(v: View): void;\n}\n\n/** The camera animation surface. Its own animations run one at a time. */\nexport interface ViewAnimationApi {\n /** Glide from the live view to `to`. A thunk receives the pending target when\n * one is in flight, so successive discrete steps compound. */\n animate(to: View | ((base: View) => View), opts?: ViewAnimationOptions): void;\n /** `fitViewToBounds` composed with `animate`. */\n animateToBounds(bounds: Bounds, dims: ViewportDims, opts?: AnimateToBoundsOptions): void;\n /** Cancel. The view stays where it is — no jump to the target. */\n stop(): void;\n isAnimating(): boolean;\n /** Where the in-flight animation is heading, or null when none is. */\n target(): View | null;\n /** Cancel unless the write that prompted this came from the runner's own\n * per-frame write. Feed it from every channel that can move the camera. */\n stopIfExternal(): void;\n /**\n * The view as a {@link Target}, for running another animation on it: a shake\n * or a nudge adds to a camera animation in flight, and neither cancels the\n * other. Move it with the animator this runner was given, and without this\n * runner's cancel key. `stop` and an outside write leave such an animation\n * running.\n */\n readonly viewTarget: Target<View>;\n}\n\n/** A `View` as the four numbers an animation moves: `x`, `y`, `scale.x`, `scale.y`. */\nexport const viewAxes: TargetAxes<View> = {\n count: 4,\n names: ['x', 'y', 'scale.x', 'scale.y'],\n to: (v) => [v.x, v.y, v.scale.x, v.scale.y],\n from: (a) => ({ x: a[0]!, y: a[1]!, scale: { x: a[2]!, y: a[3]! } }),\n add: (v, d) => ({ x: v.x + d[0]!, y: v.y + d[1]!, scale: { x: v.scale.x + d[2]!, y: v.scale.y + d[3]! } }),\n write: (v, out, at) => {\n out[at] = v.x;\n out[at + 1] = v.y;\n out[at + 2] = v.scale.x;\n out[at + 3] = v.scale.y;\n },\n};\n\n/**\n * Animate the viewport `View`. Runs on the kit's {@link Animator} — pass one to\n * share a canvas's animator, or omit it and the hook makes its own. A caller\n * that always has one should use {@link useViewAnimationOn}, which builds none.\n *\n * Every animation from one instance registers under that instance's cancel key,\n * so starting one cancels whatever *it* had in flight, and each starts from where\n * the last one was rather than a captured value — an interrupted camera never jumps.\n * Two instances on one animator are independent. The view is a target\n * ({@link ViewAnimationApi.viewTarget}), so other animations can add to the camera's.\n */\nexport function useViewAnimation(view: ViewChannel, animator?: Animator): ViewAnimationApi {\n const own = useAnimator();\n return useViewAnimationOn(view, animator ?? own);\n}\n\n/** {@link useViewAnimation} on an animator the caller owns. Constructs no\n * animator of its own, so the camera runs on exactly the one passed. */\nexport function useViewAnimationOn(view: ViewChannel, animator: Animator): ViewAnimationApi {\n const key = `${VIEW_ANIMATION_KEY}:${useId()}`;\n const viewRef = useLatest(view);\n const animatorRef = useLatest(animator);\n /** Where this runner's own animation is heading. */\n const targetRef = useRef<View | null>(null);\n const writingRef = useRef(false);\n\n return useMemo<ViewAnimationApi>(() => {\n // The value the target's animations add to. While any is in flight it is kept here, apart\n // from the channel, which holds the folded view; null says to work it out from the channel.\n let committed: View | null = null;\n /** What the animations in flight added to the channel's view when `committed` was dropped. */\n let offset: number[] | null = null;\n const base = (): View => {\n if (committed) return committed;\n const live = viewRef.current.get();\n return offset ? viewAxes.from(viewAxes.to(live).map((n, i) => n - offset![i]!)) : live;\n };\n const write = (v: View): void => {\n writingRef.current = true;\n try { viewRef.current.set(normalizeView(v)); } finally { writingRef.current = false; }\n };\n const viewTarget: Target<View> = createTarget<View>({\n axes: viewAxes,\n get: base,\n set: (v) => { committed = v; offset = null; },\n show: (v) => {\n if (viewTarget.active) {\n if (!committed) { committed = base(); offset = null; }\n write(v);\n return;\n }\n // At rest. A stop or an outside write left the view as it stands; a finish lands it.\n const landed = committed;\n committed = offset = null;\n if (landed) write(landed);\n },\n });\n\n const isAnimating = () => animatorRef.current.isActive(key);\n const target = () => (isAnimating() ? targetRef.current : null);\n /** Cancels this runner's animation; whatever else moves the view goes on from where it was. */\n const cancel = (): void => {\n animatorRef.current.cancelKey(key);\n targetRef.current = null;\n };\n const stop = () => {\n cancel();\n if (!viewTarget.active) committed = offset = null;\n };\n\n const animate: ViewAnimationApi['animate'] = (to, opts = {}) => {\n const resolved = normalizeView(typeof to === 'function' ? to(target() ?? base()) : to);\n targetRef.current = resolved;\n animatorRef.current.tween<View>({\n target: viewTarget,\n to: resolved,\n ms: opts.ms ?? DEFAULT_MS,\n easing: opts.easing ?? easeOutCubic,\n interpolator: opts.interpolator ?? interpolateView,\n cancelKey: key,\n onDone: () => {\n targetRef.current = null;\n // Ahead of the frame's own write, so `onDone` reads the view it finished on.\n write(viewTarget.read());\n if (!viewTarget.active) committed = offset = null;\n opts.onDone?.();\n },\n });\n };\n\n return {\n animate,\n animateToBounds: (bounds, dims, opts = {}) => {\n const current = viewRef.current.get();\n const fitted = fitViewToBounds(bounds, dims, current, opts);\n if (fitted === current) return; // helper bailed (zero-area bounds/viewport)\n animate(fitted, opts);\n },\n stop,\n isAnimating,\n target,\n stopIfExternal: () => {\n if (writingRef.current) return;\n cancel();\n // The writer read the folded view, so what is still in flight stays on top of its write.\n if (viewTarget.active) {\n const shown = viewAxes.to(viewTarget.read());\n const under = viewAxes.to(base());\n offset = shown.map((n, i) => n - under[i]!);\n } else {\n offset = null;\n }\n committed = null;\n },\n viewTarget,\n };\n }, [key, viewRef, animatorRef]);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useViewAnimation.js","names":["VIEW_ANIMATION_KEY","DEFAULT_MS","viewAxes","v","a","d","at","useViewAnimation","own","useAnimator","useViewAnimationOn","view","animator","key","useId","viewRef","useLatest","animatorRef","targetRef","useRef","writingRef","useMemo","createViewAnimation","committed","offset","base","live","n","i","write","normalizeView","viewTarget","createTarget","landed","isAnimating","target","cancel","stop","animate","resolved","to","opts","easeOutCubic","interpolateView","current","fitted","fitViewToBounds","bounds","dims","shown","under"],"sources":["../../../src/core/viewport/useViewAnimation.ts"],"sourcesContent":["import { useId, useMemo, useRef } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { useAnimator } from '@weasel-js/animator';\nimport { easeOutCubic } from '@weasel-js/geom';\nimport type { Animator, EasingSpec, InterpolatorFactory } from '@weasel-js/animator';\nimport { createTarget, type Target, type TargetAxes } from '@weasel-js/animator';\nimport { interpolateView } from './interpolateView';\nimport { fitViewToBounds } from './fitViewToBounds';\nimport type { Bounds, FitViewToBoundsOptions, ViewportDims } from './fitViewToBounds';\nimport { normalizeView, type View } from './view';\n\n/** Cancel-key prefix. Each hook instance appends its own id, so two runners\n * sharing an animator do not cancel each other. */\nexport const VIEW_ANIMATION_KEY = 'view';\n\nconst DEFAULT_MS = 250;\n\n/** How the camera should move. */\nexport interface ViewAnimationOptions {\n /** Duration in ms. Default 250. */\n ms?: number;\n /** Easing curve. Default `easeOutCubic`. */\n easing?: EasingSpec;\n /** Replace the kit's log-scale / fixed-anchor curve. */\n interpolator?: InterpolatorFactory<View>;\n /** Fires when the target is reached. Not called on cancel. */\n onDone?: () => void;\n}\n\n/** Options accepted by {@link ViewAnimationApi.animateToBounds}. */\nexport interface AnimateToBoundsOptions extends FitViewToBoundsOptions, ViewAnimationOptions {}\n\n/** What the runner reads and writes. On `<SceneCanvas>` this is the same\n * channel `view.set` uses, so a camera animation on an uncontrolled canvas\n * costs no React render. */\nexport interface ViewChannel {\n get(): View;\n set(v: View): void;\n}\n\n/** The camera animation surface. Its own animations run one at a time. */\nexport interface ViewAnimationApi {\n /** Glide from the live view to `to`. A thunk receives the pending target when\n * one is in flight, so successive discrete steps compound. */\n animate(to: View | ((base: View) => View), opts?: ViewAnimationOptions): void;\n /** `fitViewToBounds` composed with `animate`. */\n animateToBounds(bounds: Bounds, dims: ViewportDims, opts?: AnimateToBoundsOptions): void;\n /** Cancel. The view stays where it is — no jump to the target. */\n stop(): void;\n isAnimating(): boolean;\n /** Where the in-flight animation is heading, or null when none is. */\n target(): View | null;\n /** Cancel unless the write that prompted this came from the runner's own\n * per-frame write. Feed it from every channel that can move the camera. */\n stopIfExternal(): void;\n /**\n * The view as a {@link Target}, for running another animation on it: a shake\n * or a nudge adds to a camera animation in flight, and neither cancels the\n * other. Move it with the animator this runner was given, and without this\n * runner's cancel key. `stop` and an outside write leave such an animation\n * running.\n */\n readonly viewTarget: Target<View>;\n}\n\n/** A `View` as the four numbers an animation moves: `x`, `y`, `scale.x`, `scale.y`. */\nexport const viewAxes: TargetAxes<View> = {\n count: 4,\n names: ['x', 'y', 'scale.x', 'scale.y'],\n to: (v) => [v.x, v.y, v.scale.x, v.scale.y],\n from: (a) => ({ x: a[0]!, y: a[1]!, scale: { x: a[2]!, y: a[3]! } }),\n add: (v, d) => ({ x: v.x + d[0]!, y: v.y + d[1]!, scale: { x: v.scale.x + d[2]!, y: v.scale.y + d[3]! } }),\n write: (v, out, at) => {\n out[at] = v.x;\n out[at + 1] = v.y;\n out[at + 2] = v.scale.x;\n out[at + 3] = v.scale.y;\n },\n};\n\n/**\n * Animate the viewport `View`. Runs on the kit's {@link Animator} — pass one to\n * share a canvas's animator, or omit it and the hook makes its own. A caller\n * that always has one should use {@link useViewAnimationOn}, which builds none.\n *\n * Every animation from one instance registers under that instance's cancel key,\n * so starting one cancels whatever *it* had in flight, and each starts from where\n * the last one was rather than a captured value — an interrupted camera never jumps.\n * Two instances on one animator are independent. The view is a target\n * ({@link ViewAnimationApi.viewTarget}), so other animations can add to the camera's.\n */\nexport function useViewAnimation(view: ViewChannel, animator?: Animator): ViewAnimationApi {\n const own = useAnimator();\n return useViewAnimationOn(view, animator ?? own);\n}\n\n/** {@link useViewAnimation} on an animator the caller owns. Constructs no\n * animator of its own, so the camera runs on exactly the one passed. */\nexport function useViewAnimationOn(view: ViewChannel, animator: Animator): ViewAnimationApi {\n const key = `${VIEW_ANIMATION_KEY}:${useId()}`;\n const viewRef = useLatest(view);\n const animatorRef = useLatest(animator);\n /** Where this runner's own animation is heading. */\n const targetRef = useRef<View | null>(null);\n const writingRef = useRef(false);\n\n return useMemo(\n () => createViewAnimation(key, viewRef, animatorRef, targetRef, writingRef),\n [key, viewRef, animatorRef],\n );\n}\n\n/** The runner behind {@link useViewAnimationOn}. */\nfunction createViewAnimation(\n key: string,\n viewRef: { readonly current: ViewChannel },\n animatorRef: { readonly current: Animator },\n targetRef: { current: View | null },\n writingRef: { current: boolean },\n): ViewAnimationApi {\n // The value the target's animations add to. While any is in flight it is kept here, apart\n // from the channel, which holds the folded view; null says to work it out from the channel.\n let committed: View | null = null;\n /** What the animations in flight added to the channel's view when `committed` was dropped. */\n let offset: number[] | null = null;\n const base = (): View => {\n if (committed) return committed;\n const live = viewRef.current.get();\n return offset ? viewAxes.from(viewAxes.to(live).map((n, i) => n - offset![i]!)) : live;\n };\n const write = (v: View): void => {\n writingRef.current = true;\n try { viewRef.current.set(normalizeView(v)); } finally { writingRef.current = false; }\n };\n const viewTarget: Target<View> = createTarget<View>({\n axes: viewAxes,\n get: base,\n set: (v) => { committed = v; offset = null; },\n show: (v) => {\n if (viewTarget.active) {\n if (!committed) { committed = base(); offset = null; }\n write(v);\n return;\n }\n // At rest. A stop or an outside write left the view as it stands; a finish lands it.\n const landed = committed;\n committed = offset = null;\n if (landed) write(landed);\n },\n });\n\n const isAnimating = () => animatorRef.current.isActive(key);\n const target = () => (isAnimating() ? targetRef.current : null);\n /** Cancels this runner's animation; whatever else moves the view goes on from where it was. */\n const cancel = (): void => {\n animatorRef.current.cancelKey(key);\n targetRef.current = null;\n };\n const stop = () => {\n cancel();\n if (!viewTarget.active) committed = offset = null;\n };\n\n const animate: ViewAnimationApi['animate'] = (to, opts = {}) => {\n const resolved = normalizeView(typeof to === 'function' ? to(target() ?? base()) : to);\n targetRef.current = resolved;\n animatorRef.current.tween<View>({\n target: viewTarget,\n to: resolved,\n ms: opts.ms ?? DEFAULT_MS,\n easing: opts.easing ?? easeOutCubic,\n interpolator: opts.interpolator ?? interpolateView,\n cancelKey: key,\n onDone: () => {\n targetRef.current = null;\n // Ahead of the frame's own write, so `onDone` reads the view it finished on.\n write(viewTarget.read());\n if (!viewTarget.active) committed = offset = null;\n opts.onDone?.();\n },\n });\n };\n\n return {\n animate,\n animateToBounds: (bounds, dims, opts = {}) => {\n const current = viewRef.current.get();\n const fitted = fitViewToBounds(bounds, dims, current, opts);\n if (fitted === current) return; // helper bailed (zero-area bounds/viewport)\n animate(fitted, opts);\n },\n stop,\n isAnimating,\n target,\n stopIfExternal: () => {\n if (writingRef.current) return;\n cancel();\n // The writer read the folded view, so what is still in flight stays on top of its write.\n if (viewTarget.active) {\n const shown = viewAxes.to(viewTarget.read());\n const under = viewAxes.to(base());\n offset = shown.map((n, i) => n - under[i]!);\n } else {\n offset = null;\n }\n committed = null;\n },\n viewTarget,\n };\n}\n"],"mappings":"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"}
|