@dxos/react-ui-canvas 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/lib/index.mjs +9044 -1135
- package/dist/lib/index.mjs.map +1 -1
- package/dist/types/src/components/About/About.d.ts +13 -0
- package/dist/types/src/components/About/About.d.ts.map +1 -0
- package/dist/types/src/components/About/About.stories.d.ts +7 -0
- package/dist/types/src/components/About/About.stories.d.ts.map +1 -0
- package/dist/types/src/components/About/index.d.ts +2 -0
- package/dist/types/src/components/About/index.d.ts.map +1 -0
- package/dist/types/src/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -1
- package/dist/types/src/components/ControlFrame/ControlFrame.d.ts +8 -2
- package/dist/types/src/components/ControlFrame/ControlFrame.d.ts.map +1 -1
- package/dist/types/src/components/Layers/Layers.d.ts +30 -0
- package/dist/types/src/components/Layers/Layers.d.ts.map +1 -0
- package/dist/types/src/components/Layers/Layers.stories.d.ts +11 -0
- package/dist/types/src/components/Layers/Layers.stories.d.ts.map +1 -0
- package/dist/types/src/components/Layers/index.d.ts +2 -0
- package/dist/types/src/components/Layers/index.d.ts.map +1 -0
- package/dist/types/src/components/Palette/Palette.d.ts +20 -4
- package/dist/types/src/components/Palette/Palette.d.ts.map +1 -1
- package/dist/types/src/components/Palette/Palette.stories.d.ts +22 -0
- package/dist/types/src/components/Palette/Palette.stories.d.ts.map +1 -0
- package/dist/types/src/components/PartEditor/PartEditor.d.ts.map +1 -1
- package/dist/types/src/components/Properties/AlignField.d.ts +23 -0
- package/dist/types/src/components/Properties/AlignField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/ClassField.d.ts +23 -0
- package/dist/types/src/components/Properties/ClassField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/FontField.d.ts +7 -0
- package/dist/types/src/components/Properties/FontField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/LayerField.d.ts +17 -0
- package/dist/types/src/components/Properties/LayerField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/Properties.d.ts +27 -3
- package/dist/types/src/components/Properties/Properties.d.ts.map +1 -1
- package/dist/types/src/components/Properties/Properties.stories.d.ts +20 -0
- package/dist/types/src/components/Properties/Properties.stories.d.ts.map +1 -0
- package/dist/types/src/components/Properties/SceneField.d.ts +8 -0
- package/dist/types/src/components/Properties/SceneField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/StyleGrid.d.ts +15 -5
- package/dist/types/src/components/Properties/StyleGrid.d.ts.map +1 -1
- package/dist/types/src/components/Properties/StyleGrid.stories.d.ts +7 -0
- package/dist/types/src/components/Properties/StyleGrid.stories.d.ts.map +1 -0
- package/dist/types/src/components/SceneLayer/SceneLayer.d.ts +31 -7
- package/dist/types/src/components/SceneLayer/SceneLayer.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/Dock.d.ts +22 -0
- package/dist/types/src/components/SceneView/Dock.d.ts.map +1 -0
- package/dist/types/src/components/SceneView/Dock.stories.d.ts +15 -0
- package/dist/types/src/components/SceneView/Dock.stories.d.ts.map +1 -0
- package/dist/types/src/components/SceneView/SceneView.d.ts +33 -7
- package/dist/types/src/components/SceneView/SceneView.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/SceneView.stories.d.ts +16 -6
- package/dist/types/src/components/SceneView/SceneView.stories.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/SceneViewContext.d.ts +36 -4
- package/dist/types/src/components/SceneView/SceneViewContext.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/Scored.stories.d.ts +4 -4
- package/dist/types/src/components/SceneView/Scored.stories.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/usePointerMachine.d.ts +3 -1
- package/dist/types/src/components/SceneView/usePointerMachine.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/useSceneCamera.d.ts +2 -2
- package/dist/types/src/components/SceneView/useSceneCamera.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/useSceneClipboard.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/useSceneKeys.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/useSceneNavigation.d.ts +7 -4
- package/dist/types/src/components/SceneView/useSceneNavigation.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +2 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useWheel.d.ts +5 -0
- package/dist/types/src/hooks/useWheel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +4 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/model/atoms.d.ts +21 -3
- package/dist/types/src/model/atoms.d.ts.map +1 -1
- package/dist/types/src/model/projection.d.ts.map +1 -1
- package/dist/types/src/model/projections/constrained.d.ts +2 -1
- package/dist/types/src/model/projections/constrained.d.ts.map +1 -1
- package/dist/types/src/model/projections/dynamic.d.ts.map +1 -1
- package/dist/types/src/model/projections/lattice.d.ts.map +1 -1
- package/dist/types/src/model/registry.d.ts +21 -3
- package/dist/types/src/model/registry.d.ts.map +1 -1
- package/dist/types/src/model/store.d.ts +10 -2
- package/dist/types/src/model/store.d.ts.map +1 -1
- package/dist/types/src/model/types.d.ts +699 -338
- package/dist/types/src/model/types.d.ts.map +1 -1
- package/dist/types/src/playwright/SceneManager.d.ts +1 -1
- package/dist/types/src/playwright/SceneManager.d.ts.map +1 -1
- package/dist/types/src/utils/builder.d.ts +22 -41
- package/dist/types/src/utils/builder.d.ts.map +1 -1
- package/dist/types/src/utils/camera.d.ts +19 -10
- package/dist/types/src/utils/camera.d.ts.map +1 -1
- package/dist/types/src/utils/clipboard.d.ts +4 -2
- package/dist/types/src/utils/clipboard.d.ts.map +1 -1
- package/dist/types/src/utils/diagram.d.ts.map +1 -1
- package/dist/types/src/utils/group.d.ts +14 -0
- package/dist/types/src/utils/group.d.ts.map +1 -0
- package/dist/types/src/utils/group.test.d.ts +2 -0
- package/dist/types/src/utils/group.test.d.ts.map +1 -0
- package/dist/types/src/utils/gutter-route.d.ts +7 -0
- package/dist/types/src/utils/gutter-route.d.ts.map +1 -1
- package/dist/types/src/utils/hit.d.ts +3 -20
- package/dist/types/src/utils/hit.d.ts.map +1 -1
- package/dist/types/src/utils/index.d.ts +3 -0
- package/dist/types/src/utils/index.d.ts.map +1 -1
- package/dist/types/src/utils/lattice.d.ts +5 -0
- package/dist/types/src/utils/lattice.d.ts.map +1 -1
- package/dist/types/src/utils/layers.d.ts +35 -0
- package/dist/types/src/utils/layers.d.ts.map +1 -0
- package/dist/types/src/utils/layers.test.d.ts +2 -0
- package/dist/types/src/utils/layers.test.d.ts.map +1 -0
- package/dist/types/src/utils/parts.d.ts +25 -9
- package/dist/types/src/utils/parts.d.ts.map +1 -1
- package/dist/types/src/utils/ports.d.ts +2 -0
- package/dist/types/src/utils/ports.d.ts.map +1 -1
- package/dist/types/src/utils/properties.d.ts +4 -3
- package/dist/types/src/utils/properties.d.ts.map +1 -1
- package/dist/types/src/utils/route.d.ts +5 -0
- package/dist/types/src/utils/route.d.ts.map +1 -1
- package/dist/types/src/utils/scenes.d.ts +14 -0
- package/dist/types/src/utils/scenes.d.ts.map +1 -0
- package/dist/types/src/utils/scenes.test.d.ts +2 -0
- package/dist/types/src/utils/scenes.test.d.ts.map +1 -0
- package/dist/types/src/utils/shapes.d.ts +23 -9
- package/dist/types/src/utils/shapes.d.ts.map +1 -1
- package/dist/types/src/utils/style.d.ts +29 -6
- package/dist/types/src/utils/style.d.ts.map +1 -1
- package/dist/types/src/utils/style.test.d.ts +2 -0
- package/dist/types/src/utils/style.test.d.ts.map +1 -0
- package/dist/types/src/utils/testing.d.ts +2 -2
- package/dist/types/src/utils/testing.d.ts.map +1 -1
- package/dist/types/src/utils/undo.d.ts +15 -4
- package/dist/types/src/utils/undo.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -26
- package/src/components/About/About.stories.tsx +32 -0
- package/src/components/About/About.tsx +38 -0
- package/src/components/About/index.ts +5 -0
- package/src/components/Breadcrumbs/Breadcrumbs.tsx +17 -3
- package/src/components/ControlFrame/ControlFrame.tsx +138 -82
- package/src/components/Grid/Grid.stories.tsx +1 -1
- package/src/components/Layers/Layers.stories.tsx +131 -0
- package/src/components/Layers/Layers.tsx +216 -0
- package/src/components/Layers/index.ts +5 -0
- package/src/components/Palette/Palette.stories.tsx +135 -0
- package/src/components/Palette/Palette.tsx +230 -39
- package/src/components/PartEditor/PartEditor.tsx +4 -3
- package/src/components/Properties/AlignField.tsx +117 -0
- package/src/components/Properties/ClassField.tsx +44 -0
- package/src/components/Properties/FontField.tsx +73 -0
- package/src/components/Properties/LayerField.tsx +61 -0
- package/src/components/Properties/Properties.stories.tsx +113 -0
- package/src/components/Properties/Properties.tsx +240 -60
- package/src/components/Properties/SceneField.tsx +22 -0
- package/src/components/Properties/StyleGrid.stories.tsx +50 -0
- package/src/components/Properties/StyleGrid.tsx +54 -26
- package/src/components/SceneLayer/SceneLayer.tsx +497 -188
- package/src/components/SceneView/Constrained.stories.tsx +1 -1
- package/src/components/SceneView/Dock.stories.tsx +95 -0
- package/src/components/SceneView/Dock.tsx +146 -0
- package/src/components/SceneView/Dynamic.stories.tsx +1 -1
- package/src/components/SceneView/SceneView.stories.tsx +170 -18
- package/src/components/SceneView/SceneView.tsx +565 -151
- package/src/components/SceneView/SceneViewContext.ts +23 -2
- package/src/components/SceneView/Scored.stories.tsx +11 -11
- package/src/components/SceneView/usePointerMachine.ts +80 -20
- package/src/components/SceneView/useSceneCamera.ts +15 -8
- package/src/components/SceneView/useSceneClipboard.ts +4 -0
- package/src/components/SceneView/useSceneKeys.ts +6 -4
- package/src/components/SceneView/useSceneNavigation.ts +44 -33
- package/src/components/Toolbar/Toolbar.stories.tsx +14 -15
- package/src/components/Toolbar/Toolbar.tsx +15 -4
- package/src/components/index.ts +2 -0
- package/src/hooks/useWheel.ts +35 -0
- package/src/index.ts +4 -3
- package/src/model/atoms.ts +27 -3
- package/src/model/projection.ts +5 -0
- package/src/model/projections/constrained.test.ts +18 -10
- package/src/model/projections/constrained.ts +19 -8
- package/src/model/projections/dynamic.ts +5 -2
- package/src/model/projections/lattice.ts +15 -3
- package/src/model/registry.test.ts +14 -9
- package/src/model/registry.ts +39 -34
- package/src/model/store.ts +12 -2
- package/src/model/types.ts +124 -36
- package/src/playwright/SceneManager.ts +2 -2
- package/src/playwright/create.spec.ts +5 -3
- package/src/playwright/demo.spec.ts +2 -2
- package/src/playwright/lattice.spec.ts +4 -5
- package/src/playwright/scene.spec.ts +27 -25
- package/src/utils/builder.test.ts +46 -9
- package/src/utils/builder.ts +42 -18
- package/src/utils/camera.test.ts +22 -51
- package/src/utils/camera.ts +39 -32
- package/src/utils/clipboard.test.ts +23 -2
- package/src/utils/clipboard.ts +8 -4
- package/src/utils/diagram.test.ts +3 -1
- package/src/utils/diagram.ts +3 -25
- package/src/utils/group.test.ts +69 -0
- package/src/utils/group.ts +94 -0
- package/src/utils/gutter-route.test.ts +49 -0
- package/src/utils/gutter-route.ts +142 -16
- package/src/utils/hit.test.ts +4 -38
- package/src/utils/hit.ts +4 -26
- package/src/utils/index.ts +3 -0
- package/src/utils/lattice.ts +7 -1
- package/src/utils/layers.test.ts +139 -0
- package/src/utils/layers.ts +175 -0
- package/src/utils/parts.test.ts +66 -32
- package/src/utils/parts.ts +63 -41
- package/src/utils/ports.ts +7 -4
- package/src/utils/properties.test.ts +23 -6
- package/src/utils/properties.ts +66 -16
- package/src/utils/route.test.ts +51 -2
- package/src/utils/route.ts +133 -23
- package/src/utils/scenes.test.ts +38 -0
- package/src/utils/scenes.ts +54 -0
- package/src/utils/shapes.test.ts +33 -8
- package/src/utils/shapes.ts +51 -35
- package/src/utils/style.test.ts +54 -0
- package/src/utils/style.ts +153 -40
- package/src/utils/testing.test.ts +3 -3
- package/src/utils/testing.ts +21 -30
- package/src/utils/undo.test.ts +25 -2
- package/src/utils/undo.ts +50 -13
- package/dist/lib/chunk-Grid.mjs +0 -99
- package/dist/lib/chunk-Grid.mjs.map +0 -1
- package/dist/lib/scene.mjs +0 -6835
- package/dist/lib/scene.mjs.map +0 -1
- package/dist/lib/types.mjs +0 -16
- package/dist/lib/types.mjs.map +0 -1
- package/dist/types/src/archive/components/Canvas/Canvas.d.ts +0 -15
- package/dist/types/src/archive/components/Canvas/Canvas.d.ts.map +0 -1
- package/dist/types/src/archive/components/Canvas/Canvas.stories.d.ts +0 -18
- package/dist/types/src/archive/components/Canvas/Canvas.stories.d.ts.map +0 -1
- package/dist/types/src/archive/components/Canvas/index.d.ts +0 -2
- package/dist/types/src/archive/components/Canvas/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/CellGrid.d.ts +0 -21
- package/dist/types/src/archive/components/CellGrid/CellGrid.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/CellGrid.stories.d.ts +0 -21
- package/dist/types/src/archive/components/CellGrid/CellGrid.stories.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/headers/Ruler.d.ts +0 -15
- package/dist/types/src/archive/components/CellGrid/headers/Ruler.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/headers/TrackHeader.d.ts +0 -19
- package/dist/types/src/archive/components/CellGrid/headers/TrackHeader.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/headers/index.d.ts +0 -3
- package/dist/types/src/archive/components/CellGrid/headers/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/index.d.ts +0 -6
- package/dist/types/src/archive/components/CellGrid/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/input/index.d.ts +0 -3
- package/dist/types/src/archive/components/CellGrid/input/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/input/pointer.d.ts +0 -33
- package/dist/types/src/archive/components/CellGrid/input/pointer.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/input/wheel.d.ts +0 -14
- package/dist/types/src/archive/components/CellGrid/input/wheel.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/render/index.d.ts +0 -3
- package/dist/types/src/archive/components/CellGrid/render/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/render/overlay-layer.d.ts +0 -21
- package/dist/types/src/archive/components/CellGrid/render/overlay-layer.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/render/static-layer.d.ts +0 -36
- package/dist/types/src/archive/components/CellGrid/render/static-layer.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/atoms.d.ts +0 -23
- package/dist/types/src/archive/components/CellGrid/state/atoms.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/index.d.ts +0 -4
- package/dist/types/src/archive/components/CellGrid/state/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/types.d.ts +0 -39
- package/dist/types/src/archive/components/CellGrid/state/types.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/viewport.d.ts +0 -52
- package/dist/types/src/archive/components/CellGrid/state/viewport.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/viewport.test.d.ts +0 -2
- package/dist/types/src/archive/components/CellGrid/state/viewport.test.d.ts.map +0 -1
- package/dist/types/src/archive/components/FPS.d.ts +0 -9
- package/dist/types/src/archive/components/FPS.d.ts.map +0 -1
- package/dist/types/src/archive/components/Grid/Grid.d.ts +0 -6
- package/dist/types/src/archive/components/Grid/Grid.d.ts.map +0 -1
- package/dist/types/src/archive/components/Grid/index.d.ts +0 -2
- package/dist/types/src/archive/components/Grid/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/index.d.ts +0 -5
- package/dist/types/src/archive/components/index.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/index.d.ts +0 -5
- package/dist/types/src/archive/hooks/index.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/projection.d.ts +0 -60
- package/dist/types/src/archive/hooks/projection.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/useCanvasContext.d.ts +0 -13
- package/dist/types/src/archive/hooks/useCanvasContext.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/useDrag.d.ts +0 -6
- package/dist/types/src/archive/hooks/useDrag.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/useWheel.d.ts +0 -8
- package/dist/types/src/archive/hooks/useWheel.d.ts.map +0 -1
- package/dist/types/src/archive/index.d.ts +0 -5
- package/dist/types/src/archive/index.d.ts.map +0 -1
- package/dist/types/src/archive/types.d.ts +0 -19
- package/dist/types/src/archive/types.d.ts.map +0 -1
- package/dist/types/src/archive/util/index.d.ts +0 -4
- package/dist/types/src/archive/util/index.d.ts.map +0 -1
- package/dist/types/src/archive/util/svg-path.d.ts +0 -4
- package/dist/types/src/archive/util/svg-path.d.ts.map +0 -1
- package/dist/types/src/archive/util/svg.d.ts +0 -32
- package/dist/types/src/archive/util/svg.d.ts.map +0 -1
- package/dist/types/src/archive/util/svg.stories.d.ts +0 -15
- package/dist/types/src/archive/util/svg.stories.d.ts.map +0 -1
- package/dist/types/src/archive/util/util.d.ts +0 -17
- package/dist/types/src/archive/util/util.d.ts.map +0 -1
- package/dist/types/src/scene.d.ts +0 -5
- package/dist/types/src/scene.d.ts.map +0 -1
- package/src/archive/components/Canvas/Canvas.stories.tsx +0 -111
- package/src/archive/components/Canvas/Canvas.tsx +0 -87
- package/src/archive/components/Canvas/index.ts +0 -5
- package/src/archive/components/CellGrid/CellGrid.stories.tsx +0 -239
- package/src/archive/components/CellGrid/CellGrid.tsx +0 -270
- package/src/archive/components/CellGrid/headers/Ruler.tsx +0 -71
- package/src/archive/components/CellGrid/headers/TrackHeader.tsx +0 -58
- package/src/archive/components/CellGrid/headers/index.ts +0 -6
- package/src/archive/components/CellGrid/index.ts +0 -9
- package/src/archive/components/CellGrid/input/index.ts +0 -6
- package/src/archive/components/CellGrid/input/pointer.ts +0 -236
- package/src/archive/components/CellGrid/input/wheel.ts +0 -68
- package/src/archive/components/CellGrid/render/index.ts +0 -6
- package/src/archive/components/CellGrid/render/overlay-layer.ts +0 -66
- package/src/archive/components/CellGrid/render/static-layer.ts +0 -112
- package/src/archive/components/CellGrid/state/atoms.ts +0 -43
- package/src/archive/components/CellGrid/state/index.ts +0 -7
- package/src/archive/components/CellGrid/state/types.ts +0 -40
- package/src/archive/components/CellGrid/state/viewport.test.ts +0 -50
- package/src/archive/components/CellGrid/state/viewport.ts +0 -94
- package/src/archive/components/FPS.tsx +0 -98
- package/src/archive/components/Grid/Grid.tsx +0 -17
- package/src/archive/components/Grid/index.ts +0 -5
- package/src/archive/components/index.ts +0 -8
- package/src/archive/hooks/index.ts +0 -8
- package/src/archive/hooks/projection.tsx +0 -156
- package/src/archive/hooks/useCanvasContext.ts +0 -32
- package/src/archive/hooks/useDrag.tsx +0 -96
- package/src/archive/hooks/useWheel.tsx +0 -80
- package/src/archive/index.ts +0 -8
- package/src/archive/types.ts +0 -14
- package/src/archive/util/index.ts +0 -7
- package/src/archive/util/svg-path.ts +0 -13
- package/src/archive/util/svg.stories.tsx +0 -46
- package/src/archive/util/svg.tsx +0 -128
- package/src/archive/util/util.ts +0 -50
- package/src/scene.ts +0 -9
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
13
13
|
import { useAtomValue } from '@effect/atom-react/Hooks';
|
|
14
|
-
import React, { type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
14
|
+
import React, { type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
|
|
16
16
|
import * as Menu from '@dxos/react-ui/Menu';
|
|
17
17
|
import type * as Util from '@dxos/react-ui/Util';
|
|
18
18
|
import * as VirtualAnchor from '@dxos/react-ui/VirtualAnchor';
|
|
19
19
|
import { mx } from '@dxos/ui-theme';
|
|
20
20
|
|
|
21
|
-
import { useRegistry, useSceneProjection, useViewport, useWheel } from '../../hooks/index.ts';
|
|
22
|
-
import { type Drag, type SceneViewAtoms, createSceneViewAtoms } from '../../model/atoms.ts';
|
|
21
|
+
import { SCENE_OVERLAY_ATTRIBUTE, useRegistry, useSceneProjection, useViewport, useWheel } from '../../hooks/index.ts';
|
|
22
|
+
import { type Drag, type PanelMode, type SceneViewAtoms, createSceneViewAtoms, isMoving } from '../../model/atoms.ts';
|
|
23
|
+
import { keyAction } from '../../model/keys.ts';
|
|
23
24
|
import { nodeDef } from '../../model/node-def.ts';
|
|
24
25
|
import {
|
|
25
26
|
type FreehandProjectionOptions,
|
|
@@ -35,32 +36,62 @@ import {
|
|
|
35
36
|
} from '../../model/registry.ts';
|
|
36
37
|
import { type SceneStore } from '../../model/store.ts';
|
|
37
38
|
import {
|
|
39
|
+
type Bounds,
|
|
40
|
+
type Camera,
|
|
38
41
|
DEFAULT_GRID,
|
|
39
42
|
type ElementId,
|
|
40
43
|
type Endpoint,
|
|
41
44
|
type Intent,
|
|
45
|
+
type Layer,
|
|
46
|
+
type LayerId,
|
|
47
|
+
type LinkId,
|
|
42
48
|
MAJOR_GRID_RATIO,
|
|
43
49
|
type Node,
|
|
44
50
|
type NodeType,
|
|
51
|
+
type Point,
|
|
45
52
|
type Scene,
|
|
46
53
|
type SceneId,
|
|
54
|
+
isFrameNode,
|
|
47
55
|
isPointEndpoint,
|
|
48
56
|
} from '../../model/types.ts';
|
|
49
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
MIN_ZOOM,
|
|
59
|
+
NOMINAL_ZOOM,
|
|
60
|
+
cameraTransform,
|
|
61
|
+
fitBounds,
|
|
62
|
+
layerOpacity,
|
|
63
|
+
panBy,
|
|
64
|
+
sceneToScreen,
|
|
65
|
+
screenToScene,
|
|
66
|
+
zoomAt,
|
|
67
|
+
} from '../../utils/camera.ts';
|
|
50
68
|
import { duplicateSelection } from '../../utils/clipboard.ts';
|
|
51
69
|
import { nodeDragType } from '../../utils/dnd.ts';
|
|
52
|
-
import {
|
|
70
|
+
import { groupIntoScene } from '../../utils/group.ts';
|
|
71
|
+
import { boundsFromPoints, hitTest } from '../../utils/hit.ts';
|
|
72
|
+
import {
|
|
73
|
+
activeLayer,
|
|
74
|
+
createLayer,
|
|
75
|
+
mergeLayersIntent,
|
|
76
|
+
moveLayer,
|
|
77
|
+
sceneLayers,
|
|
78
|
+
visibleScene,
|
|
79
|
+
} from '../../utils/layers.ts';
|
|
53
80
|
import { topZ } from '../../utils/order.ts';
|
|
54
|
-
import { type PartKey,
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
81
|
+
import { type PartKey, partText, partValues } from '../../utils/parts.ts';
|
|
82
|
+
import { sceneOptions } from '../../utils/scenes.ts';
|
|
83
|
+
import { createLink, nodeBounds, nominalSize } from '../../utils/shapes.ts';
|
|
84
|
+
import { recordScenes, redo, undo } from '../../utils/undo.ts';
|
|
85
|
+
import { About, type AboutStat } from '../About/About.tsx';
|
|
57
86
|
import { ControlFrame } from '../ControlFrame/ControlFrame.tsx';
|
|
58
87
|
import { GridComponent } from '../Grid/index.ts';
|
|
59
88
|
import { LatticeGrid } from '../LatticeGrid/index.ts';
|
|
89
|
+
import { LayersPanel } from '../Layers/Layers.tsx';
|
|
60
90
|
import { Palette } from '../Palette/Palette.tsx';
|
|
61
91
|
import { Properties, type PropertiesProps } from '../Properties/Properties.tsx';
|
|
62
92
|
import { type ElementHandlers, MAX_LIVE_DEPTH, SceneLayer } from '../SceneLayer/SceneLayer.tsx';
|
|
63
93
|
import { ActionToolbar, CameraToolbar, NavigationToolbar, type ToolbarActions } from '../Toolbar/Toolbar.tsx';
|
|
94
|
+
import { Dock, DockProvider, type DockSection, useDockSection } from './Dock.tsx';
|
|
64
95
|
import { SceneViewProvider, useSceneViewContext } from './SceneViewContext.ts';
|
|
65
96
|
import { PREVIEW_NODE_ID, createId, isLinkDrawn, usePointerMachine } from './usePointerMachine.ts';
|
|
66
97
|
import { useSceneCamera } from './useSceneCamera.ts';
|
|
@@ -70,11 +101,20 @@ import { useSceneNavigation } from './useSceneNavigation.ts';
|
|
|
70
101
|
import { GRID_LEVELS, GRID_RANGE, useSceneSnap } from './useSceneSnap.ts';
|
|
71
102
|
|
|
72
103
|
/** Major cells between the scene's frame and the viewport edge when fitting; `margin` overrides it. */
|
|
73
|
-
const DEFAULT_MARGIN =
|
|
104
|
+
const DEFAULT_MARGIN = 2;
|
|
105
|
+
/** Quiet time after the camera's last move before `onCameraChange` reports it. */
|
|
106
|
+
const CAMERA_SETTLE_MS = 300;
|
|
74
107
|
/** Zoom factor of one toolbar step. */
|
|
75
108
|
const ZOOM_STEP = 1.25;
|
|
76
|
-
/**
|
|
77
|
-
const
|
|
109
|
+
/** How long a link stays hovered after the pointer leaves it, so the pointer can reach its end handles. */
|
|
110
|
+
const LINK_HOVER_GRACE_MS = 150;
|
|
111
|
+
|
|
112
|
+
/** How long a move's pointer rests before the shapes snap to where they will land. */
|
|
113
|
+
const SETTLE_MS = 200;
|
|
114
|
+
|
|
115
|
+
/** Where the properties and layers panels float: the top right, one at a time (properties with a selection). */
|
|
116
|
+
const PANEL_CLASSES = 'absolute top-2 right-2 w-80 h-auto max-h-[calc(100%-1rem)]';
|
|
117
|
+
|
|
78
118
|
/** The link drawn as a preview during a drag; it never reaches the model. */
|
|
79
119
|
const PREVIEW_LINK_ID = 'preview-link';
|
|
80
120
|
|
|
@@ -89,15 +129,21 @@ export type SceneViewRootProps = Util.ThemedClassName<{
|
|
|
89
129
|
projection?: Projection;
|
|
90
130
|
/** Externally owned view state, e.g. to drive two views or persist the camera. */
|
|
91
131
|
atoms?: SceneViewAtoms;
|
|
132
|
+
/** Where the camera starts on the root scene, e.g. as last left; the scene is fitted when unset. */
|
|
133
|
+
initialCamera?: Camera;
|
|
134
|
+
/** Called once the camera settles on the root scene, so a host can persist it. */
|
|
135
|
+
onCameraChange?: (camera: Camera) => void;
|
|
92
136
|
/** Minor grid spacing in scene px; moves snap to it, creation and resizing to the major grid, `MAJOR_GRID_RATIO` times it. */
|
|
93
137
|
grid?: number;
|
|
94
|
-
/** Least gap between the scene's frame and each viewport edge when fitting, in
|
|
138
|
+
/** Least gap between the scene's frame and each viewport edge when fitting, in major cells. */
|
|
95
139
|
margin?: number;
|
|
96
140
|
/**
|
|
97
141
|
* Look, select and navigate only: no gesture or key reaches the model, and no handle or port is drawn,
|
|
98
142
|
* whatever the projection would allow.
|
|
99
143
|
*/
|
|
100
144
|
readonly?: boolean;
|
|
145
|
+
/** Where the properties and layers panels sit: docked in a column beside the canvas, or floating over it. */
|
|
146
|
+
panels?: PanelMode;
|
|
101
147
|
children?: ReactNode;
|
|
102
148
|
}>;
|
|
103
149
|
|
|
@@ -110,9 +156,12 @@ const SceneViewRoot = ({
|
|
|
110
156
|
createProjection,
|
|
111
157
|
projection: projectionProp,
|
|
112
158
|
atoms: atomsProp,
|
|
159
|
+
initialCamera,
|
|
160
|
+
onCameraChange,
|
|
113
161
|
grid = DEFAULT_GRID,
|
|
114
162
|
margin = DEFAULT_MARGIN,
|
|
115
163
|
readonly = false,
|
|
164
|
+
panels = 'docked',
|
|
116
165
|
children,
|
|
117
166
|
}: SceneViewRootProps) => {
|
|
118
167
|
const registry = useRegistry();
|
|
@@ -123,6 +172,7 @@ const SceneViewRoot = ({
|
|
|
123
172
|
const path = useAtomValue(atoms.path);
|
|
124
173
|
const projection = useSceneProjection({ store, atoms, createProjection, projection: projectionProp });
|
|
125
174
|
const scene = useAtomValue(projection.scene);
|
|
175
|
+
const visible = useMemo(() => visibleScene(scene), [scene]);
|
|
126
176
|
const scenes = useAtomValue(store.scenes);
|
|
127
177
|
const camera = useAtomValue(atoms.camera);
|
|
128
178
|
const selection = useAtomValue(atoms.selection);
|
|
@@ -133,11 +183,20 @@ const SceneViewRoot = ({
|
|
|
133
183
|
// The margin is in major cells, taken from the model's grid rather than the level currently drawn,
|
|
134
184
|
// so a fit puts the same gap around the scene whatever the zoom.
|
|
135
185
|
const inset = margin * grid * MAJOR_GRID_RATIO;
|
|
186
|
+
// A new node's default size is nominal: major cells of the model's grid, not of the level drawn at this zoom.
|
|
187
|
+
const cell = grid * MAJOR_GRID_RATIO;
|
|
136
188
|
|
|
137
189
|
const drag = useAtomValue(atoms.drag);
|
|
138
190
|
const undoState = useAtomValue(atoms.undo);
|
|
139
191
|
const clipboard = useAtomValue(atoms.clipboard);
|
|
140
192
|
const editing = useAtomValue(atoms.editing);
|
|
193
|
+
const active = useAtomValue(atoms.active);
|
|
194
|
+
// A node's content stays live only while the node is the selection; selecting anything else makes it inert again.
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (active !== undefined && (selection.size !== 1 || !selection.has(active))) {
|
|
197
|
+
registry.set(atoms.active, undefined);
|
|
198
|
+
}
|
|
199
|
+
}, [active, selection, registry, atoms.active]);
|
|
141
200
|
const debug = useAtomValue(atoms.debug);
|
|
142
201
|
const guides = useAtomValue(atoms.guides);
|
|
143
202
|
const latticeOn = useAtomValue(atoms.lattice);
|
|
@@ -167,42 +226,66 @@ const SceneViewRoot = ({
|
|
|
167
226
|
|
|
168
227
|
// Keep the scene fitted while the viewport settles, until the user takes the camera over. A layout
|
|
169
228
|
// effect, so the fit lands before the first paint instead of one frame after it.
|
|
170
|
-
|
|
229
|
+
// A restored camera counts as taken over, so the fit leaves it where it was.
|
|
230
|
+
const interactedRef = useRef(initialCamera !== undefined);
|
|
171
231
|
|
|
232
|
+
// Read-only, nothing is selected: a selection only exists to be edited.
|
|
172
233
|
const select = useCallback(
|
|
173
234
|
(ids: Iterable<ElementId>) => {
|
|
174
|
-
registry.set(atoms.selection, new Set(ids));
|
|
235
|
+
registry.set(atoms.selection, new Set(readonly ? [] : ids));
|
|
175
236
|
registry.set(atoms.point, undefined);
|
|
176
237
|
},
|
|
177
|
-
[registry, atoms.selection, atoms.point],
|
|
238
|
+
[registry, atoms.selection, atoms.point, readonly],
|
|
178
239
|
);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (readonly) {
|
|
242
|
+
select([]);
|
|
243
|
+
}
|
|
244
|
+
}, [readonly, select]);
|
|
179
245
|
|
|
180
|
-
const { nameOf, portalTo,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
246
|
+
const { nameOf, portalTo, bounds, fitTarget, pushHistory, drillIn, drillOut, goHistory } = useSceneNavigation({
|
|
247
|
+
registry,
|
|
248
|
+
atoms,
|
|
249
|
+
nodeRegistry,
|
|
250
|
+
store,
|
|
251
|
+
scenes,
|
|
252
|
+
scene,
|
|
253
|
+
path,
|
|
254
|
+
camera,
|
|
255
|
+
viewport,
|
|
256
|
+
inset,
|
|
257
|
+
drag,
|
|
258
|
+
interactedRef,
|
|
259
|
+
select,
|
|
260
|
+
animateTo,
|
|
261
|
+
setCamera,
|
|
262
|
+
setOpening,
|
|
263
|
+
isAnimating,
|
|
264
|
+
});
|
|
199
265
|
|
|
200
266
|
const measured = viewport.width > 0 && viewport.height > 0;
|
|
267
|
+
useLayoutEffect(() => {
|
|
268
|
+
if (initialCamera) {
|
|
269
|
+
setCamera(initialCamera);
|
|
270
|
+
}
|
|
271
|
+
// Only the camera the view opened with is restored; later values are the host echoing ours back.
|
|
272
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
273
|
+
}, [setCamera]);
|
|
201
274
|
useLayoutEffect(() => {
|
|
202
275
|
if (!interactedRef.current && measured) {
|
|
203
|
-
setCamera(fitBounds(
|
|
276
|
+
setCamera(fitBounds(fitTarget, viewport, inset, NOMINAL_ZOOM));
|
|
277
|
+
}
|
|
278
|
+
}, [measured, viewport, fitTarget, inset, setCamera]);
|
|
279
|
+
|
|
280
|
+
// Reported after a quiet beat, so a wheel gesture or an animation is persisted once, where it ends.
|
|
281
|
+
const atRoot = path.length === 1;
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (!onCameraChange || !atRoot || !interactedRef.current) {
|
|
284
|
+
return;
|
|
204
285
|
}
|
|
205
|
-
|
|
286
|
+
const timeout = setTimeout(() => onCameraChange(camera), CAMERA_SETTLE_MS);
|
|
287
|
+
return () => clearTimeout(timeout);
|
|
288
|
+
}, [onCameraChange, atRoot, camera]);
|
|
206
289
|
|
|
207
290
|
useWheel(
|
|
208
291
|
rootRef,
|
|
@@ -227,15 +310,15 @@ const SceneViewRoot = ({
|
|
|
227
310
|
|
|
228
311
|
// Undo restores a whole model snapshot, so the selection may name elements that no longer exist.
|
|
229
312
|
const onUndo = useCallback(() => {
|
|
230
|
-
if (!readonly && undo(projection, registry, atoms.undo, sceneId)) {
|
|
313
|
+
if (!readonly && undo(projection, registry, atoms.undo, sceneId, store)) {
|
|
231
314
|
select([]);
|
|
232
315
|
}
|
|
233
|
-
}, [readonly, projection, registry, atoms.undo, sceneId, select]);
|
|
316
|
+
}, [readonly, projection, registry, atoms.undo, sceneId, store, select]);
|
|
234
317
|
const onRedo = useCallback(() => {
|
|
235
|
-
if (!readonly && redo(projection, registry, atoms.undo, sceneId)) {
|
|
318
|
+
if (!readonly && redo(projection, registry, atoms.undo, sceneId, store)) {
|
|
236
319
|
select([]);
|
|
237
320
|
}
|
|
238
|
-
}, [readonly, projection, registry, atoms.undo, sceneId, select]);
|
|
321
|
+
}, [readonly, projection, registry, atoms.undo, sceneId, store, select]);
|
|
239
322
|
|
|
240
323
|
//
|
|
241
324
|
// Pointer state machine.
|
|
@@ -282,7 +365,8 @@ const SceneViewRoot = ({
|
|
|
282
365
|
registry,
|
|
283
366
|
atoms,
|
|
284
367
|
store,
|
|
285
|
-
scene
|
|
368
|
+
// A hidden layer's elements are not hit: gestures see the scene as drawn.
|
|
369
|
+
scene: visible,
|
|
286
370
|
nodeRegistry,
|
|
287
371
|
projection,
|
|
288
372
|
capabilities,
|
|
@@ -293,12 +377,20 @@ const SceneViewRoot = ({
|
|
|
293
377
|
setCamera,
|
|
294
378
|
cancelAnimation,
|
|
295
379
|
isNavigating,
|
|
380
|
+
cell,
|
|
296
381
|
minor,
|
|
297
382
|
major,
|
|
298
383
|
snap,
|
|
299
384
|
snapMinor,
|
|
300
385
|
});
|
|
301
386
|
|
|
387
|
+
// A gesture begun before the view turned read-only is dropped, not committed on release.
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
if (readonly) {
|
|
390
|
+
cancelDrag();
|
|
391
|
+
}
|
|
392
|
+
}, [readonly, cancelDrag]);
|
|
393
|
+
|
|
302
394
|
//
|
|
303
395
|
// Clipboard.
|
|
304
396
|
//
|
|
@@ -324,7 +416,7 @@ const SceneViewRoot = ({
|
|
|
324
416
|
projection,
|
|
325
417
|
capabilities,
|
|
326
418
|
viewport,
|
|
327
|
-
bounds,
|
|
419
|
+
bounds: fitTarget,
|
|
328
420
|
inset,
|
|
329
421
|
grid,
|
|
330
422
|
select,
|
|
@@ -353,9 +445,13 @@ const SceneViewRoot = ({
|
|
|
353
445
|
(event) => {
|
|
354
446
|
if (event.target === event.currentTarget) {
|
|
355
447
|
onSceneKey(event);
|
|
448
|
+
} else if (keyAction(event) === 'cancel' && registry.get(atoms.active) !== undefined) {
|
|
449
|
+
// Escape out of a node's live content leaves it inert and the node selected, as a click outside would not.
|
|
450
|
+
registry.set(atoms.active, undefined);
|
|
451
|
+
rootRef.current?.focus();
|
|
356
452
|
}
|
|
357
453
|
},
|
|
358
|
-
[onSceneKey],
|
|
454
|
+
[onSceneKey, registry, atoms.active],
|
|
359
455
|
);
|
|
360
456
|
|
|
361
457
|
//
|
|
@@ -372,18 +468,51 @@ const SceneViewRoot = ({
|
|
|
372
468
|
// a link being drawn or re-attached over a drop target looks exactly as it will once dropped. Geometry
|
|
373
469
|
// previews pass through the projection's `constrain`, so a drag shows where the drop will land, and
|
|
374
470
|
// `blocked` says when the drop would be refused (drawn as is, outlined in red).
|
|
375
|
-
|
|
471
|
+
// A move follows the pointer while it moves; once it rests for `SETTLE_MS`, the shapes snap to where they will land.
|
|
472
|
+
const moveRaw = drag?.kind === 'move' ? drag.raw : undefined;
|
|
473
|
+
const [settled, setSettled] = useState(false);
|
|
474
|
+
useEffect(() => {
|
|
475
|
+
setSettled(false);
|
|
476
|
+
if (!moveRaw) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const timer = setTimeout(() => setSettled(true), SETTLE_MS);
|
|
480
|
+
return () => clearTimeout(timer);
|
|
481
|
+
}, [moveRaw?.x, moveRaw?.y]);
|
|
482
|
+
|
|
483
|
+
const { displayScene, blocked, landing } = useMemo<{
|
|
484
|
+
displayScene: Scene;
|
|
485
|
+
blocked: boolean;
|
|
486
|
+
/** Where a move in flight will land (snapped and constrained), while the nodes follow the pointer. */
|
|
487
|
+
landing?: Bounds[];
|
|
488
|
+
}>(() => {
|
|
376
489
|
const preview = (intent: Intent) => {
|
|
377
490
|
const constrained = projection.constrain ? projection.constrain(intent) : intent;
|
|
378
491
|
return { displayScene: reduceIntent(scene, constrained ?? intent), blocked: constrained === undefined };
|
|
379
492
|
};
|
|
380
493
|
if (drag?.kind === 'move') {
|
|
381
494
|
// A copy previews beside the originals, which stay; the drop mints the copies' real ids.
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
495
|
+
const { ids, copy } = drag;
|
|
496
|
+
const moveBy = (delta: Point): Intent => {
|
|
497
|
+
let next = 0;
|
|
498
|
+
const duplicate = copy
|
|
499
|
+
? duplicateSelection(scene, ids, delta, (prefix) => `${PREVIEW_NODE_ID}-${prefix}-${next++}`)
|
|
500
|
+
: undefined;
|
|
501
|
+
return duplicate ? duplicate.intent : { kind: 'move', ids, delta };
|
|
502
|
+
};
|
|
503
|
+
// The nodes follow the pointer smoothly; the frames they will snap to on the drop are drawn under them.
|
|
504
|
+
const landed = preview(moveBy(drag.delta));
|
|
505
|
+
const moved = copy
|
|
506
|
+
? Object.values(landed.displayScene.nodes).filter((node) => !scene.nodes[node.id])
|
|
507
|
+
: ids.flatMap((id) => landed.displayScene.nodes[id] ?? []);
|
|
508
|
+
return settled
|
|
509
|
+
? { displayScene: landed.displayScene, blocked: landed.blocked }
|
|
510
|
+
: {
|
|
511
|
+
displayScene: reduceIntent(scene, moveBy(drag.raw ?? drag.delta)),
|
|
512
|
+
blocked: landed.blocked,
|
|
513
|
+
// The landing is drawn only when the drop would be refused, the one thing following the pointer hides.
|
|
514
|
+
landing: landed.blocked ? moved.map(nodeBounds) : undefined,
|
|
515
|
+
};
|
|
387
516
|
}
|
|
388
517
|
if (drag?.kind === 'resize') {
|
|
389
518
|
return preview({ kind: 'resize', id: drag.id, bounds: drag.bounds });
|
|
@@ -421,7 +550,7 @@ const SceneViewRoot = ({
|
|
|
421
550
|
};
|
|
422
551
|
}
|
|
423
552
|
return { displayScene: scene, blocked: false };
|
|
424
|
-
}, [scene, drag, createPreview, projection, minor]);
|
|
553
|
+
}, [scene, drag, settled, createPreview, projection, minor]);
|
|
425
554
|
|
|
426
555
|
/** The bounds a create gesture would land, drawn as a frame whether or not the node itself previews. */
|
|
427
556
|
const createFrame = useMemo(() => {
|
|
@@ -432,18 +561,35 @@ const SceneViewRoot = ({
|
|
|
432
561
|
const onPartCommit = useCallback(
|
|
433
562
|
(node: Node, part: PartKey, text: string) => {
|
|
434
563
|
registry.set(atoms.editing, undefined);
|
|
435
|
-
const values = partValues(node, part, text);
|
|
436
|
-
if (values && capabilities.update && text !== partText(node, part)) {
|
|
564
|
+
const values = partValues(nodeRegistry, node, part, text);
|
|
565
|
+
if (values && capabilities.update && text !== partText(nodeRegistry, node, part)) {
|
|
437
566
|
projection.apply({ kind: 'update', id: node.id, values });
|
|
438
567
|
}
|
|
439
568
|
},
|
|
440
|
-
[registry, atoms.editing, capabilities.update, projection],
|
|
569
|
+
[registry, atoms.editing, capabilities.update, projection, nodeRegistry],
|
|
441
570
|
);
|
|
442
571
|
const onPartCancel = useCallback(() => registry.set(atoms.editing, undefined), [registry, atoms.editing]);
|
|
443
572
|
|
|
573
|
+
// Leaving a link waits a moment before it clears, so the pointer can reach the end handles drawn over it.
|
|
574
|
+
const linkHover = useAtomValue(atoms.linkHover);
|
|
575
|
+
const linkHoverTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
576
|
+
const onLinkHover = useCallback(
|
|
577
|
+
(id: LinkId | undefined) => {
|
|
578
|
+
clearTimeout(linkHoverTimer.current);
|
|
579
|
+
if (id) {
|
|
580
|
+
registry.set(atoms.linkHover, id);
|
|
581
|
+
} else {
|
|
582
|
+
linkHoverTimer.current = setTimeout(() => registry.set(atoms.linkHover, undefined), LINK_HOVER_GRACE_MS);
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
[registry, atoms.linkHover],
|
|
586
|
+
);
|
|
587
|
+
useEffect(() => () => clearTimeout(linkHoverTimer.current), []);
|
|
588
|
+
|
|
444
589
|
const handlers = useMemo<ElementHandlers>(
|
|
445
590
|
() => ({
|
|
446
591
|
onNodePointerDown,
|
|
592
|
+
onLinkHover,
|
|
447
593
|
onLinkPointerDown,
|
|
448
594
|
onLinkDoubleClick,
|
|
449
595
|
onLinkContextMenu,
|
|
@@ -451,7 +597,16 @@ const SceneViewRoot = ({
|
|
|
451
597
|
onPartCancel,
|
|
452
598
|
onNodeOpen: drillIn,
|
|
453
599
|
}),
|
|
454
|
-
[
|
|
600
|
+
[
|
|
601
|
+
onNodePointerDown,
|
|
602
|
+
onLinkHover,
|
|
603
|
+
onLinkPointerDown,
|
|
604
|
+
onLinkDoubleClick,
|
|
605
|
+
onLinkContextMenu,
|
|
606
|
+
onPartCommit,
|
|
607
|
+
onPartCancel,
|
|
608
|
+
drillIn,
|
|
609
|
+
],
|
|
455
610
|
);
|
|
456
611
|
|
|
457
612
|
// Resolved at the root from the model: pointer capture during a drag retargets the click, so a
|
|
@@ -465,16 +620,21 @@ const SceneViewRoot = ({
|
|
|
465
620
|
return;
|
|
466
621
|
}
|
|
467
622
|
const target = document.elementFromPoint(event.clientX, event.clientY);
|
|
468
|
-
// A floating panel over the node took the clicks, so the node beneath is not the one meant
|
|
469
|
-
|
|
623
|
+
// A floating panel over the node took the clicks, so the node beneath is not the one meant; content embedded in
|
|
624
|
+
// the node (an editor) keeps its own double-click (selecting a word).
|
|
625
|
+
if (
|
|
626
|
+
!(target instanceof Element) ||
|
|
627
|
+
!target.closest('[data-node-id]') ||
|
|
628
|
+
target.closest(`[${SCENE_OVERLAY_ATTRIBUTE}]`)
|
|
629
|
+
) {
|
|
470
630
|
return;
|
|
471
631
|
}
|
|
472
632
|
const partElement = target.closest('[data-part]');
|
|
473
633
|
const part =
|
|
474
634
|
partElement?.closest('[data-node-id]')?.getAttribute('data-node-id') === node.id
|
|
475
|
-
?
|
|
635
|
+
? (partElement?.getAttribute('data-part') ?? undefined)
|
|
476
636
|
: undefined;
|
|
477
|
-
if (part && capabilities.update && partText(node, part) !== undefined) {
|
|
637
|
+
if (part && capabilities.update && partText(nodeRegistry, node, part) !== undefined) {
|
|
478
638
|
select([node.id]);
|
|
479
639
|
registry.set(atoms.editing, { id: node.id, part });
|
|
480
640
|
} else if (nodeDef(nodeRegistry, node)?.openable) {
|
|
@@ -493,11 +653,13 @@ const SceneViewRoot = ({
|
|
|
493
653
|
if (!element) {
|
|
494
654
|
return;
|
|
495
655
|
}
|
|
496
|
-
// The pointer is the shape's centre; its top-left
|
|
656
|
+
// The pointer is the shape's centre; its top-left snaps to the minor grid, as a move does, so the shape
|
|
657
|
+
// lands where it was dropped rather than up to half a major cell away.
|
|
497
658
|
const dragAt = (type: NodeType, input: { clientX: number; clientY: number }): Drag => {
|
|
498
659
|
const point = toScene(input);
|
|
499
|
-
const
|
|
500
|
-
const
|
|
660
|
+
const def = nodeRegistry[type];
|
|
661
|
+
const size = def ? nominalSize(def.defaultSize, cell) : { width: 0, height: 0 };
|
|
662
|
+
const from = { x: snapMinor(point.x - size.width / 2), y: snapMinor(point.y - size.height / 2) };
|
|
501
663
|
return { kind: 'create', type, from, to: from, dropped: true };
|
|
502
664
|
};
|
|
503
665
|
return dropTargetForElements({
|
|
@@ -516,23 +678,45 @@ const SceneViewRoot = ({
|
|
|
516
678
|
}
|
|
517
679
|
},
|
|
518
680
|
onDragLeave: cancelDrag,
|
|
519
|
-
|
|
681
|
+
// Placed where it is released: the last drag-over can lag the pointer by a step.
|
|
682
|
+
onDrop: ({ source, location }) => {
|
|
683
|
+
const type = nodeDragType(source.data);
|
|
684
|
+
if (type !== undefined) {
|
|
685
|
+
setDrag(dragAt(type, location.current.input));
|
|
686
|
+
}
|
|
687
|
+
onPointerUpRef.current();
|
|
688
|
+
},
|
|
520
689
|
});
|
|
521
|
-
}, [capabilities.create, nodeRegistry, toScene,
|
|
690
|
+
}, [capabilities.create, nodeRegistry, cell, toScene, snapMinor, setDrag, cancelDrag]);
|
|
522
691
|
|
|
523
692
|
const pointer = useMemo(
|
|
524
693
|
() => screenToScene(camera, { x: viewport.width / 2, y: viewport.height / 2 }),
|
|
525
694
|
[camera, viewport],
|
|
526
695
|
);
|
|
527
696
|
|
|
528
|
-
|
|
529
|
-
|
|
697
|
+
// Shapes may land on free cells beyond the scene's frame, so the cells cover what is in view; only the view,
|
|
698
|
+
// since a union with a distant frame would exceed the grid's cell budget and hide the lattice.
|
|
699
|
+
const latticeBounds = useMemo(
|
|
700
|
+
() =>
|
|
701
|
+
boundsFromPoints(
|
|
702
|
+
screenToScene(camera, { x: 0, y: 0 }),
|
|
703
|
+
screenToScene(camera, { x: viewport.width, y: viewport.height }),
|
|
704
|
+
),
|
|
705
|
+
[camera, viewport],
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
const zoomTo = useCallback(
|
|
709
|
+
(zoom: number) => {
|
|
530
710
|
interactedRef.current = true;
|
|
531
711
|
const centre = { x: viewport.width / 2, y: viewport.height / 2 };
|
|
532
|
-
animateTo(zoomAt(registry.get(atoms.camera), centre,
|
|
712
|
+
animateTo(zoomAt(registry.get(atoms.camera), centre, zoom));
|
|
533
713
|
},
|
|
534
714
|
[viewport, animateTo, registry, atoms.camera],
|
|
535
715
|
);
|
|
716
|
+
const zoomBy = useCallback(
|
|
717
|
+
(factor: number) => zoomTo(registry.get(atoms.camera).zoom * factor),
|
|
718
|
+
[zoomTo, registry, atoms.camera],
|
|
719
|
+
);
|
|
536
720
|
|
|
537
721
|
const deleteSelection = useCallback(() => {
|
|
538
722
|
const ids = [...registry.get(atoms.selection)];
|
|
@@ -549,15 +733,15 @@ const SceneViewRoot = ({
|
|
|
549
733
|
if (!def || !capabilities.create) {
|
|
550
734
|
return;
|
|
551
735
|
}
|
|
552
|
-
const size = def.defaultSize;
|
|
553
|
-
const from = { x:
|
|
736
|
+
const size = nominalSize(def.defaultSize, cell);
|
|
737
|
+
const from = { x: snapMinor(pointer.x - size.width / 2), y: snapMinor(pointer.y - size.height / 2) };
|
|
554
738
|
// `dropped`: there is no drawn box, so the type's default size applies, as for a palette drop.
|
|
555
739
|
const node = createdNode({ kind: 'create', type, from, to: from, dropped: true }, createId(type));
|
|
556
740
|
if (node) {
|
|
557
741
|
commitCreated(node);
|
|
558
742
|
}
|
|
559
743
|
},
|
|
560
|
-
[nodeRegistry, capabilities.create,
|
|
744
|
+
[nodeRegistry, capabilities.create, cell, snapMinor, pointer, createdNode, commitCreated],
|
|
561
745
|
);
|
|
562
746
|
|
|
563
747
|
const toolbarActions = useMemo<ToolbarActions>(
|
|
@@ -565,7 +749,8 @@ const SceneViewRoot = ({
|
|
|
565
749
|
path,
|
|
566
750
|
nameOf,
|
|
567
751
|
onPath: (index) => drillOut(path.length - 1 - index),
|
|
568
|
-
fit: () => animateTo(fitBounds(
|
|
752
|
+
fit: () => animateTo(fitBounds(fitTarget, viewport, inset, NOMINAL_ZOOM)),
|
|
753
|
+
zoomReset: () => zoomTo(NOMINAL_ZOOM),
|
|
569
754
|
zoomIn: () => zoomBy(ZOOM_STEP),
|
|
570
755
|
zoomOut: () => zoomBy(1 / ZOOM_STEP),
|
|
571
756
|
snap: snapEnabled,
|
|
@@ -597,10 +782,11 @@ const SceneViewRoot = ({
|
|
|
597
782
|
nameOf,
|
|
598
783
|
drillOut,
|
|
599
784
|
animateTo,
|
|
600
|
-
|
|
785
|
+
fitTarget,
|
|
601
786
|
viewport,
|
|
602
787
|
inset,
|
|
603
788
|
zoomBy,
|
|
789
|
+
zoomTo,
|
|
604
790
|
snapEnabled,
|
|
605
791
|
toggleSnap,
|
|
606
792
|
guides,
|
|
@@ -625,6 +811,28 @@ const SceneViewRoot = ({
|
|
|
625
811
|
],
|
|
626
812
|
);
|
|
627
813
|
|
|
814
|
+
// The portal on screen that fills most of the view; the rest of the layer fades as it grows, so zooming into
|
|
815
|
+
// a scene (by wheel or drill-in) fades out what surrounds it and zooming out fades it back in.
|
|
816
|
+
const focus = useMemo(() => {
|
|
817
|
+
let best: { id: ElementId; opacity: number } | undefined;
|
|
818
|
+
for (const node of Object.values(displayScene.nodes)) {
|
|
819
|
+
// A frame the host opens itself (one showing an object) is no scene to zoom into, so it keeps its frame.
|
|
820
|
+
if (!isFrameNode(node) || nodeDef(nodeRegistry, node)?.hostOpen?.(node)) {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const bounds = nodeBounds(node);
|
|
824
|
+
const centre = sceneToScreen(camera, { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 });
|
|
825
|
+
if (centre.x < 0 || centre.y < 0 || centre.x > viewport.width || centre.y > viewport.height) {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const opacity = layerOpacity(camera, bounds, viewport);
|
|
829
|
+
if (!best || opacity < best.opacity) {
|
|
830
|
+
best = { id: node.id, opacity };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return best;
|
|
834
|
+
}, [displayScene.nodes, nodeRegistry, camera, viewport]);
|
|
835
|
+
|
|
628
836
|
/** One screen pixel in scene units, for chrome that should not grow with the camera. */
|
|
629
837
|
const frameUnit = 1 / Math.max(camera.zoom, MIN_ZOOM);
|
|
630
838
|
|
|
@@ -635,18 +843,20 @@ const SceneViewRoot = ({
|
|
|
635
843
|
store={store}
|
|
636
844
|
projection={projection}
|
|
637
845
|
capabilities={capabilities}
|
|
846
|
+
readonly={readonly}
|
|
638
847
|
nodeRegistry={nodeRegistry}
|
|
639
848
|
linkRegistry={linkRegistry}
|
|
640
849
|
scene={scene}
|
|
641
850
|
displayScene={displayScene}
|
|
642
851
|
blocked={blocked}
|
|
643
852
|
bounds={bounds}
|
|
853
|
+
latticeBounds={latticeBounds}
|
|
644
854
|
path={path}
|
|
645
855
|
camera={camera}
|
|
646
|
-
nominalZoom={nominalZoom}
|
|
647
856
|
pointer={pointer}
|
|
648
857
|
measured={measured}
|
|
649
858
|
frameUnit={frameUnit}
|
|
859
|
+
focus={focus}
|
|
650
860
|
grid={grid}
|
|
651
861
|
snapEnabled={snapEnabled}
|
|
652
862
|
guides={guides}
|
|
@@ -655,12 +865,16 @@ const SceneViewRoot = ({
|
|
|
655
865
|
hover={hover}
|
|
656
866
|
selectedPoint={selectedPoint}
|
|
657
867
|
editing={editing}
|
|
868
|
+
active={active}
|
|
658
869
|
clipboard={clipboard}
|
|
659
870
|
drag={drag}
|
|
660
871
|
tool={tool}
|
|
661
872
|
debug={debug}
|
|
873
|
+
panels={panels}
|
|
662
874
|
createFrame={createFrame}
|
|
875
|
+
landing={landing}
|
|
663
876
|
handlers={handlers}
|
|
877
|
+
linkHover={linkHover}
|
|
664
878
|
select={select}
|
|
665
879
|
toolbarActions={toolbarActions}
|
|
666
880
|
navigating={navigating}
|
|
@@ -690,29 +904,36 @@ const SceneViewRoot = ({
|
|
|
690
904
|
onKeyDown={onKeyDown}
|
|
691
905
|
rootRef={rootRef}
|
|
692
906
|
>
|
|
693
|
-
<
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
907
|
+
<DockProvider>
|
|
908
|
+
{/* The canvas beside the dock, which docked panels move into; with none docked, it takes no space.
|
|
909
|
+
Clipped rather than hidden: a hidden box still scrolls, so focusing content embedded in a node (an editor's
|
|
910
|
+
caret) would scroll the whole canvas to reveal it, out from under the camera. */}
|
|
911
|
+
<div className={mx('flex dx-fill overflow-clip', classNames)}>
|
|
912
|
+
<div
|
|
913
|
+
ref={rootRef}
|
|
914
|
+
tabIndex={0}
|
|
915
|
+
className={mx(
|
|
916
|
+
'relative grow h-full overflow-clip bg-base-surface outline-none touch-none select-none',
|
|
917
|
+
tool.kind === 'hand' && 'cursor-grab',
|
|
918
|
+
tool.kind === 'node' && 'cursor-crosshair',
|
|
919
|
+
)}
|
|
920
|
+
style={{ contain: 'strict' }}
|
|
921
|
+
data-testid='scene-view'
|
|
922
|
+
onPointerDown={onBackgroundPointerDown}
|
|
923
|
+
onPointerMove={onPointerMove}
|
|
924
|
+
onPointerUp={onPointerUp}
|
|
925
|
+
// A cancelled pointer (a touch the browser took over) abandons the gesture rather than landing it.
|
|
926
|
+
onPointerCancel={cancelDrag}
|
|
927
|
+
onPointerLeave={() => updateHover(undefined)}
|
|
928
|
+
onDoubleClick={onDoubleClick}
|
|
929
|
+
onContextMenu={onContextMenu}
|
|
930
|
+
onKeyDown={onKeyDown}
|
|
931
|
+
>
|
|
932
|
+
{children}
|
|
933
|
+
</div>
|
|
934
|
+
<Dock />
|
|
935
|
+
</div>
|
|
936
|
+
</DockProvider>
|
|
716
937
|
</SceneViewProvider>
|
|
717
938
|
);
|
|
718
939
|
};
|
|
@@ -737,14 +958,17 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
|
|
|
737
958
|
atoms,
|
|
738
959
|
store,
|
|
739
960
|
capabilities,
|
|
961
|
+
readonly,
|
|
740
962
|
projection,
|
|
741
963
|
nodeRegistry,
|
|
742
964
|
displayScene,
|
|
743
965
|
blocked,
|
|
744
966
|
bounds,
|
|
967
|
+
latticeBounds,
|
|
745
968
|
camera,
|
|
746
969
|
measured,
|
|
747
970
|
frameUnit,
|
|
971
|
+
focus,
|
|
748
972
|
grid,
|
|
749
973
|
snapEnabled,
|
|
750
974
|
guides,
|
|
@@ -753,11 +977,14 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
|
|
|
753
977
|
hover,
|
|
754
978
|
selectedPoint,
|
|
755
979
|
editing,
|
|
980
|
+
active,
|
|
756
981
|
clipboard,
|
|
757
982
|
drag,
|
|
758
983
|
debug,
|
|
759
984
|
createFrame,
|
|
985
|
+
landing,
|
|
760
986
|
handlers,
|
|
987
|
+
linkHover,
|
|
761
988
|
select,
|
|
762
989
|
navigating,
|
|
763
990
|
opening,
|
|
@@ -776,12 +1003,15 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
|
|
|
776
1003
|
menuAnchorRef,
|
|
777
1004
|
} = useSceneViewContext('SceneView.Canvas');
|
|
778
1005
|
|
|
1006
|
+
// Handles and ports belong to what is drawn: a hidden layer's elements show none.
|
|
1007
|
+
const shownScene = useMemo(() => visibleScene(displayScene), [displayScene]);
|
|
1008
|
+
|
|
779
1009
|
return (
|
|
780
1010
|
<>
|
|
781
1011
|
{/* Only while snapping: the lines are what a gesture lands on, so drawing them when nothing snaps
|
|
782
1012
|
states a constraint the canvas is not applying. The minor level goes when its cells get too
|
|
783
|
-
small to read. */}
|
|
784
|
-
{snapEnabled && (
|
|
1013
|
+
small to read. Read-only, nothing snaps, so neither grid nor guides are drawn. */}
|
|
1014
|
+
{snapEnabled && !readonly && (
|
|
785
1015
|
<GridComponent
|
|
786
1016
|
size={grid}
|
|
787
1017
|
scale={camera.zoom}
|
|
@@ -795,27 +1025,9 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
|
|
|
795
1025
|
className={mx('absolute pointer-events-none', !measured && 'invisible')}
|
|
796
1026
|
style={{ transform: cameraTransform(camera), transformOrigin: '0 0' }}
|
|
797
1027
|
>
|
|
798
|
-
{/* The frame is chrome rather than content, so its stroke and dashes are divided by the zoom
|
|
799
|
-
the parent applies, the way the control frame's are. It is drawn as a stroke rather than a
|
|
800
|
-
CSS border because a border's width is rounded to whole local pixels, which puts a floor of
|
|
801
|
-
one scene unit under it — exactly the thickening that zooming in would cause. */}
|
|
802
1028
|
{/* A lattice scene shows its cells: the places a shape may land, separated by the gutters. */}
|
|
803
|
-
{guides && latticeOn && projection.lattice && (
|
|
804
|
-
<LatticeGrid spec={projection.lattice} bounds={
|
|
805
|
-
)}
|
|
806
|
-
{guides && (
|
|
807
|
-
<svg className='absolute overflow-visible pointer-events-none' width={1} height={1}>
|
|
808
|
-
<rect
|
|
809
|
-
data-testid='scene-frame'
|
|
810
|
-
x={bounds.x}
|
|
811
|
-
y={bounds.y}
|
|
812
|
-
width={bounds.width}
|
|
813
|
-
height={bounds.height}
|
|
814
|
-
className='fill-none stroke-orange-border opacity-50'
|
|
815
|
-
strokeWidth={frameUnit}
|
|
816
|
-
strokeDasharray={`${FRAME_DASH * frameUnit} ${FRAME_DASH * frameUnit}`}
|
|
817
|
-
/>
|
|
818
|
-
</svg>
|
|
1029
|
+
{guides && !readonly && latticeOn && projection.lattice && (
|
|
1030
|
+
<LatticeGrid spec={projection.lattice} bounds={latticeBounds} unit={frameUnit} />
|
|
819
1031
|
)}
|
|
820
1032
|
<div className='pointer-events-auto'>
|
|
821
1033
|
<SceneLayer
|
|
@@ -826,35 +1038,46 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
|
|
|
826
1038
|
depth={0}
|
|
827
1039
|
liveDepth={liveDepth}
|
|
828
1040
|
selected={selection}
|
|
1041
|
+
plain={isMoving(drag)}
|
|
829
1042
|
hover={hover}
|
|
1043
|
+
hoveredLink={linkHover}
|
|
830
1044
|
opening={opening}
|
|
1045
|
+
focus={focus}
|
|
831
1046
|
editing={editing}
|
|
1047
|
+
active={active}
|
|
832
1048
|
ghost={drag?.kind === 'create' ? PREVIEW_NODE_ID : undefined}
|
|
833
1049
|
debug={debug}
|
|
834
1050
|
handlers={handlers}
|
|
835
1051
|
// Routes follow the gutters in lattice mode, whether or not snap is on.
|
|
836
1052
|
lattice={latticeOn ? projection.lattice : undefined}
|
|
1053
|
+
cell={grid * MAJOR_GRID_RATIO}
|
|
837
1054
|
/>
|
|
838
1055
|
</div>
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1056
|
+
{/* Read-only, nothing is edited, so no selection frame, handle or port is drawn. */}
|
|
1057
|
+
{!readonly && (
|
|
1058
|
+
<ControlFrame
|
|
1059
|
+
scene={shownScene}
|
|
1060
|
+
registry={nodeRegistry}
|
|
1061
|
+
selection={selection}
|
|
1062
|
+
hover={hover}
|
|
1063
|
+
hoveredLink={drag ? undefined : linkHover}
|
|
1064
|
+
onLinkHover={handlers.onLinkHover}
|
|
1065
|
+
selectedPoint={selectedPoint}
|
|
1066
|
+
zoom={camera.zoom}
|
|
1067
|
+
drag={drag}
|
|
1068
|
+
capabilities={capabilities}
|
|
1069
|
+
createFrame={createFrame}
|
|
1070
|
+
landing={landing}
|
|
1071
|
+
blocked={blocked}
|
|
1072
|
+
lattice={latticeOn ? projection.lattice : undefined}
|
|
1073
|
+
onHandlePointerDown={onHandlePointerDown}
|
|
1074
|
+
onPortPointerDown={onPortPointerDown}
|
|
1075
|
+
onEndPointerDown={onEndPointerDown}
|
|
1076
|
+
onPointPointerDown={onPointPointerDown}
|
|
1077
|
+
onMidpointPointerDown={onMidpointPointerDown}
|
|
1078
|
+
onPointContextMenu={onPointContextMenu}
|
|
1079
|
+
/>
|
|
1080
|
+
)}
|
|
858
1081
|
{overlay}
|
|
859
1082
|
</div>
|
|
860
1083
|
{/* Wheel events still bubble to the root through the shield, so a zoom keeps zooming. */}
|
|
@@ -937,10 +1160,10 @@ const barFrame = 'absolute w-max max-w-[50%]';
|
|
|
937
1160
|
|
|
938
1161
|
/** Where the view is in the scene tree. */
|
|
939
1162
|
const SceneViewNavigation = ({ classNames = 'top-2 left-2' }: SceneViewBarProps) => {
|
|
940
|
-
const { toolbarActions
|
|
1163
|
+
const { toolbarActions } = useSceneViewContext('SceneView.Navigation');
|
|
941
1164
|
return (
|
|
942
1165
|
<div className={mx(barFrame, classNames)}>
|
|
943
|
-
<NavigationToolbar actions={toolbarActions}
|
|
1166
|
+
<NavigationToolbar actions={toolbarActions} />
|
|
944
1167
|
</div>
|
|
945
1168
|
);
|
|
946
1169
|
};
|
|
@@ -949,7 +1172,11 @@ SceneViewNavigation.displayName = 'SceneView.Navigation';
|
|
|
949
1172
|
|
|
950
1173
|
/** Everything that changes the view or the scene. */
|
|
951
1174
|
const SceneViewActions = ({ classNames = 'bottom-2 left-1/2 -translate-x-1/2' }: SceneViewBarProps) => {
|
|
952
|
-
const { toolbarActions, nodeRegistry, capabilities } = useSceneViewContext('SceneView.Actions');
|
|
1175
|
+
const { toolbarActions, nodeRegistry, capabilities, readonly } = useSceneViewContext('SceneView.Actions');
|
|
1176
|
+
// Every action here edits the scene or how edits snap.
|
|
1177
|
+
if (readonly) {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
953
1180
|
return (
|
|
954
1181
|
<div className={mx(barFrame, classNames)}>
|
|
955
1182
|
<ActionToolbar actions={toolbarActions} nodes={nodeRegistry} capabilities={capabilities} />
|
|
@@ -961,11 +1188,11 @@ SceneViewActions.displayName = 'SceneView.Actions';
|
|
|
961
1188
|
|
|
962
1189
|
/** The camera's controls and numbers; nothing here changes the scene. */
|
|
963
1190
|
const SceneViewDebug = ({ classNames = 'bottom-2 left-2' }: SceneViewBarProps) => {
|
|
964
|
-
const { toolbarActions,
|
|
1191
|
+
const { toolbarActions, camera, pointer } = useSceneViewContext('SceneView.Debug');
|
|
965
1192
|
return (
|
|
966
1193
|
<div className={mx(barFrame, classNames)}>
|
|
967
1194
|
<CameraToolbar actions={toolbarActions}>
|
|
968
|
-
{Math.round(
|
|
1195
|
+
{Math.round(camera.zoom * 100)}% · ({Math.round(pointer.x)}, {Math.round(pointer.y)})
|
|
969
1196
|
</CameraToolbar>
|
|
970
1197
|
</div>
|
|
971
1198
|
);
|
|
@@ -978,10 +1205,11 @@ SceneViewDebug.displayName = 'SceneView.Debug';
|
|
|
978
1205
|
//
|
|
979
1206
|
|
|
980
1207
|
/** The tool rail: what the next gesture will draw. */
|
|
981
|
-
const SceneViewPalette = ({ classNames = 'absolute top-14 left-2' }: SceneViewBarProps) => {
|
|
1208
|
+
const SceneViewPalette = ({ classNames = 'absolute top-14 bottom-14 left-2' }: SceneViewBarProps) => {
|
|
982
1209
|
const { tool, nodeRegistry, linkRegistry, capabilities, setTool } = useSceneViewContext('SceneView.Palette');
|
|
983
1210
|
return (
|
|
984
|
-
|
|
1211
|
+
// Spans the room the rail may take, so it can fold its groups to fit; the canvas below the rail keeps the pointer.
|
|
1212
|
+
<div className={mx('pointer-events-none', classNames)}>
|
|
985
1213
|
<Palette
|
|
986
1214
|
tool={tool}
|
|
987
1215
|
nodes={nodeRegistry}
|
|
@@ -999,32 +1227,216 @@ SceneViewPalette.displayName = 'SceneView.Palette';
|
|
|
999
1227
|
// Properties
|
|
1000
1228
|
//
|
|
1001
1229
|
|
|
1002
|
-
export type SceneViewPropertiesProps = Util.ThemedClassName<
|
|
1230
|
+
export type SceneViewPropertiesProps = Util.ThemedClassName<
|
|
1231
|
+
Pick<PropertiesProps, 'fields' | 'db' | 'getOptions' | 'overrides'> & {
|
|
1232
|
+
/** Narrows the scenes a scene shape may open (e.g. to the host's own, not those it shows from elsewhere). */
|
|
1233
|
+
sceneFilter?: (id: SceneId) => boolean;
|
|
1234
|
+
}
|
|
1235
|
+
>;
|
|
1003
1236
|
|
|
1004
1237
|
/** The selected element's properties as a floating panel; absent while nothing is selected. */
|
|
1005
1238
|
const SceneViewProperties = ({
|
|
1006
|
-
classNames =
|
|
1239
|
+
classNames = PANEL_CLASSES,
|
|
1007
1240
|
fields,
|
|
1241
|
+
db,
|
|
1242
|
+
getOptions,
|
|
1243
|
+
overrides,
|
|
1244
|
+
sceneFilter,
|
|
1008
1245
|
}: SceneViewPropertiesProps) => {
|
|
1009
|
-
const { projection, atoms, nodeRegistry, capabilities, selection } =
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1246
|
+
const { projection, atoms, nodeRegistry, capabilities, readonly, selection, store, path, panels } =
|
|
1247
|
+
useSceneViewContext('SceneView.Properties');
|
|
1248
|
+
const docked = panels === 'docked' && !readonly;
|
|
1249
|
+
const registry = useRegistry();
|
|
1250
|
+
const scenes = useAtomValue(store.scenes);
|
|
1251
|
+
const options = useMemo(() => sceneOptions(scenes, path, sceneFilter), [scenes, path, sceneFilter]);
|
|
1252
|
+
|
|
1253
|
+
// The selection moves into a new scene, opened by a shape where it was; the shape is then the selection.
|
|
1254
|
+
const onGroup = useCallback(() => {
|
|
1255
|
+
const id = createId('scene');
|
|
1256
|
+
const current = registry.get(projection.scene);
|
|
1257
|
+
const group = groupIntoScene(current, selection, id, activeLayer(current, registry.get(atoms.layer)));
|
|
1258
|
+
if (!group) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const before = registry.get(store.scenes);
|
|
1262
|
+
registry.set(store.scenes, { ...before, [id]: { ...group.child, name: 'Untitled' } });
|
|
1263
|
+
projection.apply({ kind: 'batch', intents: group.intents });
|
|
1264
|
+
// A projection may refuse the batch (`apply` reports nothing), so the shape's presence is the result: without
|
|
1265
|
+
// it the new scene would be an orphan, so it goes and the selection stays.
|
|
1266
|
+
if (!registry.get(projection.scene).nodes[id]) {
|
|
1267
|
+
const { [id]: _, ...scenes } = registry.get(store.scenes);
|
|
1268
|
+
registry.set(store.scenes, scenes);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
// One undo step takes the new scene away with the shape that opens it.
|
|
1272
|
+
recordScenes(registry, atoms.undo, path[path.length - 1], before);
|
|
1273
|
+
registry.set(atoms.selection, new Set([id]));
|
|
1274
|
+
registry.set(atoms.point, undefined);
|
|
1275
|
+
}, [registry, projection, selection, store.scenes, atoms.layer, atoms.selection, atoms.point, atoms.undo, path]);
|
|
1013
1276
|
|
|
1014
|
-
|
|
1277
|
+
const panel = (
|
|
1015
1278
|
<Properties
|
|
1016
|
-
classNames={mx('rounded-sm bg-modal-surface border border-separator', classNames)}
|
|
1279
|
+
classNames={docked ? 'h-auto' : mx('rounded-sm bg-modal-surface border border-separator', classNames)}
|
|
1280
|
+
docked={docked}
|
|
1017
1281
|
projection={projection}
|
|
1018
1282
|
atoms={atoms}
|
|
1019
1283
|
nodes={nodeRegistry}
|
|
1020
1284
|
fields={fields}
|
|
1285
|
+
db={db}
|
|
1286
|
+
getOptions={getOptions}
|
|
1287
|
+
overrides={overrides}
|
|
1288
|
+
sceneOptions={options}
|
|
1289
|
+
styles={store.styles}
|
|
1290
|
+
onGroup={capabilities.create && capabilities.delete ? onGroup : undefined}
|
|
1021
1291
|
readonly={!capabilities.update}
|
|
1022
1292
|
/>
|
|
1023
1293
|
);
|
|
1294
|
+
// Docked, the panel stays in its section whatever is selected (it says when nothing is); floating, it shows only
|
|
1295
|
+
// with a selection.
|
|
1296
|
+
const dockedPanel = useDockSection(PROPERTIES_SECTION, docked, panel);
|
|
1297
|
+
if (docked || readonly) {
|
|
1298
|
+
return dockedPanel;
|
|
1299
|
+
}
|
|
1300
|
+
return selection.size > 0 ? panel : null;
|
|
1024
1301
|
};
|
|
1025
1302
|
|
|
1026
1303
|
SceneViewProperties.displayName = 'SceneView.Properties';
|
|
1027
1304
|
|
|
1305
|
+
//
|
|
1306
|
+
// Dock
|
|
1307
|
+
//
|
|
1308
|
+
|
|
1309
|
+
const PROPERTIES_SECTION: DockSection = {
|
|
1310
|
+
id: 'properties',
|
|
1311
|
+
title: 'Properties',
|
|
1312
|
+
icon: 'ph--sliders-horizontal--regular',
|
|
1313
|
+
order: 0,
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
const LAYERS_SECTION: DockSection = { id: 'layers', title: 'Layers', icon: 'ph--stack--regular', order: 1 };
|
|
1317
|
+
|
|
1318
|
+
//
|
|
1319
|
+
// Layers
|
|
1320
|
+
//
|
|
1321
|
+
|
|
1322
|
+
export type SceneViewLayersProps = Util.ThemedClassName<{}>;
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* The current scene's layers as a floating panel where the properties panel goes, shown while nothing is selected
|
|
1326
|
+
* (the two take turns). Each edit is one intent, so one undo step.
|
|
1327
|
+
*/
|
|
1328
|
+
const SceneViewLayers = ({ classNames = PANEL_CLASSES }: SceneViewLayersProps) => {
|
|
1329
|
+
const {
|
|
1330
|
+
projection,
|
|
1331
|
+
atoms,
|
|
1332
|
+
capabilities,
|
|
1333
|
+
readonly: viewReadonly,
|
|
1334
|
+
selection,
|
|
1335
|
+
panels,
|
|
1336
|
+
} = useSceneViewContext('SceneView.Layers');
|
|
1337
|
+
const docked = panels === 'docked' && !viewReadonly;
|
|
1338
|
+
const registry = useRegistry();
|
|
1339
|
+
const scene = useAtomValue(projection.scene);
|
|
1340
|
+
const active = useAtomValue(atoms.layer);
|
|
1341
|
+
const layers = useMemo(() => sceneLayers(scene), [scene]);
|
|
1342
|
+
const readonly = !capabilities.update;
|
|
1343
|
+
const setLayer = useCallback((layer: Layer) => projection.apply({ kind: 'layer', layer }), [projection]);
|
|
1344
|
+
const byId = useCallback((id: LayerId) => layers.find((layer) => layer.id === id), [layers]);
|
|
1345
|
+
// The layers picked in the panel; the active layer (the top-most of them) is the one new shapes go on.
|
|
1346
|
+
const [picked, setPicked] = useState<LayerId[]>([]);
|
|
1347
|
+
const top = activeLayer(scene, active);
|
|
1348
|
+
const selected = useMemo(() => {
|
|
1349
|
+
const ids = picked.filter((id) => byId(id));
|
|
1350
|
+
return ids.includes(top) ? ids : [top];
|
|
1351
|
+
}, [picked, byId, top]);
|
|
1352
|
+
const select = useCallback(
|
|
1353
|
+
(ids: LayerId[]) => {
|
|
1354
|
+
setPicked(ids);
|
|
1355
|
+
const topMost = [...layers].reverse().find((layer) => ids.includes(layer.id));
|
|
1356
|
+
topMost && registry.set(atoms.layer, topMost.id);
|
|
1357
|
+
},
|
|
1358
|
+
[layers, registry, atoms],
|
|
1359
|
+
);
|
|
1360
|
+
const panel = (
|
|
1361
|
+
<LayersPanel
|
|
1362
|
+
classNames={docked ? 'h-auto' : mx('rounded-sm bg-modal-surface border border-separator', classNames)}
|
|
1363
|
+
docked={docked}
|
|
1364
|
+
layers={layers}
|
|
1365
|
+
selected={selected}
|
|
1366
|
+
readonly={readonly}
|
|
1367
|
+
onSelectedChange={select}
|
|
1368
|
+
onToggle={(id) => {
|
|
1369
|
+
const layer = byId(id);
|
|
1370
|
+
layer && setLayer({ ...layer, hidden: !layer.hidden });
|
|
1371
|
+
}}
|
|
1372
|
+
onRename={(id, name) => {
|
|
1373
|
+
const layer = byId(id);
|
|
1374
|
+
layer && setLayer({ ...layer, name });
|
|
1375
|
+
}}
|
|
1376
|
+
onMove={(id, index) => {
|
|
1377
|
+
const layer = moveLayer(scene, id, index);
|
|
1378
|
+
layer && setLayer(layer);
|
|
1379
|
+
}}
|
|
1380
|
+
onCreate={() => {
|
|
1381
|
+
const layer = createLayer(scene, createId('layer'));
|
|
1382
|
+
setLayer(layer);
|
|
1383
|
+
select([layer.id]);
|
|
1384
|
+
return layer.id;
|
|
1385
|
+
}}
|
|
1386
|
+
onDelete={
|
|
1387
|
+
capabilities.delete
|
|
1388
|
+
? (ids) => {
|
|
1389
|
+
projection.apply({ kind: 'batch', intents: ids.map((id): Intent => ({ kind: 'removeLayer', id })) });
|
|
1390
|
+
setPicked([]);
|
|
1391
|
+
}
|
|
1392
|
+
: undefined
|
|
1393
|
+
}
|
|
1394
|
+
onMerge={(ids, into) => {
|
|
1395
|
+
const merge = mergeLayersIntent(scene, ids, into);
|
|
1396
|
+
if (merge) {
|
|
1397
|
+
projection.apply(merge);
|
|
1398
|
+
select([into]);
|
|
1399
|
+
}
|
|
1400
|
+
}}
|
|
1401
|
+
/>
|
|
1402
|
+
);
|
|
1403
|
+
const dockedPanel = useDockSection(LAYERS_SECTION, docked, panel);
|
|
1404
|
+
if (docked || viewReadonly) {
|
|
1405
|
+
return dockedPanel;
|
|
1406
|
+
}
|
|
1407
|
+
// Floating, the layers take the properties panel's place while nothing is selected.
|
|
1408
|
+
return selection.size > 0 ? null : panel;
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
SceneViewLayers.displayName = 'SceneView.Layers';
|
|
1412
|
+
|
|
1413
|
+
//
|
|
1414
|
+
// About
|
|
1415
|
+
//
|
|
1416
|
+
|
|
1417
|
+
const ABOUT_SECTION: DockSection = { id: 'about', title: 'About', icon: 'ph--info--regular', order: 2 };
|
|
1418
|
+
|
|
1419
|
+
export type SceneViewAboutProps = Util.ThemedClassName<{}>;
|
|
1420
|
+
|
|
1421
|
+
/** Counts of the scene's objects and the drawing's scenes; a dock section only, since it has no place over the canvas. */
|
|
1422
|
+
const SceneViewAbout = ({ classNames }: SceneViewAboutProps) => {
|
|
1423
|
+
const { scene, store, panels } = useSceneViewContext('SceneView.About');
|
|
1424
|
+
const scenes = useAtomValue(store.scenes);
|
|
1425
|
+
const docked = panels === 'docked';
|
|
1426
|
+
const stats = useMemo<AboutStat[]>(
|
|
1427
|
+
() => [
|
|
1428
|
+
{ id: 'nodes', label: 'Nodes', value: Object.keys(scene.nodes).length },
|
|
1429
|
+
{ id: 'links', label: 'Links', value: Object.keys(scene.links).length },
|
|
1430
|
+
{ id: 'layers', label: 'Layers', value: Object.keys(scene.layers ?? {}).length },
|
|
1431
|
+
{ id: 'scenes', label: 'Scenes', value: Object.keys(scenes).length },
|
|
1432
|
+
],
|
|
1433
|
+
[scene, scenes],
|
|
1434
|
+
);
|
|
1435
|
+
return useDockSection(ABOUT_SECTION, docked, <About classNames={mx('h-auto', classNames)} stats={stats} />);
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
SceneViewAbout.displayName = 'SceneView.About';
|
|
1439
|
+
|
|
1028
1440
|
export const SceneView = {
|
|
1029
1441
|
Root: SceneViewRoot,
|
|
1030
1442
|
Canvas: SceneViewCanvas,
|
|
@@ -1033,4 +1445,6 @@ export const SceneView = {
|
|
|
1033
1445
|
Debug: SceneViewDebug,
|
|
1034
1446
|
Palette: SceneViewPalette,
|
|
1035
1447
|
Properties: SceneViewProperties,
|
|
1448
|
+
Layers: SceneViewLayers,
|
|
1449
|
+
About: SceneViewAbout,
|
|
1036
1450
|
};
|