@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderSceneToPixels.js","names":["viewToMat3"],"sources":["../../src/canvas/renderSceneToPixels.ts"],"sourcesContent":["/**\n * `renderSceneToPixels` — headless rasterization of a scene-space rect to raw RGBA pixels at an\n * explicit, per-axis scale. It drives the same WebGL2 pipeline (`WeaselRenderer` +\n * `buildSceneViewCommands`) as the on-screen view, so print, thumbnail, and export callers get\n * the same pixels the screen would produce at that scale.\n *\n * Environment contract:\n * - Density arrives exclusively via `scale`; this function never reads\n * `window.devicePixelRatio`.\n * - Rounding: output width = max(1, round(sourceRect.width × |scale.x|)), height analogously.\n * When round() lands below rect × scale, the rightmost/bottom sub-pixel band of scene content\n * is cropped; when it rounds up, that band is padded (the `background` fill, or transparent).\n * - Context lifetime: a caller-supplied `gl` is caller-owned. It is never disposed here, and\n * its drawing buffer must be at least width × height pixels (the render targets the\n * bottom-left region). Auto-created canvases are discarded with the session.\n * - Context loss: throws — a lost context cannot produce pixels, and the silent-noop\n * convention of the screen path would return all-zero bytes.\n * - Output: top-down row order, NON-premultiplied (\"straight\") RGBA. The GL framebuffer is\n * premultiplied and bottom-up; readback flips and unpremultiplies.\n * - Determinism: same context + same inputs → same bytes. Cross-driver / cross-machine byte\n * equality is NOT guaranteed; do not build golden-image tests on committed bytes.\n * - Image quality: image textures upload with mipmaps so minified bitmaps don't moiré, and\n * curve tessellation runs at an output-scale tolerance (`flattenTolerancePx`, default 0.25\n * output px).\n */\nimport { WeaselRenderer } from '@weasel-js/renderer';\nimport { viewToMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node, Scene } from 'core/scene/types';\nimport { buildSceneViewCommands, type SceneViewDrawOne } from './sceneViewRender';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport { defaultDrawOne } from './defaultDrawOne';\nimport { warmFonts, type FontRequest } from '@weasel-js/font';\nimport { isPaintKindKnown, warmPaintKinds } from '@weasel-js/renderer';\nimport { renderNeeds } from './renderNeeds';\nimport { straightTopDown } from './rasterReadback';\n\n/** Plain RGBA raster — structurally `ImageData`-compatible ({ width, height,\n * data }), deliberately free of printer/dpi/physical-unit concepts. */\nexport interface RasterImage {\n width: number;\n height: number;\n /** Top-down, straight (non-premultiplied) RGBA, 4 bytes per pixel. */\n data: Uint8ClampedArray;\n}\n\n/** Minimal canvas contract for `createCanvas` injection: `OffscreenCanvas`,\n * an HTML canvas, or a test fake. */\nexport interface HeadlessCanvasLike {\n width: number;\n height: number;\n getContext(contextId: 'webgl2', options?: WebGLContextAttributes): unknown;\n}\n\n/** What to render, at what resolution, and with which quality settings. */\nexport interface RenderSceneToPixelsArgs<TData, TLayer extends string, TPose> {\n scene: Scene<TData, TLayer, TPose>;\n /** Scene-space rect to render (origin + size in scene units). Output pixel\n * dimensions follow from rect × scale (round, min 1 — see module doc). */\n sourceRect: { x: number; y: number; width: number; height: number };\n /** Output pixels per scene unit, per axis. Anisotropic values supported. A\n * negative axis is flipped: the output is sized from its magnitude, and the\n * rect's far edge on that axis lands on the output's top or left. */\n scale: { x: number; y: number };\n /** Per-node draw callback. Default: `defaultDrawOne` with `resolveImage`\n * merged into the `NodePaintCtx` the scene walk supplies. Custom `drawOne`\n * callers that still want resolver injection should call\n * `defaultDrawOne(node, pose, view, { ...ctx, resolveImage })` themselves —\n * dropping the walk's `ctx` drops derived geometry with it. */\n drawOne?: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Bitmap resolver for image nodes — lets consumers reuse their own decode\n * caches. `undefined` results paint the deterministic grey placeholder\n * outline (see `NodePaintCtx.resolveImage`). */\n resolveImage?: (node: Node<TData, TLayer, TPose>) => ImageBitmap | undefined;\n /** Per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the on-screen canvas uses so an\n * export matches what the user is looking at. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides`, so an export shows the frame on screen. */\n colorOverrides?: ColorOverrideRegistry;\n /** Background fill (any CSS color accepted by the renderer). Default:\n * fully transparent. Passing a color is always valid. */\n background?: string;\n /** Caller-owned WebGL2 context to render with. Mutually exclusive with\n * `createCanvas`. Never disposed by this call. */\n gl?: WebGL2RenderingContext;\n /** One-shot canvas factory (DOM canvas, OffscreenCanvas, or test fake).\n * Mutually exclusive with `gl`. Default: `OffscreenCanvas` when\n * available, else `document.createElement('canvas')`. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n /** Max curve-flattening error in OUTPUT pixels. Default 0.25. Measured\n * along each path's most stretched axis. */\n flattenTolerancePx?: number;\n /** Commands drawn over the scene in output-pixel space — origin at the\n * output's top-left, one unit per output pixel. Annotations, watermarks,\n * and `renderDebugSnapshot`'s overlay ride here. */\n overlay?: readonly DrawCommand[];\n}\n\n/** The result of planning a headless render: how big the output is, the view\n * that maps the source rect onto it, and the commands to draw. */\nexport interface PixelRenderPlan {\n width: number;\n height: number;\n view: View;\n commands: DrawCommand[];\n}\n\n/** Pure planning half of `renderSceneToPixels`: output dimensions, the\n * anisotropic `View`, and the full command list (background + view-wrapped\n * scene). Exported for tests and for callers targeting their own renderer. */\nexport function planPixelRender<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas' | 'flattenTolerancePx'>,\n): PixelRenderPlan {\n const { sourceRect, scale } = args;\n for (const [label, v] of [\n ['sourceRect.width', sourceRect.width], ['sourceRect.height', sourceRect.height],\n ] as const) {\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a positive finite number, got ${v}`);\n }\n }\n for (const [label, v] of [['scale.x', scale.x], ['scale.y', scale.y]] as const) {\n if (!Number.isFinite(v) || v === 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a nonzero finite number, got ${v}`);\n }\n }\n for (const [label, v] of [\n ['sourceRect.x', sourceRect.x], ['sourceRect.y', sourceRect.y],\n ] as const) {\n if (!Number.isFinite(v)) {\n throw new Error(`renderSceneToPixels: ${label} must be a finite number, got ${v}`);\n }\n }\n const width = Math.max(1, Math.round(sourceRect.width * Math.abs(scale.x)));\n const height = Math.max(1, Math.round(sourceRect.height * Math.abs(scale.y)));\n const view: View = {\n x: scale.x < 0 ? sourceRect.x + sourceRect.width : sourceRect.x,\n y: scale.y < 0 ? sourceRect.y + sourceRect.height : sourceRect.y,\n scale: { x: scale.x, y: scale.y },\n };\n\n const resolveImage = args.resolveImage as ((n: Node<unknown, string, unknown>) => ImageBitmap | undefined) | undefined;\n const drawOne: SceneViewDrawOne<TData, TLayer, TPose> =\n args.drawOne ?? ((node, pose, view, ctx) => defaultDrawOne(node, pose, view, { ...ctx, resolveImage }));\n\n const commands: DrawCommand[] = [];\n if (args.background !== undefined) {\n // Screen-space (pre-view) fill so rounding can never leave uncovered\n // edge pixels.\n commands.push({\n kind: 'path',\n path: { kind: 'rect', x: 0, y: 0, width, height },\n fill: { paint: { kind: 'solid', color: args.background } },\n });\n }\n commands.push(...buildSceneViewCommands(\n args.scene, view, drawOne, undefined, args.alphaFor, undefined, args.colorOverrides,\n ));\n if (args.overlay) commands.push(...args.overlay);\n return { width, height, view, commands };\n}\n\nfunction defaultCreateCanvas(width: number, height: number): HeadlessCanvasLike {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n if (typeof document !== 'undefined') {\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n return c;\n }\n throw new Error('renderSceneToPixels: no canvas source in this environment — supply `gl` or `createCanvas`');\n}\n\n/** Where a {@link RasterSession} draws. Pass `gl` or `createCanvas`, or\n * neither for the default canvas source. */\nexport interface RasterSessionOptions {\n /** Caller-owned WebGL2 context. The session never disposes it. Its drawing\n * buffer must be at least as large as every render the session does. */\n gl?: WebGL2RenderingContext;\n /** Canvas factory, called once, at the size of the first render. The\n * session grows the canvas when a later render needs more room and never\n * shrinks it. Default: `OffscreenCanvas` when available, else a detached\n * DOM canvas. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n}\n\n/** One render through a {@link RasterSession}: everything\n * `renderSceneToPixels` takes except where to draw. */\nexport type RasterRenderArgs<TData, TLayer extends string, TPose> =\n Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas'>;\n\n/** One renderer and its GPU caches, kept across any number of headless\n * renders. Scenes, source rects and scales may differ per render. */\nexport interface RasterSession {\n /** Same contract as `renderSceneToPixels`. Throws once disposed. */\n render<TData, TLayer extends string, TPose>(args: RasterRenderArgs<TData, TLayer, TPose>): RasterImage;\n /** Free every GL resource the session created — programs, geometry, and\n * the texture and mesh caches — and drop its own canvas. A caller-owned\n * context stays open. Idempotent. */\n dispose(): void;\n}\n\n/**\n * Open a raster session: one `WeaselRenderer` that many headless renders\n * share, for a consumer producing thumbnails or pages in bulk. Programs\n * compile once, and a bitmap or recurring mesh uploads once, rather than per\n * render. `renderSceneToPixels` is a session opened for one render.\n *\n * The renderer is created on the first render, so opening a session costs\n * nothing and fails nothing. Each render draws into the bottom-left\n * `width × height` of the buffer and reads that region back.\n */\nexport function createRasterSession(options: RasterSessionOptions = {}): RasterSession {\n if (options.gl && options.createCanvas) {\n throw new Error('raster session: `gl` and `createCanvas` are mutually exclusive');\n }\n let canvas: HeadlessCanvasLike | null = null;\n let gl: WebGL2RenderingContext | undefined = options.gl;\n let renderer: WeaselRenderer | null = null;\n let disposed = false;\n\n function contextFor(width: number, height: number): WebGL2RenderingContext {\n if (!options.gl) {\n if (!canvas) {\n canvas = (options.createCanvas ?? defaultCreateCanvas)(width, height);\n canvas.width = width;\n canvas.height = height;\n // Same context attributes as the screen path (Canvas.tsx).\n gl = (canvas.getContext('webgl2', { preserveDrawingBuffer: true, stencil: true }) as WebGL2RenderingContext | null) ?? undefined;\n } else {\n if (canvas.width < width) canvas.width = width;\n if (canvas.height < height) canvas.height = height;\n }\n }\n if (!gl || typeof (gl as Partial<WebGL2RenderingContext>).enable !== 'function') {\n throw new Error('raster session: WebGL2 is unavailable — supply `gl` or a WebGL2-capable `createCanvas`');\n }\n if (isLost(gl)) {\n throw new Error('raster session: the WebGL2 context is lost — dispose this session and open another');\n }\n // A context clamps a drawing buffer it cannot allocate, and reading past\n // the real one returns transparent pixels rather than failing.\n const { drawingBufferWidth: bufferW, drawingBufferHeight: bufferH } = gl;\n if (bufferW < width || bufferH < height) {\n throw new Error(\n `raster session: a ${width} × ${height} render does not fit the context's `\n + `${bufferW} × ${bufferH} drawing buffer — lower \\`scale\\` or render the source rect in tiles`,\n );\n }\n return gl;\n }\n\n return {\n render(args) {\n if (disposed) throw new Error('raster session: render() on a disposed session');\n const plan = planPixelRender(args);\n const { width, height } = plan;\n const ctx = contextFor(width, height);\n const flattenTolerance = { px: args.flattenTolerancePx ?? 0.25 };\n if (!renderer) {\n renderer = new WeaselRenderer({\n gl: ctx,\n width,\n height,\n dpr: 1,\n imageMinification: 'mipmap',\n flattenTolerance,\n // Dynamic canvas-SDF glyphs must all bake inline — this path is\n // synchronous with no notify-and-redraw, and print must be complete.\n bakeBudget: Infinity,\n });\n } else {\n renderer.resize({ width, height, dpr: 1 });\n renderer.setFlattenTolerance(flattenTolerance);\n }\n renderer.render(plan.commands, viewToMat3(plan.view));\n if (isLost(ctx)) {\n throw new Error('raster session: the WebGL2 context was lost during render');\n }\n const raw = new Uint8Array(width * height * 4);\n ctx.readPixels(0, 0, width, height, ctx.RGBA, ctx.UNSIGNED_BYTE, raw);\n return { width, height, data: straightTopDown(raw, width, height) };\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n renderer?.dispose();\n renderer = null;\n // A browser caps live contexts per page and evicts the oldest, which is\n // a canvas on screen; a dropped reference frees nothing until collected.\n if (canvas && !options.createCanvas) gl?.getExtension('WEBGL_lose_context')?.loseContext();\n canvas = null;\n gl = undefined;\n },\n };\n}\n\nfunction isLost(gl: WebGL2RenderingContext): boolean {\n return typeof gl.isContextLost === 'function' && gl.isContextLost() === true;\n}\n\n/** What {@link warmRender} loads. */\nexport interface WarmRenderOptions<TData = unknown, TLayer extends string = string, TPose = unknown> {\n /** The render about to happen, as `renderSceneToPixels` or\n * `RasterSession.render` will take it. Its commands are built the way the\n * render builds them, and only the faces and paint kinds they draw with\n * load. */\n render?: RasterRenderArgs<TData, TLayer, TPose>;\n /** Commands about to be drawn, read the same way as `render`'s. Both may be\n * given; what either needs loads. */\n commands?: readonly DrawCommand[];\n /** Fonts to load in place of the ones derived — family names or single\n * variants, as `warmFonts` takes them. */\n families?: readonly (string | FontRequest)[];\n /** Paint kinds to load in place of the ones derived, as `warmPaintKinds`\n * takes them. */\n paintKinds?: readonly string[];\n}\n\n/**\n * Load everything a synchronous render would otherwise draw as nothing — the\n * lazily registered font atlases and the paint kinds loaded on demand — so\n * that a `renderSceneToPixels` or `RasterSession.render` issued after it\n * resolves is complete.\n *\n * Pass the render (`{ render: args }`) or its commands, and only what they\n * draw with loads: each face their text is set in, through the same\n * resolution the renderer does, and each paint kind they name. A rejection\n * then means something this render needs failed to load. `families` and\n * `paintKinds` replace either derived list.\n *\n * With neither `render` nor `commands`, an omitted list loads everything\n * registered: every font family and every kind that has a loader — which\n * fails when any one of them does, and fetches what nothing draws.\n */\nexport function warmRender<TData, TLayer extends string, TPose>(\n opts: WarmRenderOptions<TData, TLayer, TPose> = {},\n): Promise<void> {\n let { families, paintKinds } = opts;\n if (opts.render || opts.commands) {\n let planned: DrawCommand[];\n try {\n planned = opts.render ? planPixelRender(opts.render).commands : [];\n } catch (err) {\n return Promise.reject(err);\n }\n const needs = renderNeeds([...planned, ...(opts.commands ?? [])]);\n families ??= needs.fonts;\n // A kind nothing registered draws nothing whether or not this waits.\n paintKinds ??= needs.paintKinds.filter(isPaintKindKnown);\n }\n return Promise.all([warmFonts(families), warmPaintKinds(paintKinds)]).then(() => undefined);\n}\n\n/**\n * Render part of a scene to raw pixels, with no canvas mounted and no React\n * involved — for export, thumbnails, print, and pixel-diff tests.\n *\n * Output size follows from the source rect and the requested scale, so\n * rendering the same region at a higher scale is a real resolution increase\n * rather than an upscale: curves are re-flattened and glyphs re-rasterized for\n * the output resolution.\n *\n * Synchronous, so nothing lands mid-render: a paint kind loaded on demand\n * (`mesh-gradient`, or one declared with `registerPaintKindLoader`) that has\n * not loaded yet draws nothing, and neither does text set in a font atlas\n * still fetching — one registered `{ lazy: true }` is not fetched until text\n * first asks for it. `await warmRender({ render: args })` first.\n *\n * Each call opens a {@link RasterSession} and disposes it before returning.\n * Rendering many scenes, open one with `createRasterSession` instead.\n */\nexport function renderSceneToPixels<TData, TLayer extends string, TPose>(\n args: RenderSceneToPixelsArgs<TData, TLayer, TPose>,\n): RasterImage {\n const { gl, createCanvas, ...renderArgs } = args;\n const session = createRasterSession({ gl, createCanvas });\n try {\n return session.render(renderArgs);\n } finally {\n session.dispose();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderSceneToPixels.js","names":["viewToMat3"],"sources":["../../src/canvas/renderSceneToPixels.ts"],"sourcesContent":["/**\n * `renderSceneToPixels` — headless rasterization of a scene-space rect to raw RGBA pixels at an\n * explicit, per-axis scale. It drives the same WebGL2 pipeline (`WeaselRenderer` +\n * `buildSceneViewCommands`) as the on-screen view, so print, thumbnail, and export callers get\n * the same pixels the screen would produce at that scale.\n *\n * Environment contract:\n * - Density arrives exclusively via `scale`; this function never reads\n * `window.devicePixelRatio`.\n * - Rounding: output width = max(1, round(sourceRect.width × |scale.x|)), height analogously.\n * When round() lands below rect × scale, the rightmost/bottom sub-pixel band of scene content\n * is cropped; when it rounds up, that band is padded (the `background` fill, or transparent).\n * - Context lifetime: a caller-supplied `gl` is caller-owned. It is never disposed here, and\n * its drawing buffer must be at least width × height pixels (the render targets the\n * bottom-left region). Auto-created canvases are discarded with the session.\n * - Context loss: throws — a lost context cannot produce pixels, and the silent-noop\n * convention of the screen path would return all-zero bytes.\n * - Output: top-down row order, NON-premultiplied (\"straight\") RGBA. The GL framebuffer is\n * premultiplied and bottom-up; readback flips and unpremultiplies.\n * - Determinism: same context + same inputs → same bytes. Cross-driver / cross-machine byte\n * equality is NOT guaranteed; do not build golden-image tests on committed bytes.\n * - Image quality: image textures upload with mipmaps so minified bitmaps don't moiré, and\n * curve tessellation runs at an output-scale tolerance (`flattenTolerancePx`, default 0.25\n * output px).\n */\nimport { WeaselRenderer } from '@weasel-js/renderer';\nimport { viewToMat3 } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { View } from '../core/viewport/view';\nimport type { Node, Scene } from 'core/scene/types';\nimport { buildSceneViewCommands, type SceneViewDrawOne } from './sceneViewRender';\nimport type { ColorOverrideRegistry } from '@weasel-js/animator';\nimport { defaultDrawOne } from './defaultDrawOne';\nimport { warmFonts, type FontRequest } from '@weasel-js/font';\nimport { isPaintKindKnown, warmPaintKinds } from '@weasel-js/renderer';\nimport { renderNeeds } from './renderNeeds';\nimport { straightTopDown } from './rasterReadback';\nimport { annotationLayerIds } from 'core/scene/annotation';\nimport { sceneLayerKey } from './sceneLayerPaint';\nimport { CANVAS_CONTEXT_ATTRIBUTES } from './contextAttributes';\n\n/** Plain RGBA raster — structurally `ImageData`-compatible ({ width, height,\n * data }), deliberately free of printer/dpi/physical-unit concepts. */\nexport interface RasterImage {\n width: number;\n height: number;\n /** Top-down, straight (non-premultiplied) RGBA, 4 bytes per pixel. */\n data: Uint8ClampedArray;\n}\n\n/** Minimal canvas contract for `createCanvas` injection: `OffscreenCanvas`,\n * an HTML canvas, or a test fake. */\nexport interface HeadlessCanvasLike {\n width: number;\n height: number;\n getContext(contextId: 'webgl2', options?: WebGLContextAttributes): unknown;\n}\n\n/** What to render, at what resolution, and with which quality settings. */\nexport interface RenderSceneToPixelsArgs<TData, TLayer extends string, TPose> {\n scene: Scene<TData, TLayer, TPose>;\n /** Scene-space rect to render (origin + size in scene units). Output pixel\n * dimensions follow from rect × scale (round, min 1 — see module doc). */\n sourceRect: { x: number; y: number; width: number; height: number };\n /** Output pixels per scene unit, per axis. Anisotropic values supported. A\n * negative axis is flipped: the output is sized from its magnitude, and the\n * rect's far edge on that axis lands on the output's top or left. */\n scale: { x: number; y: number };\n /** Per-node draw callback. Default: `defaultDrawOne` with `resolveImage`\n * merged into the `NodePaintCtx` the scene walk supplies. Custom `drawOne`\n * callers that still want resolver injection should call\n * `defaultDrawOne(node, pose, view, { ...ctx, resolveImage })` themselves —\n * dropping the walk's `ctx` drops derived geometry with it. */\n drawOne?: SceneViewDrawOne<TData, TLayer, TPose>;\n /** Bitmap resolver for image nodes — lets consumers reuse their own decode\n * caches. `undefined` results paint the deterministic grey placeholder\n * outline (see `NodePaintCtx.resolveImage`). */\n resolveImage?: (node: Node<TData, TLayer, TPose>) => ImageBitmap | undefined;\n /** Per-id alpha multiplier, mirroring `<SceneCanvas>`'s scene-slot\n * `alphaFor`. Pass the same function the on-screen canvas uses so an\n * export matches what the user is looking at. Defaults to `() => 1`. */\n alphaFor?: (id: string) => number;\n /** Animated vertex colors to paint, typically an animator's\n * `colorOverrides`, so an export shows the frame on screen. */\n colorOverrides?: ColorOverrideRegistry;\n /** Background fill (any CSS color accepted by the renderer). Default:\n * fully transparent. Passing a color is always valid. */\n background?: string;\n /** Caller-owned WebGL2 context to render with. Mutually exclusive with\n * `createCanvas`. Never disposed by this call. */\n gl?: WebGL2RenderingContext;\n /** One-shot canvas factory (DOM canvas, OffscreenCanvas, or test fake).\n * Mutually exclusive with `gl`. Default: `OffscreenCanvas` when\n * available, else `document.createElement('canvas')`. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n /** Max curve-flattening error in OUTPUT pixels. Default 0.25. Measured\n * along each path's most stretched axis. */\n flattenTolerancePx?: number;\n /** Commands drawn over the scene in output-pixel space — origin at the\n * output's top-left, one unit per output pixel. Annotations, watermarks,\n * and `renderDebugSnapshot`'s overlay ride here. */\n overlay?: readonly DrawCommand[];\n /** Draw the scene's annotation layers (`isAnnotationLayer`). Default\n * `false`: a raster is an export, which leaves them out as the SVG export\n * does. Pass `true` to reproduce the canvas as it looked. */\n annotations?: boolean;\n}\n\n/** The result of planning a headless render: how big the output is, the view\n * that maps the source rect onto it, and the commands to draw. */\nexport interface PixelRenderPlan {\n width: number;\n height: number;\n view: View;\n commands: DrawCommand[];\n}\n\n/** Pure planning half of `renderSceneToPixels`: output dimensions, the\n * anisotropic `View`, and the full command list (background + view-wrapped\n * scene). Exported for tests and for callers targeting their own renderer. */\nexport function planPixelRender<TData, TLayer extends string, TPose>(\n args: Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas' | 'flattenTolerancePx'>,\n): PixelRenderPlan {\n const { sourceRect, scale } = args;\n for (const [label, v] of [\n ['sourceRect.width', sourceRect.width], ['sourceRect.height', sourceRect.height],\n ] as const) {\n if (!Number.isFinite(v) || v <= 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a positive finite number, got ${v}`);\n }\n }\n for (const [label, v] of [['scale.x', scale.x], ['scale.y', scale.y]] as const) {\n if (!Number.isFinite(v) || v === 0) {\n throw new Error(`renderSceneToPixels: ${label} must be a nonzero finite number, got ${v}`);\n }\n }\n for (const [label, v] of [\n ['sourceRect.x', sourceRect.x], ['sourceRect.y', sourceRect.y],\n ] as const) {\n if (!Number.isFinite(v)) {\n throw new Error(`renderSceneToPixels: ${label} must be a finite number, got ${v}`);\n }\n }\n const width = Math.max(1, Math.round(sourceRect.width * Math.abs(scale.x)));\n const height = Math.max(1, Math.round(sourceRect.height * Math.abs(scale.y)));\n const view: View = {\n x: scale.x < 0 ? sourceRect.x + sourceRect.width : sourceRect.x,\n y: scale.y < 0 ? sourceRect.y + sourceRect.height : sourceRect.y,\n scale: { x: scale.x, y: scale.y },\n };\n\n const resolveImage = args.resolveImage as ((n: Node<unknown, string, unknown>) => ImageBitmap | undefined) | undefined;\n const drawOne: SceneViewDrawOne<TData, TLayer, TPose> =\n args.drawOne ?? ((node, pose, view, ctx) => defaultDrawOne(node, pose, view, { ...ctx, resolveImage }));\n\n const commands: DrawCommand[] = [];\n if (args.background !== undefined) {\n // Screen-space (pre-view) fill so rounding can never leave uncovered\n // edge pixels.\n commands.push({\n kind: 'path',\n path: { kind: 'rect', x: 0, y: 0, width, height },\n fill: { paint: { kind: 'solid', color: args.background } },\n });\n }\n const layers = args.annotations ? undefined : {\n layerVisibility: Object.fromEntries([...annotationLayerIds(args.scene.layers)].map((id) => [sceneLayerKey(id), false])),\n };\n commands.push(...buildSceneViewCommands(\n args.scene, view, drawOne, undefined, args.alphaFor, layers, args.colorOverrides,\n ));\n if (args.overlay) commands.push(...args.overlay);\n return { width, height, view, commands };\n}\n\nfunction defaultCreateCanvas(width: number, height: number): HeadlessCanvasLike {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n if (typeof document !== 'undefined') {\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n return c;\n }\n throw new Error('renderSceneToPixels: no canvas source in this environment — supply `gl` or `createCanvas`');\n}\n\n/** Where a {@link RasterSession} draws. Pass `gl` or `createCanvas`, or\n * neither for the default canvas source. */\nexport interface RasterSessionOptions {\n /** Caller-owned WebGL2 context. The session never disposes it. Its drawing\n * buffer must be at least as large as every render the session does. */\n gl?: WebGL2RenderingContext;\n /** Canvas factory, called once, at the size of the first render. The\n * session grows the canvas when a later render needs more room and never\n * shrinks it. Default: `OffscreenCanvas` when available, else a detached\n * DOM canvas. */\n createCanvas?: (widthPx: number, heightPx: number) => HeadlessCanvasLike;\n}\n\n/** One render through a {@link RasterSession}: everything\n * `renderSceneToPixels` takes except where to draw. */\nexport type RasterRenderArgs<TData, TLayer extends string, TPose> =\n Omit<RenderSceneToPixelsArgs<TData, TLayer, TPose>, 'gl' | 'createCanvas'>;\n\n/** One renderer and its GPU caches, kept across any number of headless\n * renders. Scenes, source rects and scales may differ per render. */\nexport interface RasterSession {\n /** Same contract as `renderSceneToPixels`. Throws once disposed. */\n render<TData, TLayer extends string, TPose>(args: RasterRenderArgs<TData, TLayer, TPose>): RasterImage;\n /** Free every GL resource the session created — programs, geometry, and\n * the texture and mesh caches — and drop its own canvas. A caller-owned\n * context stays open. Idempotent. */\n dispose(): void;\n}\n\n/**\n * Open a raster session: one `WeaselRenderer` that many headless renders\n * share, for a consumer producing thumbnails or pages in bulk. Programs\n * compile once, and a bitmap or recurring mesh uploads once, rather than per\n * render. `renderSceneToPixels` is a session opened for one render.\n *\n * The renderer is created on the first render, so opening a session costs\n * nothing and fails nothing. Each render draws into the bottom-left\n * `width × height` of the buffer and reads that region back.\n */\nexport function createRasterSession(options: RasterSessionOptions = {}): RasterSession {\n if (options.gl && options.createCanvas) {\n throw new Error('raster session: `gl` and `createCanvas` are mutually exclusive');\n }\n let canvas: HeadlessCanvasLike | null = null;\n let gl: WebGL2RenderingContext | undefined = options.gl;\n let renderer: WeaselRenderer | null = null;\n let disposed = false;\n\n function contextFor(width: number, height: number): WebGL2RenderingContext {\n if (!options.gl) {\n if (!canvas) {\n canvas = (options.createCanvas ?? defaultCreateCanvas)(width, height);\n canvas.width = width;\n canvas.height = height;\n gl = (canvas.getContext('webgl2', CANVAS_CONTEXT_ATTRIBUTES) as WebGL2RenderingContext | null) ?? undefined;\n } else {\n if (canvas.width < width) canvas.width = width;\n if (canvas.height < height) canvas.height = height;\n }\n }\n if (!gl || typeof (gl as Partial<WebGL2RenderingContext>).enable !== 'function') {\n throw new Error('raster session: WebGL2 is unavailable — supply `gl` or a WebGL2-capable `createCanvas`');\n }\n if (isLost(gl)) {\n throw new Error('raster session: the WebGL2 context is lost — dispose this session and open another');\n }\n // A context clamps a drawing buffer it cannot allocate, and reading past\n // the real one returns transparent pixels rather than failing.\n const { drawingBufferWidth: bufferW, drawingBufferHeight: bufferH } = gl;\n if (bufferW < width || bufferH < height) {\n throw new Error(\n `raster session: a ${width} × ${height} render does not fit the context's `\n + `${bufferW} × ${bufferH} drawing buffer — lower \\`scale\\` or render the source rect in tiles`,\n );\n }\n return gl;\n }\n\n return {\n render(args) {\n if (disposed) throw new Error('raster session: render() on a disposed session');\n const plan = planPixelRender(args);\n const { width, height } = plan;\n const ctx = contextFor(width, height);\n const flattenTolerance = { px: args.flattenTolerancePx ?? 0.25 };\n if (!renderer) {\n renderer = new WeaselRenderer({\n gl: ctx,\n width,\n height,\n dpr: 1,\n imageMinification: 'mipmap',\n flattenTolerance,\n // Dynamic canvas-SDF glyphs must all bake inline — this path is\n // synchronous with no notify-and-redraw, and print must be complete.\n bakeBudget: Infinity,\n });\n } else {\n renderer.resize({ width, height, dpr: 1 });\n renderer.setFlattenTolerance(flattenTolerance);\n }\n renderer.render(plan.commands, viewToMat3(plan.view));\n if (isLost(ctx)) {\n throw new Error('raster session: the WebGL2 context was lost during render');\n }\n const raw = new Uint8Array(width * height * 4);\n ctx.readPixels(0, 0, width, height, ctx.RGBA, ctx.UNSIGNED_BYTE, raw);\n return { width, height, data: straightTopDown(raw, width, height) };\n },\n dispose() {\n if (disposed) return;\n disposed = true;\n renderer?.dispose();\n renderer = null;\n // A browser caps live contexts per page and evicts the oldest, which is\n // a canvas on screen; a dropped reference frees nothing until collected.\n if (canvas && !options.createCanvas) gl?.getExtension('WEBGL_lose_context')?.loseContext();\n canvas = null;\n gl = undefined;\n },\n };\n}\n\nfunction isLost(gl: WebGL2RenderingContext): boolean {\n return typeof gl.isContextLost === 'function' && gl.isContextLost() === true;\n}\n\n/** What {@link warmRender} loads. */\nexport interface WarmRenderOptions<TData = unknown, TLayer extends string = string, TPose = unknown> {\n /** The render about to happen, as `renderSceneToPixels` or\n * `RasterSession.render` will take it. Its commands are built the way the\n * render builds them, and only the faces and paint kinds they draw with\n * load. */\n render?: RasterRenderArgs<TData, TLayer, TPose>;\n /** Commands about to be drawn, read the same way as `render`'s. Both may be\n * given; what either needs loads. */\n commands?: readonly DrawCommand[];\n /** Fonts to load in place of the ones derived — family names or single\n * variants, as `warmFonts` takes them. */\n families?: readonly (string | FontRequest)[];\n /** Paint kinds to load in place of the ones derived, as `warmPaintKinds`\n * takes them. */\n paintKinds?: readonly string[];\n}\n\n/**\n * Load everything a synchronous render would otherwise draw as nothing — the\n * lazily registered font atlases and the paint kinds loaded on demand — so\n * that a `renderSceneToPixels` or `RasterSession.render` issued after it\n * resolves is complete.\n *\n * Pass the render (`{ render: args }`) or its commands, and only what they\n * draw with loads: each face their text is set in, through the same\n * resolution the renderer does, and each paint kind they name. A rejection\n * then means something this render needs failed to load. `families` and\n * `paintKinds` replace either derived list.\n *\n * With neither `render` nor `commands`, an omitted list loads everything\n * registered: every font family and every kind that has a loader — which\n * fails when any one of them does, and fetches what nothing draws.\n */\nexport function warmRender<TData, TLayer extends string, TPose>(\n opts: WarmRenderOptions<TData, TLayer, TPose> = {},\n): Promise<void> {\n let { families, paintKinds } = opts;\n if (opts.render || opts.commands) {\n let planned: DrawCommand[];\n try {\n planned = opts.render ? planPixelRender(opts.render).commands : [];\n } catch (err) {\n return Promise.reject(err);\n }\n const needs = renderNeeds([...planned, ...(opts.commands ?? [])]);\n families ??= needs.fonts;\n // A kind nothing registered draws nothing whether or not this waits.\n paintKinds ??= needs.paintKinds.filter(isPaintKindKnown);\n }\n return Promise.all([warmFonts(families), warmPaintKinds(paintKinds)]).then(() => undefined);\n}\n\n/**\n * Render part of a scene to raw pixels, with no canvas mounted and no React\n * involved — for export, thumbnails, print, and pixel-diff tests.\n *\n * Output size follows from the source rect and the requested scale, so\n * rendering the same region at a higher scale is a real resolution increase\n * rather than an upscale: curves are re-flattened and glyphs re-rasterized for\n * the output resolution.\n *\n * Synchronous, so nothing lands mid-render: a paint kind loaded on demand\n * (`mesh-gradient`, or one declared with `registerPaintKindLoader`) that has\n * not loaded yet draws nothing, and neither does text set in a font atlas\n * still fetching — one registered `{ lazy: true }` is not fetched until text\n * first asks for it. `await warmRender({ render: args })` first.\n *\n * Each call opens a {@link RasterSession} and disposes it before returning.\n * Rendering many scenes, open one with `createRasterSession` instead.\n */\nexport function renderSceneToPixels<TData, TLayer extends string, TPose>(\n args: RenderSceneToPixelsArgs<TData, TLayer, TPose>,\n): RasterImage {\n const { gl, createCanvas, ...renderArgs } = args;\n const session = createRasterSession({ gl, createCanvas });\n try {\n return session.render(renderArgs);\n } finally {\n session.dispose();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sceneAdapter.js","names":[],"sources":["../../src/canvas/sceneAdapter.ts"],"sourcesContent":["/**\n * Synthesize a `<Canvas>`-compatible adapter from a `Scene` primitive.\n * `getPose` / `setPose` return / write **local** coordinates (relative to\n * parent); world composition is the renderer's job via `composeWorldPose`.\n */\nimport type {\n AreaSelectAdapter,\n ClipboardSnapshot,\n InsertAdapter,\n PasteCtx,\n LassoHitMode,\n LassoSelectAdapter,\n RegionPickView,\n LayerEnumerableAdapter,\n MoveAdapter,\n ResizeAdapter,\n RotateAdapter,\n} from 'core/adapters/types';\nimport type { LayoutStrategy } from '../layout/types';\nimport type { Op } from 'core/ops/types';\nimport type { Node, Scene } from 'core/scene/types';\nimport { definesFrame, effectivePose } from 'core/scene/effectivePose';\nimport { sceneMutators } from 'core/scene/sceneMutators';\nimport { asNodeId } from 'core/scene/types';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport { applyOpsTo } from 'core/applyOps';\nimport { hitTestArea, hitTestLassoPolygon, regionPickOptions } from 'canvas/deps/hitTestArea';\nimport type { ScenePickSourceOptions } from 'canvas/pickWalk';\nimport {\n poseDescriptorForNode,\n translatePoseViaDescriptor,\n visualBoundsViaDescriptor,\n type PoseDescriptor,\n} from 'interactions/actions/resize/geometry';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport { composesFrames, composeWorldPose, frameAtOrAbove } from 'core/scene/composePose';\nimport type { Bounds } from '../core/viewport/fitViewToBounds';\nimport { scenePoseTarget } from '../animation/poseTarget';\nimport { cascadingSetPose, containerCascade } from './poseCascade';\nimport { pasteSceneSnapshot, snapshotSceneSelection, type SceneClipboardEnv } from './sceneClipboard';\n\n/** Minimal selection contract `sceneToAdapter` needs to wire `getSelection` /\n * `setSelection`. Matches `useSelection().adapterMethods` plus an imperative\n * read; pass `useSelection()` itself or `selection.adapterMethods`. */\nexport interface SceneAdapterSelection {\n get?(): string[];\n set?(ids: string[]): void;\n getSelection?(): string[];\n setSelection?(ids: string[]): void;\n}\n\n/** Sibling z-order surface used by reorder ops. Inlined here (rather than\n * pulled from `core/ops/reorder` to avoid import cycles); the `null` parent\n * channel addresses the root sibling list. */\n/** @internal */\ninterface ReorderAdapter {\n getChildren(parentId: string | null): string[];\n setChildOrder(parentId: string | null, ids: string[]): void;\n}\n\n/** Everything the interaction layer can ask of a scene, as one type: the\n * move, resize, rotate, selection, layer and reorder contracts combined.\n * `<SceneCanvas>` synthesizes one of these, which is why most consumers never\n * touch an adapter directly. */\nexport type SceneCanvasAdapter<TData, TLayer extends string, TPose> =\n & MoveAdapter<Node<TData, TLayer, TPose>, TPose>\n & ResizeAdapter<Node<TData, TLayer, TPose>, TPose>\n & RotateAdapter<Node<TData, TLayer, TPose>, TPose>\n & AreaSelectAdapter\n & LassoSelectAdapter\n & LayerEnumerableAdapter<TLayer>\n & ReorderAdapter\n & Partial<InsertAdapter<Node<TData, TLayer, TPose>>>\n // Optional on the underlying adapter contracts, but unconditionally\n // provided by a scene-backed adapter.\n & {\n getParent(id: string): string | null;\n getLayers: NonNullable<LayerEnumerableAdapter<TLayer>['getLayers']>;\n /** The node's pose with every ancestor's frame folded in. Equal to\n * `getPose` unless the scene composes poses. */\n getWorldPose(id: string): TPose;\n /** Whether `id`'s pose is a frame its children are expressed in — see\n * `definesFrame`. */\n definesFrame(id: string): boolean;\n /** Present only when a composing strategy is configured; the render\n * walk feature-detects it. */\n composePose?(parent: TPose, child: TPose): TPose;\n getSelection(): string[];\n setSelection(ids: string[]): void;\n insertNode(node: Node<TData, TLayer, TPose>, index?: number): void;\n removeNode(id: string): void;\n /** Replace a node's `data` — what a `setData` op writes through. */\n setData(id: string, data: TData): void;\n /** Swap a container's layout — what a `setLayout` op writes through. */\n setLayout(id: string, layout: LayoutStrategy<TPose> | string | null): void;\n /** Move a node to another layer — what a `setLayer` op writes through. */\n setLayer(id: string, layer: TLayer): void;\n /** Hide or show a node — what a `setHidden` op writes through. */\n setHidden(id: string, hidden: boolean): void;\n /** Lock or unlock a node — what a `setLocked` op writes through. */\n setLocked(id: string, locked: boolean): void;\n getRemovalClosure(ids: readonly string[]): string[];\n applyOps(ops: Op[], label?: string): void;\n snapshotSelection(ids: string[]): ClipboardSnapshot;\n commitPaste(\n clipboard: ClipboardSnapshot,\n offset: { dx: number; dy: number },\n ctx?: PasteCtx,\n ): Node<TData, TLayer, TPose>[];\n getPasteOffset(clipboard: ClipboardSnapshot): { dx: number; dy: number };\n };\n\n/** Optional extras for the synthesized adapter. Pass `commitInsert` to wire\n * the insert gesture into a Scene-owned canvas; the returned object becomes\n * a leaf on the layer named by `layer` (default `'default'`). */\nexport interface SceneToAdapterOptions<TData, TLayer extends string, TPose> {\n /** Factory for new objects. Returning `null` aborts the insert. */\n commitInsert?: (bounds: Bounds) => {\n pose: TPose;\n data: TData;\n id?: string;\n } | null;\n /** Layer to place inserted nodes on. Defaults to the trivial-form layer. */\n insertLayer?: TLayer;\n /** Selection source: the result of `useSelection()` (or its\n * `adapterMethods`). When omitted, `getSelection` returns `[]` and\n * `setSelection` is a noop, so the marquee gesture updates nothing. */\n selection?: SceneAdapterSelection;\n /** How to read and rewrite this scene's poses. Default `AUTO_POSE_DESCRIPTOR`. */\n poseDescriptor?: PoseDescriptor<TPose>;\n}\n\n/** Derive the adapter for a scene. Reads go straight to the scene; writes are\n * ops applied through its history, so anything done through the adapter is\n * undoable.\n *\n * Moving a container carries its contents either way the scene composes\n * poses. Under a frame (`scene.composition`) the one pose written moves them.\n * Under the absolute-pose model `setPose` on a container translates every\n * descendant by the same delta, and a batch that poses descendants too undoes\n * exactly only when it goes through this adapter's `applyOps`. */\nexport function sceneToAdapter<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n options: SceneToAdapterOptions<TData, TLayer, TPose> = {},\n): SceneCanvasAdapter<TData, TLayer, TPose> {\n const sel = options.selection;\n const getSelection = sel?.getSelection ?? sel?.get ?? (() => [] as string[]);\n const setSelection = sel?.setSelection ?? sel?.set ?? (() => {});\n const d = (options.poseDescriptor ?? AUTO_POSE_DESCRIPTOR) as PoseDescriptor<TPose>;\n const poseBounds = (p: TPose, node: unknown): Bounds =>\n visualBoundsViaDescriptor(p, poseDescriptorForNode(d, node));\n const translate = (p: TPose, dx: number, dy: number, node?: unknown): TPose =>\n translatePoseViaDescriptor(\n p, dx, dy, node === undefined ? d : poseDescriptorForNode(d, node),\n );\n\n const composition = scene.composition;\n const composes = composesFrames(composition);\n const pickOpts: ScenePickSourceOptions<unknown> = {};\n // The asking view's layer gate and camera, over the adapter's own options.\n const pickOptsFor = (view: RegionPickView | undefined): ScenePickSourceOptions<unknown> =>\n (view ? { ...pickOpts, ...regionPickOptions(view, undefined) } : pickOpts);\n\n const cascade = composes ? null : { setPose: cascadingSetPose(scene, d) };\n\n const clipboardEnv: SceneClipboardEnv<TData, TLayer, TPose> = {\n scene,\n composes,\n worldPoseOf: (id) => adapter.getWorldPose(id),\n frameAtOrAbove: (id) => frameAtOrAbove(adapter, id),\n poseBounds,\n translate,\n };\n\n const adapter: SceneCanvasAdapter<TData, TLayer, TPose> = {\n ...sceneMutators(scene),\n // What is shown, where the mutators' other users read the document:\n // the nodes not hidden, and the effective pose.\n getNodes() {\n const gates = nodeGates(scene);\n return scene.renderOrderNodes().filter((n) => !gates.hidden(n.id));\n },\n getPose(id) {\n const nid = asNodeId(id);\n const n = scene.get(nid);\n if (!n) throw new Error(`sceneToAdapter: unknown node \"${id}\"`);\n return effectivePose(scene, n);\n },\n definesFrame(id) {\n const n = scene.get(asNodeId(id));\n return n === undefined || definesFrame(n);\n },\n getWorldPose(id) {\n if (!composes) return adapter.getPose(id);\n return composeWorldPose(adapter, id, composition.compose);\n },\n // Only present when it would do something: the render walk skips the fold\n // entirely when this is absent, which is the absolute-pose path.\n ...(composes ? { composePose: composition.compose } : {}),\n ...(cascade ? { setPose: (id: string, pose: TPose) => cascade.setPose(asNodeId(id), pose) } : {}),\n poseTarget(id, descriptor) {\n if (cascade === null || scene.get(asNodeId(id))?.kind !== 'container') {\n return scenePoseTarget(scene, id, descriptor ?? d);\n }\n // Its descendants hold poses of their own: a commit moves them, and they are shown beside it.\n const carried = containerCascade(scene, asNodeId(id), d, (pose) => adapter.setPose(id, pose));\n return scenePoseTarget(scene, id, descriptor ?? d, carried);\n },\n getLayers: () => scene.layers.map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax })),\n subscribeLayers: (listener) => scene.subscribe(listener),\n getLayout: (id) => scene.layoutOf(asNodeId(id)),\n // With a label the ops are one batch checkpoint; without one they are\n // applied transiently, with no checkpoint (the AreaSelectAdapter contract).\n applyOps(ops: Op[], label?: string) {\n if (label !== undefined) {\n scene.applyBatch(ops, label, this);\n } else {\n applyOpsTo(this, ops);\n }\n },\n getSelection,\n setSelection,\n // The live marquee's and lasso's own hit-tests. Containers come back:\n // this adapter's consumer has nothing to fold children into them.\n hitTestArea(rect: Bounds, view?: RegionPickView) {\n return hitTestArea(\n scene,\n rect,\n pickOptsFor(view),\n d as PoseDescriptor<unknown>,\n { includeContainers: true },\n );\n },\n hitTestLasso(polygon, mode: LassoHitMode, view?: RegionPickView) {\n return hitTestLassoPolygon(\n scene,\n polygon,\n mode,\n pickOptsFor(view),\n d as PoseDescriptor<unknown>,\n { includeContainers: true },\n );\n },\n // A pure factory: it constructs a Node spec but does NOT touch the scene.\n // The `InsertOp` → `adapter.insertNode` → `scene.add` path is the sole\n // insertion path. Calling `scene.add` here too would double-insert, and\n // the second add throws on id collision.\n ...(options.commitInsert\n ? {\n commitInsert: (bounds: { x: number; y: number; width: number; height: number }) => {\n const created = options.commitInsert!(bounds);\n if (!created) return null;\n const layer = (options.insertLayer ?? ('default' as TLayer));\n const id = created.id === undefined ? scene.mintId() : asNodeId(created.id);\n return {\n kind: 'leaf',\n id,\n layer,\n pose: created.pose,\n data: created.data,\n parent: null,\n } as Node<TData, TLayer, TPose>;\n },\n }\n : {}),\n // Clipboard seam (`useClipboardOps` / the paste ingestion path).\n snapshotSelection(ids: string[]): ClipboardSnapshot {\n return snapshotSceneSelection(clipboardEnv, ids);\n },\n commitPaste(\n clipboard: ClipboardSnapshot,\n offset: { dx: number; dy: number },\n ctx?: PasteCtx,\n ): Node<TData, TLayer, TPose>[] {\n return pasteSceneSnapshot(clipboardEnv, clipboard, offset, ctx);\n },\n // Constant cascade: the hook re-snapshots the just-created items, so\n // repeated pastes stack +12/+12 each time.\n getPasteOffset(): { dx: number; dy: number } {\n return { dx: 12, dy: 12 };\n },\n };\n\n return adapter;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sceneAdapter.js","names":[],"sources":["../../src/canvas/sceneAdapter.ts"],"sourcesContent":["/**\n * Synthesize a `<Canvas>`-compatible adapter from a `Scene` primitive.\n * `getPose` / `setPose` return / write **local** coordinates (relative to\n * parent); world composition is the renderer's job via `composeWorldPose`.\n */\nimport type {\n AreaSelectAdapter,\n ClipboardSnapshot,\n InsertAdapter,\n PasteCtx,\n LassoHitMode,\n LassoSelectAdapter,\n RegionPickView,\n LayerEnumerableAdapter,\n MoveAdapter,\n ResizeAdapter,\n RotateAdapter,\n} from 'core/adapters/types';\nimport type { LayoutStrategy } from '../layout/types';\nimport type { Op } from 'core/ops/types';\nimport type { Node, Scene } from 'core/scene/types';\nimport { definesFrame, effectivePose } from 'core/scene/effectivePose';\nimport { sceneMutators } from 'core/scene/sceneMutators';\nimport { asNodeId } from 'core/scene/types';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport { applyOpsTo } from 'core/applyOps';\nimport { hitTestArea, hitTestLassoPolygon, regionPickOptions } from 'canvas/deps/hitTestArea';\nimport type { ScenePickSourceOptions } from 'canvas/pickWalk';\nimport {\n poseDescriptorForNode,\n translatePoseViaDescriptor,\n visualBoundsViaDescriptor,\n type PoseDescriptor,\n} from 'interactions/actions/resize/geometry';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport { composesFrames, composeWorldPose, frameAtOrAbove } from 'core/scene/composePose';\nimport type { Bounds } from '../core/viewport/fitViewToBounds';\nimport { scenePoseTarget } from '../animation/poseTarget';\nimport { cascadingSetPose, containerCascade } from './poseCascade';\nimport { pasteSceneSnapshot, snapshotSceneSelection, type SceneClipboardEnv } from './sceneClipboard';\n\n/** Minimal selection contract `sceneToAdapter` needs to wire `getSelection` /\n * `setSelection`. Matches `useSelection().adapterMethods` plus an imperative\n * read; pass `useSelection()` itself or `selection.adapterMethods`. */\nexport interface SceneAdapterSelection {\n get?(): string[];\n set?(ids: string[]): void;\n getSelection?(): string[];\n setSelection?(ids: string[]): void;\n}\n\n/** Sibling z-order surface used by reorder ops. Inlined here (rather than\n * pulled from `core/ops/reorder` to avoid import cycles); the `null` parent\n * channel addresses the root sibling list. */\n/** @internal */\ninterface ReorderAdapter {\n getChildren(parentId: string | null): string[];\n setChildOrder(parentId: string | null, ids: string[]): void;\n}\n\n/** Everything the interaction layer can ask of a scene, as one type: the\n * move, resize, rotate, selection, layer and reorder contracts combined.\n * `<SceneCanvas>` synthesizes one of these, which is why most consumers never\n * touch an adapter directly. */\nexport type SceneCanvasAdapter<TData, TLayer extends string, TPose> =\n & MoveAdapter<Node<TData, TLayer, TPose>, TPose>\n & ResizeAdapter<Node<TData, TLayer, TPose>, TPose>\n & RotateAdapter<Node<TData, TLayer, TPose>, TPose>\n & AreaSelectAdapter\n & LassoSelectAdapter\n & LayerEnumerableAdapter<TLayer>\n & ReorderAdapter\n & Partial<InsertAdapter<Node<TData, TLayer, TPose>>>\n // Optional on the underlying adapter contracts, but unconditionally\n // provided by a scene-backed adapter.\n & {\n getParent(id: string): string | null;\n getLayers: NonNullable<LayerEnumerableAdapter<TLayer>['getLayers']>;\n /** The node's pose with every ancestor's frame folded in. Equal to\n * `getPose` unless the scene composes poses. */\n getWorldPose(id: string): TPose;\n /** Whether `id`'s pose is a frame its children are expressed in — see\n * `definesFrame`. */\n definesFrame(id: string): boolean;\n /** Present only when a composing strategy is configured; the render\n * walk feature-detects it. */\n composePose?(parent: TPose, child: TPose): TPose;\n getSelection(): string[];\n setSelection(ids: string[]): void;\n insertNode(node: Node<TData, TLayer, TPose>, index?: number): void;\n removeNode(id: string): void;\n /** Replace a node's `data` — what a `setData` op writes through. */\n setData(id: string, data: TData): void;\n /** Swap a container's layout — what a `setLayout` op writes through. */\n setLayout(id: string, layout: LayoutStrategy<TPose> | string | null): void;\n /** Move a node to another layer — what a `setLayer` op writes through. */\n setLayer(id: string, layer: TLayer): void;\n /** Retarget a derived node's sources — what a `setDependsOn` op writes through. */\n setDependsOn(id: string, dependsOn: readonly string[] | 'children' | undefined): void;\n /** Hide or show a node — what a `setHidden` op writes through. */\n setHidden(id: string, hidden: boolean): void;\n /** Lock or unlock a node — what a `setLocked` op writes through. */\n setLocked(id: string, locked: boolean): void;\n getRemovalClosure(ids: readonly string[]): string[];\n applyOps(ops: Op[], label?: string): void;\n snapshotSelection(ids: string[]): ClipboardSnapshot;\n commitPaste(\n clipboard: ClipboardSnapshot,\n offset: { dx: number; dy: number },\n ctx?: PasteCtx,\n ): Node<TData, TLayer, TPose>[];\n getPasteOffset(clipboard: ClipboardSnapshot): { dx: number; dy: number };\n };\n\n/** Optional extras for the synthesized adapter. Pass `commitInsert` to wire\n * the insert gesture into a Scene-owned canvas; the returned object becomes\n * a leaf on the layer named by `layer` (default `'default'`). */\nexport interface SceneToAdapterOptions<TData, TLayer extends string, TPose> {\n /** Factory for new objects. Returning `null` aborts the insert. */\n commitInsert?: (bounds: Bounds) => {\n pose: TPose;\n data: TData;\n id?: string;\n } | null;\n /** Layer to place inserted nodes on. Defaults to the trivial-form layer. */\n insertLayer?: TLayer;\n /** Selection source: the result of `useSelection()` (or its\n * `adapterMethods`). When omitted, `getSelection` returns `[]` and\n * `setSelection` is a noop, so the marquee gesture updates nothing. */\n selection?: SceneAdapterSelection;\n /** How to read and rewrite this scene's poses. Default `AUTO_POSE_DESCRIPTOR`. */\n poseDescriptor?: PoseDescriptor<TPose>;\n}\n\n/** Derive the adapter for a scene. Reads go straight to the scene; writes are\n * ops applied through its history, so anything done through the adapter is\n * undoable.\n *\n * Moving a container carries its contents either way the scene composes\n * poses. Under a frame (`scene.composition`) the one pose written moves them.\n * Under the absolute-pose model `setPose` on a container translates every\n * descendant by the same delta, and a batch that poses descendants too undoes\n * exactly only when it goes through this adapter's `applyOps`. */\nexport function sceneToAdapter<TData, TLayer extends string, TPose>(\n scene: Scene<TData, TLayer, TPose>,\n options: SceneToAdapterOptions<TData, TLayer, TPose> = {},\n): SceneCanvasAdapter<TData, TLayer, TPose> {\n const sel = options.selection;\n const getSelection = sel?.getSelection ?? sel?.get ?? (() => [] as string[]);\n const setSelection = sel?.setSelection ?? sel?.set ?? (() => {});\n const d = (options.poseDescriptor ?? AUTO_POSE_DESCRIPTOR) as PoseDescriptor<TPose>;\n const poseBounds = (p: TPose, node: unknown): Bounds =>\n visualBoundsViaDescriptor(p, poseDescriptorForNode(d, node));\n const translate = (p: TPose, dx: number, dy: number, node?: unknown): TPose =>\n translatePoseViaDescriptor(\n p, dx, dy, node === undefined ? d : poseDescriptorForNode(d, node),\n );\n\n const composition = scene.composition;\n const composes = composesFrames(composition);\n const pickOpts: ScenePickSourceOptions<unknown> = {};\n // The asking view's layer gate and camera, over the adapter's own options.\n const pickOptsFor = (view: RegionPickView | undefined): ScenePickSourceOptions<unknown> =>\n (view ? { ...pickOpts, ...regionPickOptions(view, undefined) } : pickOpts);\n\n const cascade = composes ? null : { setPose: cascadingSetPose(scene, d) };\n\n const clipboardEnv: SceneClipboardEnv<TData, TLayer, TPose> = {\n scene,\n composes,\n worldPoseOf: (id) => adapter.getWorldPose(id),\n frameAtOrAbove: (id) => frameAtOrAbove(adapter, id),\n poseBounds,\n translate,\n };\n\n const adapter: SceneCanvasAdapter<TData, TLayer, TPose> = {\n ...sceneMutators(scene),\n // What is shown, where the mutators' other users read the document:\n // the nodes not hidden, and the effective pose.\n getNodes() {\n const gates = nodeGates(scene);\n return scene.renderOrderNodes().filter((n) => !gates.hidden(n.id));\n },\n getPose(id) {\n const nid = asNodeId(id);\n const n = scene.get(nid);\n if (!n) throw new Error(`sceneToAdapter: unknown node \"${id}\"`);\n return effectivePose(scene, n);\n },\n definesFrame(id) {\n const n = scene.get(asNodeId(id));\n return n === undefined || definesFrame(n);\n },\n getWorldPose(id) {\n if (!composes) return adapter.getPose(id);\n return composeWorldPose(adapter, id, composition.compose);\n },\n // Only present when it would do something: the render walk skips the fold\n // entirely when this is absent, which is the absolute-pose path.\n ...(composes ? { composePose: composition.compose } : {}),\n ...(cascade ? { setPose: (id: string, pose: TPose) => cascade.setPose(asNodeId(id), pose) } : {}),\n poseTarget(id, descriptor) {\n if (cascade === null || scene.get(asNodeId(id))?.kind !== 'container') {\n return scenePoseTarget(scene, id, descriptor ?? d);\n }\n // Its descendants hold poses of their own: a commit moves them, and they are shown beside it.\n const carried = containerCascade(scene, asNodeId(id), d, (pose) => adapter.setPose(id, pose));\n return scenePoseTarget(scene, id, descriptor ?? d, carried);\n },\n getLayers: () => scene.layers.map((l) => ({ id: l.id, visible: l.visible, parallax: l.parallax })),\n subscribeLayers: (listener) => scene.subscribe(listener),\n getLayout: (id) => scene.layoutOf(asNodeId(id)),\n // With a label the ops are one batch checkpoint; without one they are\n // applied transiently, with no checkpoint (the AreaSelectAdapter contract).\n applyOps(ops: Op[], label?: string) {\n if (label !== undefined) {\n scene.applyBatch(ops, label, this);\n } else {\n applyOpsTo(this, ops);\n }\n },\n getSelection,\n setSelection,\n // The live marquee's and lasso's own hit-tests. Containers come back:\n // this adapter's consumer has nothing to fold children into them.\n hitTestArea(rect: Bounds, view?: RegionPickView) {\n return hitTestArea(\n scene,\n rect,\n pickOptsFor(view),\n d as PoseDescriptor<unknown>,\n { includeContainers: true },\n );\n },\n hitTestLasso(polygon, mode: LassoHitMode, view?: RegionPickView) {\n return hitTestLassoPolygon(\n scene,\n polygon,\n mode,\n pickOptsFor(view),\n d as PoseDescriptor<unknown>,\n { includeContainers: true },\n );\n },\n // A pure factory: it constructs a Node spec but does NOT touch the scene.\n // The `InsertOp` → `adapter.insertNode` → `scene.add` path is the sole\n // insertion path. Calling `scene.add` here too would double-insert, and\n // the second add throws on id collision.\n ...(options.commitInsert\n ? {\n commitInsert: (bounds: { x: number; y: number; width: number; height: number }) => {\n const created = options.commitInsert!(bounds);\n if (!created) return null;\n const layer = (options.insertLayer ?? ('default' as TLayer));\n const id = created.id === undefined ? scene.mintId() : asNodeId(created.id);\n return {\n kind: 'leaf',\n id,\n layer,\n pose: created.pose,\n data: created.data,\n parent: null,\n } as Node<TData, TLayer, TPose>;\n },\n }\n : {}),\n // Clipboard seam (`useClipboardOps` / the paste ingestion path).\n snapshotSelection(ids: string[]): ClipboardSnapshot {\n return snapshotSceneSelection(clipboardEnv, ids);\n },\n commitPaste(\n clipboard: ClipboardSnapshot,\n offset: { dx: number; dy: number },\n ctx?: PasteCtx,\n ): Node<TData, TLayer, TPose>[] {\n return pasteSceneSnapshot(clipboardEnv, clipboard, offset, ctx);\n },\n // Constant cascade: the hook re-snapshots the just-created items, so\n // repeated pastes stack +12/+12 each time.\n getPasteOffset(): { dx: number; dy: number } {\n return { dx: 12, dy: 12 };\n },\n };\n\n return adapter;\n}\n"],"mappings":"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"}
|
|
@@ -39,7 +39,7 @@ function useCanvasPaint(paintRef, lease, target, frame, markPainted) {
|
|
|
39
39
|
const c = paintTargetRef.current ?? ownCanvasRef.current;
|
|
40
40
|
if (!c) return false;
|
|
41
41
|
const rect = paintRectRef.current;
|
|
42
|
-
const { layers: paintLayers, width: w, height: h, debugSink: sink, frameStats: timed, dpr: dprIn, flattenTolerance: paintFlattenTolerance, layerGroups: groups, shaders: paintShaders, retain } = inputsRef.current;
|
|
42
|
+
const { layers: paintLayers, width: w, height: h, debugSink: sink, frameStats: timed, dpr: dprIn, flattenTolerance: paintFlattenTolerance, layerGroups: groups, shaders: paintShaders, retain, imageMinification } = inputsRef.current;
|
|
43
43
|
if (w === 0 || h === 0) return false;
|
|
44
44
|
const debugFrame = beginDebugFrame(sink, timed, paintLayers);
|
|
45
45
|
const renderer = lease.bind(c, {
|
|
@@ -50,6 +50,7 @@ function useCanvasPaint(paintRef, lease, target, frame, markPainted) {
|
|
|
50
50
|
tile: rect,
|
|
51
51
|
flattenTolerance: paintFlattenTolerance,
|
|
52
52
|
retainBudget: retain ? retain.budget : void 0,
|
|
53
|
+
imageMinification,
|
|
53
54
|
shaders: paintShaders
|
|
54
55
|
});
|
|
55
56
|
if (!renderer) return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCanvasPaint.js","names":[],"sources":["../../src/canvas/useCanvasPaint.ts"],"sourcesContent":["import { useCallback, useEffect, useInsertionEffect, useRef } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { viewToMat3 } from '@weasel-js/renderer';\nimport type { View } from 'core/viewport/view';\nimport {\n DEFAULT_LAYER_RETAIN, drawLayers, type LayerCommandCache, type LayerRetainPolicy, type RenderLayer,\n} from 'core/layers/render';\nimport type { CanvasDebugSink } from '../debug/types';\nimport { beginDebugFrame } from '../debug/useDebugSink';\nimport type { CanvasRendererLease } from './canvasRenderer';\nimport type { CanvasProps } from './canvasProps';\nimport type { CanvasElements } from './useCanvasElements';\nimport { NO_LAYER_VISIBILITY, type LayerGate } from './useExtraLayers';\n\n/** What a frame's paint reads that a React render owns. */\nexport interface PaintInputs extends Pick<CanvasProps<unknown>, 'dpr' | 'layerGroups' | 'shaders' | 'flattenTolerance' | 'retain'> {\n layers: RenderLayer<unknown>[];\n width: number;\n height: number;\n debugSink: CanvasDebugSink | null;\n frameStats: boolean;\n}\n\n/** What a frame's paint reads that changes outside a render. */\nexport interface PaintFrame {\n inputsRef: { readonly current: PaintInputs };\n viewRef: { readonly current: View };\n gateRef: { readonly current: LayerGate };\n helpersRef: { readonly current: unknown };\n}\n\n/**\n * Which content version the last paint showed. `markPainted` records the current one, read\n * from `contentVersion`'s latest committed value.\n */\nexport function usePaintedVersion(contentVersion: (() => number) | undefined): {\n getPaintedVersion: () => number;\n markPainted: () => void;\n} {\n const contentVersionRef = useLatest(contentVersion);\n const paintedVersionRef = useRef(0);\n const getPaintedVersion = useCallback(() => paintedVersionRef.current, []);\n const markPainted = useCallback(() => {\n paintedVersionRef.current = contentVersionRef.current?.() ?? 0;\n }, [contentVersionRef]);\n return { getPaintedVersion, markPainted };\n}\n\nfunction retainPolicy(retain: PaintInputs['retain']): LayerRetainPolicy | false {\n if (retain === false) return false;\n const minCommands = retain?.minCommands;\n return minCommands === undefined ? DEFAULT_LAYER_RETAIN : { minCommands };\n}\n\n/**\n * Publish the canvas's paint into `paintRef`, which the frame loop calls. The paint binds the\n * leased renderer to whichever canvas pixels go to and draws the layer stack through the\n * current view. The layer command cache lives as long as the canvas.\n */\nexport function useCanvasPaint(\n paintRef: { current: () => boolean },\n lease: CanvasRendererLease,\n target: Pick<CanvasElements, 'paintTargetRef' | 'ownCanvasRef' | 'paintRectRef'>,\n frame: PaintFrame,\n markPainted: () => void,\n): void {\n const { paintTargetRef, ownCanvasRef, paintRectRef } = target;\n const { inputsRef, viewRef, gateRef, helpersRef } = frame;\n const layerCacheRef = useRef<LayerCommandCache>(new Map());\n\n const paint = useCallback((): boolean => {\n const c = paintTargetRef.current ?? ownCanvasRef.current;\n if (!c) return false;\n const rect = paintRectRef.current;\n const {\n layers: paintLayers, width: w, height: h, debugSink: sink, frameStats: timed,\n dpr: dprIn,\n flattenTolerance: paintFlattenTolerance,\n layerGroups: groups, shaders: paintShaders, retain,\n } = inputsRef.current;\n if (w === 0 || h === 0) return false;\n\n const debugFrame = beginDebugFrame(sink, timed, paintLayers);\n\n const renderer = lease.bind(c, { width: w, height: h, dpr: dprIn }, {\n tile: rect,\n flattenTolerance: paintFlattenTolerance,\n retainBudget: retain ? retain.budget : undefined,\n shaders: paintShaders,\n });\n // No WebGL2 (jsdom), or a context that is lost for now: nothing to paint.\n if (!renderer) return false;\n\n const view = viewRef.current;\n const { layerVisibility: vis, layerOrder: order } = gateRef.current;\n const spans = debugFrame?.time();\n const commands = drawLayers(\n paintLayers,\n helpersRef.current,\n vis ?? NO_LAYER_VISIBILITY,\n order,\n view,\n { width: w, height: h },\n layerCacheRef.current,\n undefined,\n groups,\n spans,\n retainPolicy(retain),\n );\n renderer.render(commands, viewToMat3(view), spans ? { spans } : undefined);\n debugFrame?.end(renderer.lastFrameStats());\n markPainted();\n return true;\n }, [gateRef, helpersRef, inputsRef, lease, markPainted, ownCanvasRef, paintRectRef, paintTargetRef, viewRef]);\n useInsertionEffect(() => {\n paintRef.current = paint;\n });\n\n useEffect(() => {\n const layerCache = layerCacheRef.current;\n return () => { layerCache.clear(); };\n }, []);\n}\n"],"mappings":";;;;;;;;;;;;;AAmCA,SAAgB,kBAAkB,gBAGhC;CACA,MAAM,oBAAoB,UAAU,cAAc;CAClD,MAAM,oBAAoB,OAAO,CAAC;CAKlC,OAAO;EAAE,mBAJiB,kBAAkB,kBAAkB,SAAS,CAAC,CAI/D;EAAmB,aAHR,kBAAkB;GACpC,kBAAkB,UAAU,kBAAkB,UAAU,KAAK;EAC/D,GAAG,CAAC,iBAAiB,CACO;CAAY;AAC1C;AAEA,SAAS,aAAa,QAA0D;CAC9E,IAAI,WAAW,OAAO,OAAO;CAC7B,MAAM,cAAc,QAAQ;CAC5B,OAAO,gBAAgB,SAAY,uBAAuB,EAAE,YAAY;AAC1E;;;;;;AAOA,SAAgB,eACd,UACA,OACA,QACA,OACA,aACM;CACN,MAAM,EAAE,gBAAgB,cAAc,iBAAiB;CACvD,MAAM,EAAE,WAAW,SAAS,SAAS,eAAe;CACpD,MAAM,gBAAgB,uBAA0B,IAAI,IAAI,CAAC;CAEzD,MAAM,QAAQ,kBAA2B;EACvC,MAAM,IAAI,eAAe,WAAW,aAAa;EACjD,IAAI,CAAC,GAAG,OAAO;EACf,MAAM,OAAO,aAAa;EAC1B,MAAM,EACJ,QAAQ,aAAa,OAAO,GAAG,QAAQ,GAAG,WAAW,MAAM,YAAY,OACvE,KAAK,OACL,kBAAkB,uBAClB,aAAa,QAAQ,SAAS,cAAc,
|
|
1
|
+
{"version":3,"file":"useCanvasPaint.js","names":[],"sources":["../../src/canvas/useCanvasPaint.ts"],"sourcesContent":["import { useCallback, useEffect, useInsertionEffect, useRef } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { viewToMat3 } from '@weasel-js/renderer';\nimport type { View } from 'core/viewport/view';\nimport {\n DEFAULT_LAYER_RETAIN, drawLayers, type LayerCommandCache, type LayerRetainPolicy, type RenderLayer,\n} from 'core/layers/render';\nimport type { CanvasDebugSink } from '../debug/types';\nimport { beginDebugFrame } from '../debug/useDebugSink';\nimport type { CanvasRendererLease } from './canvasRenderer';\nimport type { CanvasProps } from './canvasProps';\nimport type { CanvasElements } from './useCanvasElements';\nimport { NO_LAYER_VISIBILITY, type LayerGate } from './useExtraLayers';\n\n/** What a frame's paint reads that a React render owns. */\nexport interface PaintInputs extends Pick<CanvasProps<unknown>, 'dpr' | 'layerGroups' | 'shaders' | 'flattenTolerance' | 'imageMinification' | 'retain'> {\n layers: RenderLayer<unknown>[];\n width: number;\n height: number;\n debugSink: CanvasDebugSink | null;\n frameStats: boolean;\n}\n\n/** What a frame's paint reads that changes outside a render. */\nexport interface PaintFrame {\n inputsRef: { readonly current: PaintInputs };\n viewRef: { readonly current: View };\n gateRef: { readonly current: LayerGate };\n helpersRef: { readonly current: unknown };\n}\n\n/**\n * Which content version the last paint showed. `markPainted` records the current one, read\n * from `contentVersion`'s latest committed value.\n */\nexport function usePaintedVersion(contentVersion: (() => number) | undefined): {\n getPaintedVersion: () => number;\n markPainted: () => void;\n} {\n const contentVersionRef = useLatest(contentVersion);\n const paintedVersionRef = useRef(0);\n const getPaintedVersion = useCallback(() => paintedVersionRef.current, []);\n const markPainted = useCallback(() => {\n paintedVersionRef.current = contentVersionRef.current?.() ?? 0;\n }, [contentVersionRef]);\n return { getPaintedVersion, markPainted };\n}\n\nfunction retainPolicy(retain: PaintInputs['retain']): LayerRetainPolicy | false {\n if (retain === false) return false;\n const minCommands = retain?.minCommands;\n return minCommands === undefined ? DEFAULT_LAYER_RETAIN : { minCommands };\n}\n\n/**\n * Publish the canvas's paint into `paintRef`, which the frame loop calls. The paint binds the\n * leased renderer to whichever canvas pixels go to and draws the layer stack through the\n * current view. The layer command cache lives as long as the canvas.\n */\nexport function useCanvasPaint(\n paintRef: { current: () => boolean },\n lease: CanvasRendererLease,\n target: Pick<CanvasElements, 'paintTargetRef' | 'ownCanvasRef' | 'paintRectRef'>,\n frame: PaintFrame,\n markPainted: () => void,\n): void {\n const { paintTargetRef, ownCanvasRef, paintRectRef } = target;\n const { inputsRef, viewRef, gateRef, helpersRef } = frame;\n const layerCacheRef = useRef<LayerCommandCache>(new Map());\n\n const paint = useCallback((): boolean => {\n const c = paintTargetRef.current ?? ownCanvasRef.current;\n if (!c) return false;\n const rect = paintRectRef.current;\n const {\n layers: paintLayers, width: w, height: h, debugSink: sink, frameStats: timed,\n dpr: dprIn,\n flattenTolerance: paintFlattenTolerance,\n layerGroups: groups, shaders: paintShaders, retain, imageMinification,\n } = inputsRef.current;\n if (w === 0 || h === 0) return false;\n\n const debugFrame = beginDebugFrame(sink, timed, paintLayers);\n\n const renderer = lease.bind(c, { width: w, height: h, dpr: dprIn }, {\n tile: rect,\n flattenTolerance: paintFlattenTolerance,\n retainBudget: retain ? retain.budget : undefined,\n imageMinification,\n shaders: paintShaders,\n });\n // No WebGL2 (jsdom), or a context that is lost for now: nothing to paint.\n if (!renderer) return false;\n\n const view = viewRef.current;\n const { layerVisibility: vis, layerOrder: order } = gateRef.current;\n const spans = debugFrame?.time();\n const commands = drawLayers(\n paintLayers,\n helpersRef.current,\n vis ?? NO_LAYER_VISIBILITY,\n order,\n view,\n { width: w, height: h },\n layerCacheRef.current,\n undefined,\n groups,\n spans,\n retainPolicy(retain),\n );\n renderer.render(commands, viewToMat3(view), spans ? { spans } : undefined);\n debugFrame?.end(renderer.lastFrameStats());\n markPainted();\n return true;\n }, [gateRef, helpersRef, inputsRef, lease, markPainted, ownCanvasRef, paintRectRef, paintTargetRef, viewRef]);\n useInsertionEffect(() => {\n paintRef.current = paint;\n });\n\n useEffect(() => {\n const layerCache = layerCacheRef.current;\n return () => { layerCache.clear(); };\n }, []);\n}\n"],"mappings":";;;;;;;;;;;;;AAmCA,SAAgB,kBAAkB,gBAGhC;CACA,MAAM,oBAAoB,UAAU,cAAc;CAClD,MAAM,oBAAoB,OAAO,CAAC;CAKlC,OAAO;EAAE,mBAJiB,kBAAkB,kBAAkB,SAAS,CAAC,CAI/D;EAAmB,aAHR,kBAAkB;GACpC,kBAAkB,UAAU,kBAAkB,UAAU,KAAK;EAC/D,GAAG,CAAC,iBAAiB,CACO;CAAY;AAC1C;AAEA,SAAS,aAAa,QAA0D;CAC9E,IAAI,WAAW,OAAO,OAAO;CAC7B,MAAM,cAAc,QAAQ;CAC5B,OAAO,gBAAgB,SAAY,uBAAuB,EAAE,YAAY;AAC1E;;;;;;AAOA,SAAgB,eACd,UACA,OACA,QACA,OACA,aACM;CACN,MAAM,EAAE,gBAAgB,cAAc,iBAAiB;CACvD,MAAM,EAAE,WAAW,SAAS,SAAS,eAAe;CACpD,MAAM,gBAAgB,uBAA0B,IAAI,IAAI,CAAC;CAEzD,MAAM,QAAQ,kBAA2B;EACvC,MAAM,IAAI,eAAe,WAAW,aAAa;EACjD,IAAI,CAAC,GAAG,OAAO;EACf,MAAM,OAAO,aAAa;EAC1B,MAAM,EACJ,QAAQ,aAAa,OAAO,GAAG,QAAQ,GAAG,WAAW,MAAM,YAAY,OACvE,KAAK,OACL,kBAAkB,uBAClB,aAAa,QAAQ,SAAS,cAAc,QAAQ,sBAClD,UAAU;EACd,IAAI,MAAM,KAAK,MAAM,GAAG,OAAO;EAE/B,MAAM,aAAa,gBAAgB,MAAM,OAAO,WAAW;EAE3D,MAAM,WAAW,MAAM,KAAK,GAAG;GAAE,OAAO;GAAG,QAAQ;GAAG,KAAK;EAAM,GAAG;GAClE,MAAM;GACN,kBAAkB;GAClB,cAAc,SAAS,OAAO,SAAS;GACvC;GACA,SAAS;EACX,CAAC;EAED,IAAI,CAAC,UAAU,OAAO;EAEtB,MAAM,OAAO,QAAQ;EACrB,MAAM,EAAE,iBAAiB,KAAK,YAAY,UAAU,QAAQ;EAC5D,MAAM,QAAQ,YAAY,KAAK;EAC/B,MAAM,WAAW,WACf,aACA,WAAW,SACX,OAAO,qBACP,OACA,MACA;GAAE,OAAO;GAAG,QAAQ;EAAE,GACtB,cAAc,SACd,QACA,QACA,OACA,aAAa,MAAM,CACrB;EACA,SAAS,OAAO,UAAU,WAAW,IAAI,GAAG,QAAQ,EAAE,MAAM,IAAI,MAAS;EACzE,YAAY,IAAI,SAAS,eAAe,CAAC;EACzC,YAAY;EACZ,OAAO;CACT,GAAG;EAAC;EAAS;EAAY;EAAW;EAAO;EAAa;EAAc;EAAc;EAAgB;CAAO,CAAC;CAC5G,yBAAyB;EACvB,SAAS,UAAU;CACrB,CAAC;CAED,gBAAgB;EACd,MAAM,aAAa,cAAc;EACjC,aAAa;GAAE,WAAW,MAAM;EAAG;CACrC,GAAG,CAAC,CAAC;AACP"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { useLatest } from "@weasel-js/react";
|
|
3
|
+
|
|
4
|
+
//#region src/canvas/useInFlightBounds.ts
|
|
5
|
+
/**
|
|
6
|
+
* Where a view boxes the nodes an in-flight gesture names: at the pose a
|
|
7
|
+
* source previews, else at the node's effective pose — a transform's poses
|
|
8
|
+
* live in the scene's overrides, not on its handle. Boxed through `boxOf`
|
|
9
|
+
* either way, since a consumer `boundsOf` answers for the document pose only.
|
|
10
|
+
*/
|
|
11
|
+
function useInFlightBounds(args) {
|
|
12
|
+
const { gestureSource, boxOf, committedPoseOf } = args;
|
|
13
|
+
const previewPoseExtraRef = useLatest(args.previewPoseExtra);
|
|
14
|
+
const previewPoseOf = useCallback((id) => {
|
|
15
|
+
const p = previewPoseExtraRef.current?.(id);
|
|
16
|
+
return p != null ? p : null;
|
|
17
|
+
}, [previewPoseExtraRef]);
|
|
18
|
+
const inFlight = useRef(null);
|
|
19
|
+
const isInFlight = useCallback((id) => {
|
|
20
|
+
if (!gestureSource) return false;
|
|
21
|
+
const version = gestureSource.getVersion();
|
|
22
|
+
let entry = inFlight.current;
|
|
23
|
+
if (entry === null || entry.source !== gestureSource || entry.version !== version) {
|
|
24
|
+
entry = {
|
|
25
|
+
source: gestureSource,
|
|
26
|
+
version,
|
|
27
|
+
ids: new Set(gestureSource.ids() ?? [])
|
|
28
|
+
};
|
|
29
|
+
inFlight.current = entry;
|
|
30
|
+
}
|
|
31
|
+
return entry.ids.has(id);
|
|
32
|
+
}, [gestureSource]);
|
|
33
|
+
return {
|
|
34
|
+
previewPoseOf,
|
|
35
|
+
previewBoundsOf: useCallback((id) => {
|
|
36
|
+
const p = previewPoseOf(id);
|
|
37
|
+
if (p != null) return boxOf(id, p);
|
|
38
|
+
if (!isInFlight(id)) return null;
|
|
39
|
+
const shown = committedPoseOf(id);
|
|
40
|
+
return shown != null ? boxOf(id, shown) : null;
|
|
41
|
+
}, [
|
|
42
|
+
previewPoseOf,
|
|
43
|
+
boxOf,
|
|
44
|
+
isInFlight,
|
|
45
|
+
committedPoseOf
|
|
46
|
+
])
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { useInFlightBounds };
|
|
52
|
+
//# sourceMappingURL=useInFlightBounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInFlightBounds.js","names":[],"sources":["../../src/canvas/useInFlightBounds.ts"],"sourcesContent":["import { useCallback, useRef } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { GestureSource } from './gestureBounds';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\n\n/**\n * Where a view boxes the nodes an in-flight gesture names: at the pose a\n * source previews, else at the node's effective pose — a transform's poses\n * live in the scene's overrides, not on its handle. Boxed through `boxOf`\n * either way, since a consumer `boundsOf` answers for the document pose only.\n */\nexport function useInFlightBounds<TPose>(args: {\n gestureSource: GestureSource | undefined;\n /** Read through a ref, so a change mid-gesture rebuilds nothing. */\n previewPoseExtra: ((id: string) => unknown) | undefined;\n boxOf: (id: string, pose: TPose) => Bounds | null;\n /** The node's effective world pose. */\n committedPoseOf: (id: string) => TPose | null;\n}): {\n previewPoseOf: (id: string) => TPose | null;\n previewBoundsOf: (id: string) => Bounds | null;\n} {\n const { gestureSource, boxOf, committedPoseOf } = args;\n const previewPoseExtraRef = useLatest(args.previewPoseExtra);\n const previewPoseOf = useCallback((id: string): TPose | null => {\n const p = previewPoseExtraRef.current?.(id);\n return p != null ? (p as TPose) : null;\n }, [previewPoseExtraRef]);\n\n // The source's ids, re-read only when its version moves.\n const inFlight = useRef<{ source: GestureSource; version: number; ids: ReadonlySet<string> } | null>(null);\n const isInFlight = useCallback((id: string): boolean => {\n if (!gestureSource) return false;\n const version = gestureSource.getVersion();\n let entry = inFlight.current;\n if (entry === null || entry.source !== gestureSource || entry.version !== version) {\n entry = { source: gestureSource, version, ids: new Set(gestureSource.ids() ?? []) };\n inFlight.current = entry;\n }\n return entry.ids.has(id);\n }, [gestureSource]);\n\n const previewBoundsOf = useCallback((id: string): Bounds | null => {\n const p = previewPoseOf(id);\n if (p != null) return boxOf(id, p);\n if (!isInFlight(id)) return null;\n const shown = committedPoseOf(id);\n return shown != null ? boxOf(id, shown) : null;\n }, [previewPoseOf, boxOf, isInFlight, committedPoseOf]);\n\n return { previewPoseOf, previewBoundsOf };\n}\n"],"mappings":";;;;;;;;;;AAWA,SAAgB,kBAAyB,MAUvC;CACA,MAAM,EAAE,eAAe,OAAO,oBAAoB;CAClD,MAAM,sBAAsB,UAAU,KAAK,gBAAgB;CAC3D,MAAM,gBAAgB,aAAa,OAA6B;EAC9D,MAAM,IAAI,oBAAoB,UAAU,EAAE;EAC1C,OAAO,KAAK,OAAQ,IAAc;CACpC,GAAG,CAAC,mBAAmB,CAAC;CAGxB,MAAM,WAAW,OAAoF,IAAI;CACzG,MAAM,aAAa,aAAa,OAAwB;EACtD,IAAI,CAAC,eAAe,OAAO;EAC3B,MAAM,UAAU,cAAc,WAAW;EACzC,IAAI,QAAQ,SAAS;EACrB,IAAI,UAAU,QAAQ,MAAM,WAAW,iBAAiB,MAAM,YAAY,SAAS;GACjF,QAAQ;IAAE,QAAQ;IAAe;IAAS,KAAK,IAAI,IAAI,cAAc,IAAI,KAAK,CAAC,CAAC;GAAE;GAClF,SAAS,UAAU;EACrB;EACA,OAAO,MAAM,IAAI,IAAI,EAAE;CACzB,GAAG,CAAC,aAAa,CAAC;CAUlB,OAAO;EAAE;EAAe,iBARA,aAAa,OAA8B;GACjE,MAAM,IAAI,cAAc,EAAE;GAC1B,IAAI,KAAK,MAAM,OAAO,MAAM,IAAI,CAAC;GACjC,IAAI,CAAC,WAAW,EAAE,GAAG,OAAO;GAC5B,MAAM,QAAQ,gBAAgB,EAAE;GAChC,OAAO,SAAS,OAAO,MAAM,IAAI,KAAK,IAAI;EAC5C,GAAG;GAAC;GAAe;GAAO;GAAY;EAAe,CAE7B;CAAgB;AAC1C"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { unionAABB } from "../core/geometry/unionBounds.js";
|
|
2
2
|
import { framedBounds } from "../core/geometry/poseDescriptor.js";
|
|
3
|
+
import { useInFlightBounds } from "./useInFlightBounds.js";
|
|
3
4
|
import { buildChromeState } from "../core/selection/chromeState.js";
|
|
4
5
|
import { drawnBoundsOf } from "./drawnBounds.js";
|
|
5
6
|
import { useCallback, useMemo } from "react";
|
|
@@ -58,16 +59,13 @@ function useViewHelpers(opts) {
|
|
|
58
59
|
}
|
|
59
60
|
}, [adapter]);
|
|
60
61
|
const multiActive = selection.length > 1;
|
|
61
|
-
const previewPoseExtraRef = useLatest(previewPoseExtra);
|
|
62
62
|
const effectiveBoundsOf = useMemo(() => boundsOf ?? baseBoundsOf, [boundsOf, baseBoundsOf]);
|
|
63
|
-
const previewPoseOf =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return p != null ? boxOf(id, p) : null;
|
|
70
|
-
}, [previewPoseOf, boxOf]);
|
|
63
|
+
const { previewPoseOf, previewBoundsOf } = useInFlightBounds({
|
|
64
|
+
gestureSource,
|
|
65
|
+
previewPoseExtra,
|
|
66
|
+
boxOf,
|
|
67
|
+
committedPoseOf
|
|
68
|
+
});
|
|
71
69
|
/**
|
|
72
70
|
* Bounds for `id` with any in-flight gesture folded in — what the user can
|
|
73
71
|
* see, not what is committed. Both the chrome state and the layer helpers
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useViewHelpers.js","names":[],"sources":["../../src/canvas/useViewHelpers.ts"],"sourcesContent":["import { useCallback, useMemo } from 'react';\nimport { framedBounds } from 'core/geometry/poseDescriptor';\nimport { useLatest } from '@weasel-js/react';\nimport type { GestureSource, GesturePreviewSource } from './gestureBounds';\nimport type { OngoingOverlay } from '@weasel-js/routing';\nimport { unionAABB } from 'core/geometry/unionBounds';\nimport { buildChromeState, type ChromeState } from 'core/selection/chromeState';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type { NodeId } from 'core/scene/types';\nimport type { DebugSink } from '../debug/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { Scale2 } from '@weasel-js/geom';\nimport { drawnBoundsOf, type DrawnBoundsScene } from './drawnBounds';\n\nconst noOpUnsubscribe = (): void => {};\n\nconst alwaysVisible = (): boolean => true;\n\n/**\n * The half of {@link CanvasHelpers} that belongs to one view — everything\n * answered by a camera's own tools, gestures and selection. A canvas hosting\n * several viewports needs one of these per view; the surface half is shared.\n */\nexport interface CanvasViewHelpers<TPose> {\n /** Pose currently displayed for `id` — drag/resize/rotate overlay if active,\n * otherwise the committed pose from the adapter. Returns `null` if the id\n * isn't known. */\n getEffectivePose(id: string): TPose | null;\n /** Overlay-aware bounds for `id`. */\n getEffectiveBounds(id: string): Bounds | null;\n /**\n * World-space AABB of everything the in-flight gesture proposes — the\n * displaced poses of nodes being moved / resized / rotated / cloned, plus\n * any nascent insert that has no scene node yet. `null` when no gesture is\n * in flight.\n *\n * This reports the *gesture*, not the document: committed content the\n * gesture isn't touching is excluded. Select-only gestures are excluded\n * too: a marquee or lasso has geometry but proposes no content.\n *\n * The result is a plain AABB — never rotated. Rotated parts are folded in\n * by their rotated extent.\n */\n getGestureBounds(): Bounds | null;\n /**\n * Subscribe to the gesture layer's change signal, for everything on this\n * object that moves during a drag (`getEffectivePose`, `getEffectiveBounds`,\n * `getGestureBounds`). Returns an unsubscribe.\n *\n * Fires once per dispatcher pump: gesture start, every pointermove that\n * reaches an in-flight handle, end, and cancel — plus UI-driven ongoing\n * actions. It fires on the pump, not on a diff: a pump that changed nothing\n * observable still notifies, so don't hang expensive work directly off the\n * callback.\n *\n * It does **not** cover committed scene edits (subscribe to the scene for\n * those) or previews a consumer's own tool publishes from React state.\n * Without a gesture source wired — a bare `<Canvas>` — it never fires.\n */\n subscribeGestures(fn: () => void): () => void;\n /**\n * Monotonic counter bumped on exactly the events `subscribeGestures` fires\n * on. Pair the two for `useSyncExternalStore`:\n *\n * ```ts\n * const gestureVersion = useSyncExternalStore(\n * useCallback((cb) => helpersRef.current?.subscribeGestures(cb) ?? (() => {}), []),\n * () => helpersRef.current?.getGestureVersion() ?? 0,\n * );\n * ```\n *\n * Starts at 0 and only increases. `0` is also what a bare `<Canvas>` with\n * no gesture source reports, forever.\n */\n getGestureVersion(): number;\n /** The live ChromeState built once per render: overlay-aware selection\n * state for affordances and custom layers. */\n getChromeState(): ChromeState;\n /**\n * Everything publishing an in-flight preview for this view: the handles in\n * flight on its dispatcher, in dispatch order. Readers take the first\n * non-null answer per id.\n *\n * A ghost layer must read this off its draw envelope rather than close over\n * a dispatcher: one layer array paints every view, so a closure ghosts view\n * zero's drag into every panel.\n */\n getPreviewSources(): readonly GesturePreviewSource[];\n /** Overlay shapes this view's in-flight handles publish — marquee, lasso,\n * insert preview, raw commands. Same envelope rule as\n * {@link CanvasViewHelpers.getPreviewSources}. */\n getGestureOverlays(): readonly OngoingOverlay[];\n /**\n * Chrome-caps visibility predicate, keyed by chrome id. Overlay layers and\n * affordances call it per element to decide whether to draw / hit-test.\n * Unwired, it is the universal `() => true`.\n */\n getIsVisible(): (id: string) => boolean;\n}\n\n/**\n * The half of {@link CanvasHelpers} that belongs to the surface — one GL\n * context, one debug sink, however many views are drawn on it.\n */\nexport interface CanvasSurfaceHelpers {\n /** Active debug sink, when `<Canvas debug=...>` is enabled, for layers to\n * call from their `draw` callback. `null` when debug is off. */\n getDebug(): DebugSink | null;\n}\n\n/** Live overlay-aware lookups exposed to custom layers via `helpersRef`, and\n * what a layer receives as its `data` argument. */\nexport interface CanvasHelpers<TPose> extends CanvasViewHelpers<TPose>, CanvasSurfaceHelpers {\n /**\n * The view this draw is for: a `<CanvasView>` id, or `null` for the\n * surface's own camera. One layer array paints every view, so a layer that\n * belongs in some views and not others — a minimap's indicator — declines\n * the rest by this id.\n */\n readonly viewId: string | null;\n}\n\n/** What a view's pose and bounds lookups read of an adapter. */\nexport interface ViewPoseAdapter<TPose> {\n getPose(id: string): TPose;\n /** Where the node is drawn, when the adapter composes poses. An adapter\n * without it stores absolute poses, where the two agree. */\n getWorldPose?(id: string): TPose;\n /** The node an id names, whose `layer` decides whether a view that hides\n * that layer shows its selection chrome. */\n getNode?(id: string): { layer?: string } | undefined;\n}\n\nexport interface UseViewHelpersOpts<TPose> {\n adapter: ViewPoseAdapter<TPose> | undefined;\n geometry: PoseDescriptor<TPose>;\n /** Consumer-supplied bounds resolver. Falls back to the adapter's pose. */\n boundsOf: ((id: string) => Bounds | null) | undefined;\n /** Bounds of `id` drawn at an in-flight `pose`. Falls back to the\n * geometry's box of that pose, which is wrong wherever the surface draws a\n * node somewhere its pose does not say — a parallax plane. */\n boundsOfPose?: ((id: string, pose: TPose) => Bounds | null) | undefined;\n /** The nodes the adapter's ids name: with them, the fallbacks box each as\n * drawn, stroke included, rather than by its pose. */\n nodes?: DrawnBoundsScene | undefined;\n /** The view's scale, which a screen-pixel stroke is resolved at. Default 1. */\n getScale?: (() => Scale2) | undefined;\n selection: readonly NodeId[];\n gestureSource: GestureSource | undefined;\n /** The dispatcher's preview pose for an id, read through a ref so a change\n * mid-gesture doesn't rebuild the memos that depend on it. */\n previewPoseExtra: ((id: string) => unknown) | undefined;\n /** Chrome-caps predicate for this view. Absent means everything is visible. */\n getIsVisible?: (() => (id: string) => boolean) | undefined;\n /** Whether a scene layer reaches the screen in this view. Absent means\n * every layer the scene shows does. */\n layerIsPainted?: ((layerId: string) => boolean) | undefined;\n}\n\nexport interface ViewHelpersBundle<TPose> {\n helpers: CanvasViewHelpers<TPose>;\n}\n\nfunction worldPoseOf<TPose>(adapter: ViewPoseAdapter<TPose>, id: string): TPose {\n return typeof adapter.getWorldPose === 'function' ? adapter.getWorldPose(id) : adapter.getPose(id);\n}\n\n/**\n * Build one view's overlay-aware state: its chrome and the lookups its layers\n * draw and hit-test against. A canvas with N views mounts N components each\n * calling this once.\n */\nexport function useViewHelpers<TPose>(\n opts: UseViewHelpersOpts<TPose>,\n): ViewHelpersBundle<TPose> {\n const {\n adapter, geometry, boundsOf, boundsOfPose, nodes, getScale, selection, gestureSource,\n previewPoseExtra, getIsVisible, layerIsPainted,\n } = opts;\n const getScaleRef = useLatest(getScale);\n const poseBoxOf = useCallback((id: string, pose: TPose): Bounds => {\n if (!nodes) return framedBounds(pose, geometry);\n const scale = getScaleRef.current?.();\n return drawnBoundsOf(nodes, id as NodeId, pose, {\n descriptor: geometry as PoseDescriptor<unknown>,\n ...(adapter ? { worldPoseOf: (m: NodeId) => worldPoseOf(adapter, m) } : {}),\n ...(scale ? { scale } : {}),\n });\n }, [nodes, geometry, adapter, getScaleRef]);\n const boxOf = useCallback(\n (id: string, pose: TPose): Bounds | null =>\n (boundsOfPose ? boundsOfPose(id, pose) : poseBoxOf(id, pose)),\n [boundsOfPose, poseBoxOf],\n );\n\n const baseBoundsOf = useMemo(() => {\n if (boundsOf) return boundsOf;\n if (!adapter) return undefined;\n return (id: string): Bounds | null => {\n try {\n // World: chrome is drawn over the picture, so it has to agree with\n // where the renderer put the node, not with where its pose is stored.\n return poseBoxOf(id, worldPoseOf(adapter, id));\n } catch {\n return null;\n }\n };\n }, [boundsOf, adapter, poseBoxOf]);\n\n const committedPoseOf = useCallback((id: string): TPose | null => {\n if (!adapter) return null;\n try {\n return worldPoseOf(adapter, id);\n } catch {\n return null;\n }\n }, [adapter]);\n\n const multiActive = selection.length > 1;\n\n const previewPoseExtraRef = useLatest(previewPoseExtra);\n\n const effectiveBoundsOf = useMemo(() => boundsOf ?? baseBoundsOf, [boundsOf, baseBoundsOf]);\n\n const previewPoseOf = useCallback((id: string): TPose | null => {\n const p = previewPoseExtraRef.current?.(id);\n return p != null ? (p as TPose) : null;\n }, [previewPoseExtraRef]);\n const previewBoundsOf = useCallback((id: string): Bounds | null => {\n const p = previewPoseOf(id);\n return p != null ? boxOf(id, p) : null;\n }, [previewPoseOf, boxOf]);\n\n /**\n * Bounds for `id` with any in-flight gesture folded in — what the user can\n * see, not what is committed. Both the chrome state and the layer helpers\n * answer bounds questions with this, so resize handles cannot end up a frame\n * away from the shape they belong to.\n */\n const boundsWithPreview = useCallback((id: string): Bounds | null => {\n const previewed = previewBoundsOf(id);\n if (previewed) return previewed;\n return effectiveBoundsOf ? effectiveBoundsOf(id) : null;\n }, [previewBoundsOf, effectiveBoundsOf]);\n\n const chromeState: ChromeState = useMemo(\n () => buildChromeState({\n selection,\n multiActive,\n effectiveBoundsOf: boundsWithPreview,\n modifiers: { alt: false, shift: false, meta: false, ctrl: false },\n // Absent adapter / unknown ids fall back to `true` so the affordance\n // stays visible for consumers that wire bounds without an adapter.\n canRotate: (id) => {\n if (!geometry.supportsRotation) return true;\n const pose = committedPoseOf(id);\n if (pose == null) return true;\n return geometry.supportsRotation(pose);\n },\n ...(layerIsPainted ? { layerIsPainted } : {}),\n ...(adapter?.getNode ? { layerOf: (id: string) => adapter.getNode?.(id)?.layer } : {}),\n }),\n [selection, multiActive, boundsWithPreview, geometry, committedPoseOf, layerIsPainted, adapter],\n );\n\n const helpers: CanvasViewHelpers<TPose> = {\n getEffectivePose: (id: string): TPose | null => {\n return previewPoseOf(id) ?? committedPoseOf(id);\n },\n getEffectiveBounds: boundsWithPreview,\n getGestureBounds: (): Bounds | null => {\n // The same id sets the ghost layer walks, so this cannot disagree with\n // what the user sees ghosted.\n const parts: (Bounds | null)[] = [];\n for (const id of new Set(gestureSource?.ids() ?? [])) parts.push(previewBoundsOf(id));\n const extraBounds = gestureSource?.bounds();\n if (extraBounds) for (const b of extraBounds) parts.push(b);\n return unionAABB(parts);\n },\n subscribeGestures: (fn: () => void): (() => void) =>\n gestureSource?.subscribe(fn) ?? noOpUnsubscribe,\n getGestureVersion: (): number => gestureSource?.getVersion() ?? 0,\n getChromeState: () => chromeState,\n getPreviewSources: (): readonly GesturePreviewSource[] => gestureSource?.previewSources() ?? [],\n getGestureOverlays: (): readonly OngoingOverlay[] => gestureSource?.overlays() ?? [],\n getIsVisible: (): ((id: string) => boolean) => getIsVisible?.() ?? alwaysVisible,\n };\n\n return { helpers };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useViewHelpers.js","names":[],"sources":["../../src/canvas/useViewHelpers.ts"],"sourcesContent":["import { useCallback, useMemo } from 'react';\nimport { framedBounds } from 'core/geometry/poseDescriptor';\nimport { useLatest } from '@weasel-js/react';\nimport type { GestureSource, GesturePreviewSource } from './gestureBounds';\nimport { useInFlightBounds } from './useInFlightBounds';\nimport type { OngoingOverlay } from '@weasel-js/routing';\nimport { unionAABB } from 'core/geometry/unionBounds';\nimport { buildChromeState, type ChromeState } from 'core/selection/chromeState';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport type { NodeId } from 'core/scene/types';\nimport type { DebugSink } from '../debug/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { Scale2 } from '@weasel-js/geom';\nimport { drawnBoundsOf, type DrawnBoundsScene } from './drawnBounds';\n\nconst noOpUnsubscribe = (): void => {};\n\nconst alwaysVisible = (): boolean => true;\n\n/**\n * The half of {@link CanvasHelpers} that belongs to one view — everything\n * answered by a camera's own tools, gestures and selection. A canvas hosting\n * several viewports needs one of these per view; the surface half is shared.\n */\nexport interface CanvasViewHelpers<TPose> {\n /** Pose currently displayed for `id` — drag/resize/rotate overlay if active,\n * otherwise the committed pose from the adapter. Returns `null` if the id\n * isn't known. */\n getEffectivePose(id: string): TPose | null;\n /** Overlay-aware bounds for `id`. */\n getEffectiveBounds(id: string): Bounds | null;\n /**\n * World-space AABB of everything the in-flight gesture proposes — the\n * displaced poses of nodes being moved / resized / rotated / cloned, plus\n * any nascent insert that has no scene node yet. `null` when no gesture is\n * in flight.\n *\n * This reports the *gesture*, not the document: committed content the\n * gesture isn't touching is excluded. Select-only gestures are excluded\n * too: a marquee or lasso has geometry but proposes no content.\n *\n * The result is a plain AABB — never rotated. Rotated parts are folded in\n * by their rotated extent.\n */\n getGestureBounds(): Bounds | null;\n /**\n * Subscribe to the gesture layer's change signal, for everything on this\n * object that moves during a drag (`getEffectivePose`, `getEffectiveBounds`,\n * `getGestureBounds`). Returns an unsubscribe.\n *\n * Fires once per dispatcher pump: gesture start, every pointermove that\n * reaches an in-flight handle, end, and cancel — plus UI-driven ongoing\n * actions. It fires on the pump, not on a diff: a pump that changed nothing\n * observable still notifies, so don't hang expensive work directly off the\n * callback.\n *\n * It does **not** cover committed scene edits (subscribe to the scene for\n * those) or previews a consumer's own tool publishes from React state.\n * Without a gesture source wired — a bare `<Canvas>` — it never fires.\n */\n subscribeGestures(fn: () => void): () => void;\n /**\n * Monotonic counter bumped on exactly the events `subscribeGestures` fires\n * on. Pair the two for `useSyncExternalStore`:\n *\n * ```ts\n * const gestureVersion = useSyncExternalStore(\n * useCallback((cb) => helpersRef.current?.subscribeGestures(cb) ?? (() => {}), []),\n * () => helpersRef.current?.getGestureVersion() ?? 0,\n * );\n * ```\n *\n * Starts at 0 and only increases. `0` is also what a bare `<Canvas>` with\n * no gesture source reports, forever.\n */\n getGestureVersion(): number;\n /** The live ChromeState built once per render: overlay-aware selection\n * state for affordances and custom layers. */\n getChromeState(): ChromeState;\n /**\n * Everything publishing an in-flight preview for this view: the handles in\n * flight on its dispatcher, in dispatch order. Readers take the first\n * non-null answer per id.\n *\n * A ghost layer must read this off its draw envelope rather than close over\n * a dispatcher: one layer array paints every view, so a closure ghosts view\n * zero's drag into every panel.\n */\n getPreviewSources(): readonly GesturePreviewSource[];\n /** Overlay shapes this view's in-flight handles publish — marquee, lasso,\n * insert preview, raw commands. Same envelope rule as\n * {@link CanvasViewHelpers.getPreviewSources}. */\n getGestureOverlays(): readonly OngoingOverlay[];\n /**\n * Chrome-caps visibility predicate, keyed by chrome id. Overlay layers and\n * affordances call it per element to decide whether to draw / hit-test.\n * Unwired, it is the universal `() => true`.\n */\n getIsVisible(): (id: string) => boolean;\n}\n\n/**\n * The half of {@link CanvasHelpers} that belongs to the surface — one GL\n * context, one debug sink, however many views are drawn on it.\n */\nexport interface CanvasSurfaceHelpers {\n /** Active debug sink, when `<Canvas debug=...>` is enabled, for layers to\n * call from their `draw` callback. `null` when debug is off. */\n getDebug(): DebugSink | null;\n}\n\n/** Live overlay-aware lookups exposed to custom layers via `helpersRef`, and\n * what a layer receives as its `data` argument. */\nexport interface CanvasHelpers<TPose> extends CanvasViewHelpers<TPose>, CanvasSurfaceHelpers {\n /**\n * The view this draw is for: a `<CanvasView>` id, or `null` for the\n * surface's own camera. One layer array paints every view, so a layer that\n * belongs in some views and not others — a minimap's indicator — declines\n * the rest by this id.\n */\n readonly viewId: string | null;\n}\n\n/** What a view's pose and bounds lookups read of an adapter. */\nexport interface ViewPoseAdapter<TPose> {\n getPose(id: string): TPose;\n /** Where the node is drawn, when the adapter composes poses. An adapter\n * without it stores absolute poses, where the two agree. */\n getWorldPose?(id: string): TPose;\n /** The node an id names, whose `layer` decides whether a view that hides\n * that layer shows its selection chrome. */\n getNode?(id: string): { layer?: string } | undefined;\n}\n\nexport interface UseViewHelpersOpts<TPose> {\n adapter: ViewPoseAdapter<TPose> | undefined;\n geometry: PoseDescriptor<TPose>;\n /** Consumer-supplied bounds resolver. Falls back to the adapter's pose. */\n boundsOf: ((id: string) => Bounds | null) | undefined;\n /** Bounds of `id` drawn at an in-flight `pose`. Falls back to the\n * geometry's box of that pose, which is wrong wherever the surface draws a\n * node somewhere its pose does not say — a parallax plane. */\n boundsOfPose?: ((id: string, pose: TPose) => Bounds | null) | undefined;\n /** The nodes the adapter's ids name: with them, the fallbacks box each as\n * drawn, stroke included, rather than by its pose. */\n nodes?: DrawnBoundsScene | undefined;\n /** The view's scale, which a screen-pixel stroke is resolved at. Default 1. */\n getScale?: (() => Scale2) | undefined;\n selection: readonly NodeId[];\n gestureSource: GestureSource | undefined;\n /** The dispatcher's preview pose for an id, read through a ref so a change\n * mid-gesture doesn't rebuild the memos that depend on it. */\n previewPoseExtra: ((id: string) => unknown) | undefined;\n /** Chrome-caps predicate for this view. Absent means everything is visible. */\n getIsVisible?: (() => (id: string) => boolean) | undefined;\n /** Whether a scene layer reaches the screen in this view. Absent means\n * every layer the scene shows does. */\n layerIsPainted?: ((layerId: string) => boolean) | undefined;\n}\n\nexport interface ViewHelpersBundle<TPose> {\n helpers: CanvasViewHelpers<TPose>;\n}\n\nfunction worldPoseOf<TPose>(adapter: ViewPoseAdapter<TPose>, id: string): TPose {\n return typeof adapter.getWorldPose === 'function' ? adapter.getWorldPose(id) : adapter.getPose(id);\n}\n\n/**\n * Build one view's overlay-aware state: its chrome and the lookups its layers\n * draw and hit-test against. A canvas with N views mounts N components each\n * calling this once.\n */\nexport function useViewHelpers<TPose>(\n opts: UseViewHelpersOpts<TPose>,\n): ViewHelpersBundle<TPose> {\n const {\n adapter, geometry, boundsOf, boundsOfPose, nodes, getScale, selection, gestureSource,\n previewPoseExtra, getIsVisible, layerIsPainted,\n } = opts;\n const getScaleRef = useLatest(getScale);\n const poseBoxOf = useCallback((id: string, pose: TPose): Bounds => {\n if (!nodes) return framedBounds(pose, geometry);\n const scale = getScaleRef.current?.();\n return drawnBoundsOf(nodes, id as NodeId, pose, {\n descriptor: geometry as PoseDescriptor<unknown>,\n ...(adapter ? { worldPoseOf: (m: NodeId) => worldPoseOf(adapter, m) } : {}),\n ...(scale ? { scale } : {}),\n });\n }, [nodes, geometry, adapter, getScaleRef]);\n const boxOf = useCallback(\n (id: string, pose: TPose): Bounds | null =>\n (boundsOfPose ? boundsOfPose(id, pose) : poseBoxOf(id, pose)),\n [boundsOfPose, poseBoxOf],\n );\n\n const baseBoundsOf = useMemo(() => {\n if (boundsOf) return boundsOf;\n if (!adapter) return undefined;\n return (id: string): Bounds | null => {\n try {\n // World: chrome is drawn over the picture, so it has to agree with\n // where the renderer put the node, not with where its pose is stored.\n return poseBoxOf(id, worldPoseOf(adapter, id));\n } catch {\n return null;\n }\n };\n }, [boundsOf, adapter, poseBoxOf]);\n\n const committedPoseOf = useCallback((id: string): TPose | null => {\n if (!adapter) return null;\n try {\n return worldPoseOf(adapter, id);\n } catch {\n return null;\n }\n }, [adapter]);\n\n const multiActive = selection.length > 1;\n\n const effectiveBoundsOf = useMemo(() => boundsOf ?? baseBoundsOf, [boundsOf, baseBoundsOf]);\n const { previewPoseOf, previewBoundsOf } = useInFlightBounds({ gestureSource, previewPoseExtra, boxOf, committedPoseOf });\n\n /**\n * Bounds for `id` with any in-flight gesture folded in — what the user can\n * see, not what is committed. Both the chrome state and the layer helpers\n * answer bounds questions with this, so resize handles cannot end up a frame\n * away from the shape they belong to.\n */\n const boundsWithPreview = useCallback((id: string): Bounds | null => {\n const previewed = previewBoundsOf(id);\n if (previewed) return previewed;\n return effectiveBoundsOf ? effectiveBoundsOf(id) : null;\n }, [previewBoundsOf, effectiveBoundsOf]);\n\n const chromeState: ChromeState = useMemo(\n () => buildChromeState({\n selection,\n multiActive,\n effectiveBoundsOf: boundsWithPreview,\n modifiers: { alt: false, shift: false, meta: false, ctrl: false },\n // Absent adapter / unknown ids fall back to `true` so the affordance\n // stays visible for consumers that wire bounds without an adapter.\n canRotate: (id) => {\n if (!geometry.supportsRotation) return true;\n const pose = committedPoseOf(id);\n if (pose == null) return true;\n return geometry.supportsRotation(pose);\n },\n ...(layerIsPainted ? { layerIsPainted } : {}),\n ...(adapter?.getNode ? { layerOf: (id: string) => adapter.getNode?.(id)?.layer } : {}),\n }),\n [selection, multiActive, boundsWithPreview, geometry, committedPoseOf, layerIsPainted, adapter],\n );\n\n const helpers: CanvasViewHelpers<TPose> = {\n getEffectivePose: (id: string): TPose | null => {\n return previewPoseOf(id) ?? committedPoseOf(id);\n },\n getEffectiveBounds: boundsWithPreview,\n getGestureBounds: (): Bounds | null => {\n // The same id sets the ghost layer walks, so this cannot disagree with\n // what the user sees ghosted.\n const parts: (Bounds | null)[] = [];\n for (const id of new Set(gestureSource?.ids() ?? [])) parts.push(previewBoundsOf(id));\n const extraBounds = gestureSource?.bounds();\n if (extraBounds) for (const b of extraBounds) parts.push(b);\n return unionAABB(parts);\n },\n subscribeGestures: (fn: () => void): (() => void) =>\n gestureSource?.subscribe(fn) ?? noOpUnsubscribe,\n getGestureVersion: (): number => gestureSource?.getVersion() ?? 0,\n getChromeState: () => chromeState,\n getPreviewSources: (): readonly GesturePreviewSource[] => gestureSource?.previewSources() ?? [],\n getGestureOverlays: (): readonly OngoingOverlay[] => gestureSource?.overlays() ?? [],\n getIsVisible: (): ((id: string) => boolean) => getIsVisible?.() ?? alwaysVisible,\n };\n\n return { helpers };\n}\n"],"mappings":"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"}
|
package/dist/clipboard.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NodeId } from '@weasel-js/routing';
|
|
2
|
-
import {
|
|
3
|
-
import '
|
|
2
|
+
import { P as PasteOmission, C as ClipboardSnapshot, I as InsertAdapter } from './types-oFeQ5VjV.js';
|
|
3
|
+
import './types-7puAuVD2.js';
|
|
4
4
|
import '@weasel-js/geom';
|
|
5
|
+
import '@weasel-js/history';
|
|
5
6
|
import './poseDescriptor-DN2yfnq_.js';
|
|
6
7
|
import '@weasel-js/animator';
|
|
7
8
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as PlaneMap } from './types-7puAuVD2.js';
|
|
2
2
|
import { Op } from '@weasel-js/history';
|
|
3
|
+
import { S as SnapTarget, M as MoveAdapter } from './types-oFeQ5VjV.js';
|
|
3
4
|
import { FramedBox, View } from '@weasel-js/geom';
|
|
4
5
|
import { KitSettingsStore } from '@weasel-js/react';
|
|
5
6
|
import { ModifierState, ResizeAnchor } from '@weasel-js/routing';
|
|
@@ -140,29 +141,15 @@ interface BehaviorEnd {
|
|
|
140
141
|
answered: boolean;
|
|
141
142
|
}
|
|
142
143
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
144
|
+
* The translation a drag proposes and its behaviors shape, applied identically
|
|
145
|
+
* to every dragged id so a multi-select gesture cannot drift apart. Resize and
|
|
146
|
+
* rotate behaviors answer in their own terms (`ResizeMoveResult`,
|
|
147
|
+
* `RotateMoveResult`), not through this.
|
|
146
148
|
*/
|
|
147
149
|
type GroupTransform = {
|
|
148
150
|
kind: 'translate';
|
|
149
151
|
dx: number;
|
|
150
152
|
dy: number;
|
|
151
|
-
} | {
|
|
152
|
-
kind: 'scale';
|
|
153
|
-
pivot: {
|
|
154
|
-
x: number;
|
|
155
|
-
y: number;
|
|
156
|
-
};
|
|
157
|
-
sx: number;
|
|
158
|
-
sy: number;
|
|
159
|
-
} | {
|
|
160
|
-
kind: 'rotate';
|
|
161
|
-
pivot: {
|
|
162
|
-
x: number;
|
|
163
|
-
y: number;
|
|
164
|
-
};
|
|
165
|
-
angle: number;
|
|
166
153
|
};
|
|
167
154
|
/**
|
|
168
155
|
* Per-frame result a `MoveBehavior.onMove` can return. `snap` announces a
|
|
@@ -202,8 +189,8 @@ interface MoveEnd extends BehaviorEnd {
|
|
|
202
189
|
};
|
|
203
190
|
}
|
|
204
191
|
/** A behavior on a move — shapes the proposed transform during a drag.
|
|
205
|
-
* Behaviors receive a `GroupTransform`
|
|
206
|
-
*
|
|
192
|
+
* Behaviors receive a `GroupTransform` and return a `BehaviorResult` shaping
|
|
193
|
+
* it. */
|
|
207
194
|
type MoveBehavior<TPose> = ActionBehavior<TPose, GroupTransform, BehaviorResult<TPose>, MoveEnd>;
|
|
208
195
|
|
|
209
196
|
/** Per-frame proposed resize: pose plus the anchor pinning the opposite corner. */
|
package/dist/clone.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { C as CloneBehavior, d as CloneBehaviorResult } from './clone-
|
|
2
|
-
import './types-
|
|
3
|
-
import '@weasel-js/history';
|
|
1
|
+
export { C as CloneBehavior, d as CloneBehaviorResult } from './clone-C3e0OTuF.js';
|
|
2
|
+
import './types-7puAuVD2.js';
|
|
4
3
|
import '@weasel-js/geom';
|
|
4
|
+
import '@weasel-js/history';
|
|
5
|
+
import './types-oFeQ5VjV.js';
|
|
5
6
|
import './poseDescriptor-DN2yfnq_.js';
|
|
6
7
|
import '@weasel-js/animator';
|
|
7
8
|
import '@weasel-js/react';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { pathPoseDescriptor } from "./pathPoseDescriptor.js";
|
|
2
1
|
import { RECT_POSE_DESCRIPTOR } from "./poseDescriptor.js";
|
|
2
|
+
import { pathPoseDescriptor } from "./pathPoseDescriptor.js";
|
|
3
3
|
|
|
4
4
|
//#region src/core/geometry/autoPoseDescriptor.ts
|
|
5
5
|
/** True for Path-shaped poses (`{kind: 'polygon' | 'rect'}`). Useful for
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { registerOpFactory } from "./registry.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/ops/setDependsOn.ts
|
|
4
|
+
/** Op: set what a derived node's geometry derives from, inverting back to
|
|
5
|
+
* `from`. */
|
|
6
|
+
function createSetDependsOnOp(args) {
|
|
7
|
+
const { id, from, to, label, coalesceKey } = args;
|
|
8
|
+
return {
|
|
9
|
+
name: "setDependsOn",
|
|
10
|
+
args: {
|
|
11
|
+
id,
|
|
12
|
+
from,
|
|
13
|
+
to,
|
|
14
|
+
label,
|
|
15
|
+
coalesceKey
|
|
16
|
+
},
|
|
17
|
+
label,
|
|
18
|
+
coalesceKey,
|
|
19
|
+
apply(adapter) {
|
|
20
|
+
adapter.setDependsOn(id, to);
|
|
21
|
+
},
|
|
22
|
+
invert() {
|
|
23
|
+
return createSetDependsOnOp({
|
|
24
|
+
id,
|
|
25
|
+
from: to,
|
|
26
|
+
to: from,
|
|
27
|
+
label,
|
|
28
|
+
coalesceKey
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
registerOpFactory("setDependsOn", (args) => createSetDependsOnOp(args));
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { createSetDependsOnOp };
|
|
37
|
+
//# sourceMappingURL=setDependsOn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setDependsOn.js","names":[],"sources":["../../../src/core/ops/setDependsOn.ts"],"sourcesContent":["import type { Op } from './types';\nimport { registerOpFactory } from './registry';\n\ntype DependsOn = readonly string[] | 'children' | undefined;\n\ninterface SetDependsOnAdapter {\n setDependsOn(id: string, dependsOn: DependsOn): void;\n}\n\n/** Arguments to {@link createSetDependsOnOp}: retarget node `id`'s derivation\n * from the sources `from` to `to`. `undefined` is no derivation. */\nexport interface SetDependsOnArgs {\n id: string;\n from: DependsOn;\n to: DependsOn;\n label?: string;\n coalesceKey?: string;\n}\n\n/** Op: set what a derived node's geometry derives from, inverting back to\n * `from`. */\nexport function createSetDependsOnOp(args: SetDependsOnArgs): Op {\n const { id, from, to, label, coalesceKey } = args;\n return {\n name: 'setDependsOn',\n args: { id, from, to, label, coalesceKey },\n label,\n coalesceKey,\n apply(adapter) {\n (adapter as SetDependsOnAdapter).setDependsOn(id, to);\n },\n invert() {\n return createSetDependsOnOp({ id, from: to, to: from, label, coalesceKey });\n },\n };\n}\n\nregisterOpFactory<SetDependsOnArgs>('setDependsOn', (args) => createSetDependsOnOp(args));\n"],"mappings":";;;;;AAqBA,SAAgB,qBAAqB,MAA4B;CAC/D,MAAM,EAAE,IAAI,MAAM,IAAI,OAAO,gBAAgB;CAC7C,OAAO;EACL,MAAM;EACN,MAAM;GAAE;GAAI;GAAM;GAAI;GAAO;EAAY;EACzC;EACA;EACA,MAAM,SAAS;GACb,QAAiC,aAAa,IAAI,EAAE;EACtD;EACA,SAAS;GACP,OAAO,qBAAqB;IAAE;IAAI,MAAM;IAAI,IAAI;IAAM;IAAO;GAAY,CAAC;EAC5E;CACF;AACF;AAEA,kBAAoC,iBAAiB,SAAS,qBAAqB,IAAI,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { sceneMutators } from "../scene/sceneMutators.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/ops/writtenBy.ts
|
|
4
|
+
/**
|
|
5
|
+
* The pose and data `ops` leave each node they write, by id, read by applying
|
|
6
|
+
* them to a recorder rather than the scene; null when one cannot be read that
|
|
7
|
+
* way. A node only re-parented, re-layered, inserted, or removed is not in it.
|
|
8
|
+
*/
|
|
9
|
+
function writtenBy(sc, ops) {
|
|
10
|
+
const after = /* @__PURE__ */ new Map();
|
|
11
|
+
const current = (id) => {
|
|
12
|
+
const node = after.get(id) ?? sc.get(id);
|
|
13
|
+
return node ? {
|
|
14
|
+
pose: node.pose,
|
|
15
|
+
data: node.data
|
|
16
|
+
} : null;
|
|
17
|
+
};
|
|
18
|
+
const write = (id, field, value) => {
|
|
19
|
+
const node = current(id);
|
|
20
|
+
if (node) after.set(id, {
|
|
21
|
+
...node,
|
|
22
|
+
[field]: value
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const reads = sceneMutators(sc);
|
|
26
|
+
const noop = () => {};
|
|
27
|
+
const recorder = {
|
|
28
|
+
...reads,
|
|
29
|
+
setPose: (id, pose) => write(id, "pose", pose),
|
|
30
|
+
setData: (id, data) => write(id, "data", data),
|
|
31
|
+
setParent: noop,
|
|
32
|
+
setChildOrder: noop,
|
|
33
|
+
setLayer: noop,
|
|
34
|
+
setDependsOn: noop,
|
|
35
|
+
setLayout: noop,
|
|
36
|
+
setHidden: noop,
|
|
37
|
+
setLocked: noop,
|
|
38
|
+
removeNode: noop,
|
|
39
|
+
insertNode: noop,
|
|
40
|
+
getSelection: () => [...sc.getSelection()],
|
|
41
|
+
setSelection: noop
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
for (const op of ops) op.apply(recorder);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return after;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { writtenBy };
|
|
53
|
+
//# sourceMappingURL=writtenBy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writtenBy.js","names":[],"sources":["../../../src/core/ops/writtenBy.ts"],"sourcesContent":["import { sceneMutators } from '../scene/sceneMutators';\nimport type { NodeId, Scene } from '../scene/types';\nimport type { Op } from './types';\n\n/** A node's pose and data, as a batch of ops leaves them. */\nexport interface WrittenNode {\n pose: unknown;\n data: unknown;\n}\n\n/**\n * The pose and data `ops` leave each node they write, by id, read by applying\n * them to a recorder rather than the scene; null when one cannot be read that\n * way. A node only re-parented, re-layered, inserted, or removed is not in it.\n */\nexport function writtenBy(sc: Scene<unknown, string, unknown>, ops: readonly Op[]): Map<string, WrittenNode> | null {\n const after = new Map<string, WrittenNode>();\n const current = (id: string): WrittenNode | null => {\n const node = after.get(id) ?? sc.get(id as NodeId);\n return node ? { pose: node.pose, data: node.data } : null;\n };\n const write = (id: string, field: keyof WrittenNode, value: unknown) => {\n const node = current(id);\n if (node) after.set(id, { ...node, [field]: value });\n };\n const reads = sceneMutators(sc);\n const noop = () => {};\n const recorder = {\n ...reads,\n setPose: (id: string, pose: unknown) => write(id, 'pose', pose),\n setData: (id: string, data: unknown) => write(id, 'data', data),\n setParent: noop, setChildOrder: noop, setLayer: noop, setDependsOn: noop, setLayout: noop,\n setHidden: noop, setLocked: noop, removeNode: noop, insertNode: noop,\n getSelection: (): string[] => [...sc.getSelection()],\n setSelection: noop,\n };\n try {\n for (const op of ops) op.apply(recorder);\n } catch {\n return null;\n }\n return after;\n}\n"],"mappings":";;;;;;;;AAeA,SAAgB,UAAU,IAAqC,KAAqD;CAClH,MAAM,wBAAQ,IAAI,IAAyB;CAC3C,MAAM,WAAW,OAAmC;EAClD,MAAM,OAAO,MAAM,IAAI,EAAE,KAAK,GAAG,IAAI,EAAY;EACjD,OAAO,OAAO;GAAE,MAAM,KAAK;GAAM,MAAM,KAAK;EAAK,IAAI;CACvD;CACA,MAAM,SAAS,IAAY,OAA0B,UAAmB;EACtE,MAAM,OAAO,QAAQ,EAAE;EACvB,IAAI,MAAM,MAAM,IAAI,IAAI;GAAE,GAAG;IAAO,QAAQ;EAAM,CAAC;CACrD;CACA,MAAM,QAAQ,cAAc,EAAE;CAC9B,MAAM,aAAa,CAAC;CACpB,MAAM,WAAW;EACf,GAAG;EACH,UAAU,IAAY,SAAkB,MAAM,IAAI,QAAQ,IAAI;EAC9D,UAAU,IAAY,SAAkB,MAAM,IAAI,QAAQ,IAAI;EAC9D,WAAW;EAAM,eAAe;EAAM,UAAU;EAAM,cAAc;EAAM,WAAW;EACrF,WAAW;EAAM,WAAW;EAAM,YAAY;EAAM,YAAY;EAChE,oBAA8B,CAAC,GAAG,GAAG,aAAa,CAAC;EACnD,cAAc;CAChB;CACA,IAAI;EACF,KAAK,MAAM,MAAM,KAAK,GAAG,MAAM,QAAQ;CACzC,QAAQ;EACN,OAAO;CACT;CACA,OAAO;AACT"}
|