@weasel-js/core 1.11.0 → 1.11.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 +427 -0
- package/README.md +1 -1
- package/dist/animation/index.js +3 -3
- package/dist/animation/rig/bindRig.js +1 -1
- package/dist/animation/rig/rigNode.js.map +1 -1
- package/dist/barrel/canvas.js +7 -3
- package/dist/barrel/interactions.js +5 -4
- package/dist/barrel/paths.js +3 -3
- package/dist/barrel/renderer.js +3 -2
- package/dist/barrel/routing.js +6 -11
- package/dist/barrel/scene.js +8 -5
- package/dist/barrel/viewport.js +4 -3
- package/dist/canvas/Canvas.js +3 -1
- package/dist/canvas/Canvas.js.map +1 -1
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js +17 -3
- package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
- package/dist/canvas/SceneCanvas/builtinToolSets.js +30 -0
- package/dist/canvas/SceneCanvas/builtinToolSets.js.map +1 -0
- package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -0
- package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
- package/dist/canvas/SceneCanvas/coordTrace.js +5 -16
- package/dist/canvas/SceneCanvas/coordTrace.js.map +1 -1
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js +5 -0
- package/dist/canvas/SceneCanvas/defaultNodeProperties.js.map +1 -1
- package/dist/canvas/SceneCanvas/features.js +45 -5
- package/dist/canvas/SceneCanvas/features.js.map +1 -1
- package/dist/canvas/SceneCanvas/props.js +1 -11
- package/dist/canvas/SceneCanvas/props.js.map +1 -1
- package/dist/canvas/SceneCanvas/toolRegistry.js +8 -4
- package/dist/canvas/SceneCanvas/toolRegistry.js.map +1 -1
- package/dist/canvas/SceneCanvas/useActionsPropResolver.js +1 -11
- package/dist/canvas/SceneCanvas/useActionsPropResolver.js.map +1 -1
- package/dist/canvas/SceneCanvas/useBuiltinShapeTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useBuiltinTools.js +2 -9
- package/dist/canvas/SceneCanvas/useBuiltinTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js +93 -0
- package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js.map +1 -0
- package/dist/canvas/SceneCanvas/useCanvasSelection.js +7 -2
- package/dist/canvas/SceneCanvas/useCanvasSelection.js.map +1 -1
- package/dist/canvas/SceneCanvas/useCanvasTools.js +8 -8
- package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useNodeKinds.js +1 -1
- package/dist/canvas/SceneCanvas/useNodeKinds.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +13 -2
- package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -3
- package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneChrome.js +6 -7
- package/dist/canvas/SceneCanvas/useSceneChrome.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js +5 -3
- package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js.map +1 -1
- package/dist/canvas/SceneCanvas/useScenePicking.js +2 -3
- package/dist/canvas/SceneCanvas/useScenePicking.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js +2 -1
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js +3 -4
- package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +16 -14
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SurfaceCanvas.js +5 -22
- package/dist/canvas/SurfaceCanvas.js.map +1 -1
- package/dist/canvas/canvasRenderer.js +9 -5
- package/dist/canvas/canvasRenderer.js.map +1 -1
- package/dist/canvas/contextAttributes.js +14 -0
- package/dist/canvas/contextAttributes.js.map +1 -0
- package/dist/canvas/deps/editAnchors.js +9 -113
- package/dist/canvas/deps/editAnchors.js.map +1 -1
- package/dist/canvas/deps/insert.js +4 -2
- package/dist/canvas/deps/insert.js.map +1 -1
- package/dist/canvas/deps/pathEditCommits.js +193 -0
- package/dist/canvas/deps/pathEditCommits.js.map +1 -0
- package/dist/canvas/deps/snap.js +12 -9
- package/dist/canvas/deps/snap.js.map +1 -1
- package/dist/canvas/deps/transformFollowers.js +20 -0
- package/dist/canvas/deps/transformFollowers.js.map +1 -0
- package/dist/canvas/derivedPath.js +20 -1
- package/dist/canvas/derivedPath.js.map +1 -1
- package/dist/canvas/drawnBounds.js +29 -3
- package/dist/canvas/drawnBounds.js.map +1 -1
- package/dist/canvas/nodeShapes/derivedPainter.js +9 -1
- package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
- package/dist/canvas/nodeShapes/pathPainter.js +3 -2
- package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
- package/dist/canvas/nodeShapes/shapePainter.js +1 -1
- package/dist/canvas/overlayRoleStrokes.js +12 -0
- package/dist/canvas/overlayRoleStrokes.js.map +1 -1
- package/dist/canvas/pathOverlay.js +27 -7
- package/dist/canvas/pathOverlay.js.map +1 -1
- package/dist/canvas/renderDebugSnapshot.js +1 -0
- package/dist/canvas/renderDebugSnapshot.js.map +1 -1
- package/dist/canvas/renderSceneToPixels.js +6 -5
- package/dist/canvas/renderSceneToPixels.js.map +1 -1
- package/dist/canvas/sceneAdapter.js.map +1 -1
- package/dist/canvas/useCanvasPaint.js +2 -1
- package/dist/canvas/useCanvasPaint.js.map +1 -1
- package/dist/canvas/useInFlightBounds.js +52 -0
- package/dist/canvas/useInFlightBounds.js.map +1 -0
- package/dist/canvas/useViewHelpers.js +7 -9
- package/dist/canvas/useViewHelpers.js.map +1 -1
- package/dist/clipboard.d.ts +3 -2
- package/dist/{clone-DZDrfIlU.d.ts → clone-C3e0OTuF.d.ts} +8 -21
- package/dist/clone.d.ts +4 -3
- package/dist/core/geometry/autoPoseDescriptor.js +1 -1
- package/dist/core/ops/setDependsOn.js +37 -0
- package/dist/core/ops/setDependsOn.js.map +1 -0
- package/dist/core/ops/writtenBy.js +53 -0
- package/dist/core/ops/writtenBy.js.map +1 -0
- package/dist/core/pages/fitViewToPage.js +12 -1
- package/dist/core/pages/fitViewToPage.js.map +1 -1
- package/dist/core/pages/pageEdits.js +164 -0
- package/dist/core/pages/pageEdits.js.map +1 -0
- package/dist/core/pages/pages.js +30 -1
- package/dist/core/pages/pages.js.map +1 -1
- package/dist/core/scene/NodeRouting.js +8 -3
- package/dist/core/scene/NodeRouting.js.map +1 -1
- package/dist/core/scene/arrivalWindow.js.map +1 -1
- package/dist/core/scene/carryDescendants.js +51 -0
- package/dist/core/scene/carryDescendants.js.map +1 -0
- package/dist/core/scene/docValues.js +57 -0
- package/dist/core/scene/docValues.js.map +1 -0
- package/dist/core/scene/editPass.js +111 -0
- package/dist/core/scene/editPass.js.map +1 -0
- package/dist/core/scene/effectivePose.js +7 -1
- package/dist/core/scene/effectivePose.js.map +1 -1
- package/dist/core/scene/fieldEdits.js +4 -1
- package/dist/core/scene/fieldEdits.js.map +1 -1
- package/dist/core/scene/historyFacade.js +1 -0
- package/dist/core/scene/historyFacade.js.map +1 -1
- package/dist/core/scene/layoutPass.js +28 -3
- package/dist/core/scene/layoutPass.js.map +1 -1
- package/dist/core/scene/recorder.js +28 -19
- package/dist/core/scene/recorder.js.map +1 -1
- package/dist/core/scene/scene.js +15 -9
- package/dist/core/scene/scene.js.map +1 -1
- package/dist/core/scene/sceneChanges.js +13 -0
- package/dist/core/scene/sceneChanges.js.map +1 -0
- package/dist/core/scene/sceneEdits.js +8 -1
- package/dist/core/scene/sceneEdits.js.map +1 -1
- package/dist/core/scene/sceneMutators.js +1 -0
- package/dist/core/scene/sceneMutators.js.map +1 -1
- package/dist/core/scene/sceneRecording.js +20 -3
- package/dist/core/scene/sceneRecording.js.map +1 -1
- package/dist/core/scene/sceneSelection.js +4 -1
- package/dist/core/scene/sceneSelection.js.map +1 -1
- package/dist/core/scene/sceneState.js +11 -5
- package/dist/core/scene/sceneState.js.map +1 -1
- package/dist/core/scene/serialization.js +5 -3
- package/dist/core/scene/serialization.js.map +1 -1
- package/dist/core/scene/types.js.map +1 -1
- package/dist/core/scene/unitScopes.js +19 -1
- package/dist/core/scene/unitScopes.js.map +1 -1
- package/dist/core/selection/usePublishSceneSelection.js +6 -12
- package/dist/core/selection/usePublishSceneSelection.js.map +1 -1
- package/dist/core/shapeKinds.js +25 -11
- package/dist/core/shapeKinds.js.map +1 -1
- package/dist/{cornerHandles-Camw_yeu.d.ts → cornerHandles-BHuoMs1o.d.ts} +1 -1
- package/dist/debug/createDebugOverlayLayer.js +16 -111
- package/dist/debug/createDebugOverlayLayer.js.map +1 -1
- package/dist/debug/createDebugSink.js +2 -1
- package/dist/debug/createDebugSink.js.map +1 -1
- package/dist/debug/defaultTheme.js +4 -2
- package/dist/debug/defaultTheme.js.map +1 -1
- package/dist/debug/features.js +17 -0
- package/dist/debug/features.js.map +1 -0
- package/dist/debug/overlayPanels.js +117 -0
- package/dist/debug/overlayPanels.js.map +1 -0
- package/dist/debug/parseDebugFlags.js +4 -13
- package/dist/debug/parseDebugFlags.js.map +1 -1
- package/dist/{depSchema-Cgfu_G9s.d.ts → depSchema-BpZNOv7q.d.ts} +114 -892
- package/dist/devtools/handlers.js +112 -0
- package/dist/devtools/handlers.js.map +1 -0
- package/dist/devtools/history.js +71 -0
- package/dist/devtools/history.js.map +1 -0
- package/dist/devtools/hook.js +74 -0
- package/dist/devtools/hook.js.map +1 -0
- package/dist/devtools/label.js +25 -0
- package/dist/devtools/label.js.map +1 -0
- package/dist/devtools/notices.js +58 -0
- package/dist/devtools/notices.js.map +1 -0
- package/dist/devtools/protocol.js +7 -0
- package/dist/devtools/protocol.js.map +1 -0
- package/dist/devtools/registry.js +90 -0
- package/dist/devtools/registry.js.map +1 -0
- package/dist/devtools/root.js +80 -0
- package/dist/devtools/root.js.map +1 -0
- package/dist/devtools/serialize.js +86 -0
- package/dist/devtools/serialize.js.map +1 -0
- package/dist/devtools/session.js +111 -0
- package/dist/devtools/session.js.map +1 -0
- package/dist/devtools/transport.js +67 -0
- package/dist/devtools/transport.js.map +1 -0
- package/dist/devtools/useDevtoolsRoot.js +81 -0
- package/dist/devtools/useDevtoolsRoot.js.map +1 -0
- package/dist/devtools-protocol.d.ts +435 -0
- package/dist/devtools-protocol.js +7 -0
- package/dist/devtools.d.ts +8 -0
- package/dist/devtools.js +12 -0
- package/dist/devtools.js.map +1 -0
- package/dist/features/chrome-caps/useHoverTracking.js +16 -10
- package/dist/features/chrome-caps/useHoverTracking.js.map +1 -1
- package/dist/features/ingestion/contentHandlers.js +25 -2
- package/dist/features/ingestion/contentHandlers.js.map +1 -1
- package/dist/features/measure/measureCommands.js +7 -5
- package/dist/features/measure/measureCommands.js.map +1 -1
- package/dist/features/paths/index.js +2 -2
- package/dist/features/paths/pathEditExtension.js.map +1 -1
- package/dist/features/paths/pathSnap/pathSnapSource.js +5 -5
- package/dist/features/paths/pathSnap/pathSnapSource.js.map +1 -1
- package/dist/features/paths/pathSnap/sceneIndex.js +10 -3
- package/dist/features/paths/pathSnap/sceneIndex.js.map +1 -1
- package/dist/features/selection/selectionPose.js +4 -1
- package/dist/features/selection/selectionPose.js.map +1 -1
- package/dist/features/snap/gather.js +9 -1
- package/dist/features/snap/gather.js.map +1 -1
- package/dist/features/snap/lazySession.js +17 -0
- package/dist/features/snap/lazySession.js.map +1 -0
- package/dist/features/snap/marks.js.map +1 -1
- package/dist/features/snap/probes2d.js +27 -19
- package/dist/features/snap/probes2d.js.map +1 -1
- package/dist/features/snap/readoutLayer.js.map +1 -1
- package/dist/features/snap/roles.js +2 -1
- package/dist/features/snap/roles.js.map +1 -1
- package/dist/features/snap/rotationSource.js +25 -8
- package/dist/features/snap/rotationSource.js.map +1 -1
- package/dist/features/snap/sceneSession2d.js +6 -4
- package/dist/features/snap/sceneSession2d.js.map +1 -1
- package/dist/features/snap/session2d.js +37 -23
- package/dist/features/snap/session2d.js.map +1 -1
- package/dist/features/snap/spanSnap.js +5 -2
- package/dist/features/snap/spanSnap.js.map +1 -1
- package/dist/icons.table.js +1 -1
- package/dist/icons.table.js.map +1 -1
- package/dist/icons.table.json +0 -10
- package/dist/index.d.ts +701 -696
- package/dist/index.js +35 -29
- package/dist/interactions/actions/align/action.js +6 -4
- package/dist/interactions/actions/align/action.js.map +1 -1
- package/dist/interactions/actions/align/align.js.map +1 -1
- package/dist/interactions/actions/booleans/action.js +7 -1
- package/dist/interactions/actions/booleans/action.js.map +1 -1
- package/dist/interactions/actions/clone/action.js +1 -1
- package/dist/interactions/actions/clone/behaviors.js +4 -4
- package/dist/interactions/actions/clone/behaviors.js.map +1 -1
- package/dist/interactions/actions/defaults/bendSegment.js +1 -1
- package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
- package/dist/interactions/actions/defaults/createPaintAction.js +1 -1
- package/dist/interactions/actions/defaults/delete.js +48 -39
- package/dist/interactions/actions/defaults/delete.js.map +1 -1
- package/dist/interactions/actions/defaults/duplicate.js +1 -1
- package/dist/interactions/actions/defaults/group.js +2 -2
- package/dist/interactions/actions/defaults/ingest.js +7 -5
- package/dist/interactions/actions/defaults/ingest.js.map +1 -1
- package/dist/interactions/actions/defaults/nudge.js +13 -38
- package/dist/interactions/actions/defaults/nudge.js.map +1 -1
- package/dist/interactions/actions/distribute/action.js +6 -4
- package/dist/interactions/actions/distribute/action.js.map +1 -1
- package/dist/interactions/actions/drawnBoxes.js +2 -1
- package/dist/interactions/actions/drawnBoxes.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/action.js +3 -6
- package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
- package/dist/interactions/actions/edit-anchors/preview.js +41 -8
- package/dist/interactions/actions/edit-anchors/preview.js.map +1 -1
- package/dist/interactions/actions/flip/action.js +6 -4
- package/dist/interactions/actions/flip/action.js.map +1 -1
- package/dist/interactions/actions/gestureLifecycle.js +5 -2
- package/dist/interactions/actions/gestureLifecycle.js.map +1 -1
- package/dist/interactions/actions/insert/action.js +1 -2
- package/dist/interactions/actions/insert/action.js.map +1 -1
- package/dist/interactions/actions/insert/commit.js +1 -1
- package/dist/interactions/actions/insert/parent.js +11 -0
- package/dist/interactions/actions/insert/parent.js.map +1 -0
- package/dist/interactions/actions/insert/placeAction.js +7 -2
- package/dist/interactions/actions/insert/placeAction.js.map +1 -1
- package/dist/interactions/actions/insert/session.js +5 -1
- package/dist/interactions/actions/insert/session.js.map +1 -1
- package/dist/interactions/actions/move/action.js +8 -4
- package/dist/interactions/actions/move/action.js.map +1 -1
- package/dist/interactions/actions/move/behaviors/layoutDrop.js +0 -1
- package/dist/interactions/actions/move/behaviors/layoutDrop.js.map +1 -1
- package/dist/interactions/actions/move/behaviors/snapToContainer.js +1 -1
- package/dist/interactions/actions/move/capture.js +1 -1
- package/dist/interactions/actions/move/commit/index.js +0 -2
- package/dist/interactions/actions/move/commit/index.js.map +1 -1
- package/dist/interactions/actions/move/commit/layoutDrop.js +2 -2
- package/dist/interactions/actions/move/commit/layoutRelease.js +2 -2
- package/dist/interactions/actions/move/commit/reparent.js +11 -5
- package/dist/interactions/actions/move/commit/reparent.js.map +1 -1
- package/dist/interactions/actions/move/frame.js +4 -10
- package/dist/interactions/actions/move/frame.js.map +1 -1
- package/dist/interactions/actions/move/landedDescendants.js +20 -17
- package/dist/interactions/actions/move/landedDescendants.js.map +1 -1
- package/dist/interactions/actions/move/layout/container.js +2 -1
- package/dist/interactions/actions/move/layout/container.js.map +1 -1
- package/dist/interactions/actions/move/layout/dragged.js +1 -1
- package/dist/interactions/actions/move/layout/dropPass.js +18 -1
- package/dist/interactions/actions/move/layout/dropPass.js.map +1 -1
- package/dist/interactions/actions/move/layout/exits.js +5 -5
- package/dist/interactions/actions/move/layout/exits.js.map +1 -1
- package/dist/interactions/actions/move/session.js +14 -3
- package/dist/interactions/actions/move/session.js.map +1 -1
- package/dist/interactions/actions/move/translate.js +8 -37
- package/dist/interactions/actions/move/translate.js.map +1 -1
- package/dist/interactions/actions/moveNodes.js +87 -0
- package/dist/interactions/actions/moveNodes.js.map +1 -0
- package/dist/interactions/actions/planeInput.js.map +1 -1
- package/dist/interactions/actions/resize/action.js +83 -72
- package/dist/interactions/actions/resize/action.js.map +1 -1
- package/dist/interactions/actions/resize/commit.js +2 -1
- package/dist/interactions/actions/resize/commit.js.map +1 -1
- package/dist/interactions/actions/resize/session.js +2 -0
- package/dist/interactions/actions/resize/session.js.map +1 -1
- package/dist/interactions/actions/resolvePreviews.js +3 -3
- package/dist/interactions/actions/resolvePreviews.js.map +1 -1
- package/dist/interactions/actions/rotate/action.js +7 -4
- package/dist/interactions/actions/rotate/action.js.map +1 -1
- package/dist/interactions/actions/rotate/commit.js +2 -1
- package/dist/interactions/actions/rotate/commit.js.map +1 -1
- package/dist/interactions/actions/rotate/session.js +2 -0
- package/dist/interactions/actions/rotate/session.js.map +1 -1
- package/dist/interactions/actions/selectionSubtree.js +13 -2
- package/dist/interactions/actions/selectionSubtree.js.map +1 -1
- package/dist/interactions/actions/testUtils.js.map +1 -1
- package/dist/interactions/actions/transform-again/action.js +5 -6
- package/dist/interactions/actions/transform-again/action.js.map +1 -1
- package/dist/interactions/actions/transformFinish.js +106 -0
- package/dist/interactions/actions/transformFinish.js.map +1 -0
- package/dist/kitSettingsSchema.js +0 -4
- package/dist/kitSettingsSchema.js.map +1 -1
- package/dist/move.d.ts +5 -4
- package/dist/{options-p9GZMXbY.d.ts → options-DBxEryYq.d.ts} +1 -1
- package/dist/resize.d.ts +4 -3
- package/dist/routing.d.ts +1 -1
- package/dist/routing.js +2 -2
- package/dist/test-hook/createTestHook.js +1 -1
- package/dist/test-hook/createTestHook.js.map +1 -1
- package/dist/test-hook/useTestHook.js +2 -1
- package/dist/test-hook/useTestHook.js.map +1 -1
- package/dist/test-seams.d.ts +9 -5
- package/dist/tools/builtin/pen/penHandle.js +3 -5
- package/dist/tools/builtin/pen/penHandle.js.map +1 -1
- package/dist/tools/index.js +26 -0
- package/dist/{types-CHVXGfYm.d.ts → types-7puAuVD2.d.ts} +3 -262
- package/dist/types-BN9hbdMI.d.ts +184 -0
- package/dist/types-CniVctmk.d.ts +949 -0
- package/dist/types-oFeQ5VjV.d.ts +264 -0
- package/dist/version.js +1 -1
- package/package.json +35 -25
- package/dist/canvas/SceneCanvas/useBuiltinRigTools.js +0 -65
- package/dist/canvas/SceneCanvas/useBuiltinRigTools.js.map +0 -1
- package/dist/tools/builtin/bone/boneActions.js +0 -156
- package/dist/tools/builtin/bone/boneActions.js.map +0 -1
- package/dist/tools/builtin/bone/boneOverlay.js +0 -71
- package/dist/tools/builtin/bone/boneOverlay.js.map +0 -1
- package/dist/tools/builtin/bone/placedRig.js +0 -28
- package/dist/tools/builtin/bone/placedRig.js.map +0 -1
- package/dist/tools/builtin/bone/rigEditState.js +0 -30
- package/dist/tools/builtin/bone/rigEditState.js.map +0 -1
- package/dist/tools/builtin/bone/useBoneTool.js +0 -125
- package/dist/tools/builtin/bone/useBoneTool.js.map +0 -1
- package/dist/tools/builtin/weight/useWeightTool.js +0 -101
- package/dist/tools/builtin/weight/useWeightTool.js.map +0 -1
- package/dist/tools/builtin/weight/weightActions.js +0 -140
- package/dist/tools/builtin/weight/weightActions.js.map +0 -1
- package/dist/tools/builtin/weight/weightOverlay.js +0 -78
- package/dist/tools/builtin/weight/weightOverlay.js.map +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
//#region src/core/scene/editPass.ts
|
|
2
|
+
var emptyCollecting = () => ({
|
|
3
|
+
added: /* @__PURE__ */ new Set(),
|
|
4
|
+
removed: /* @__PURE__ */ new Set(),
|
|
5
|
+
posed: /* @__PURE__ */ new Set(),
|
|
6
|
+
reparented: /* @__PURE__ */ new Set(),
|
|
7
|
+
dataChanged: /* @__PURE__ */ new Set(),
|
|
8
|
+
docChanged: /* @__PURE__ */ new Set()
|
|
9
|
+
});
|
|
10
|
+
var snapshot = (c) => ({
|
|
11
|
+
added: new Set(c.added),
|
|
12
|
+
removed: new Set(c.removed),
|
|
13
|
+
posed: new Set(c.posed),
|
|
14
|
+
reparented: new Set(c.reparented),
|
|
15
|
+
dataChanged: new Set(c.dataChanged),
|
|
16
|
+
docChanged: new Set(c.docChanged)
|
|
17
|
+
});
|
|
18
|
+
var NOOP = {
|
|
19
|
+
name: "editPass",
|
|
20
|
+
args: {},
|
|
21
|
+
elideNoop: true,
|
|
22
|
+
apply: () => "noop",
|
|
23
|
+
invert: () => NOOP
|
|
24
|
+
};
|
|
25
|
+
/** Note one kit op's ids into `into`. */
|
|
26
|
+
function noteInto(into, kind, payload) {
|
|
27
|
+
const p = payload;
|
|
28
|
+
switch (kind) {
|
|
29
|
+
case "kit:add":
|
|
30
|
+
into.added.add(p.id);
|
|
31
|
+
break;
|
|
32
|
+
case "kit:remove":
|
|
33
|
+
for (const n of p.nodes ?? []) into.removed.add(n.id);
|
|
34
|
+
break;
|
|
35
|
+
case "kit:setPose":
|
|
36
|
+
into.posed.add(p.id);
|
|
37
|
+
break;
|
|
38
|
+
case "kit:setData":
|
|
39
|
+
into.dataChanged.add(p.id);
|
|
40
|
+
break;
|
|
41
|
+
case "kit:setDoc":
|
|
42
|
+
into.docChanged.add(p.key);
|
|
43
|
+
break;
|
|
44
|
+
case "kit:move": if (p.fromParent !== p.toParent) into.reparented.add(p.id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function createEditPasses(env) {
|
|
48
|
+
/** Sorted by order; equal orders keep the order they were added in. */
|
|
49
|
+
const passes = [];
|
|
50
|
+
let collecting = null;
|
|
51
|
+
function run() {
|
|
52
|
+
const summary = collecting;
|
|
53
|
+
if (summary === null) return 0;
|
|
54
|
+
let applied = 0;
|
|
55
|
+
try {
|
|
56
|
+
for (const { pass } of [...passes]) {
|
|
57
|
+
const ops = pass(snapshot(summary));
|
|
58
|
+
if (ops === null) continue;
|
|
59
|
+
const adapter = env.adapter();
|
|
60
|
+
for (const op of ops) {
|
|
61
|
+
op.apply(adapter);
|
|
62
|
+
applied++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
collecting = null;
|
|
67
|
+
}
|
|
68
|
+
return applied;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
add(pass, opts) {
|
|
72
|
+
const entry = {
|
|
73
|
+
pass,
|
|
74
|
+
order: opts?.order ?? 0
|
|
75
|
+
};
|
|
76
|
+
const at = passes.findIndex((p) => p.order > entry.order);
|
|
77
|
+
passes.splice(at < 0 ? passes.length : at, 0, entry);
|
|
78
|
+
return () => {
|
|
79
|
+
const i = passes.indexOf(entry);
|
|
80
|
+
if (i >= 0) passes.splice(i, 1);
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
any: () => passes.length > 0,
|
|
84
|
+
open() {
|
|
85
|
+
if (collecting !== null || passes.length === 0) return false;
|
|
86
|
+
collecting = emptyCollecting();
|
|
87
|
+
return true;
|
|
88
|
+
},
|
|
89
|
+
note(kind, payload) {
|
|
90
|
+
if (collecting !== null && !env.untracked()) noteInto(collecting, kind, payload);
|
|
91
|
+
},
|
|
92
|
+
run,
|
|
93
|
+
abandon() {
|
|
94
|
+
collecting = null;
|
|
95
|
+
},
|
|
96
|
+
op() {
|
|
97
|
+
return {
|
|
98
|
+
name: "editPass",
|
|
99
|
+
args: {},
|
|
100
|
+
label: "Edit pass",
|
|
101
|
+
elideNoop: true,
|
|
102
|
+
apply: () => run() > 0 ? void 0 : "noop",
|
|
103
|
+
invert: () => NOOP
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
export { createEditPasses };
|
|
111
|
+
//# sourceMappingURL=editPass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editPass.js","names":[],"sources":["../../../src/core/scene/editPass.ts"],"sourcesContent":["/**\n * Scene edit passes: functions that hear what a live edit did, once its own\n * mutations and the arrival handler have run, and answer with ops that join\n * the same undo entry.\n *\n * Passes run by ascending order, and each hears the edit's own changes plus\n * every write the passes before it made. A pass still runs once per edit, so\n * none re-triggers itself or another. Undo, redo, and history replays never\n * run passes: they restore an edit whose passes already ran.\n */\nimport type { Op } from 'core/ops/types';\nimport type { NodeId } from './types';\n\n/** What one live edit did to the scene's nodes, by id. */\nexport interface SceneEditSummary {\n added: ReadonlySet<NodeId>;\n removed: ReadonlySet<NodeId>;\n posed: ReadonlySet<NodeId>;\n /** Nodes that changed parent. A reorder among siblings is not here. */\n reparented: ReadonlySet<NodeId>;\n dataChanged: ReadonlySet<NodeId>;\n /** The document-value keys (`scene.doc`) set. */\n docChanged: ReadonlySet<string>;\n}\n\n/** Where a pass runs among the others. */\nexport interface SceneEditPassOptions {\n /** Passes run by ascending order, ties in the order they were added.\n * Default 0. */\n order?: number;\n}\n\n/** A scene edit pass: the ops `summary`'s edit also makes, or null for none. */\nexport type SceneEditPass = (summary: SceneEditSummary) => readonly Op[] | null;\n\n/** The scene's edit-pass member. */\nexport interface SceneEditPasses {\n /** Add a pass that hears each live edit once its own mutations and the\n * arrival handler have run, and answers with ops that join the same undo\n * entry. A bare scene call runs as an edit of its own while any pass is\n * added. A pass hears what the passes ordered before it wrote, never what\n * it or a later one writes, and undo, redo, and history replays never run\n * it. Returns the pass's remover. */\n addEditPass(pass: SceneEditPass, opts?: SceneEditPassOptions): () => void;\n}\n\n/** What the passes need from the scene they serve. */\nexport interface EditPassEnv {\n /** The adapter a pass's ops apply through. */\n adapter(): unknown;\n /** Whether an edit made now is untracked, and so not summarized. */\n untracked(): boolean;\n}\n\nexport interface EditPasses {\n /** Add a pass after every one of its order or lower. Returns its remover. */\n add(pass: SceneEditPass, opts?: SceneEditPassOptions): () => void;\n /** Whether any pass is installed. */\n any(): boolean;\n /** Start summarizing an edit when a pass is installed and no enclosing edit\n * is summarizing already; true when this caller now owns the summary. */\n open(): boolean;\n /** Note what a live kit op is about to write. */\n note(kind: string, payload: unknown): void;\n /** Run every pass in order, each over the summary as it stands at its\n * turn, applying its ops into that summary; then close it. Answers how\n * many ops applied. */\n run(): number;\n /** Close the summary with nothing run. */\n abandon(): void;\n /** An op that runs the passes when it first applies, for the end of an\n * op-driven edit whose summary the caller owns. */\n op(): Op;\n}\n\ninterface Collecting {\n added: Set<NodeId>;\n removed: Set<NodeId>;\n posed: Set<NodeId>;\n reparented: Set<NodeId>;\n dataChanged: Set<NodeId>;\n docChanged: Set<string>;\n}\n\nconst emptyCollecting = (): Collecting => ({\n added: new Set(), removed: new Set(), posed: new Set(), reparented: new Set(), dataChanged: new Set(), docChanged: new Set(),\n});\n\nconst snapshot = (c: Collecting): SceneEditSummary => ({\n added: new Set(c.added),\n removed: new Set(c.removed),\n posed: new Set(c.posed),\n reparented: new Set(c.reparented),\n dataChanged: new Set(c.dataChanged),\n docChanged: new Set(c.docChanged),\n});\n\nconst NOOP: Op = { name: 'editPass', args: {}, elideNoop: true, apply: () => 'noop', invert: () => NOOP };\n\n/** Note one kit op's ids into `into`. */\nfunction noteInto(into: Collecting, kind: string, payload: unknown): void {\n const p = payload as { id?: NodeId; key?: string; nodes?: readonly { id: NodeId }[]; fromParent?: NodeId | null; toParent?: NodeId | null };\n switch (kind) {\n case 'kit:add': into.added.add(p.id!); break;\n case 'kit:remove': for (const n of p.nodes ?? []) into.removed.add(n.id); break;\n case 'kit:setPose': into.posed.add(p.id!); break;\n case 'kit:setData': into.dataChanged.add(p.id!); break;\n case 'kit:setDoc': into.docChanged.add(p.key!); break;\n case 'kit:move': if (p.fromParent !== p.toParent) into.reparented.add(p.id!); break;\n default: break;\n }\n}\n\nexport function createEditPasses(env: EditPassEnv): EditPasses {\n /** Sorted by order; equal orders keep the order they were added in. */\n const passes: { pass: SceneEditPass; order: number }[] = [];\n let collecting: Collecting | null = null;\n\n function run(): number {\n const summary = collecting;\n if (summary === null) return 0;\n let applied = 0;\n try {\n for (const { pass } of [...passes]) {\n const ops = pass(snapshot(summary));\n if (ops === null) continue;\n const adapter = env.adapter();\n for (const op of ops) {\n op.apply(adapter);\n applied++;\n }\n }\n } finally {\n collecting = null;\n }\n return applied;\n }\n\n return {\n add(pass, opts) {\n const entry = { pass, order: opts?.order ?? 0 };\n const at = passes.findIndex((p) => p.order > entry.order);\n passes.splice(at < 0 ? passes.length : at, 0, entry);\n return () => {\n const i = passes.indexOf(entry);\n if (i >= 0) passes.splice(i, 1);\n };\n },\n any: () => passes.length > 0,\n open() {\n if (collecting !== null || passes.length === 0) return false;\n collecting = emptyCollecting();\n return true;\n },\n note(kind, payload) {\n if (collecting !== null && !env.untracked()) noteInto(collecting, kind, payload);\n },\n run,\n abandon() {\n collecting = null;\n },\n op() {\n return {\n name: 'editPass',\n args: {},\n label: 'Edit pass',\n elideNoop: true,\n apply: () => (run() > 0 ? undefined : 'noop'),\n invert: () => NOOP,\n };\n },\n };\n}\n"],"mappings":";AAoFA,IAAM,yBAAqC;CACzC,uBAAO,IAAI,IAAI;CAAG,yBAAS,IAAI,IAAI;CAAG,uBAAO,IAAI,IAAI;CAAG,4BAAY,IAAI,IAAI;CAAG,6BAAa,IAAI,IAAI;CAAG,4BAAY,IAAI,IAAI;AAC7H;AAEA,IAAM,YAAY,OAAqC;CACrD,OAAO,IAAI,IAAI,EAAE,KAAK;CACtB,SAAS,IAAI,IAAI,EAAE,OAAO;CAC1B,OAAO,IAAI,IAAI,EAAE,KAAK;CACtB,YAAY,IAAI,IAAI,EAAE,UAAU;CAChC,aAAa,IAAI,IAAI,EAAE,WAAW;CAClC,YAAY,IAAI,IAAI,EAAE,UAAU;AAClC;AAEA,IAAM,OAAW;CAAE,MAAM;CAAY,MAAM,CAAC;CAAG,WAAW;CAAM,aAAa;CAAQ,cAAc;AAAK;;AAGxG,SAAS,SAAS,MAAkB,MAAc,SAAwB;CACxE,MAAM,IAAI;CACV,QAAQ,MAAR;EACE,KAAK;GAAW,KAAK,MAAM,IAAI,EAAE,EAAG;GAAG;EACvC,KAAK;GAAc,KAAK,MAAM,KAAK,EAAE,SAAS,CAAC,GAAG,KAAK,QAAQ,IAAI,EAAE,EAAE;GAAG;EAC1E,KAAK;GAAe,KAAK,MAAM,IAAI,EAAE,EAAG;GAAG;EAC3C,KAAK;GAAe,KAAK,YAAY,IAAI,EAAE,EAAG;GAAG;EACjD,KAAK;GAAc,KAAK,WAAW,IAAI,EAAE,GAAI;GAAG;EAChD,KAAK,YAAY,IAAI,EAAE,eAAe,EAAE,UAAU,KAAK,WAAW,IAAI,EAAE,EAAG;CAE7E;AACF;AAEA,SAAgB,iBAAiB,KAA8B;;CAE7D,MAAM,SAAmD,CAAC;CAC1D,IAAI,aAAgC;CAEpC,SAAS,MAAc;EACrB,MAAM,UAAU;EAChB,IAAI,YAAY,MAAM,OAAO;EAC7B,IAAI,UAAU;EACd,IAAI;GACF,KAAK,MAAM,EAAE,UAAU,CAAC,GAAG,MAAM,GAAG;IAClC,MAAM,MAAM,KAAK,SAAS,OAAO,CAAC;IAClC,IAAI,QAAQ,MAAM;IAClB,MAAM,UAAU,IAAI,QAAQ;IAC5B,KAAK,MAAM,MAAM,KAAK;KACpB,GAAG,MAAM,OAAO;KAChB;IACF;GACF;EACF,UAAU;GACR,aAAa;EACf;EACA,OAAO;CACT;CAEA,OAAO;EACL,IAAI,MAAM,MAAM;GACd,MAAM,QAAQ;IAAE;IAAM,OAAO,MAAM,SAAS;GAAE;GAC9C,MAAM,KAAK,OAAO,WAAW,MAAM,EAAE,QAAQ,MAAM,KAAK;GACxD,OAAO,OAAO,KAAK,IAAI,OAAO,SAAS,IAAI,GAAG,KAAK;GACnD,aAAa;IACX,MAAM,IAAI,OAAO,QAAQ,KAAK;IAC9B,IAAI,KAAK,GAAG,OAAO,OAAO,GAAG,CAAC;GAChC;EACF;EACA,WAAW,OAAO,SAAS;EAC3B,OAAO;GACL,IAAI,eAAe,QAAQ,OAAO,WAAW,GAAG,OAAO;GACvD,aAAa,gBAAgB;GAC7B,OAAO;EACT;EACA,KAAK,MAAM,SAAS;GAClB,IAAI,eAAe,QAAQ,CAAC,IAAI,UAAU,GAAG,SAAS,YAAY,MAAM,OAAO;EACjF;EACA;EACA,UAAU;GACR,aAAa;EACf;EACA,KAAK;GACH,OAAO;IACL,MAAM;IACN,MAAM,CAAC;IACP,OAAO;IACP,WAAW;IACX,aAAc,IAAI,IAAI,IAAI,SAAY;IACtC,cAAc;GAChB;EACF;CACF;AACF"}
|
|
@@ -86,6 +86,12 @@ function run(node, derive, deps, into) {
|
|
|
86
86
|
function derivedDepOf(source, id, reader) {
|
|
87
87
|
return reader === void 0 ? depIn(source, id, "effective") : depsAsRead(source, reader, "effective")(id);
|
|
88
88
|
}
|
|
89
|
+
/** `id` as {@link derivedDepOf} answers it, but at the pose and path the
|
|
90
|
+
* document holds: no override anywhere down its dependencies counts. For a
|
|
91
|
+
* reader that must not see an in-flight gesture, such as an edit pass. */
|
|
92
|
+
function documentDepOf(source, id) {
|
|
93
|
+
return depIn(source, id, "document");
|
|
94
|
+
}
|
|
89
95
|
/** `(id) => that dependency as `reader`'s derivation is handed it`: its pose
|
|
90
96
|
* in `reader`'s frame where the scene composes frames. A `'children'`
|
|
91
97
|
* dependent shares its children's frame, so it reads them as they are. */
|
|
@@ -193,5 +199,5 @@ function definesFrame(node) {
|
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
//#endregion
|
|
196
|
-
export { definesFrame, derivedDepOf, derivedPose, documentPose, effectivePose };
|
|
202
|
+
export { definesFrame, derivedDepOf, derivedPose, documentDepOf, documentPose, effectivePose };
|
|
197
203
|
//# sourceMappingURL=effectivePose.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effectivePose.js","names":[],"sources":["../../../src/core/scene/effectivePose.ts"],"sourcesContent":["/**\n * `effectivePose` — the pose to draw, pick and measure a node at.\n *\n * Three answers, in order: the ephemeral override a gesture published, the\n * pose the node derives from its dependencies, or the pose the document\n * stores. A reader of `node.pose` disagrees with the render about where a\n * dragged or derived node is.\n */\nimport { deriveRecorded, emptyDepRecord, sameDeps, type DepRecord } from './depMemo';\nimport type { PoseComposition } from './composeFrame';\nimport { carryDepPath, depFrameRebase, pathAsPainted } from './depFrame';\nimport { dependencyIdsOf } from './dependents';\nimport { resolveDerivedPath } from './derivedPath';\nimport { dropPoseKeyedMemoSlots, nodeMemo } from './nodeMemo';\nimport type { Path } from '@weasel-js/geom';\nimport type { DerivedDep, DerivePathFn, DerivePoseFn, Node, NodeId, PoseOverrides } from './types';\n\n/**\n * Which pose a resolution answers: `effective` honors overrides all the way\n * down the dependency chain, `document` ignores them all the way down. Each\n * reading memoizes in its own slots — the memo key is the node and its\n * authored pose, which is the same for both.\n */\ntype Reading = 'effective' | 'document';\n\nconst POSE_SLOT: Record<Reading, string> = {\n effective: 'kit:derivedPose',\n document: 'kit:derivedPose:document',\n};\nconst PATH_SLOT: Record<Reading, string | undefined> = {\n effective: undefined,\n document: 'kit:derivedPath:document',\n};\n\n/** The subset of a node this module reads. Structural so a `Node` satisfies\n * it without importing the full generic shape. */\nexport interface PosedNode<TPose> {\n id: NodeId;\n kind?: 'leaf' | 'container';\n pose: TPose;\n /** Read only by the memo, which keys on its reference alongside the pose. */\n data?: unknown;\n dependsOn?: readonly NodeId[] | 'children';\n /** Read under a composing strategy, to find the frame its pose is in. */\n parent?: NodeId | null;\n derivePose?: DerivePoseFn<TPose>;\n /** Read only when something asks a dependency for its `path`. */\n derivePath?: DerivePathFn<TPose>;\n}\n\n/** What resolving a pose needs: the overrides, and enough of the scene to\n * reach a node's dependencies. A `Scene` satisfies it. */\nexport interface PoseSource<TPose> {\n readonly overrides: Pick<PoseOverrides<TPose>, 'read'>;\n get(id: NodeId): PosedNode<TPose> | undefined;\n childrenOf(id: NodeId): readonly NodeId[];\n /** When it composes frames, a derivation reads each dependency in the\n * frame of the node deriving. */\n readonly composition?: PoseComposition<TPose>;\n}\n\n/** A cached derived pose, with what it was derived from. */\ninterface PoseMemo<TPose> extends DepRecord<TPose> {\n value: TPose | null;\n}\n\n/** Ids whose derivation is on the stack. A dependency graph is meant to be\n * acyclic and `dependsOn` is unvalidated, so a cycle resolves to the authored\n * pose at whichever node closes it rather than overflowing the stack. */\nconst resolving = new Set<NodeId>();\n\n/** Bumped each time the guard above fires. A derivation that closed a cycle\n * answered from a fallback rather than from its dependencies, so its value is\n * an artifact of which node was asked first and must not be cached. */\nlet cycleHits = 0;\n\n/**\n * The pose `node` computes from its dependencies, or `null` when it derives\n * nothing — it has no `derivePose`, nothing to derive from, or its\n * `derivePose` returned `null`.\n *\n * A dependency is read through {@link effectivePose}, so a group of groups\n * resolves bottom-up and a dragged dependency's override reaches the\n * derivation the same frame it is published.\n */\nexport function derivedPose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose | null {\n return derivedPoseIn(source, node, 'effective');\n}\n\nfunction derivedPoseIn<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n reading: Reading,\n): TPose | null {\n const derive = node.derivePose;\n if (derive === undefined) return null;\n const ids = dependencyIdsOf(node, source.childrenOf);\n if (ids.length === 0) return null;\n if (resolving.has(node.id)) {\n cycleHits++;\n return null;\n }\n resolving.add(node.id);\n const hitsBefore = cycleHits;\n try {\n const depOf = depsAsRead(source, node, reading);\n // Keyed on the authored pose, which only sees a change someone pushed, so\n // the record also carries what the derivation read and re-checks it by\n // value on a hit — see `depMemo.ts`.\n let computed = false;\n const record = nodeMemo<PoseMemo<TPose>>(node, POSE_SLOT[reading], node.pose, () => {\n computed = true;\n return run(node, derive, ids.map(depOf), { ...emptyDepRecord<TPose>(), value: null });\n });\n // Mutating the record in place is what updates the memo slot, which holds\n // this object.\n if (!computed && !sameDeps(record, ids, depOf)) {\n run(node, derive, ids.map(depOf), record);\n }\n if (cycleHits !== hitsBefore) dropPoseKeyedMemoSlots(node);\n return record.value;\n } finally {\n resolving.delete(node.id);\n }\n}\n\n/** Run `derivePose` against `deps` and record what it was run against. */\nfunction run<TPose>(\n node: PosedNode<TPose>,\n derive: NonNullable<PosedNode<TPose>['derivePose']>,\n deps: readonly (DerivedDep<TPose> | undefined)[],\n into: PoseMemo<TPose>,\n): PoseMemo<TPose> {\n into.value = deriveRecorded(into, deps, (handed) => derive(node as never, handed));\n return into;\n}\n\n/**\n * `id` as a derivation sees it: the node, the pose it is painted at, and the\n * path it derives.\n *\n * The pose is resolved eagerly — every derivation reads it — and the path\n * lazily, because most do not and resolving a route costs a router run. Both\n * resolve *through* this function, so a dependency that is itself derived is\n * resolved before it is read.\n */\nexport function derivedDepOf<TPose>(\n source: PoseSource<TPose>,\n id: NodeId,\n reader?: PosedNode<TPose>,\n): DerivedDep<TPose> | undefined {\n return reader === undefined ? depIn(source, id, 'effective') : depsAsRead(source, reader, 'effective')(id);\n}\n\n/** `(id) => that dependency as `reader`'s derivation is handed it`: its pose\n * in `reader`'s frame where the scene composes frames. A `'children'`\n * dependent shares its children's frame, so it reads them as they are. */\nfunction depsAsRead<TPose>(\n source: PoseSource<TPose>,\n reader: PosedNode<TPose>,\n reading: Reading,\n): (id: NodeId) => DerivedDep<TPose> | undefined {\n const plain = (id: NodeId) => depIn(source, id, reading);\n if (reader.dependsOn === 'children') return plain;\n const rebase = depFrameRebase(\n source.composition,\n reader,\n (id) => source.get(id),\n (id) => poseIn(source, source.get(id)!, reading),\n (id) => {\n const n = source.get(id);\n return n === undefined || definesFrame(n);\n },\n );\n if (rebase === null) return plain;\n return (id) => {\n const dep = plain(id);\n if (dep === undefined) return undefined;\n const pose = rebase(id, dep.pose);\n return pose === dep.pose ? dep : new RebasedDep(dep, pose);\n };\n}\n\n/** A dependency read from another frame: its pose rebased, and its path\n * carried by the same frame change when asked for. */\nclass RebasedDep<TPose> implements DerivedDep<TPose> {\n readonly node: Node<unknown, string, TPose>;\n\n constructor(private readonly dep: DerivedDep<TPose>, readonly pose: TPose) {\n this.node = dep.node;\n }\n\n get path(): Path | null {\n const path = this.dep.path;\n return path === null ? null : carryDepPath(path, this.dep.pose, this.pose);\n }\n}\n\nfunction depIn<TPose>(\n source: PoseSource<TPose>,\n id: NodeId,\n reading: Reading,\n): DerivedDep<TPose> | undefined {\n const node = source.get(id);\n if (node === undefined) return undefined;\n return new SceneDep(source, node, reading);\n}\n\n/**\n * A class rather than an object literal with a `get path()`: one of these is\n * built per dependency per derived node per frame, and an own accessor costs\n * an order of magnitude more to define than a prototype one does to inherit.\n */\nclass SceneDep<TPose> implements DerivedDep<TPose> {\n readonly node: Node<unknown, string, TPose>;\n readonly pose: TPose;\n private readonly shown: Path | undefined;\n\n constructor(\n private readonly source: PoseSource<TPose>,\n private readonly posed: PosedNode<TPose>,\n private readonly reading: Reading,\n ) {\n this.node = posed as unknown as Node<unknown, string, TPose>;\n const override = reading === 'effective' ? source.overrides.read(posed.id) : undefined;\n this.shown = override?.path;\n this.pose = override?.pose ?? derivedPoseIn(source, posed, reading) ?? posed.pose;\n }\n\n get path(): Path | null {\n if (this.shown !== undefined) return pathAsPainted(this.shown, this.pose, this.pose);\n const { source, reading } = this;\n return resolveDerivedPath(\n this.posed,\n depsAsRead(source, this.posed, reading),\n (depId) => source.childrenOf(depId),\n PATH_SLOT[reading],\n );\n }\n}\n\nfunction poseIn<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n reading: Reading,\n): TPose {\n if (reading === 'effective') {\n const override = source.overrides.read(node.id)?.pose;\n if (override !== undefined) return override;\n }\n return derivedPoseIn(source, node, reading) ?? node.pose;\n}\n\n/**\n * The pose the *document* says `node` is at: derived when it derives, else\n * authored. `effectivePose` minus the override step, at every link of the\n * dependency chain — a derived node's document pose holds still while\n * something it derives from is being dragged.\n *\n * For a reader that must not see an in-flight gesture, such as an action\n * capturing the `from` of a transform op.\n */\nexport function documentPose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose {\n return poseIn(source, node, 'document');\n}\n\n/**\n * The pose to draw, pick and measure `node` at. Every pose a reader acts on\n * comes from here.\n */\nexport function effectivePose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose {\n return poseIn(source, node, 'effective');\n}\n\n/**\n * Whether `node`'s pose is a frame its children are expressed in, under a\n * composing strategy.\n *\n * A container that derives its pose from its children cannot be one: moving\n * the frame to cover the children would move the children, and a `RectPose`\n * frame's origin is its corner, so no envelope leaves them in place. Such a\n * container is an envelope instead — its children are stored in its own\n * frame — and the composition fold passes over it.\n */\nexport function definesFrame(node: { dependsOn?: unknown; derivePose?: unknown }): boolean {\n return !(node.dependsOn === 'children' && node.derivePose !== undefined);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"effectivePose.js","names":[],"sources":["../../../src/core/scene/effectivePose.ts"],"sourcesContent":["/**\n * `effectivePose` — the pose to draw, pick and measure a node at.\n *\n * Three answers, in order: the ephemeral override a gesture published, the\n * pose the node derives from its dependencies, or the pose the document\n * stores. A reader of `node.pose` disagrees with the render about where a\n * dragged or derived node is.\n */\nimport { deriveRecorded, emptyDepRecord, sameDeps, type DepRecord } from './depMemo';\nimport type { PoseComposition } from './composeFrame';\nimport { carryDepPath, depFrameRebase, pathAsPainted } from './depFrame';\nimport { dependencyIdsOf } from './dependents';\nimport { resolveDerivedPath } from './derivedPath';\nimport { dropPoseKeyedMemoSlots, nodeMemo } from './nodeMemo';\nimport type { Path } from '@weasel-js/geom';\nimport type { DerivedDep, DerivePathFn, DerivePoseFn, Node, NodeId, PoseOverrides } from './types';\n\n/**\n * Which pose a resolution answers: `effective` honors overrides all the way\n * down the dependency chain, `document` ignores them all the way down. Each\n * reading memoizes in its own slots — the memo key is the node and its\n * authored pose, which is the same for both.\n */\ntype Reading = 'effective' | 'document';\n\nconst POSE_SLOT: Record<Reading, string> = {\n effective: 'kit:derivedPose',\n document: 'kit:derivedPose:document',\n};\nconst PATH_SLOT: Record<Reading, string | undefined> = {\n effective: undefined,\n document: 'kit:derivedPath:document',\n};\n\n/** The subset of a node this module reads. Structural so a `Node` satisfies\n * it without importing the full generic shape. */\nexport interface PosedNode<TPose> {\n id: NodeId;\n kind?: 'leaf' | 'container';\n pose: TPose;\n /** Read only by the memo, which keys on its reference alongside the pose. */\n data?: unknown;\n dependsOn?: readonly NodeId[] | 'children';\n /** Read under a composing strategy, to find the frame its pose is in. */\n parent?: NodeId | null;\n derivePose?: DerivePoseFn<TPose>;\n /** Read only when something asks a dependency for its `path`. */\n derivePath?: DerivePathFn<TPose>;\n}\n\n/** What resolving a pose needs: the overrides, and enough of the scene to\n * reach a node's dependencies. A `Scene` satisfies it. */\nexport interface PoseSource<TPose> {\n readonly overrides: Pick<PoseOverrides<TPose>, 'read'>;\n get(id: NodeId): PosedNode<TPose> | undefined;\n childrenOf(id: NodeId): readonly NodeId[];\n /** When it composes frames, a derivation reads each dependency in the\n * frame of the node deriving. */\n readonly composition?: PoseComposition<TPose>;\n}\n\n/** A cached derived pose, with what it was derived from. */\ninterface PoseMemo<TPose> extends DepRecord<TPose> {\n value: TPose | null;\n}\n\n/** Ids whose derivation is on the stack. A dependency graph is meant to be\n * acyclic and `dependsOn` is unvalidated, so a cycle resolves to the authored\n * pose at whichever node closes it rather than overflowing the stack. */\nconst resolving = new Set<NodeId>();\n\n/** Bumped each time the guard above fires. A derivation that closed a cycle\n * answered from a fallback rather than from its dependencies, so its value is\n * an artifact of which node was asked first and must not be cached. */\nlet cycleHits = 0;\n\n/**\n * The pose `node` computes from its dependencies, or `null` when it derives\n * nothing — it has no `derivePose`, nothing to derive from, or its\n * `derivePose` returned `null`.\n *\n * A dependency is read through {@link effectivePose}, so a group of groups\n * resolves bottom-up and a dragged dependency's override reaches the\n * derivation the same frame it is published.\n */\nexport function derivedPose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose | null {\n return derivedPoseIn(source, node, 'effective');\n}\n\nfunction derivedPoseIn<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n reading: Reading,\n): TPose | null {\n const derive = node.derivePose;\n if (derive === undefined) return null;\n const ids = dependencyIdsOf(node, source.childrenOf);\n if (ids.length === 0) return null;\n if (resolving.has(node.id)) {\n cycleHits++;\n return null;\n }\n resolving.add(node.id);\n const hitsBefore = cycleHits;\n try {\n const depOf = depsAsRead(source, node, reading);\n // Keyed on the authored pose, which only sees a change someone pushed, so\n // the record also carries what the derivation read and re-checks it by\n // value on a hit — see `depMemo.ts`.\n let computed = false;\n const record = nodeMemo<PoseMemo<TPose>>(node, POSE_SLOT[reading], node.pose, () => {\n computed = true;\n return run(node, derive, ids.map(depOf), { ...emptyDepRecord<TPose>(), value: null });\n });\n // Mutating the record in place is what updates the memo slot, which holds\n // this object.\n if (!computed && !sameDeps(record, ids, depOf)) {\n run(node, derive, ids.map(depOf), record);\n }\n if (cycleHits !== hitsBefore) dropPoseKeyedMemoSlots(node);\n return record.value;\n } finally {\n resolving.delete(node.id);\n }\n}\n\n/** Run `derivePose` against `deps` and record what it was run against. */\nfunction run<TPose>(\n node: PosedNode<TPose>,\n derive: NonNullable<PosedNode<TPose>['derivePose']>,\n deps: readonly (DerivedDep<TPose> | undefined)[],\n into: PoseMemo<TPose>,\n): PoseMemo<TPose> {\n into.value = deriveRecorded(into, deps, (handed) => derive(node as never, handed));\n return into;\n}\n\n/**\n * `id` as a derivation sees it: the node, the pose it is painted at, and the\n * path it derives.\n *\n * The pose is resolved eagerly — every derivation reads it — and the path\n * lazily, because most do not and resolving a route costs a router run. Both\n * resolve *through* this function, so a dependency that is itself derived is\n * resolved before it is read.\n */\nexport function derivedDepOf<TPose>(\n source: PoseSource<TPose>,\n id: NodeId,\n reader?: PosedNode<TPose>,\n): DerivedDep<TPose> | undefined {\n return reader === undefined ? depIn(source, id, 'effective') : depsAsRead(source, reader, 'effective')(id);\n}\n\n/** `id` as {@link derivedDepOf} answers it, but at the pose and path the\n * document holds: no override anywhere down its dependencies counts. For a\n * reader that must not see an in-flight gesture, such as an edit pass. */\nexport function documentDepOf<TPose>(source: PoseSource<TPose>, id: NodeId): DerivedDep<TPose> | undefined {\n return depIn(source, id, 'document');\n}\n\n/** `(id) => that dependency as `reader`'s derivation is handed it`: its pose\n * in `reader`'s frame where the scene composes frames. A `'children'`\n * dependent shares its children's frame, so it reads them as they are. */\nfunction depsAsRead<TPose>(\n source: PoseSource<TPose>,\n reader: PosedNode<TPose>,\n reading: Reading,\n): (id: NodeId) => DerivedDep<TPose> | undefined {\n const plain = (id: NodeId) => depIn(source, id, reading);\n if (reader.dependsOn === 'children') return plain;\n const rebase = depFrameRebase(\n source.composition,\n reader,\n (id) => source.get(id),\n (id) => poseIn(source, source.get(id)!, reading),\n (id) => {\n const n = source.get(id);\n return n === undefined || definesFrame(n);\n },\n );\n if (rebase === null) return plain;\n return (id) => {\n const dep = plain(id);\n if (dep === undefined) return undefined;\n const pose = rebase(id, dep.pose);\n return pose === dep.pose ? dep : new RebasedDep(dep, pose);\n };\n}\n\n/** A dependency read from another frame: its pose rebased, and its path\n * carried by the same frame change when asked for. */\nclass RebasedDep<TPose> implements DerivedDep<TPose> {\n readonly node: Node<unknown, string, TPose>;\n\n constructor(private readonly dep: DerivedDep<TPose>, readonly pose: TPose) {\n this.node = dep.node;\n }\n\n get path(): Path | null {\n const path = this.dep.path;\n return path === null ? null : carryDepPath(path, this.dep.pose, this.pose);\n }\n}\n\nfunction depIn<TPose>(\n source: PoseSource<TPose>,\n id: NodeId,\n reading: Reading,\n): DerivedDep<TPose> | undefined {\n const node = source.get(id);\n if (node === undefined) return undefined;\n return new SceneDep(source, node, reading);\n}\n\n/**\n * A class rather than an object literal with a `get path()`: one of these is\n * built per dependency per derived node per frame, and an own accessor costs\n * an order of magnitude more to define than a prototype one does to inherit.\n */\nclass SceneDep<TPose> implements DerivedDep<TPose> {\n readonly node: Node<unknown, string, TPose>;\n readonly pose: TPose;\n private readonly shown: Path | undefined;\n\n constructor(\n private readonly source: PoseSource<TPose>,\n private readonly posed: PosedNode<TPose>,\n private readonly reading: Reading,\n ) {\n this.node = posed as unknown as Node<unknown, string, TPose>;\n const override = reading === 'effective' ? source.overrides.read(posed.id) : undefined;\n this.shown = override?.path;\n this.pose = override?.pose ?? derivedPoseIn(source, posed, reading) ?? posed.pose;\n }\n\n get path(): Path | null {\n if (this.shown !== undefined) return pathAsPainted(this.shown, this.pose, this.pose);\n const { source, reading } = this;\n return resolveDerivedPath(\n this.posed,\n depsAsRead(source, this.posed, reading),\n (depId) => source.childrenOf(depId),\n PATH_SLOT[reading],\n );\n }\n}\n\nfunction poseIn<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n reading: Reading,\n): TPose {\n if (reading === 'effective') {\n const override = source.overrides.read(node.id)?.pose;\n if (override !== undefined) return override;\n }\n return derivedPoseIn(source, node, reading) ?? node.pose;\n}\n\n/**\n * The pose the *document* says `node` is at: derived when it derives, else\n * authored. `effectivePose` minus the override step, at every link of the\n * dependency chain — a derived node's document pose holds still while\n * something it derives from is being dragged.\n *\n * For a reader that must not see an in-flight gesture, such as an action\n * capturing the `from` of a transform op.\n */\nexport function documentPose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose {\n return poseIn(source, node, 'document');\n}\n\n/**\n * The pose to draw, pick and measure `node` at. Every pose a reader acts on\n * comes from here.\n */\nexport function effectivePose<TPose>(\n source: PoseSource<TPose>,\n node: PosedNode<TPose>,\n): TPose {\n return poseIn(source, node, 'effective');\n}\n\n/**\n * Whether `node`'s pose is a frame its children are expressed in, under a\n * composing strategy.\n *\n * A container that derives its pose from its children cannot be one: moving\n * the frame to cover the children would move the children, and a `RectPose`\n * frame's origin is its corner, so no envelope leaves them in place. Such a\n * container is an envelope instead — its children are stored in its own\n * frame — and the composition fold passes over it.\n */\nexport function definesFrame(node: { dependsOn?: unknown; derivePose?: unknown }): boolean {\n return !(node.dependsOn === 'children' && node.derivePose !== undefined);\n}\n"],"mappings":"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"}
|
|
@@ -23,8 +23,11 @@ function createFieldEdits(env) {
|
|
|
23
23
|
const node = requireNode(id);
|
|
24
24
|
refuse(id, "set the pose of");
|
|
25
25
|
const resizes = node.kind === "container" && resized(layoutFrame, id, node.pose, pose);
|
|
26
|
-
|
|
26
|
+
const parent = node.parent === null ? void 0 : requireNode(node.parent);
|
|
27
|
+
const arranger = parent?.kind === "container" && parent.layout !== void 0 ? parent.id : null;
|
|
28
|
+
edit("setPose", resizes || arranger !== null, () => {
|
|
27
29
|
if (resizes) noteChange(id);
|
|
30
|
+
noteChange(arranger);
|
|
28
31
|
record("kit:setPose", {
|
|
29
32
|
id,
|
|
30
33
|
from: node.pose,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fieldEdits.js","names":[],"sources":["../../../src/core/scene/fieldEdits.ts"],"sourcesContent":["/**\n * The scene's edits to one node's fields: its data, pose, layer, dependency\n * list, layout, and unit scope. Each checks its arguments, then records.\n */\nimport type { LayoutStrategy } from '../../layout/types';\nimport type { ArrivalWindow } from './arrivalWindow';\nimport { sameDependsOn } from './dependents';\nimport type { SetLayoutPayload } from './kitOps';\nimport type { createLayerStack } from './layerStack';\nimport { resized, type ResolvedLayoutFrame } from './layoutPass';\nimport type { LockGuard } from './lockGuard';\nimport type { NodeFnField } from './nodeFnFields';\nimport type { createNodeStore } from './nodeStore';\nimport type { Recorder } from './recorder';\nimport type { NodeId, Scene } from './types';\n\nexport interface FieldEditsEnv<TData, TLayer extends string, TPose> {\n store: Pick<ReturnType<typeof createNodeStore<TData, TLayer, TPose>>, 'requireNode' | 'descendants'>;\n locks: Pick<LockGuard<TLayer>, 'refuse' | 'refuseLayer'>;\n layers: Pick<ReturnType<typeof createLayerStack<TLayer>>, 'requireLayerIndex' | 'assertSubtreeLayer'>;\n recorder: Pick<Recorder, 'record' | 'edit' | 'batch'>;\n arrivals: Pick<ArrivalWindow<TPose>, 'noteChange' | 'layoutDeclared'>;\n layoutFrame: ResolvedLayoutFrame<TPose>;\n /** The registry's layouts, by key and back. */\n layoutField: NodeFnField;\n /** Where a live `kit:setLayout` payload's strategies are kept. */\n layoutRefs: WeakMap<object, { from: LayoutStrategy<TPose> | null; to: LayoutStrategy<TPose> | null }>;\n}\n\nexport type FieldEdits<TData, TLayer extends string, TPose> = Pick<\n Scene<TData, TLayer, TPose>,\n 'update' | 'setPose' | 'setLayer' | 'setDependsOn' | 'setLayout' | 'setUnits'\n>;\n\nexport function createFieldEdits<TData, TLayer extends string, TPose>(\n env: FieldEditsEnv<TData, TLayer, TPose>,\n): FieldEdits<TData, TLayer, TPose> {\n const { layoutFrame, layoutField, layoutRefs } = env;\n const { requireNode, descendants } = env.store;\n const { refuse, refuseLayer } = env.locks;\n const { requireLayerIndex, assertSubtreeLayer } = env.layers;\n const { record, edit, batch } = env.recorder;\n const { noteChange, layoutDeclared } = env.arrivals;\n\n return {\n update(id, patch) {\n const node = requireNode(id);\n refuse(id, 'update');\n record('kit:setData', { id, from: node.data, to: patch.data }, 'update');\n },\n\n setPose(id, pose) {\n const node = requireNode(id);\n refuse(id, 'set the pose of');\n const resizes = node.kind === 'container' && resized(layoutFrame, id, node.pose, pose);\n edit('setPose', resizes, () => {\n if (resizes) noteChange(id);\n record('kit:setPose', { id, from: node.pose, to: pose }, 'setPose');\n });\n },\n\n setLayer(id, layer) {\n requireLayerIndex(layer);\n const node = requireNode(id);\n refuse(id, 'relayer');\n if (node.layer !== layer) refuseLayer(layer, 'move nodes onto');\n if (node.parent !== null) {\n const parentNode = requireNode(node.parent);\n assertSubtreeLayer(id, layer, node.parent, parentNode.layer, 'relayer');\n }\n if (node.layer === layer) return;\n const subtree: NodeId[] = [id];\n descendants(id, subtree);\n for (const sid of subtree) refuse(sid, 'relayer');\n if (subtree.length === 1) {\n record('kit:setLayer', { id, from: node.layer, to: layer }, 'setLayer');\n } else {\n batch('setLayer', () => {\n for (const sid of subtree) {\n const cur = requireNode(sid);\n if (cur.layer === layer) continue;\n record('kit:setLayer', { id: sid, from: cur.layer, to: layer }, 'setLayer');\n }\n });\n }\n },\n\n setDependsOn(id, dependsOn) {\n const node = requireNode(id);\n refuse(id, 'retarget');\n if (sameDependsOn(node.dependsOn, dependsOn)) return;\n record('kit:setDependsOn', { id, from: node.dependsOn, to: dependsOn }, 'setDependsOn');\n },\n\n setLayout(id, layout) {\n const node = requireNode(id);\n if (node.kind !== 'container') throw new Error(`Scene: \"${id}\" is not a container`);\n refuse(id, 'set the layout of');\n let to: LayoutStrategy<TPose> | null;\n if (typeof layout === 'string') {\n to = (layoutField.fnOf(layout) as LayoutStrategy<TPose> | undefined) ?? null;\n if (to === null) throw new Error(`Scene: no layout \"${layout}\" in this scene's registry`);\n } else {\n to = layout;\n }\n const from = node.layout ?? null;\n if (from === to) return;\n if (to !== null) layoutDeclared();\n const fromKey = from === null ? undefined : layoutField.keyOf(from);\n const toKey = to === null ? undefined : layoutField.keyOf(to);\n const payload: SetLayoutPayload = {\n id,\n ...(fromKey !== undefined ? { fromKey } : {}),\n ...(toKey !== undefined ? { toKey } : {}),\n };\n layoutRefs.set(payload, { from, to });\n edit('setLayout', true, () => {\n noteChange(id);\n record('kit:setLayout', payload, 'setLayout');\n });\n },\n\n setUnits(id, units) {\n const node = requireNode(id);\n if (node.kind !== 'container') throw new Error(`Scene: \"${id}\" is not a container`);\n refuse(id, 'set the units of');\n record('kit:setUnits', { id, from: node.units, to: units ?? undefined }, 'setUnits');\n },\n };\n}\n"],"mappings":";;;;AAkCA,SAAgB,iBACd,KACkC;CAClC,MAAM,EAAE,aAAa,aAAa,eAAe;CACjD,MAAM,EAAE,aAAa,gBAAgB,IAAI;CACzC,MAAM,EAAE,QAAQ,gBAAgB,IAAI;CACpC,MAAM,EAAE,mBAAmB,uBAAuB,IAAI;CACtD,MAAM,EAAE,QAAQ,MAAM,UAAU,IAAI;CACpC,MAAM,EAAE,YAAY,mBAAmB,IAAI;CAE3C,OAAO;EACL,OAAO,IAAI,OAAO;GAChB,MAAM,OAAO,YAAY,EAAE;GAC3B,OAAO,IAAI,QAAQ;GACnB,OAAO,eAAe;IAAE;IAAI,MAAM,KAAK;IAAM,IAAI,MAAM;GAAK,GAAG,QAAQ;EACzE;EAEA,QAAQ,IAAI,MAAM;GAChB,MAAM,OAAO,YAAY,EAAE;GAC3B,OAAO,IAAI,iBAAiB;GAC5B,MAAM,UAAU,KAAK,SAAS,eAAe,QAAQ,aAAa,IAAI,KAAK,MAAM,IAAI;
|
|
1
|
+
{"version":3,"file":"fieldEdits.js","names":[],"sources":["../../../src/core/scene/fieldEdits.ts"],"sourcesContent":["/**\n * The scene's edits to one node's fields: its data, pose, layer, dependency\n * list, layout, and unit scope. Each checks its arguments, then records.\n */\nimport type { LayoutStrategy } from '../../layout/types';\nimport type { ArrivalWindow } from './arrivalWindow';\nimport { sameDependsOn } from './dependents';\nimport type { SetLayoutPayload } from './kitOps';\nimport type { createLayerStack } from './layerStack';\nimport { resized, type ResolvedLayoutFrame } from './layoutPass';\nimport type { LockGuard } from './lockGuard';\nimport type { NodeFnField } from './nodeFnFields';\nimport type { createNodeStore } from './nodeStore';\nimport type { Recorder } from './recorder';\nimport type { NodeId, Scene } from './types';\n\nexport interface FieldEditsEnv<TData, TLayer extends string, TPose> {\n store: Pick<ReturnType<typeof createNodeStore<TData, TLayer, TPose>>, 'requireNode' | 'descendants'>;\n locks: Pick<LockGuard<TLayer>, 'refuse' | 'refuseLayer'>;\n layers: Pick<ReturnType<typeof createLayerStack<TLayer>>, 'requireLayerIndex' | 'assertSubtreeLayer'>;\n recorder: Pick<Recorder, 'record' | 'edit' | 'batch'>;\n arrivals: Pick<ArrivalWindow<TPose>, 'noteChange' | 'layoutDeclared'>;\n layoutFrame: ResolvedLayoutFrame<TPose>;\n /** The registry's layouts, by key and back. */\n layoutField: NodeFnField;\n /** Where a live `kit:setLayout` payload's strategies are kept. */\n layoutRefs: WeakMap<object, { from: LayoutStrategy<TPose> | null; to: LayoutStrategy<TPose> | null }>;\n}\n\nexport type FieldEdits<TData, TLayer extends string, TPose> = Pick<\n Scene<TData, TLayer, TPose>,\n 'update' | 'setPose' | 'setLayer' | 'setDependsOn' | 'setLayout' | 'setUnits'\n>;\n\nexport function createFieldEdits<TData, TLayer extends string, TPose>(\n env: FieldEditsEnv<TData, TLayer, TPose>,\n): FieldEdits<TData, TLayer, TPose> {\n const { layoutFrame, layoutField, layoutRefs } = env;\n const { requireNode, descendants } = env.store;\n const { refuse, refuseLayer } = env.locks;\n const { requireLayerIndex, assertSubtreeLayer } = env.layers;\n const { record, edit, batch } = env.recorder;\n const { noteChange, layoutDeclared } = env.arrivals;\n\n return {\n update(id, patch) {\n const node = requireNode(id);\n refuse(id, 'update');\n record('kit:setData', { id, from: node.data, to: patch.data }, 'update');\n },\n\n setPose(id, pose) {\n const node = requireNode(id);\n refuse(id, 'set the pose of');\n const resizes = node.kind === 'container' && resized(layoutFrame, id, node.pose, pose);\n // A container with a layout owns where its children go, however one is moved.\n const parent = node.parent === null ? undefined : requireNode(node.parent);\n const arranger = parent?.kind === 'container' && parent.layout !== undefined ? parent.id : null;\n edit('setPose', resizes || arranger !== null, () => {\n if (resizes) noteChange(id);\n noteChange(arranger);\n record('kit:setPose', { id, from: node.pose, to: pose }, 'setPose');\n });\n },\n\n setLayer(id, layer) {\n requireLayerIndex(layer);\n const node = requireNode(id);\n refuse(id, 'relayer');\n if (node.layer !== layer) refuseLayer(layer, 'move nodes onto');\n if (node.parent !== null) {\n const parentNode = requireNode(node.parent);\n assertSubtreeLayer(id, layer, node.parent, parentNode.layer, 'relayer');\n }\n if (node.layer === layer) return;\n const subtree: NodeId[] = [id];\n descendants(id, subtree);\n for (const sid of subtree) refuse(sid, 'relayer');\n if (subtree.length === 1) {\n record('kit:setLayer', { id, from: node.layer, to: layer }, 'setLayer');\n } else {\n batch('setLayer', () => {\n for (const sid of subtree) {\n const cur = requireNode(sid);\n if (cur.layer === layer) continue;\n record('kit:setLayer', { id: sid, from: cur.layer, to: layer }, 'setLayer');\n }\n });\n }\n },\n\n setDependsOn(id, dependsOn) {\n const node = requireNode(id);\n refuse(id, 'retarget');\n if (sameDependsOn(node.dependsOn, dependsOn)) return;\n record('kit:setDependsOn', { id, from: node.dependsOn, to: dependsOn }, 'setDependsOn');\n },\n\n setLayout(id, layout) {\n const node = requireNode(id);\n if (node.kind !== 'container') throw new Error(`Scene: \"${id}\" is not a container`);\n refuse(id, 'set the layout of');\n let to: LayoutStrategy<TPose> | null;\n if (typeof layout === 'string') {\n to = (layoutField.fnOf(layout) as LayoutStrategy<TPose> | undefined) ?? null;\n if (to === null) throw new Error(`Scene: no layout \"${layout}\" in this scene's registry`);\n } else {\n to = layout;\n }\n const from = node.layout ?? null;\n if (from === to) return;\n if (to !== null) layoutDeclared();\n const fromKey = from === null ? undefined : layoutField.keyOf(from);\n const toKey = to === null ? undefined : layoutField.keyOf(to);\n const payload: SetLayoutPayload = {\n id,\n ...(fromKey !== undefined ? { fromKey } : {}),\n ...(toKey !== undefined ? { toKey } : {}),\n };\n layoutRefs.set(payload, { from, to });\n edit('setLayout', true, () => {\n noteChange(id);\n record('kit:setLayout', payload, 'setLayout');\n });\n },\n\n setUnits(id, units) {\n const node = requireNode(id);\n if (node.kind !== 'container') throw new Error(`Scene: \"${id}\" is not a container`);\n refuse(id, 'set the units of');\n record('kit:setUnits', { id, from: node.units, to: units ?? undefined }, 'setUnits');\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -154,6 +154,7 @@ function createSceneHistory(env) {
|
|
|
154
154
|
branches: () => engine.branches(),
|
|
155
155
|
getVersion: () => engine.getVersion(),
|
|
156
156
|
subscribe: (listener) => engine.subscribe(listener),
|
|
157
|
+
observe: (listener) => engine.observe(listener),
|
|
157
158
|
serialize: methods.serializeHistory,
|
|
158
159
|
allForwardOps: () => engine.allForwardOps(),
|
|
159
160
|
seal: () => engine.seal(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"historyFacade.js","names":[],"sources":["../../../src/core/scene/historyFacade.ts"],"sourcesContent":["/**\n * `scene.history`: the `History` the kit's `undo`/`redo` actions resolve\n * their `history` dep to, and the handle a mode machine opens journals\n * against.\n *\n * Handing out the engine directly would let an action-driven undo replay ops\n * with scene-side recording still armed and without bumping the scene's\n * version, so every member that mutates routes through the scene's own\n * wrappers and one `notify()`; reads forward to the engine.\n */\nimport type { History, Journal } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport { dwarn } from 'debug/flag';\nimport { SCENE_HISTORY_FORM, type SerializedSceneHistory } from './historyForm';\nimport type { Scene } from './types';\n\n/** What the facade needs from the scene it fronts. */\nexport interface SceneHistoryEnv {\n /** The scene's own undo/redo engine. */\n engine: History;\n /** Run a step that applies ops through an engine: all-or-nothing, with\n * scene-side recording suppressed. */\n applying(step: () => void): void;\n /** An op as the engine applies it: its entry records the kit ops it\n * caused. */\n causing(op: Op): Op;\n /** Run a step that replays recorded ops: an undo, a redo, a cancel. */\n replay(step: () => void): void;\n notify(): void;\n refuseUntracked(verb: string): void;\n /** Run an op-driven edit with its arrivals settled into the same batch. */\n runArrivalEdit(ops: Op[], run: (ops: Op[]) => void): void;\n /** Run an edit applying ops, labeled `label` for the pose writes it reports. */\n labeled<T>(label: string, fn: () => T): T;\n /** The journal `applyBatch` writes to now, or null. Undo and redo step it\n * instead of the engine, so they take back the edits that went there. */\n activeJournal(): Journal | null;\n}\n\n/** The scene's own spellings of its history, which answer what a caller\n * outside the engine's vocabulary asks: whether a step moved anything. */\nexport type SceneHistoryMethods = Pick<\n Scene<unknown, string, unknown>,\n | 'undo' | 'redo' | 'canUndo' | 'canRedo' | 'historyEntries' | 'historyIndex'\n | 'jumpToHistoryIndex' | 'serializeHistory' | 'restoreHistory'\n>;\n\n/** A journal drives adapter mutation itself, so its applies and undos\n * re-enter scene mutation methods the way the facade's do. */\nfunction wrapJournal(raw: Journal, env: SceneHistoryEnv): Journal {\n const { replay, notify } = env;\n return {\n targetId: raw.targetId,\n forkedAtEntryId: raw.forkedAtEntryId,\n applyBatch(ops, label) {\n env.labeled(label, () => env.runArrivalEdit(ops, (all) => {\n env.applying(() => raw.applyBatch(all.map(env.causing), label));\n notify();\n }));\n },\n undo() {\n replay(() => raw.undo());\n notify();\n },\n redo() {\n replay(() => raw.redo());\n notify();\n },\n canUndo: () => raw.canUndo(),\n canRedo: () => raw.canRedo(),\n entries: () => raw.entries(),\n seal: () => raw.seal(),\n getVersion: () => raw.getVersion(),\n subscribe: (listener) => raw.subscribe(listener),\n commit(label) {\n // Flushes the session's net ops to the parent stack — no apply, but\n // the parent's undo depth changes and listeners read that.\n raw.commit(label);\n notify();\n },\n cancel() {\n replay(() => raw.cancel());\n notify();\n },\n suspend: () => raw.suspend(),\n isActive: () => raw.isActive(),\n };\n}\n\nexport function createSceneHistory(env: SceneHistoryEnv): { history: History; methods: SceneHistoryMethods } {\n const { engine, replay, notify, refuseUntracked, runArrivalEdit } = env;\n\n /** Wrapped journal → the engine's own, for `resumeJournal`: the engine keys\n * resumability off journal identity, which a wrapper would not match. */\n const rawJournals = new WeakMap<Journal, Journal>();\n\n function step(can: () => boolean, run: () => void): boolean {\n if (!can()) return false;\n replay(run);\n notify();\n return true;\n }\n\n /** What undo and redo step: the open journal, else the engine. */\n const stack = (): Pick<History, 'canUndo' | 'canRedo' | 'undo' | 'redo'> => env.activeJournal() ?? engine;\n\n const methods: SceneHistoryMethods = {\n undo() {\n const j = env.activeJournal();\n if (j) { if (!j.canUndo()) return false; j.undo(); return true; }\n return step(() => engine.canUndo(), () => engine.undo());\n },\n redo() {\n const j = env.activeJournal();\n if (j) { if (!j.canRedo()) return false; j.redo(); return true; }\n return step(() => engine.canRedo(), () => engine.redo());\n },\n canUndo: () => stack().canUndo(),\n canRedo: () => stack().canRedo(),\n historyEntries() {\n const { undo, redo } = engine.entries();\n // entries().redo is already chronological: the next redo comes first.\n return [...undo, ...redo].map((e) => ({ id: String(e.id), label: e.label }));\n },\n historyIndex: () => engine.undoDepth(),\n jumpToHistoryIndex(index) {\n const total = engine.undoDepth() + engine.redoDepth();\n const target = Math.max(0, Math.min(total, index));\n return step(() => target !== engine.undoDepth(), () => engine.goto(target));\n },\n serializeHistory: () => ({ ...engine.serialize(), form: SCENE_HISTORY_FORM }),\n restoreHistory(snapshot) {\n if ((snapshot as Partial<SerializedSceneHistory>).form !== SCENE_HISTORY_FORM) {\n dwarn('scene', 'restoreHistory: snapshot is not in the current recorded form — dropped');\n return;\n }\n // Only the stacks are rebuilt: no op applies.\n engine.restore(snapshot);\n notify();\n },\n };\n\n const history: History = {\n // Mutating: applied ops re-enter the scene's own mutation methods.\n apply(op, label) {\n refuseUntracked('history.apply');\n env.labeled(label ?? op.label ?? '', () => runArrivalEdit([op], (all) => {\n const run = () => (all.length === 1\n ? engine.apply(env.causing(op), label)\n : engine.applyOps(all.map(env.causing), label ?? op.label ?? ''));\n env.applying(run);\n notify();\n }));\n },\n applyOps(ops, label) {\n refuseUntracked('history.applyOps');\n env.labeled(label, () => runArrivalEdit(ops, (all) => {\n env.applying(() => engine.applyOps(all.map(env.causing), label));\n notify();\n }));\n },\n undo: () => { methods.undo(); },\n redo: () => { methods.redo(); },\n goto: (n) => { methods.jumpToHistoryIndex(n); },\n // Stack-only: nothing applies, but what `canUndo()` answers changes.\n clear() {\n engine.clear();\n notify();\n },\n recordEntry(ops, label, opts) {\n engine.recordEntry(ops, label, opts);\n notify();\n },\n prune(t) {\n engine.prune(t);\n notify();\n },\n switchBranch(id) {\n engine.switchBranch(id);\n notify();\n },\n restore: methods.restoreHistory,\n // Reads forward to the engine, `version` included: it counts history\n // operations, where the scene's counts every mutation.\n canUndo: methods.canUndo,\n canRedo: methods.canRedo,\n undoDepth: () => engine.undoDepth(),\n redoDepth: () => engine.redoDepth(),\n entries: () => engine.entries(),\n timestampAt: (i) => engine.timestampAt(i),\n depthAt: (t) => engine.depthAt(t),\n branches: () => engine.branches(),\n getVersion: () => engine.getVersion(),\n subscribe: (listener) => engine.subscribe(listener),\n serialize: methods.serializeHistory,\n allForwardOps: () => engine.allForwardOps(),\n seal: () => engine.seal(),\n currentEntryId: () => engine.currentEntryId(),\n beginJournal(opts) {\n const raw = engine.beginJournal(opts);\n const wrapped = wrapJournal(raw, env);\n rawJournals.set(wrapped, raw);\n return wrapped;\n },\n resumeJournal: (journal) => engine.resumeJournal(rawJournals.get(journal) ?? journal),\n };\n return { history, methods };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"historyFacade.js","names":[],"sources":["../../../src/core/scene/historyFacade.ts"],"sourcesContent":["/**\n * `scene.history`: the `History` the kit's `undo`/`redo` actions resolve\n * their `history` dep to, and the handle a mode machine opens journals\n * against.\n *\n * Handing out the engine directly would let an action-driven undo replay ops\n * with scene-side recording still armed and without bumping the scene's\n * version, so every member that mutates routes through the scene's own\n * wrappers and one `notify()`; reads forward to the engine.\n */\nimport type { History, Journal } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport { dwarn } from 'debug/flag';\nimport { SCENE_HISTORY_FORM, type SerializedSceneHistory } from './historyForm';\nimport type { Scene } from './types';\n\n/** What the facade needs from the scene it fronts. */\nexport interface SceneHistoryEnv {\n /** The scene's own undo/redo engine. */\n engine: History;\n /** Run a step that applies ops through an engine: all-or-nothing, with\n * scene-side recording suppressed. */\n applying(step: () => void): void;\n /** An op as the engine applies it: its entry records the kit ops it\n * caused. */\n causing(op: Op): Op;\n /** Run a step that replays recorded ops: an undo, a redo, a cancel. */\n replay(step: () => void): void;\n notify(): void;\n refuseUntracked(verb: string): void;\n /** Run an op-driven edit with its arrivals settled into the same batch. */\n runArrivalEdit(ops: Op[], run: (ops: Op[]) => void): void;\n /** Run an edit applying ops, labeled `label` for the pose writes it reports. */\n labeled<T>(label: string, fn: () => T): T;\n /** The journal `applyBatch` writes to now, or null. Undo and redo step it\n * instead of the engine, so they take back the edits that went there. */\n activeJournal(): Journal | null;\n}\n\n/** The scene's own spellings of its history, which answer what a caller\n * outside the engine's vocabulary asks: whether a step moved anything. */\nexport type SceneHistoryMethods = Pick<\n Scene<unknown, string, unknown>,\n | 'undo' | 'redo' | 'canUndo' | 'canRedo' | 'historyEntries' | 'historyIndex'\n | 'jumpToHistoryIndex' | 'serializeHistory' | 'restoreHistory'\n>;\n\n/** A journal drives adapter mutation itself, so its applies and undos\n * re-enter scene mutation methods the way the facade's do. */\nfunction wrapJournal(raw: Journal, env: SceneHistoryEnv): Journal {\n const { replay, notify } = env;\n return {\n targetId: raw.targetId,\n forkedAtEntryId: raw.forkedAtEntryId,\n applyBatch(ops, label) {\n env.labeled(label, () => env.runArrivalEdit(ops, (all) => {\n env.applying(() => raw.applyBatch(all.map(env.causing), label));\n notify();\n }));\n },\n undo() {\n replay(() => raw.undo());\n notify();\n },\n redo() {\n replay(() => raw.redo());\n notify();\n },\n canUndo: () => raw.canUndo(),\n canRedo: () => raw.canRedo(),\n entries: () => raw.entries(),\n seal: () => raw.seal(),\n getVersion: () => raw.getVersion(),\n subscribe: (listener) => raw.subscribe(listener),\n commit(label) {\n // Flushes the session's net ops to the parent stack — no apply, but\n // the parent's undo depth changes and listeners read that.\n raw.commit(label);\n notify();\n },\n cancel() {\n replay(() => raw.cancel());\n notify();\n },\n suspend: () => raw.suspend(),\n isActive: () => raw.isActive(),\n };\n}\n\nexport function createSceneHistory(env: SceneHistoryEnv): { history: History; methods: SceneHistoryMethods } {\n const { engine, replay, notify, refuseUntracked, runArrivalEdit } = env;\n\n /** Wrapped journal → the engine's own, for `resumeJournal`: the engine keys\n * resumability off journal identity, which a wrapper would not match. */\n const rawJournals = new WeakMap<Journal, Journal>();\n\n function step(can: () => boolean, run: () => void): boolean {\n if (!can()) return false;\n replay(run);\n notify();\n return true;\n }\n\n /** What undo and redo step: the open journal, else the engine. */\n const stack = (): Pick<History, 'canUndo' | 'canRedo' | 'undo' | 'redo'> => env.activeJournal() ?? engine;\n\n const methods: SceneHistoryMethods = {\n undo() {\n const j = env.activeJournal();\n if (j) { if (!j.canUndo()) return false; j.undo(); return true; }\n return step(() => engine.canUndo(), () => engine.undo());\n },\n redo() {\n const j = env.activeJournal();\n if (j) { if (!j.canRedo()) return false; j.redo(); return true; }\n return step(() => engine.canRedo(), () => engine.redo());\n },\n canUndo: () => stack().canUndo(),\n canRedo: () => stack().canRedo(),\n historyEntries() {\n const { undo, redo } = engine.entries();\n // entries().redo is already chronological: the next redo comes first.\n return [...undo, ...redo].map((e) => ({ id: String(e.id), label: e.label }));\n },\n historyIndex: () => engine.undoDepth(),\n jumpToHistoryIndex(index) {\n const total = engine.undoDepth() + engine.redoDepth();\n const target = Math.max(0, Math.min(total, index));\n return step(() => target !== engine.undoDepth(), () => engine.goto(target));\n },\n serializeHistory: () => ({ ...engine.serialize(), form: SCENE_HISTORY_FORM }),\n restoreHistory(snapshot) {\n if ((snapshot as Partial<SerializedSceneHistory>).form !== SCENE_HISTORY_FORM) {\n dwarn('scene', 'restoreHistory: snapshot is not in the current recorded form — dropped');\n return;\n }\n // Only the stacks are rebuilt: no op applies.\n engine.restore(snapshot);\n notify();\n },\n };\n\n const history: History = {\n // Mutating: applied ops re-enter the scene's own mutation methods.\n apply(op, label) {\n refuseUntracked('history.apply');\n env.labeled(label ?? op.label ?? '', () => runArrivalEdit([op], (all) => {\n const run = () => (all.length === 1\n ? engine.apply(env.causing(op), label)\n : engine.applyOps(all.map(env.causing), label ?? op.label ?? ''));\n env.applying(run);\n notify();\n }));\n },\n applyOps(ops, label) {\n refuseUntracked('history.applyOps');\n env.labeled(label, () => runArrivalEdit(ops, (all) => {\n env.applying(() => engine.applyOps(all.map(env.causing), label));\n notify();\n }));\n },\n undo: () => { methods.undo(); },\n redo: () => { methods.redo(); },\n goto: (n) => { methods.jumpToHistoryIndex(n); },\n // Stack-only: nothing applies, but what `canUndo()` answers changes.\n clear() {\n engine.clear();\n notify();\n },\n recordEntry(ops, label, opts) {\n engine.recordEntry(ops, label, opts);\n notify();\n },\n prune(t) {\n engine.prune(t);\n notify();\n },\n switchBranch(id) {\n engine.switchBranch(id);\n notify();\n },\n restore: methods.restoreHistory,\n // Reads forward to the engine, `version` included: it counts history\n // operations, where the scene's counts every mutation.\n canUndo: methods.canUndo,\n canRedo: methods.canRedo,\n undoDepth: () => engine.undoDepth(),\n redoDepth: () => engine.redoDepth(),\n entries: () => engine.entries(),\n timestampAt: (i) => engine.timestampAt(i),\n depthAt: (t) => engine.depthAt(t),\n branches: () => engine.branches(),\n getVersion: () => engine.getVersion(),\n subscribe: (listener) => engine.subscribe(listener),\n observe: (listener) => engine.observe(listener),\n serialize: methods.serializeHistory,\n allForwardOps: () => engine.allForwardOps(),\n seal: () => engine.seal(),\n currentEntryId: () => engine.currentEntryId(),\n beginJournal(opts) {\n const raw = engine.beginJournal(opts);\n const wrapped = wrapJournal(raw, env);\n rawJournals.set(wrapped, raw);\n return wrapped;\n },\n resumeJournal: (journal) => engine.resumeJournal(rawJournals.get(journal) ?? journal),\n };\n return { history, methods };\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { composeWorldPose, rebaseLocalPose } from "./composeFrame.js";
|
|
2
2
|
import { samePoseValue } from "./poseSnapshot.js";
|
|
3
3
|
import { definesFrame, documentPose } from "./effectivePose.js";
|
|
4
|
+
import { carriedDescendants } from "./carryDescendants.js";
|
|
4
5
|
import { asRectPose } from "./kitRegistry.js";
|
|
5
6
|
|
|
6
7
|
//#region src/core/scene/layoutPass.ts
|
|
@@ -12,13 +13,14 @@ import { asRectPose } from "./kitRegistry.js";
|
|
|
12
13
|
* the rest through it (and may shrink). One that gained children and whose
|
|
13
14
|
* strategy has `arrive` then places them through it (and may refuse, or
|
|
14
15
|
* grow). Any other changed container — one that reordered children, was
|
|
15
|
-
* resized, or gained or lost children with neither hook —
|
|
16
|
-
* resting arrangement, `childPoses`.
|
|
16
|
+
* resized, had a child moved, or gained or lost children with neither hook —
|
|
17
|
+
* is put back in its resting arrangement, `childPoses`.
|
|
17
18
|
*
|
|
18
19
|
* `LayoutStrategy` is world-framed, so each pose is composed to world before
|
|
19
20
|
* a strategy sees it and each result is rebased into its node's parent frame
|
|
20
21
|
* after. A child already where the strategy puts it is left out, so a pass
|
|
21
|
-
* that changes nothing writes nothing. A
|
|
22
|
+
* that changes nothing writes nothing. A child the pass moves takes its own
|
|
23
|
+
* descendants along (`carriedDescendants`). A container this pass resizes is
|
|
22
24
|
* arranged in turn, so nested layouts settle in the same edit.
|
|
23
25
|
*/
|
|
24
26
|
function resolveLayoutFrame(frame) {
|
|
@@ -99,9 +101,32 @@ function runLayoutPass(source, layoutOf, arrivals, changed, frame, departures =
|
|
|
99
101
|
for (const [cid, pose] of result.poses) {
|
|
100
102
|
const id = cid;
|
|
101
103
|
if (source.get(id)?.parent !== containerId) continue;
|
|
104
|
+
const before = frame.bounds(world(id), id);
|
|
102
105
|
write(id, rebaseLocalPose(pa, pose, containerId, compose, decompose));
|
|
106
|
+
carry(id, before);
|
|
103
107
|
}
|
|
104
108
|
};
|
|
109
|
+
/** Moves `id`'s descendants by however far its write just moved it from `before`. */
|
|
110
|
+
const carry = (id, before) => {
|
|
111
|
+
const after = frame.bounds(world(id), id);
|
|
112
|
+
if (before === null || after === null) return;
|
|
113
|
+
const dx = after.x - before.x;
|
|
114
|
+
const dy = after.y - before.y;
|
|
115
|
+
if (dx === 0 && dy === 0) return;
|
|
116
|
+
const carried = carriedDescendants({
|
|
117
|
+
childrenOf: (cid) => source.childrenOf(cid),
|
|
118
|
+
poses: pa,
|
|
119
|
+
composition: source.composition
|
|
120
|
+
}, id, (pose, cid) => {
|
|
121
|
+
const b = frame.bounds(pose, cid);
|
|
122
|
+
return b === null ? null : frame.remap(pose, b, {
|
|
123
|
+
...b,
|
|
124
|
+
x: b.x + dx,
|
|
125
|
+
y: b.y + dy
|
|
126
|
+
}, cid);
|
|
127
|
+
});
|
|
128
|
+
for (const [cid, pose] of carried) write(cid, pose);
|
|
129
|
+
};
|
|
105
130
|
while (queue.length > 0) {
|
|
106
131
|
const containerId = queue.shift();
|
|
107
132
|
current = containerId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layoutPass.js","names":[],"sources":["../../../src/core/scene/layoutPass.ts"],"sourcesContent":["/**\n * The one computation behind a scene's container layouts: given the\n * containers one edit changed, the pose writes their layouts ask for.\n *\n * A container that lost children and whose strategy has `depart` rearranges\n * the rest through it (and may shrink). One that gained children and whose\n * strategy has `arrive` then places them through it (and may refuse, or\n * grow). Any other changed container — one that reordered children, was\n * resized, or gained or lost children with neither hook — is put back in its\n * resting arrangement, `childPoses`.\n *\n * `LayoutStrategy` is world-framed, so each pose is composed to world before\n * a strategy sees it and each result is rebased into its node's parent frame\n * after. A child already where the strategy puts it is left out, so a pass\n * that changes nothing writes nothing. A container this pass resizes is\n * arranged in turn, so nested layouts settle in the same edit.\n */\nimport {\n composeWorldPose,\n rebaseLocalPose,\n type PoseAdapter,\n type PoseComposition,\n} from './composeFrame';\nimport type { ContainerBounds, LayoutArrival, LayoutChild, LayoutStrategy } from '../../layout/types';\nimport { definesFrame, documentPose, type PoseSource, type PosedNode } from './effectivePose';\nimport { asRectPose } from './kitRegistry';\nimport { samePoseValue } from './poseSnapshot';\nimport type { LayoutFrame, NodeId } from './types';\n\n/** What the pass reads a scene through. */\nexport interface LayoutPassSource<TPose> extends PoseSource<TPose> {\n get(id: NodeId): (PosedNode<TPose> & { parent: NodeId | null }) | undefined;\n readonly composition: PoseComposition<TPose>;\n}\n\n/** How the pass measures and resizes poses, with every default filled. */\nexport interface ResolvedLayoutFrame<TPose> {\n bounds(pose: TPose, id: NodeId): ContainerBounds | null;\n remap(pose: TPose, from: ContainerBounds, to: ContainerBounds, id: NodeId): TPose;\n}\n\n/** The pose writes one edit's layouts ask for, or `null` when a strategy\n * refused the children that joined it. */\nexport type LayoutPassResult<TPose> = Map<NodeId, TPose> | null;\n\nexport function resolveLayoutFrame<TPose>(frame: LayoutFrame<TPose> | undefined): ResolvedLayoutFrame<TPose> {\n return {\n bounds: frame?.bounds ?? ((pose) => {\n const r = asRectPose(pose);\n return r === null ? null : { x: r.x, y: r.y, width: r.width, height: r.height };\n }),\n remap: frame?.remap ?? ((pose, _from, to) => ({ ...(pose as object), ...to }) as TPose),\n };\n}\n\n/** Whether a node going from `from` to `to` changed size. */\nexport function resized<TPose>(frame: ResolvedLayoutFrame<TPose>, id: NodeId, from: TPose, to: TPose): boolean {\n const a = frame.bounds(from, id);\n const b = frame.bounds(to, id);\n if (a === null || b === null) return a !== b;\n return a.width !== b.width || a.height !== b.height;\n}\n\n/** How many times one pass may arrange a container before giving up on it —\n * only nested layouts that keep resizing each other get close. */\nconst MAX_PASSES = 16;\n\nexport function runLayoutPass<TPose>(\n source: LayoutPassSource<TPose>,\n layoutOf: (id: NodeId) => LayoutStrategy<TPose> | null,\n arrivals: ReadonlyMap<NodeId, readonly NodeId[]>,\n changed: ReadonlySet<NodeId>,\n frame: ResolvedLayoutFrame<TPose>,\n departures: ReadonlyMap<NodeId, readonly NodeId[]> = new Map(),\n): LayoutPassResult<TPose> {\n // Poses already decided in this pass, so a container resized here is the\n // frame its children are rebased into.\n const out = new Map<NodeId, TPose>();\n const poseOf = (id: NodeId): TPose => out.get(id) ?? documentPose(source, source.get(id)!);\n const pa: PoseAdapter<TPose> = {\n getPose: (id) => poseOf(id as NodeId),\n getParent: (id) => source.get(id as NodeId)?.parent ?? null,\n definesFrame: (id) => {\n const node = source.get(id as NodeId);\n return node === undefined || definesFrame(node);\n },\n };\n const { compose, decompose } = source.composition;\n const world = (id: NodeId): TPose => composeWorldPose(pa, id, compose);\n\n const queue: NodeId[] = [...new Set<NodeId>([...arrivals.keys(), ...changed])];\n const passes = new Map<NodeId, number>();\n const write = (id: NodeId, pose: TPose): void => {\n const node = source.get(id)!;\n const before = poseOf(id);\n if (samePoseValue(before, pose)) return;\n out.set(id, pose);\n // A container its own `depart` or `arrive` resized is arranged already.\n if (id !== current && node.kind === 'container' && layoutOf(id) !== null\n && resized(frame, id, before, pose)) queue.push(id);\n };\n let current: NodeId | null = null;\n\n /** The container and its children as the strategy sees them, in world,\n * with every write this pass has made so far folded in. */\n const frameOf = (containerId: NodeId) => {\n const pose = world(containerId);\n const bounds = frame.bounds(pose, containerId);\n if (bounds === null) return null;\n const children: LayoutChild<TPose>[] = source.childrenOf(containerId).map((cid) => ({\n id: cid as string,\n pose: world(cid),\n }));\n return { world: pose, container: { id: containerId as string, bounds }, children };\n };\n const apply = (\n containerId: NodeId,\n at: NonNullable<ReturnType<typeof frameOf>>,\n result: LayoutArrival<TPose>,\n ): void => {\n if (result.bounds) {\n const node = source.get(containerId)!;\n const sized = frame.remap(at.world, at.container.bounds, result.bounds, containerId);\n write(containerId, rebaseLocalPose(pa, sized, node.parent, compose, decompose));\n }\n for (const [cid, pose] of result.poses) {\n const id = cid as NodeId;\n if (source.get(id)?.parent !== containerId) continue;\n write(id, rebaseLocalPose(pa, pose, containerId, compose, decompose));\n }\n };\n\n while (queue.length > 0) {\n const containerId = queue.shift()!;\n current = containerId;\n const n = (passes.get(containerId) ?? 0) + 1;\n passes.set(containerId, n);\n if (n > MAX_PASSES) continue;\n const layout = layoutOf(containerId);\n if (layout === null || source.get(containerId) === undefined) continue;\n const joined = n === 1 ? arrivals.get(containerId) : undefined;\n const left = n === 1 ? departures.get(containerId) : undefined;\n let arranged = false;\n if (left !== undefined && layout.depart) {\n const at = frameOf(containerId);\n if (at === null) continue;\n const joinedSet = new Set<string>(joined ?? []);\n const staying = at.children.filter((c) => !joinedSet.has(c.id));\n apply(containerId, at, layout.depart(at.container, staying, new Set<string>(left)));\n arranged = true;\n }\n if (joined !== undefined && layout.arrive) {\n const at = frameOf(containerId);\n if (at === null) continue;\n const result = layout.arrive(at.container, at.children, new Set<string>(joined));\n if (result === null) return null;\n apply(containerId, at, result);\n } else if (!arranged || joined !== undefined) {\n const at = frameOf(containerId);\n if (at === null) continue;\n apply(containerId, at, { poses: layout.childPoses(at.container, at.children) });\n }\n }\n return out;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"layoutPass.js","names":[],"sources":["../../../src/core/scene/layoutPass.ts"],"sourcesContent":["/**\n * The one computation behind a scene's container layouts: given the\n * containers one edit changed, the pose writes their layouts ask for.\n *\n * A container that lost children and whose strategy has `depart` rearranges\n * the rest through it (and may shrink). One that gained children and whose\n * strategy has `arrive` then places them through it (and may refuse, or\n * grow). Any other changed container — one that reordered children, was\n * resized, had a child moved, or gained or lost children with neither hook —\n * is put back in its resting arrangement, `childPoses`.\n *\n * `LayoutStrategy` is world-framed, so each pose is composed to world before\n * a strategy sees it and each result is rebased into its node's parent frame\n * after. A child already where the strategy puts it is left out, so a pass\n * that changes nothing writes nothing. A child the pass moves takes its own\n * descendants along (`carriedDescendants`). A container this pass resizes is\n * arranged in turn, so nested layouts settle in the same edit.\n */\nimport {\n composeWorldPose,\n rebaseLocalPose,\n type PoseAdapter,\n type PoseComposition,\n} from './composeFrame';\nimport type { ContainerBounds, LayoutArrival, LayoutChild, LayoutStrategy } from '../../layout/types';\nimport { carriedDescendants } from './carryDescendants';\nimport { definesFrame, documentPose, type PoseSource, type PosedNode } from './effectivePose';\nimport { asRectPose } from './kitRegistry';\nimport { samePoseValue } from './poseSnapshot';\nimport type { LayoutFrame, NodeId } from './types';\n\n/** What the pass reads a scene through. */\nexport interface LayoutPassSource<TPose> extends PoseSource<TPose> {\n get(id: NodeId): (PosedNode<TPose> & { parent: NodeId | null }) | undefined;\n readonly composition: PoseComposition<TPose>;\n}\n\n/** How the pass measures and resizes poses, with every default filled. */\nexport interface ResolvedLayoutFrame<TPose> {\n bounds(pose: TPose, id: NodeId): ContainerBounds | null;\n remap(pose: TPose, from: ContainerBounds, to: ContainerBounds, id: NodeId): TPose;\n}\n\n/** The pose writes one edit's layouts ask for, or `null` when a strategy\n * refused the children that joined it. */\nexport type LayoutPassResult<TPose> = Map<NodeId, TPose> | null;\n\nexport function resolveLayoutFrame<TPose>(frame: LayoutFrame<TPose> | undefined): ResolvedLayoutFrame<TPose> {\n return {\n bounds: frame?.bounds ?? ((pose) => {\n const r = asRectPose(pose);\n return r === null ? null : { x: r.x, y: r.y, width: r.width, height: r.height };\n }),\n remap: frame?.remap ?? ((pose, _from, to) => ({ ...(pose as object), ...to }) as TPose),\n };\n}\n\n/** Whether a node going from `from` to `to` changed size. */\nexport function resized<TPose>(frame: ResolvedLayoutFrame<TPose>, id: NodeId, from: TPose, to: TPose): boolean {\n const a = frame.bounds(from, id);\n const b = frame.bounds(to, id);\n if (a === null || b === null) return a !== b;\n return a.width !== b.width || a.height !== b.height;\n}\n\n/** How many times one pass may arrange a container before giving up on it —\n * only nested layouts that keep resizing each other get close. */\nconst MAX_PASSES = 16;\n\nexport function runLayoutPass<TPose>(\n source: LayoutPassSource<TPose>,\n layoutOf: (id: NodeId) => LayoutStrategy<TPose> | null,\n arrivals: ReadonlyMap<NodeId, readonly NodeId[]>,\n changed: ReadonlySet<NodeId>,\n frame: ResolvedLayoutFrame<TPose>,\n departures: ReadonlyMap<NodeId, readonly NodeId[]> = new Map(),\n): LayoutPassResult<TPose> {\n // Poses already decided in this pass, so a container resized here is the\n // frame its children are rebased into.\n const out = new Map<NodeId, TPose>();\n const poseOf = (id: NodeId): TPose => out.get(id) ?? documentPose(source, source.get(id)!);\n const pa: PoseAdapter<TPose> = {\n getPose: (id) => poseOf(id as NodeId),\n getParent: (id) => source.get(id as NodeId)?.parent ?? null,\n definesFrame: (id) => {\n const node = source.get(id as NodeId);\n return node === undefined || definesFrame(node);\n },\n };\n const { compose, decompose } = source.composition;\n const world = (id: NodeId): TPose => composeWorldPose(pa, id, compose);\n\n const queue: NodeId[] = [...new Set<NodeId>([...arrivals.keys(), ...changed])];\n const passes = new Map<NodeId, number>();\n const write = (id: NodeId, pose: TPose): void => {\n const node = source.get(id)!;\n const before = poseOf(id);\n if (samePoseValue(before, pose)) return;\n out.set(id, pose);\n // A container its own `depart` or `arrive` resized is arranged already.\n if (id !== current && node.kind === 'container' && layoutOf(id) !== null\n && resized(frame, id, before, pose)) queue.push(id);\n };\n let current: NodeId | null = null;\n\n /** The container and its children as the strategy sees them, in world,\n * with every write this pass has made so far folded in. */\n const frameOf = (containerId: NodeId) => {\n const pose = world(containerId);\n const bounds = frame.bounds(pose, containerId);\n if (bounds === null) return null;\n const children: LayoutChild<TPose>[] = source.childrenOf(containerId).map((cid) => ({\n id: cid as string,\n pose: world(cid),\n }));\n return { world: pose, container: { id: containerId as string, bounds }, children };\n };\n const apply = (\n containerId: NodeId,\n at: NonNullable<ReturnType<typeof frameOf>>,\n result: LayoutArrival<TPose>,\n ): void => {\n if (result.bounds) {\n const node = source.get(containerId)!;\n const sized = frame.remap(at.world, at.container.bounds, result.bounds, containerId);\n write(containerId, rebaseLocalPose(pa, sized, node.parent, compose, decompose));\n }\n for (const [cid, pose] of result.poses) {\n const id = cid as NodeId;\n if (source.get(id)?.parent !== containerId) continue;\n const before = frame.bounds(world(id), id);\n write(id, rebaseLocalPose(pa, pose, containerId, compose, decompose));\n carry(id, before);\n }\n };\n /** Moves `id`'s descendants by however far its write just moved it from `before`. */\n const carry = (id: NodeId, before: ContainerBounds | null): void => {\n const after = frame.bounds(world(id), id);\n if (before === null || after === null) return;\n const dx = after.x - before.x;\n const dy = after.y - before.y;\n if (dx === 0 && dy === 0) return;\n const carried = carriedDescendants(\n { childrenOf: (cid) => source.childrenOf(cid), poses: pa, composition: source.composition },\n id,\n (pose, cid) => {\n const b = frame.bounds(pose, cid);\n return b === null ? null : frame.remap(pose, b, { ...b, x: b.x + dx, y: b.y + dy }, cid);\n },\n );\n for (const [cid, pose] of carried) write(cid, pose);\n };\n\n while (queue.length > 0) {\n const containerId = queue.shift()!;\n current = containerId;\n const n = (passes.get(containerId) ?? 0) + 1;\n passes.set(containerId, n);\n if (n > MAX_PASSES) continue;\n const layout = layoutOf(containerId);\n if (layout === null || source.get(containerId) === undefined) continue;\n const joined = n === 1 ? arrivals.get(containerId) : undefined;\n const left = n === 1 ? departures.get(containerId) : undefined;\n let arranged = false;\n if (left !== undefined && layout.depart) {\n const at = frameOf(containerId);\n if (at === null) continue;\n const joinedSet = new Set<string>(joined ?? []);\n const staying = at.children.filter((c) => !joinedSet.has(c.id));\n apply(containerId, at, layout.depart(at.container, staying, new Set<string>(left)));\n arranged = true;\n }\n if (joined !== undefined && layout.arrive) {\n const at = frameOf(containerId);\n if (at === null) continue;\n const result = layout.arrive(at.container, at.children, new Set<string>(joined));\n if (result === null) return null;\n apply(containerId, at, result);\n } else if (!arranged || joined !== undefined) {\n const at = frameOf(containerId);\n if (at === null) continue;\n apply(containerId, at, { poses: layout.childPoses(at.container, at.children) });\n }\n }\n return out;\n}\n"],"mappings":"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"}
|
|
@@ -9,30 +9,35 @@ function unwind(outer) {
|
|
|
9
9
|
} catch {}
|
|
10
10
|
}
|
|
11
11
|
function createRecorder(env) {
|
|
12
|
-
const { notifier, mode, engine, log, arrivals } = env;
|
|
12
|
+
const { notifier, mode, engine, log, arrivals, passes } = env;
|
|
13
13
|
/** The outermost open batch, while it is recording. */
|
|
14
14
|
let collecting = null;
|
|
15
15
|
/** The kit ops applied since the innermost `capturing` opened. */
|
|
16
16
|
let captured = null;
|
|
17
|
-
/** `fn`, then the poses its arrivals settle to
|
|
18
|
-
function withArrivalsSettled(fn, owns) {
|
|
17
|
+
/** `fn`, then the poses its arrivals settle to and its passes' ops, for whichever this edit owns. */
|
|
18
|
+
function withArrivalsSettled(fn, owns, ownsPasses = false) {
|
|
19
19
|
const result = fn();
|
|
20
20
|
if (owns) for (const p of arrivals.settle()) env.writePose(p.id, p.to);
|
|
21
|
+
if (ownsPasses) passes.run();
|
|
21
22
|
return result;
|
|
22
23
|
}
|
|
23
|
-
function batch(label, fn) {
|
|
24
|
+
function batch(label, fn, options = {}) {
|
|
24
25
|
const outermost = !mode.open();
|
|
25
26
|
if (outermost) collecting = {
|
|
26
27
|
label,
|
|
27
28
|
ops: [],
|
|
28
29
|
outer: [],
|
|
29
|
-
|
|
30
|
+
entry: {
|
|
31
|
+
selectionBefore: env.selection(),
|
|
32
|
+
...options
|
|
33
|
+
}
|
|
30
34
|
};
|
|
31
35
|
const ownsArrivals = outermost && arrivals.open(true);
|
|
36
|
+
const ownsPasses = outermost && passes.open();
|
|
32
37
|
return env.labeled(label, "call", () => notifier.deferred(() => mode.during("batch", () => {
|
|
33
38
|
if (!outermost) return fn();
|
|
34
39
|
try {
|
|
35
|
-
const result = log.atomically(() => withArrivalsSettled(fn, ownsArrivals), () => (collecting?.ops.length ?? 0) > 0);
|
|
40
|
+
const result = log.atomically(() => withArrivalsSettled(fn, ownsArrivals, ownsPasses), () => (collecting?.ops.length ?? 0) > 0);
|
|
36
41
|
if (ownsArrivals) arrivals.emitReflow();
|
|
37
42
|
return result;
|
|
38
43
|
} catch (err) {
|
|
@@ -42,25 +47,29 @@ function createRecorder(env) {
|
|
|
42
47
|
}
|
|
43
48
|
}), () => {
|
|
44
49
|
if (ownsArrivals) arrivals.abandon();
|
|
50
|
+
if (ownsPasses) passes.abandon();
|
|
45
51
|
if (!outermost || !collecting) return;
|
|
46
52
|
const finished = collecting;
|
|
47
53
|
collecting = null;
|
|
48
|
-
if (finished.ops.length > 0) engine.recordEntry(finished.ops, finished.label,
|
|
54
|
+
if (finished.ops.length > 0) engine.recordEntry(finished.ops, finished.label, finished.entry);
|
|
49
55
|
}));
|
|
50
56
|
}
|
|
57
|
+
function record(kind, payload, label) {
|
|
58
|
+
if (mode.records() && !collecting && passes.any()) return void batch(label, () => record(kind, payload, label));
|
|
59
|
+
passes.note(kind, payload);
|
|
60
|
+
env.labeled(label, "call", () => {
|
|
61
|
+
if (mode.records() && !collecting) mode.during("own", () => engine.applyOps([env.makeOp(kind, payload)], label));
|
|
62
|
+
else {
|
|
63
|
+
env.runOp(kind, payload);
|
|
64
|
+
log.logApplied(kind, payload);
|
|
65
|
+
if (mode.records()) collecting.ops.push(env.makeOp(kind, payload));
|
|
66
|
+
else if (!mode.untracked()) captured?.push(env.makeOp(kind, payload));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
notifier.notify();
|
|
70
|
+
}
|
|
51
71
|
return {
|
|
52
|
-
record
|
|
53
|
-
env.labeled(label, "call", () => {
|
|
54
|
-
if (mode.records() && !collecting) mode.during("own", () => engine.applyOps([env.makeOp(kind, payload)], label));
|
|
55
|
-
else {
|
|
56
|
-
env.runOp(kind, payload);
|
|
57
|
-
log.logApplied(kind, payload);
|
|
58
|
-
if (mode.records()) collecting.ops.push(env.makeOp(kind, payload));
|
|
59
|
-
else if (!mode.untracked()) captured?.push(env.makeOp(kind, payload));
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
notifier.notify();
|
|
63
|
-
},
|
|
72
|
+
record,
|
|
64
73
|
edit: (label, touchesLayout, fn) => touchesLayout && arrivals.bareLayoutEdit() ? batch(label, fn) : fn(),
|
|
65
74
|
batch,
|
|
66
75
|
untracked(fn) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.js","names":[],"sources":["../../../src/core/scene/recorder.ts"],"sourcesContent":["/**\n * How a scene mutation becomes an undo entry: recorded on its own, collected\n * into the batch it runs in, or left to the engine or the untracked run that\n * is driving it.\n */\nimport type { History } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport type { AppliedLog } from './appliedLog';\nimport type { ArrivalWindow } from './arrivalWindow';\nimport { bindOpToAdapter } from './boundOps';\nimport type { EditMode } from './editMode';\nimport type { Notifier } from './notifier';\nimport type { NodeId } from './types';\n\n/** What the recorder needs from the scene it records for. */\nexport interface RecorderEnv {\n notifier: Notifier;\n mode: EditMode;\n /** The scene's own undo/redo engine. */\n engine: History;\n /** Apply a registered op's forward half. */\n runOp(kind: string, payload: unknown): void;\n /** A registered op as the engine holds it. */\n makeOp(kind: string, payload: unknown): Op;\n log: AppliedLog;\n arrivals: Pick<ArrivalWindow<unknown>, 'bareLayoutEdit' | 'open' | 'settle' | 'emitReflow' | 'abandon'>;\n selection(): readonly NodeId[];\n /** Write one pose an arrival settle decided. */\n writePose(id: string, pose: unknown): void;\n /** Run `fn` as an edit made by a scene call labeled `label`. */\n labeled<T>(label: string, via: 'call', fn: () => T): T;\n}\n\nexport interface Recorder {\n /** Apply a registered op as part of whatever edit is running. */\n record(kind: string, payload: unknown, label: string): void;\n /** Run the recording half of a mutation. One that touches a laid-out\n * container, called bare, runs as a batch of its own: that is what puts\n * the arrangement it triggers in the same undo step. */\n edit<T>(label: string, touchesLayout: boolean, fn: () => T): T;\n batch<T>(label: string, fn: () => T): T;\n untracked<T>(fn: () => T): T;\n /** Whether an edit made now would join an open `batch`'s entry. */\n joins(): boolean;\n /** Apply ops built outside the scene as part of the open `batch`, all or\n * none. Its entry holds the kit ops they cause, and beside them, bound to\n * `adapter`, any op that wrote no scene state. */\n join(ops: Op[], adapter: unknown): void;\n /** Run `step`, and answer with the kit ops applied during it. */\n capturing<T>(step: () => T): { result: T; ops: Op[] };\n /** Run `fn` as a history engine's step: the ops it applies may re-enter\n * scene mutation methods, which apply but do not record. The caller\n * notifies once after. */\n engineDriven<T>(fn: () => T): T;\n}\n\n/** Take back ops that wrote outside the scene, newest first. The scene's own\n * writes are reverted by the applied log. */\nfunction unwind(outer: readonly Op[]): void {\n for (let i = outer.length - 1; i >= 0; i--) {\n try {\n outer[i].invert().apply(undefined);\n } catch {\n // The throw being unwound is the one the caller sees.\n }\n }\n}\n\nexport function createRecorder(env: RecorderEnv): Recorder {\n const { notifier, mode, engine, log, arrivals } = env;\n /** The outermost open batch, while it is recording. */\n let collecting: { label: string; ops: Op[]; outer: Op[]; selectionBefore: readonly NodeId[] } | null = null;\n /** The kit ops applied since the innermost `capturing` opened. */\n let captured: Op[] | null = null;\n\n /** `fn`, then the poses its arrivals settle to when this edit owns them. */\n function withArrivalsSettled<T>(fn: () => T, owns: boolean): T {\n const result = fn();\n if (owns) for (const p of arrivals.settle()) env.writePose(p.id, p.to);\n return result;\n }\n\n function batch<T>(label: string, fn: () => T): T {\n const outermost = !mode.open();\n // Captured at open, not at recordEntry: by the time the batch closes\n // the selection has already moved to wherever the batch put it.\n if (outermost) collecting = { label, ops: [], outer: [], selectionBefore: env.selection() };\n const ownsArrivals = outermost && arrivals.open(true);\n return env.labeled(label, 'call', () => notifier.deferred(() => mode.during('batch', () => {\n if (!outermost) return fn();\n try {\n const result = log.atomically(\n () => withArrivalsSettled(fn, ownsArrivals),\n () => (collecting?.ops.length ?? 0) > 0,\n );\n if (ownsArrivals) arrivals.emitReflow();\n return result;\n } catch (err) {\n // Reverted already; the half-built entry must not reach history.\n unwind(collecting?.outer ?? []);\n collecting = null;\n throw err;\n }\n }), () => {\n if (ownsArrivals) arrivals.abandon();\n if (!outermost || !collecting) return;\n const finished = collecting;\n collecting = null;\n // The ops were applied as they were issued, so the entry is pushed\n // without re-applying.\n if (finished.ops.length > 0) {\n engine.recordEntry(finished.ops, finished.label, { selectionBefore: finished.selectionBefore });\n }\n }));\n }\n\n return {\n record(kind, payload, label) {\n env.labeled(label, 'call', () => {\n if (mode.records() && !collecting) {\n // The engine applies the op itself, which is what lets it coalesce.\n mode.during('own', () => engine.applyOps([env.makeOp(kind, payload)], label));\n } else {\n env.runOp(kind, payload);\n log.logApplied(kind, payload);\n if (mode.records()) collecting!.ops.push(env.makeOp(kind, payload));\n else if (!mode.untracked()) captured?.push(env.makeOp(kind, payload));\n }\n });\n notifier.notify();\n },\n edit: (label, touchesLayout, fn) => (touchesLayout && arrivals.bareLayoutEdit() ? batch(label, fn) : fn()),\n batch,\n untracked(fn) {\n const versionBefore = notifier.version();\n const ownsArrivals = arrivals.open(true);\n return notifier.deferred(() => mode.during('untracked', () => {\n const result = log.atomically(() => withArrivalsSettled(fn, ownsArrivals), () => false);\n if (ownsArrivals) arrivals.emitReflow();\n return result;\n }), () => {\n if (ownsArrivals) arrivals.abandon();\n if (notifier.version() !== versionBefore) engine.seal();\n });\n },\n joins: () => mode.records() && collecting !== null,\n join(ops, adapter) {\n const into = collecting!;\n const mark = into.ops.length;\n const outer: Op[] = [];\n try {\n log.atomically(() => {\n for (const op of ops) {\n const before = into.ops.length;\n const result = op.apply(adapter);\n if (into.ops.length > before || result === false || result === 'noop') continue;\n outer.push(bindOpToAdapter(op, adapter));\n into.ops.push(outer[outer.length - 1]);\n }\n }, () => false);\n } catch (err) {\n unwind(outer);\n into.ops.length = mark;\n throw err;\n }\n into.outer.push(...outer);\n },\n capturing(step) {\n const enclosing = captured;\n const ops: Op[] = [];\n captured = ops;\n try {\n return { result: step(), ops };\n } finally {\n captured = enclosing;\n }\n },\n engineDriven: (fn) => mode.during('engine', () => notifier.muted(fn)),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"recorder.js","names":[],"sources":["../../../src/core/scene/recorder.ts"],"sourcesContent":["/**\n * How a scene mutation becomes an undo entry: recorded on its own, collected\n * into the batch it runs in, or left to the engine or the untracked run that\n * is driving it.\n */\nimport type { History, RecordEntryOptions } from '@weasel-js/history';\nimport type { Op } from 'core/ops/types';\nimport type { AppliedLog } from './appliedLog';\nimport type { ArrivalWindow } from './arrivalWindow';\nimport { bindOpToAdapter } from './boundOps';\nimport type { EditMode } from './editMode';\nimport type { EditPasses } from './editPass';\nimport type { Notifier } from './notifier';\nimport type { NodeId } from './types';\n\n/** What the recorder needs from the scene it records for. */\nexport interface RecorderEnv {\n notifier: Notifier;\n mode: EditMode;\n /** The scene's own undo/redo engine. */\n engine: History;\n /** Apply a registered op's forward half. */\n runOp(kind: string, payload: unknown): void;\n /** A registered op as the engine holds it. */\n makeOp(kind: string, payload: unknown): Op;\n log: AppliedLog;\n arrivals: Pick<ArrivalWindow<unknown>, 'bareLayoutEdit' | 'open' | 'settle' | 'emitReflow' | 'abandon'>;\n passes: Pick<EditPasses, 'any' | 'open' | 'note' | 'run' | 'abandon'>;\n selection(): readonly NodeId[];\n /** Write one pose an arrival settle decided. */\n writePose(id: string, pose: unknown): void;\n /** Run `fn` as an edit made by a scene call labeled `label`. */\n labeled<T>(label: string, via: 'call', fn: () => T): T;\n}\n\n/** Options for `scene.batch`. */\nexport interface SceneBatchOptions {\n /** Fold this batch's entry into the one before it when that one carries the same key and\n * nothing came between: one undo entry for a run of batches, such as a color dragged. */\n coalesceKey?: string;\n}\n\nexport interface Recorder {\n /** Apply a registered op as part of whatever edit is running. */\n record(kind: string, payload: unknown, label: string): void;\n /** Run the recording half of a mutation. One that touches a laid-out\n * container, called bare, runs as a batch of its own: that is what puts\n * the arrangement it triggers in the same undo step. */\n edit<T>(label: string, touchesLayout: boolean, fn: () => T): T;\n batch<T>(label: string, fn: () => T, options?: SceneBatchOptions): T;\n untracked<T>(fn: () => T): T;\n /** Whether an edit made now would join an open `batch`'s entry. */\n joins(): boolean;\n /** Apply ops built outside the scene as part of the open `batch`, all or\n * none. Its entry holds the kit ops they cause, and beside them, bound to\n * `adapter`, any op that wrote no scene state. */\n join(ops: Op[], adapter: unknown): void;\n /** Run `step`, and answer with the kit ops applied during it. */\n capturing<T>(step: () => T): { result: T; ops: Op[] };\n /** Run `fn` as a history engine's step: the ops it applies may re-enter\n * scene mutation methods, which apply but do not record. The caller\n * notifies once after. */\n engineDriven<T>(fn: () => T): T;\n}\n\n/** Take back ops that wrote outside the scene, newest first. The scene's own\n * writes are reverted by the applied log. */\nfunction unwind(outer: readonly Op[]): void {\n for (let i = outer.length - 1; i >= 0; i--) {\n try {\n outer[i].invert().apply(undefined);\n } catch {\n // The throw being unwound is the one the caller sees.\n }\n }\n}\n\nexport function createRecorder(env: RecorderEnv): Recorder {\n const { notifier, mode, engine, log, arrivals, passes } = env;\n /** The outermost open batch, while it is recording. */\n let collecting: { label: string; ops: Op[]; outer: Op[]; entry: RecordEntryOptions } | null = null;\n /** The kit ops applied since the innermost `capturing` opened. */\n let captured: Op[] | null = null;\n\n /** `fn`, then the poses its arrivals settle to and its passes' ops, for whichever this edit owns. */\n function withArrivalsSettled<T>(fn: () => T, owns: boolean, ownsPasses = false): T {\n const result = fn();\n if (owns) for (const p of arrivals.settle()) env.writePose(p.id, p.to);\n if (ownsPasses) passes.run();\n return result;\n }\n\n function batch<T>(label: string, fn: () => T, options: SceneBatchOptions = {}): T {\n const outermost = !mode.open();\n // Captured at open, not at recordEntry: by the time the batch closes\n // the selection has already moved to wherever the batch put it.\n if (outermost) collecting = { label, ops: [], outer: [], entry: { selectionBefore: env.selection(), ...options } };\n const ownsArrivals = outermost && arrivals.open(true);\n const ownsPasses = outermost && passes.open();\n return env.labeled(label, 'call', () => notifier.deferred(() => mode.during('batch', () => {\n if (!outermost) return fn();\n try {\n const result = log.atomically(\n () => withArrivalsSettled(fn, ownsArrivals, ownsPasses),\n () => (collecting?.ops.length ?? 0) > 0,\n );\n if (ownsArrivals) arrivals.emitReflow();\n return result;\n } catch (err) {\n // Reverted already; the half-built entry must not reach history.\n unwind(collecting?.outer ?? []);\n collecting = null;\n throw err;\n }\n }), () => {\n if (ownsArrivals) arrivals.abandon();\n if (ownsPasses) passes.abandon();\n if (!outermost || !collecting) return;\n const finished = collecting;\n collecting = null;\n // The ops were applied as they were issued, so the entry is pushed\n // without re-applying.\n if (finished.ops.length > 0) {\n engine.recordEntry(finished.ops, finished.label, finished.entry);\n }\n }));\n }\n\n function record(kind: string, payload: unknown, label: string): void {\n // While passes listen, a bare edit is a batch of its own, so their ops land in its entry.\n if (mode.records() && !collecting && passes.any()) return void batch(label, () => record(kind, payload, label));\n passes.note(kind, payload);\n env.labeled(label, 'call', () => {\n if (mode.records() && !collecting) {\n // The engine applies the op itself, which is what lets it coalesce.\n mode.during('own', () => engine.applyOps([env.makeOp(kind, payload)], label));\n } else {\n env.runOp(kind, payload);\n log.logApplied(kind, payload);\n if (mode.records()) collecting!.ops.push(env.makeOp(kind, payload));\n else if (!mode.untracked()) captured?.push(env.makeOp(kind, payload));\n }\n });\n notifier.notify();\n }\n\n return {\n record,\n edit: (label, touchesLayout, fn) => (touchesLayout && arrivals.bareLayoutEdit() ? batch(label, fn) : fn()),\n batch,\n untracked(fn) {\n const versionBefore = notifier.version();\n const ownsArrivals = arrivals.open(true);\n return notifier.deferred(() => mode.during('untracked', () => {\n const result = log.atomically(() => withArrivalsSettled(fn, ownsArrivals), () => false);\n if (ownsArrivals) arrivals.emitReflow();\n return result;\n }), () => {\n if (ownsArrivals) arrivals.abandon();\n if (notifier.version() !== versionBefore) engine.seal();\n });\n },\n joins: () => mode.records() && collecting !== null,\n join(ops, adapter) {\n const into = collecting!;\n const mark = into.ops.length;\n const outer: Op[] = [];\n try {\n log.atomically(() => {\n for (const op of ops) {\n const before = into.ops.length;\n const result = op.apply(adapter);\n if (into.ops.length > before || result === false || result === 'noop') continue;\n outer.push(bindOpToAdapter(op, adapter));\n into.ops.push(outer[outer.length - 1]);\n }\n }, () => false);\n } catch (err) {\n unwind(outer);\n into.ops.length = mark;\n throw err;\n }\n into.outer.push(...outer);\n },\n capturing(step) {\n const enclosing = captured;\n const ops: Op[] = [];\n captured = ops;\n try {\n return { result: step(), ops };\n } finally {\n captured = enclosing;\n }\n },\n engineDriven: (fn) => mode.during('engine', () => notifier.muted(fn)),\n };\n}\n"],"mappings":"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"}
|