@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
|
@@ -9,9 +9,9 @@ import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
|
|
|
9
9
|
import { SceneManager } from './SceneManager.ts';
|
|
10
10
|
|
|
11
11
|
const PORT = 9006;
|
|
12
|
-
const FREEHAND_URL = storybookUrl('ui-react-ui-canvas-
|
|
12
|
+
const FREEHAND_URL = storybookUrl('ui-react-ui-canvas-sceneview--freehand', PORT);
|
|
13
13
|
|
|
14
|
-
// The fixture (`createSceneTree(1)`): rectangle A, ellipse B, text T,
|
|
14
|
+
// The fixture (`createSceneTree(1)`): rectangle A, ellipse B, text T, rectangle C; links A→B (curve), A→C
|
|
15
15
|
// (line, directed) and B→C (spline). Every edge sits on the major grid.
|
|
16
16
|
test.describe('SceneView', () => {
|
|
17
17
|
let page: Page;
|
|
@@ -37,8 +37,8 @@ test.describe('SceneView', () => {
|
|
|
37
37
|
test('draws the fixture with an arrowhead on the directed link', async () => {
|
|
38
38
|
await expect(page.locator('[data-node-id]')).toHaveCount(4);
|
|
39
39
|
expect(await scene.linkCount()).toBe(3);
|
|
40
|
-
// One marker set per layer (arrow and circle, start and end); the directed line uses the end arrow.
|
|
41
|
-
await expect(page.locator('[data-testid="scene-view"] marker')).toHaveCount(
|
|
40
|
+
// One marker set per layer (arrow, triangle and circle, start and end); the directed line uses the end arrow.
|
|
41
|
+
await expect(page.locator('[data-testid="scene-view"] marker')).toHaveCount(6);
|
|
42
42
|
await expect(page.locator('[data-testid="scene-view"] path[marker-end]')).toHaveCount(1);
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -83,7 +83,7 @@ test.describe('SceneView', () => {
|
|
|
83
83
|
await expect(ports).toHaveCount(0);
|
|
84
84
|
const box = await scene.box(scene.node('scene:root/a'));
|
|
85
85
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
86
|
-
await expect(scene.node('scene:root/a')).toHaveClass(/border-
|
|
86
|
+
await expect(scene.node('scene:root/a')).toHaveClass(/border-focus\/50/);
|
|
87
87
|
await expect(ports).not.toHaveCount(0);
|
|
88
88
|
await page.mouse.move(box.x + box.width + 200, box.y + box.height + 200);
|
|
89
89
|
await expect(ports).toHaveCount(0);
|
|
@@ -139,6 +139,8 @@ test.describe('SceneView', () => {
|
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
test('a marquee replaces the selection, shift adds and alt subtracts', async () => {
|
|
142
|
+
// Fit frames the shapes tightly, so step out to leave empty canvas around A for the marquee to start on.
|
|
143
|
+
await scene.zoomOut();
|
|
142
144
|
const a = await scene.box(scene.node('scene:root/a'));
|
|
143
145
|
// The empty canvas above and left of A, dragging back over A's corner. Not the other corner: the
|
|
144
146
|
// B→C spline passes below and right of A, and a press on a link starts an endpoint drag.
|
|
@@ -217,12 +219,12 @@ test.describe('SceneView', () => {
|
|
|
217
219
|
// No node was created: the drop landed on an existing one rather than on empty canvas.
|
|
218
220
|
expect(await scene.nodeCount()).toBe(4);
|
|
219
221
|
// The new link is the selection, and nothing else is.
|
|
220
|
-
await expect(page.locator('[data-link-id].stroke-
|
|
222
|
+
await expect(page.locator('[data-link-id].stroke-focus')).toHaveCount(1);
|
|
221
223
|
expect(await scene.selectedNodes()).toEqual([]);
|
|
222
224
|
// The hover follows the pointer, not the gesture: the source is no longer highlighted, the target under
|
|
223
225
|
// the released pointer is.
|
|
224
|
-
await expect(scene.node('scene:root/a')).not.toHaveClass(/border-
|
|
225
|
-
await expect(scene.node('scene:root/c')).toHaveClass(/border-
|
|
226
|
+
await expect(scene.node('scene:root/a')).not.toHaveClass(/border-focus\/50/);
|
|
227
|
+
await expect(scene.node('scene:root/c')).toHaveClass(/border-focus\/50/);
|
|
226
228
|
});
|
|
227
229
|
|
|
228
230
|
test('the line tool draws nothing when a press on a node never moves', async () => {
|
|
@@ -245,7 +247,7 @@ test.describe('SceneView', () => {
|
|
|
245
247
|
// One step is ×1.25; both readouts round, so they can disagree by a point.
|
|
246
248
|
await expect.poll(async () => Math.abs((await scene.zoom()) - fitted * 1.25)).toBeLessThanOrEqual(1);
|
|
247
249
|
await page.getByTestId('toolbar-create').click();
|
|
248
|
-
await page.getByTestId('create-
|
|
250
|
+
await page.getByTestId('create-note').click();
|
|
249
251
|
await expect(page.locator('[data-node-id]')).toHaveCount(5);
|
|
250
252
|
await page.getByTestId('toolbar-delete').click();
|
|
251
253
|
await expect(page.locator('[data-node-id]')).toHaveCount(4);
|
|
@@ -260,25 +262,25 @@ test.describe('SceneView', () => {
|
|
|
260
262
|
return rows[0] < rows[1] && rows[1] < rows[2];
|
|
261
263
|
})
|
|
262
264
|
.toBe(true);
|
|
263
|
-
// A
|
|
265
|
+
// A frame made from the toolbar gets its child scene, so Enter opens it.
|
|
264
266
|
await page.getByTestId('toolbar-create').click();
|
|
265
|
-
await page.getByTestId('create-
|
|
267
|
+
await page.getByTestId('create-frame').click();
|
|
266
268
|
await scene.focus();
|
|
267
269
|
await page.keyboard.press('Enter');
|
|
268
270
|
await expect(page.getByTestId('toolbar-up')).toBeEnabled();
|
|
269
271
|
});
|
|
270
272
|
|
|
271
|
-
test('a
|
|
273
|
+
test('a frame opens from its zoom-in control', async () => {
|
|
272
274
|
await page.getByTestId('toolbar-create').click();
|
|
273
|
-
await page.getByTestId('create-
|
|
275
|
+
await page.getByTestId('create-frame').click();
|
|
274
276
|
await page.getByTestId('portal-open').first().click();
|
|
275
277
|
await expect(page.getByTestId('toolbar-up')).toBeEnabled();
|
|
276
278
|
});
|
|
277
279
|
|
|
278
|
-
test('the properties panel edits the selected
|
|
280
|
+
test('the properties panel edits the selected node', async () => {
|
|
279
281
|
await scene.clickNode('scene:root/c');
|
|
280
282
|
const labels = await page.locator('[data-testid="properties"] label').allTextContents();
|
|
281
|
-
expect(labels).toEqual(expect.arrayContaining(['
|
|
283
|
+
expect(labels).toEqual(expect.arrayContaining(['Label', 'Style']));
|
|
282
284
|
// Geometry is two labelled number fields per row.
|
|
283
285
|
expect(labels).toEqual(expect.arrayContaining(['X', 'Y', 'Width', 'Height', 'Ports per side']));
|
|
284
286
|
});
|
|
@@ -289,14 +291,15 @@ test.describe('SceneView', () => {
|
|
|
289
291
|
const option = (key: string) => grid.locator(`[data-style-option="${key}"]`);
|
|
290
292
|
// An unstyled node matches no swatch.
|
|
291
293
|
await expect(grid.locator('[aria-checked="true"]')).toHaveCount(0);
|
|
292
|
-
|
|
293
|
-
await
|
|
294
|
-
await expect(
|
|
294
|
+
// Tone 1 is the strongest fill.
|
|
295
|
+
await option('blue:1').click();
|
|
296
|
+
await expect(option('blue:1')).toHaveAttribute('aria-checked', 'true');
|
|
297
|
+
await expect(scene.node('scene:root/a')).toHaveClass(/bg-blue-500/);
|
|
295
298
|
await option('neutral:0').click();
|
|
296
299
|
await expect(option('neutral:0')).toHaveAttribute('aria-checked', 'true');
|
|
297
300
|
// Outline: the fill goes (the selected frame's border shows the selection, not the hue).
|
|
298
301
|
await expect(scene.node('scene:root/a')).toHaveClass(/bg-transparent/);
|
|
299
|
-
await expect(scene.node('scene:root/a')).not.toHaveClass(/bg-blue-
|
|
302
|
+
await expect(scene.node('scene:root/a')).not.toHaveClass(/bg-blue-500/);
|
|
300
303
|
});
|
|
301
304
|
|
|
302
305
|
test('the geometry fields step by the grid and move the node', async () => {
|
|
@@ -315,10 +318,10 @@ test.describe('SceneView', () => {
|
|
|
315
318
|
await expect.poll(async () => (await scene.box(scene.node('scene:root/c'))).x).toBeGreaterThan(node.x);
|
|
316
319
|
});
|
|
317
320
|
|
|
318
|
-
test('a read-only view selects
|
|
321
|
+
test('a read-only view selects nothing, draws no handles and applies no edit', async ({ browser }) => {
|
|
319
322
|
await close?.();
|
|
320
323
|
({ page, close } = await setupPage(browser, {
|
|
321
|
-
url: storybookUrl('ui-react-ui-canvas-
|
|
324
|
+
url: storybookUrl('ui-react-ui-canvas-sceneview--readonly', PORT),
|
|
322
325
|
viewportSize: { width: 1400, height: 800 },
|
|
323
326
|
}));
|
|
324
327
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
@@ -326,16 +329,15 @@ test.describe('SceneView', () => {
|
|
|
326
329
|
await scene.ready();
|
|
327
330
|
const before = await scene.nodeCount();
|
|
328
331
|
await scene.clickNode('scene:root/a');
|
|
329
|
-
expect(await scene.selectedNodes()).toEqual([
|
|
332
|
+
expect(await scene.selectedNodes()).toEqual([]);
|
|
330
333
|
await expect(page.locator('[data-testid="scene-view"] svg rect[style*="cursor"]')).toHaveCount(0);
|
|
331
334
|
const hovered = await scene.box(scene.node('scene:root/a'));
|
|
332
335
|
await page.mouse.move(hovered.x + hovered.width / 2, hovered.y + hovered.height / 2);
|
|
333
336
|
await expect(page.locator('[data-testid="scene-view"] circle.cursor-crosshair')).toHaveCount(0);
|
|
334
337
|
await page.keyboard.press('Delete');
|
|
335
338
|
expect(await scene.nodeCount()).toBe(before);
|
|
336
|
-
// Only the select and pan tools remain, and
|
|
339
|
+
// Only the select and pan tools remain, and the actions bar, which only edits, is gone.
|
|
337
340
|
await expect(page.locator('[data-testid="palette"] button')).toHaveCount(2);
|
|
338
|
-
await expect(page.getByTestId('
|
|
339
|
-
await expect(page.getByTestId('toolbar-delete')).toBeDisabled();
|
|
341
|
+
await expect(page.getByTestId('canvas-actions')).toHaveCount(0);
|
|
340
342
|
});
|
|
341
343
|
});
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, test } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { isEllipseNode, isFrameNode, isNoteNode, isRectNode } from '../model/types.ts';
|
|
8
8
|
import { SceneBuilder } from './builder.ts';
|
|
9
|
+
import { DEFAULT_LAYER } from './layers.ts';
|
|
9
10
|
|
|
10
11
|
const box = (x: number, y: number) => ({ x, y, width: 200, height: 100 });
|
|
11
12
|
|
|
@@ -14,10 +15,7 @@ describe('SceneBuilder', () => {
|
|
|
14
15
|
const { root, scenes } = SceneBuilder.scene('s', [
|
|
15
16
|
SceneBuilder.rect('a', box(0, 0)).properties({ label: 'A' }),
|
|
16
17
|
SceneBuilder.ellipse('b', box(400, 0)).properties({ label: 'B' }),
|
|
17
|
-
SceneBuilder.
|
|
18
|
-
name: 'C',
|
|
19
|
-
attributes: ['id: string'],
|
|
20
|
-
}),
|
|
18
|
+
SceneBuilder.note('c', { x: 0, y: 300, width: 200, height: 150 }).properties({ style: { hue: 'teal' } }),
|
|
21
19
|
SceneBuilder.link('line', 'a#e2', 'b#w2'),
|
|
22
20
|
SceneBuilder.link('spline', 'b', 'c').properties({ points: [{ x: 300, y: 250 }] }),
|
|
23
21
|
SceneBuilder.link('line', '@10,20', 'a')
|
|
@@ -33,9 +31,9 @@ describe('SceneBuilder', () => {
|
|
|
33
31
|
expect(Object.keys(scene.nodes)).toEqual(['a', 'b', 'c']);
|
|
34
32
|
expect(scene.nodes.a.center).toEqual({ x: 100, y: 50 });
|
|
35
33
|
expect(isEllipseNode(scene.nodes.b) && scene.nodes.b.size).toEqual({ width: 200, height: 100 });
|
|
36
|
-
expect(
|
|
37
|
-
// A
|
|
38
|
-
expect(
|
|
34
|
+
expect(scene.nodes.c.style).toEqual({ hue: 'teal' });
|
|
35
|
+
// A note keeps its type's default text where the fixture does not set it.
|
|
36
|
+
expect(isNoteNode(scene.nodes.c) && scene.nodes.c.text).toEqual('Note');
|
|
39
37
|
expect(scene.links['a-b'].source).toEqual({ node: 'a', port: 'e2' });
|
|
40
38
|
expect(scene.links['a-b'].target).toEqual({ node: 'b', port: 'w2' });
|
|
41
39
|
expect(scene.links['b-c'].type === 'spline' && scene.links['b-c'].points).toEqual([{ x: 300, y: 250 }]);
|
|
@@ -70,7 +68,7 @@ describe('SceneBuilder', () => {
|
|
|
70
68
|
expect(root).toBe('root');
|
|
71
69
|
expect(scenes.map((scene) => scene.id)).toEqual(['root', 'f']);
|
|
72
70
|
const portal = scenes[0].nodes.f;
|
|
73
|
-
expect(
|
|
71
|
+
expect(isFrameNode(portal) && [portal.scene, portal.label, portal.center]).toEqual([
|
|
74
72
|
'f',
|
|
75
73
|
'Inner',
|
|
76
74
|
{ x: 500, y: 50 },
|
|
@@ -80,6 +78,45 @@ describe('SceneBuilder', () => {
|
|
|
80
78
|
expect(() => SceneBuilder.scene('root', [SceneBuilder.scene('f')]).build()).toThrow(/needs a frame/);
|
|
81
79
|
});
|
|
82
80
|
|
|
81
|
+
test('every scene has the default layer, and every element is on it', ({ expect }) => {
|
|
82
|
+
const { scenes } = SceneBuilder.scene('root', [
|
|
83
|
+
SceneBuilder.rect('a', box(0, 0)),
|
|
84
|
+
SceneBuilder.rect('b', box(400, 0)).properties({ layer: 'top' }),
|
|
85
|
+
SceneBuilder.link('line', 'a', 'b').id('ab'),
|
|
86
|
+
SceneBuilder.scene('f', [SceneBuilder.rect('f1', box(0, 0))]).at(box(0, 200)),
|
|
87
|
+
]).build();
|
|
88
|
+
for (const scene of scenes) {
|
|
89
|
+
expect(scene.layers).toEqual({ [DEFAULT_LAYER.id]: DEFAULT_LAYER });
|
|
90
|
+
}
|
|
91
|
+
expect([scenes[0].nodes.a.layer, scenes[0].nodes.f.layer, scenes[0].links.ab.layer]).toEqual([
|
|
92
|
+
DEFAULT_LAYER.id,
|
|
93
|
+
DEFAULT_LAYER.id,
|
|
94
|
+
DEFAULT_LAYER.id,
|
|
95
|
+
]);
|
|
96
|
+
// A layer the fixture names is kept.
|
|
97
|
+
expect(scenes[0].nodes.b.layer).toBe('top');
|
|
98
|
+
expect(scenes[1].nodes.f1.layer).toBe(DEFAULT_LAYER.id);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('two elements with one id fail the build', ({ expect }) => {
|
|
102
|
+
const rect = SceneBuilder.rect('a', box(0, 0));
|
|
103
|
+
expect(() => SceneBuilder.scene('s', [rect, rect]).build()).toThrow(/Duplicate id a in scene s/);
|
|
104
|
+
const link = SceneBuilder.link('line', 'a', 'a').id('l');
|
|
105
|
+
expect(() => SceneBuilder.scene('s', [rect, link, link]).build()).toThrow(/Duplicate id l/);
|
|
106
|
+
// An explicit id that repeats one already generated clashes too.
|
|
107
|
+
const generated = SceneBuilder.link('line', 'a', 'a');
|
|
108
|
+
expect(() => SceneBuilder.scene('s', [rect, generated, generated.id('a-a')]).build()).toThrow(/Duplicate id a-a/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('an id an object inherits is not taken, and any id is kept', ({ expect }) => {
|
|
112
|
+
const rect = SceneBuilder.rect('constructor', box(0, 0));
|
|
113
|
+
const proto = SceneBuilder.rect('__proto__', box(0, 0));
|
|
114
|
+
const link = SceneBuilder.link('line', 'constructor', '__proto__').id('toString');
|
|
115
|
+
const [scene] = SceneBuilder.scene('s', [rect, proto, link]).build().scenes;
|
|
116
|
+
expect(Object.keys(scene.nodes)).toEqual(['constructor', '__proto__']);
|
|
117
|
+
expect(Object.keys(scene.links)).toEqual(['toString']);
|
|
118
|
+
});
|
|
119
|
+
|
|
83
120
|
test('a node its type does not describe fails the build', ({ expect }) => {
|
|
84
121
|
const host = SceneBuilder.node('rect', 'a', box(0, 0)).properties({ portsPerSide: 2 });
|
|
85
122
|
expect(() => SceneBuilder.scene('s', [host]).build()).not.toThrow();
|
package/src/utils/builder.ts
CHANGED
|
@@ -17,16 +17,17 @@ import {
|
|
|
17
17
|
type BuiltinNode,
|
|
18
18
|
type BuiltinNodeType,
|
|
19
19
|
type Endpoint,
|
|
20
|
+
type FrameNode,
|
|
20
21
|
type Link,
|
|
21
22
|
type LinkType,
|
|
22
23
|
type Node,
|
|
23
24
|
type NodeBase,
|
|
24
25
|
type NodeStyle,
|
|
25
26
|
type Point,
|
|
26
|
-
type PortalNode,
|
|
27
27
|
type Scene,
|
|
28
28
|
type SceneId,
|
|
29
29
|
} from '../model/types.ts';
|
|
30
|
+
import { DEFAULT_LAYER } from './layers.ts';
|
|
30
31
|
import { initialKeys } from './order.ts';
|
|
31
32
|
import { createNode } from './shapes.ts';
|
|
32
33
|
|
|
@@ -110,7 +111,7 @@ export class SceneElement {
|
|
|
110
111
|
readonly elements: readonly BuilderElement[],
|
|
111
112
|
readonly frame?: Box,
|
|
112
113
|
readonly sceneName?: string,
|
|
113
|
-
readonly portal: NodeProperties<
|
|
114
|
+
readonly portal: NodeProperties<FrameNode> = {},
|
|
114
115
|
) {}
|
|
115
116
|
|
|
116
117
|
/** The scene's name (breadcrumbs, previews). */
|
|
@@ -124,7 +125,7 @@ export class SceneElement {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/** The scene shape's own properties (label, contents, style). */
|
|
127
|
-
properties(values: NodeProperties<
|
|
128
|
+
properties(values: NodeProperties<FrameNode>): SceneElement {
|
|
128
129
|
return new SceneElement(this.id, this.elements, this.frame, this.sceneName, {
|
|
129
130
|
...this.portal,
|
|
130
131
|
...values,
|
|
@@ -143,42 +144,59 @@ export class SceneElement {
|
|
|
143
144
|
export type SceneTree = { root: SceneId; scenes: Scene[] };
|
|
144
145
|
|
|
145
146
|
const buildScene = (element: SceneElement, registry: NodeRegistry, scenes: Scene[]): void => {
|
|
146
|
-
|
|
147
|
-
const
|
|
147
|
+
// Maps, so any id (`constructor`, `__proto__`) is an ordinary key; `fromEntries` keeps them own properties.
|
|
148
|
+
const nodes = new Map<string, Node>();
|
|
149
|
+
const links = new Map<string, Link>();
|
|
148
150
|
const keys = initialKeys(element.elements.length);
|
|
149
|
-
|
|
151
|
+
// Every element names its layer, so a fixture reads as a scene the editor wrote.
|
|
152
|
+
const index =
|
|
153
|
+
scenes.push({
|
|
154
|
+
id: element.id,
|
|
155
|
+
name: element.sceneName,
|
|
156
|
+
nodes: {},
|
|
157
|
+
links: {},
|
|
158
|
+
layers: { [DEFAULT_LAYER.id]: DEFAULT_LAYER },
|
|
159
|
+
}) - 1;
|
|
150
160
|
element.elements.forEach((child, order) => {
|
|
151
161
|
const z = keys[order];
|
|
152
162
|
switch (child.kind) {
|
|
153
163
|
case 'node': {
|
|
154
|
-
|
|
164
|
+
// A host's type starts from its own `create`, as a built-in starts from `createNode`, so a
|
|
165
|
+
// fixture sets only what it means and the type supplies the rest of its content.
|
|
166
|
+
const def = isBuiltinType(child.node.type) ? undefined : registry[child.node.type];
|
|
167
|
+
const fresh = def?.create({ id: child.node.id, z, center: child.node.center, size: child.node.size });
|
|
168
|
+
const node = { layer: DEFAULT_LAYER.id, ...fresh, ...child.node, z };
|
|
155
169
|
check(registry, node);
|
|
156
|
-
nodes
|
|
170
|
+
claim(nodes, node.id, element.id);
|
|
171
|
+
nodes.set(node.id, node);
|
|
157
172
|
break;
|
|
158
173
|
}
|
|
159
174
|
case 'link': {
|
|
160
175
|
const id = child.linkId ?? uniqueId(links, linkId(child.link.source, child.link.target));
|
|
161
|
-
links
|
|
176
|
+
claim(links, id, element.id);
|
|
177
|
+
links.set(id, { layer: DEFAULT_LAYER.id, ...child.link, id, z });
|
|
162
178
|
break;
|
|
163
179
|
}
|
|
164
180
|
case 'scene': {
|
|
165
181
|
if (!child.frame) {
|
|
166
182
|
throw new Error(`Nested scene ${child.id} needs a frame: call at(box).`);
|
|
167
183
|
}
|
|
168
|
-
const node:
|
|
169
|
-
...createNode({ type: '
|
|
184
|
+
const node: FrameNode = {
|
|
185
|
+
...createNode({ type: 'frame', id: child.id, z, center: center(child.frame), size: sizeOf(child.frame) }),
|
|
186
|
+
layer: DEFAULT_LAYER.id,
|
|
170
187
|
...child.portal,
|
|
171
|
-
type: '
|
|
188
|
+
type: 'frame',
|
|
172
189
|
scene: child.id,
|
|
173
190
|
};
|
|
174
191
|
check(registry, node);
|
|
175
|
-
nodes
|
|
192
|
+
claim(nodes, node.id, element.id);
|
|
193
|
+
nodes.set(node.id, node);
|
|
176
194
|
buildScene(child, registry, scenes);
|
|
177
195
|
break;
|
|
178
196
|
}
|
|
179
197
|
}
|
|
180
198
|
});
|
|
181
|
-
scenes[index] = { ...scenes[index], nodes, links };
|
|
199
|
+
scenes[index] = { ...scenes[index], nodes: Object.fromEntries(nodes), links: Object.fromEntries(links) };
|
|
182
200
|
};
|
|
183
201
|
|
|
184
202
|
/** A node its type does not describe (a wrong value, or a property the type does not have) fails the build. */
|
|
@@ -193,14 +211,21 @@ const check = (registry: NodeRegistry, node: Node): void => {
|
|
|
193
211
|
}
|
|
194
212
|
};
|
|
195
213
|
|
|
214
|
+
/** Two elements given one id would leave only the second, so the fixture fails instead. */
|
|
215
|
+
const claim = (taken: ReadonlyMap<string, unknown>, id: string, sceneId: SceneId): void => {
|
|
216
|
+
if (taken.has(id)) {
|
|
217
|
+
throw new Error(`Duplicate id ${id} in scene ${sceneId}.`);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
196
221
|
const linkId = (source: Endpoint, target: Endpoint): string => {
|
|
197
222
|
const name = (end: Endpoint) => ('node' in end ? end.node : `${end.point.x},${end.point.y}`);
|
|
198
223
|
return `${name(source)}-${name(target)}`;
|
|
199
224
|
};
|
|
200
225
|
|
|
201
|
-
const uniqueId = (taken:
|
|
226
|
+
const uniqueId = (taken: ReadonlyMap<string, unknown>, id: string): string => {
|
|
202
227
|
let candidate = id;
|
|
203
|
-
for (let suffix = 2; candidate
|
|
228
|
+
for (let suffix = 2; taken.has(candidate); ++suffix) {
|
|
204
229
|
candidate = `${id}-${suffix}`;
|
|
205
230
|
}
|
|
206
231
|
return candidate;
|
|
@@ -221,7 +246,7 @@ function node(type: string, id: string, box: Box): NodeElement<NodeBase> {
|
|
|
221
246
|
return new NodeElement<NodeBase>(base);
|
|
222
247
|
}
|
|
223
248
|
|
|
224
|
-
const BUILTIN_TYPES: readonly string[] = ['rect', 'ellipse', '
|
|
249
|
+
const BUILTIN_TYPES: readonly string[] = ['rect', 'ellipse', 'note', 'frame'] satisfies BuiltinNodeType[];
|
|
225
250
|
const isBuiltinType = (type: string): type is BuiltinNodeType => BUILTIN_TYPES.includes(type);
|
|
226
251
|
|
|
227
252
|
type Ends = Pick<Link, 'source' | 'target'>;
|
|
@@ -244,7 +269,6 @@ export const SceneBuilder = {
|
|
|
244
269
|
node,
|
|
245
270
|
rect: (id: string, box: Box) => node('rect', id, box),
|
|
246
271
|
ellipse: (id: string, box: Box) => node('ellipse', id, box),
|
|
247
|
-
class: (id: string, box: Box) => node('class', id, box),
|
|
248
272
|
note: (id: string, box: Box) => node('note', id, box),
|
|
249
273
|
link,
|
|
250
274
|
};
|
package/src/utils/camera.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { type ExpectStatic, describe, test } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import { type Bounds, type Camera, type
|
|
7
|
+
import { type Bounds, type Camera, type FrameNode } from '../model/types.ts';
|
|
8
8
|
import {
|
|
9
9
|
MAX_ZOOM,
|
|
10
10
|
MIN_ZOOM,
|
|
@@ -13,17 +13,16 @@ import {
|
|
|
13
13
|
exitPortal,
|
|
14
14
|
fitBounds,
|
|
15
15
|
portalFrame,
|
|
16
|
-
portalScale,
|
|
17
16
|
sceneToScreen,
|
|
18
17
|
screenToScene,
|
|
19
18
|
zoomAt,
|
|
20
19
|
} from './camera.ts';
|
|
21
|
-
import {
|
|
20
|
+
import { nodeBounds } from './shapes.ts';
|
|
22
21
|
|
|
23
22
|
const viewport = { width: 800, height: 600 };
|
|
24
23
|
|
|
25
|
-
const portal:
|
|
26
|
-
type: '
|
|
24
|
+
const portal: FrameNode = {
|
|
25
|
+
type: 'frame',
|
|
27
26
|
id: 'p',
|
|
28
27
|
z: 'V',
|
|
29
28
|
center: { x: 640, y: 620 },
|
|
@@ -101,58 +100,30 @@ describe('camera', () => {
|
|
|
101
100
|
expect(coverage(camera, { x: -100, y: -100, width: 10, height: 10 }, viewport)).toBe(0);
|
|
102
101
|
});
|
|
103
102
|
|
|
104
|
-
test(
|
|
103
|
+
test("portalFrame centres the child in the smallest region of the portal's proportions that holds it", ({
|
|
105
104
|
expect,
|
|
106
105
|
}) => {
|
|
107
106
|
const frame = portalFrame(portal, child);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
expect(
|
|
114
|
-
expect(frame.
|
|
115
|
-
expect(frame.y).toBeLessThanOrEqual(child.y);
|
|
116
|
-
expect(frame.x + frame.width).toBeGreaterThanOrEqual(child.x + child.width);
|
|
117
|
-
expect(frame.y + frame.height).toBeGreaterThanOrEqual(child.y + child.height);
|
|
118
|
-
// As near the child's centre as containing it allows.
|
|
119
|
-
expect(Math.abs(frame.x + frame.width / 2 - (child.x + child.width / 2))).toBeLessThanOrEqual(256);
|
|
120
|
-
expect(Math.abs(frame.y + frame.height / 2 - (child.y + child.height / 2))).toBeLessThanOrEqual(256);
|
|
107
|
+
const { width, height } = nodeBounds(portal);
|
|
108
|
+
// The portal's proportions, just large enough for the child on its tighter axis.
|
|
109
|
+
expect(frame.width / frame.height).toBeCloseTo(width / height);
|
|
110
|
+
expect(Math.max(child.width / frame.width, child.height / frame.height)).toBeCloseTo(1);
|
|
111
|
+
// Centred on the child.
|
|
112
|
+
expect(frame.x + frame.width / 2).toBeCloseTo(child.x + child.width / 2);
|
|
113
|
+
expect(frame.y + frame.height / 2).toBeCloseTo(child.y + child.height / 2);
|
|
121
114
|
// A frame is its own frame, so drilling in and out is stable.
|
|
122
115
|
expect(portalFrame(portal, frame)).toEqual(frame);
|
|
123
|
-
// A child smaller than the portal still gets four portals across: the parent's minor grid is the
|
|
124
|
-
// child's major one, never the same level.
|
|
125
|
-
const small = portalFrame(portal, { x: 64, y: 64, width: 128, height: 128 });
|
|
126
|
-
expect([small.width, small.height]).toEqual([1920, 1200]);
|
|
127
|
-
expect(small.x).toBeLessThanOrEqual(64);
|
|
128
|
-
expect(small.x + small.width).toBeGreaterThanOrEqual(192);
|
|
129
|
-
// A child past four portals across jumps to sixteen, never to an in-between factor whose grid
|
|
130
|
-
// would miss the parent's lines.
|
|
131
|
-
const large = portalFrame(portal, { x: 0, y: 0, width: 2400, height: 600 });
|
|
132
|
-
expect([large.width, large.height]).toEqual([7680, 4800]);
|
|
133
116
|
});
|
|
134
117
|
|
|
135
|
-
test(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
146
|
-
const scene: Scene = {
|
|
147
|
-
id: 'c',
|
|
148
|
-
nodes: { a: { type: 'rect', id: 'a', z: 'A', center: { x: 0, y: 0 }, size: { width: 128, height: 128 } } },
|
|
149
|
-
links: {},
|
|
150
|
-
};
|
|
151
|
-
const framed = portalFrame(tile, contentBounds(scene));
|
|
152
|
-
const floored = portalFrame(tile, sceneBounds(scene));
|
|
153
|
-
expect([framed.width, framed.height]).toEqual([1024, 640]);
|
|
154
|
-
expect([floored.width, floored.height]).toEqual([4096, 2560]);
|
|
155
|
-
// The child therefore draws four times the size in the same tile.
|
|
156
|
-
expect(portalScale(tile, framed) / portalScale(tile, floored)).toBe(4);
|
|
118
|
+
test("a small or empty child is never magnified: the frame is at least the portal's own box", ({ expect }) => {
|
|
119
|
+
const { width, height } = nodeBounds(portal);
|
|
120
|
+
const small = portalFrame(portal, { x: 64, y: 64, width: 128, height: 128 });
|
|
121
|
+
expect([small.width, small.height]).toEqual([width, height]);
|
|
122
|
+
expect(small.x + small.width / 2).toBe(128);
|
|
123
|
+
const empty = portalFrame(portal, undefined);
|
|
124
|
+
expect(empty).toEqual({ x: -width / 2, y: -height / 2, width, height });
|
|
125
|
+
// So a shape inside keeps its size through the drill-in: entering maps the zoom one to one.
|
|
126
|
+
const camera = { x: 0, y: 0, zoom: 0.79 };
|
|
127
|
+
expect(enterPortal(camera, portal, empty).zoom).toBeCloseTo(0.79);
|
|
157
128
|
});
|
|
158
129
|
});
|
package/src/utils/camera.ts
CHANGED
|
@@ -9,20 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
import { interpolateZoom } from 'd3';
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
type Bounds,
|
|
14
|
-
type Camera,
|
|
15
|
-
MAJOR_GRID,
|
|
16
|
-
MAJOR_GRID_RATIO,
|
|
17
|
-
type Node,
|
|
18
|
-
type Point,
|
|
19
|
-
type Size,
|
|
20
|
-
} from '../model/types.ts';
|
|
12
|
+
import { type Bounds, type Camera, type Node, type Point, type Size } from '../model/types.ts';
|
|
21
13
|
import { nodeBounds } from './shapes.ts';
|
|
22
14
|
|
|
23
15
|
export const MIN_ZOOM = 1 / 32;
|
|
24
16
|
export const MAX_ZOOM = 32;
|
|
25
17
|
|
|
18
|
+
/** True size: one scene px is one screen px, at every level. Fit never zooms past it. */
|
|
19
|
+
export const NOMINAL_ZOOM = 1;
|
|
20
|
+
|
|
26
21
|
export const clampZoom = (zoom: number) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
|
|
27
22
|
|
|
28
23
|
export const sceneToScreen = (camera: Camera, point: Point): Point => ({
|
|
@@ -82,35 +77,25 @@ export const portalScale = (portal: Node, region: Bounds) => {
|
|
|
82
77
|
};
|
|
83
78
|
|
|
84
79
|
/**
|
|
85
|
-
* The frame a portal gives its child: the child-space region
|
|
86
|
-
* the portal's box
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* shows this frame; a portal draws the child centred in it.
|
|
80
|
+
* The frame a portal gives its child: the child-space region, of the portal's proportions, that maps onto
|
|
81
|
+
* the portal's box. It is the child's content (`child`) centred in the smallest such region that holds it,
|
|
82
|
+
* and never smaller than the box itself, so the preview shrinks a large scene to fit but never magnifies a
|
|
83
|
+
* small one. Every scene keeps its own scale: the frame only says where the child sits under the portal,
|
|
84
|
+
* and entering it maps the camera through the frame, so a shape is the same size at the same zoom on any
|
|
85
|
+
* level. An empty child is the box itself, centred on its origin.
|
|
92
86
|
*/
|
|
93
|
-
export const portalFrame = (portal: Node, child: Bounds
|
|
87
|
+
export const portalFrame = (portal: Node, child: Bounds | undefined): Bounds => {
|
|
94
88
|
const { width, height } = nodeBounds(portal);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
factor *= ratio;
|
|
98
|
-
}
|
|
89
|
+
const content = child ?? { x: 0, y: 0, width: 0, height: 0 };
|
|
90
|
+
const factor = Math.max(1, content.width / width, content.height / height);
|
|
99
91
|
const frame = { width: width * factor, height: height * factor };
|
|
100
92
|
return {
|
|
101
|
-
x:
|
|
102
|
-
y:
|
|
93
|
+
x: content.x + (content.width - frame.width) / 2,
|
|
94
|
+
y: content.y + (content.height - frame.height) / 2,
|
|
103
95
|
...frame,
|
|
104
96
|
};
|
|
105
97
|
};
|
|
106
98
|
|
|
107
|
-
/** The grid-aligned start of a span of `length` centred on `[start, start + inner]` as far as containing it allows. */
|
|
108
|
-
const place = (start: number, inner: number, length: number, unit: number): number => {
|
|
109
|
-
const centred = Math.round((start + inner / 2 - length / 2) / unit) * unit;
|
|
110
|
-
// Rounding a small negative yields -0, which `toEqual` and `Object.is` tell apart from 0.
|
|
111
|
-
return Math.max(Math.min(centred, start), start + inner - length) + 0;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
99
|
/** Child-scene CSS transform inside a portal node whose own origin is the node's top-left. */
|
|
115
100
|
export const portalTransform = (portal: Node, frame: Bounds) => {
|
|
116
101
|
const scale = portalScale(portal, frame);
|
|
@@ -142,6 +127,24 @@ export const exitPortal = (camera: Camera, portal: Node, child: Bounds): Camera
|
|
|
142
127
|
};
|
|
143
128
|
};
|
|
144
129
|
|
|
130
|
+
/** A portal filling this share of the viewport (on its tighter axis) leaves the rest of its layer fully shown… */
|
|
131
|
+
const FADE_START = 0.25;
|
|
132
|
+
/** …and this share hides it: zooming into a portal fades out what is around it, and zooming out fades it back. */
|
|
133
|
+
const FADE_END = 0.75;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The opacity of a layer around a portal, from how much of the viewport the portal fills on its tighter axis:
|
|
137
|
+
* 1 while it is small, falling to 0 as it grows towards filling the view. A wheel zoom, a drill-in and a
|
|
138
|
+
* drill-out all move the camera, so all three fade alike.
|
|
139
|
+
*/
|
|
140
|
+
export const layerOpacity = (camera: Camera, bounds: Bounds, viewport: Size): number => {
|
|
141
|
+
if (viewport.width === 0 || viewport.height === 0) {
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
const fill = Math.min((bounds.width * camera.zoom) / viewport.width, (bounds.height * camera.zoom) / viewport.height);
|
|
145
|
+
return Math.min(1, Math.max(0, (FADE_END - fill) / (FADE_END - FADE_START)));
|
|
146
|
+
};
|
|
147
|
+
|
|
145
148
|
/** Fraction of the viewport covered by the bounds' screen rectangle. */
|
|
146
149
|
export const coverage = (camera: Camera, bounds: Bounds, viewport: Size) => {
|
|
147
150
|
if (viewport.width === 0 || viewport.height === 0) {
|
|
@@ -168,16 +171,20 @@ const fromView = ([cx, cy, width]: [number, number, number], viewport: Size): Ca
|
|
|
168
171
|
export const MIN_ANIMATION_MS = 250;
|
|
169
172
|
export const MAX_ANIMATION_MS = 800;
|
|
170
173
|
|
|
171
|
-
/**
|
|
174
|
+
/** How long a drill in or out takes, either way, so entering and leaving a scene feel the same. */
|
|
175
|
+
export const DRILL_ANIMATION_MS = 1000;
|
|
176
|
+
|
|
177
|
+
/** Animate between cameras, over `fixedDuration` ms or a time fitted to the distance; returns a cancel function. */
|
|
172
178
|
export const animateCamera = (
|
|
173
179
|
from: Camera,
|
|
174
180
|
to: Camera,
|
|
175
181
|
viewport: Size,
|
|
176
182
|
apply: (camera: Camera) => void,
|
|
177
183
|
done?: () => void,
|
|
184
|
+
fixedDuration?: number,
|
|
178
185
|
): (() => void) => {
|
|
179
186
|
const interpolate = interpolateZoom(toView(from, viewport), toView(to, viewport));
|
|
180
|
-
const duration = Math.min(MAX_ANIMATION_MS, Math.max(MIN_ANIMATION_MS, interpolate.duration));
|
|
187
|
+
const duration = fixedDuration ?? Math.min(MAX_ANIMATION_MS, Math.max(MIN_ANIMATION_MS, interpolate.duration));
|
|
181
188
|
const start = performance.now();
|
|
182
189
|
let frame = 0;
|
|
183
190
|
const tick = (now: number) => {
|