@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
package/src/utils/hit.ts
CHANGED
|
@@ -11,13 +11,6 @@ import { type Bounds, MAJOR_GRID, type Node, type Point, type Scene } from '../m
|
|
|
11
11
|
import { sortByZ } from './order.ts';
|
|
12
12
|
import { nodeBounds } from './shapes.ts';
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* Centred on the origin, not anchored at it: a scene is laid out around (0, 0), so an extent running
|
|
16
|
-
* from the origin to (1600, 1024) puts its own centre well below and right of the content and the
|
|
17
|
-
* initial fit — which centres the frame — pushes everything into the top-left corner. Each half is a
|
|
18
|
-
* whole number of major cells, so the frame still lands on grid lines.
|
|
19
|
-
*/
|
|
20
|
-
export const DEFAULT_EXTENT: Bounds = { x: -832, y: -512, width: 1664, height: 1024 };
|
|
21
14
|
export const BOUNDS_PADDING = MAJOR_GRID;
|
|
22
15
|
|
|
23
16
|
export const containsPoint = (bounds: Bounds, point: Point) =>
|
|
@@ -78,27 +71,12 @@ export const alignBounds = (bounds: Bounds, unit: number): Bounds => {
|
|
|
78
71
|
export const nodeList = (scene: Scene): Node[] => Object.values(scene.nodes);
|
|
79
72
|
|
|
80
73
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* content only ever grows the frame, so drawing the first node does not collapse the scene around it.
|
|
84
|
-
*
|
|
85
|
-
* This is the frame of a scene shown on its own — the surface the user edits on. A scene shown through
|
|
86
|
-
* a portal is framed by {@link contentBounds} instead.
|
|
87
|
-
*/
|
|
88
|
-
export const sceneBounds = (scene: Scene, padding = BOUNDS_PADDING, unit = MAJOR_GRID): Bounds => {
|
|
89
|
-
const content = unionBounds(nodeList(scene).map(nodeBounds));
|
|
90
|
-
const union = unionBounds(content ? [DEFAULT_EXTENT, padBounds(content, padding)] : [DEFAULT_EXTENT]);
|
|
91
|
-
return alignBounds(union ?? DEFAULT_EXTENT, unit);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* What a scene is worth showing: the same frame without the floor under it, so a child drawn in a portal
|
|
96
|
-
* fills its tile rather than being shrunk to fit an editing surface it is not being edited on. An empty
|
|
97
|
-
* scene has no content to frame, so there the floor is the frame.
|
|
74
|
+
* What a scene holds (decision 6): the union of its nodes plus padding, grown to the major grid. Fit and a
|
|
75
|
+
* portal's preview frame it; an empty scene has none.
|
|
98
76
|
*/
|
|
99
|
-
export const contentBounds = (scene: Scene, padding = BOUNDS_PADDING, unit = MAJOR_GRID): Bounds => {
|
|
77
|
+
export const contentBounds = (scene: Scene, padding = BOUNDS_PADDING, unit = MAJOR_GRID): Bounds | undefined => {
|
|
100
78
|
const content = unionBounds(nodeList(scene).map(nodeBounds));
|
|
101
|
-
return content ? alignBounds(padBounds(content, padding), unit) :
|
|
79
|
+
return content ? alignBounds(padBounds(content, padding), unit) : undefined;
|
|
102
80
|
};
|
|
103
81
|
|
|
104
82
|
/** Topmost node under `point`, or none; `margin` widens every node, e.g. to reach its ports. */
|
package/src/utils/index.ts
CHANGED
|
@@ -7,14 +7,17 @@ export * from './camera.ts';
|
|
|
7
7
|
export * from './clipboard.ts';
|
|
8
8
|
export * from './diagram.ts';
|
|
9
9
|
export * from './dnd.ts';
|
|
10
|
+
export * from './group.ts';
|
|
10
11
|
export * from './hit.ts';
|
|
11
12
|
export * from './lattice.ts';
|
|
13
|
+
export * from './layers.ts';
|
|
12
14
|
export * from './layout.ts';
|
|
13
15
|
export * from './order.ts';
|
|
14
16
|
export * from './parts.ts';
|
|
15
17
|
export * from './ports.ts';
|
|
16
18
|
export * from './resize.ts';
|
|
17
19
|
export * from './route.ts';
|
|
20
|
+
export * from './scenes.ts';
|
|
18
21
|
export * from './shapes.ts';
|
|
19
22
|
export * from './style.ts';
|
|
20
23
|
export * from './undo.ts';
|
package/src/utils/lattice.ts
CHANGED
|
@@ -110,6 +110,12 @@ export const coveredCells = ({ col, row, spanX, spanY }: LatticeCell): string[]
|
|
|
110
110
|
/** Which node covers each lattice position, for the nodes not in `except` (those being moved or resized). */
|
|
111
111
|
export type Occupancy = ReadonlyMap<string, ElementId>;
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Whether a node takes part in the lattice: a guide (a group's backdrop) or a bare caption annotates the shapes rather
|
|
115
|
+
* than being one, so it neither snaps, occupies cells nor blocks a gutter route.
|
|
116
|
+
*/
|
|
117
|
+
export const onLattice = (node: Node): boolean => !node.style?.guide && node.style?.border !== false;
|
|
118
|
+
|
|
113
119
|
export const occupancy = (
|
|
114
120
|
nodes: Iterable<Node>,
|
|
115
121
|
spec: LatticeSpec,
|
|
@@ -117,7 +123,7 @@ export const occupancy = (
|
|
|
117
123
|
): Occupancy => {
|
|
118
124
|
const cells = new Map<string, ElementId>();
|
|
119
125
|
for (const node of nodes) {
|
|
120
|
-
if (!except.has(node.id)) {
|
|
126
|
+
if (!except.has(node.id) && onLattice(node)) {
|
|
121
127
|
for (const key of coveredCells(toCell(nodeBounds(node), spec))) {
|
|
122
128
|
cells.set(key, node.id);
|
|
123
129
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { reduceIntent } from '../model/projection.ts';
|
|
8
|
+
import { type Layer, type Scene } from '../model/types.ts';
|
|
9
|
+
import { SceneBuilder } from './builder.ts';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_LAYER,
|
|
12
|
+
createLayer,
|
|
13
|
+
elementLayer,
|
|
14
|
+
layerOrder,
|
|
15
|
+
mergeLayersIntent,
|
|
16
|
+
moveLayer,
|
|
17
|
+
pinLayers,
|
|
18
|
+
sceneLayers,
|
|
19
|
+
visibleScene,
|
|
20
|
+
} from './layers.ts';
|
|
21
|
+
import { between } from './order.ts';
|
|
22
|
+
|
|
23
|
+
describe('layers', () => {
|
|
24
|
+
const box = (x: number) => ({ x, y: 0, width: 100, height: 50 });
|
|
25
|
+
const {
|
|
26
|
+
scenes: [base],
|
|
27
|
+
} = SceneBuilder.scene('s', [
|
|
28
|
+
SceneBuilder.rect('a', box(0)),
|
|
29
|
+
SceneBuilder.rect('b', box(200)),
|
|
30
|
+
SceneBuilder.link('line', 'a', 'b').id('ab'),
|
|
31
|
+
]).build();
|
|
32
|
+
|
|
33
|
+
/** The base scene with a second layer above, holding `b`. */
|
|
34
|
+
const layered = (): Scene => {
|
|
35
|
+
const top: Layer = { id: 'top', name: 'Top', z: between(DEFAULT_LAYER.z) };
|
|
36
|
+
const scene = reduceIntent(base, { kind: 'layer', layer: top });
|
|
37
|
+
return reduceIntent(scene, { kind: 'update', id: 'b', values: { layer: 'top' } });
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
test('a scene with no layers has one, and every element is on it', ({ expect }) => {
|
|
41
|
+
expect(sceneLayers(base)).toEqual([DEFAULT_LAYER]);
|
|
42
|
+
expect(elementLayer(base.nodes.a, sceneLayers(base))).toBe(DEFAULT_LAYER.id);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('a new layer goes below the others', ({ expect }) => {
|
|
46
|
+
const scene = layered();
|
|
47
|
+
const layer = createLayer(scene, 'new');
|
|
48
|
+
const added = reduceIntent(scene, { kind: 'layer', layer });
|
|
49
|
+
expect(layer.name).toBe('Layer 3');
|
|
50
|
+
expect(sceneLayers(added).map((layer) => layer.id)).toEqual(['new', DEFAULT_LAYER.id, 'top']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** The base scene as a drawing saved before layers: no layers, and no element naming one. */
|
|
54
|
+
const legacy = (): Scene => {
|
|
55
|
+
const { layers: _, ...scene } = base;
|
|
56
|
+
return {
|
|
57
|
+
...scene,
|
|
58
|
+
nodes: Object.fromEntries(Object.entries(scene.nodes).map(([id, node]) => [id, { ...node, layer: undefined }])),
|
|
59
|
+
links: Object.fromEntries(Object.entries(scene.links).map(([id, link]) => [id, { ...link, layer: undefined }])),
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
test('adding a layer below keeps elements that name no layer where they were drawn', ({ expect }) => {
|
|
64
|
+
const below = createLayer(legacy(), 'below');
|
|
65
|
+
const scene = reduceIntent(legacy(), { kind: 'layer', layer: below });
|
|
66
|
+
expect(sceneLayers(scene).map((layer) => layer.id)).toEqual(['below', DEFAULT_LAYER.id]);
|
|
67
|
+
expect([scene.nodes.a.layer, scene.nodes.b.layer, scene.links.ab.layer]).toEqual([
|
|
68
|
+
DEFAULT_LAYER.id,
|
|
69
|
+
DEFAULT_LAYER.id,
|
|
70
|
+
DEFAULT_LAYER.id,
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("pinning names the layers and each element's, and leaves a pinned scene as it is", ({ expect }) => {
|
|
75
|
+
expect(legacy().layers).toBeUndefined();
|
|
76
|
+
expect(legacy().nodes.a.layer).toBeUndefined();
|
|
77
|
+
const pinned = pinLayers(legacy());
|
|
78
|
+
expect(pinned.layers).toEqual({ [DEFAULT_LAYER.id]: DEFAULT_LAYER });
|
|
79
|
+
expect(Object.values(pinned.nodes).every((node) => node.layer === DEFAULT_LAYER.id)).toBe(true);
|
|
80
|
+
expect(pinLayers(pinned)).toBe(pinned);
|
|
81
|
+
// A layer the scene does not have resolves to the bottom one.
|
|
82
|
+
const stray = { ...pinned, nodes: { ...pinned.nodes, a: { ...pinned.nodes.a, layer: 'gone' } } };
|
|
83
|
+
expect(pinLayers(stray).nodes.a.layer).toBe(DEFAULT_LAYER.id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('a new layer takes a name no other layer has', ({ expect }) => {
|
|
87
|
+
// Two layers, one already named after the count a new one would otherwise take.
|
|
88
|
+
const scene = reduceIntent(layered(), { kind: 'layer', layer: { ...DEFAULT_LAYER, name: 'Layer 3' } });
|
|
89
|
+
expect(createLayer(scene, 'new').name).toBe('Layer 4');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a second layer makes the implicit layer real, so it keeps its elements', ({ expect }) => {
|
|
93
|
+
const scene = layered();
|
|
94
|
+
expect(sceneLayers(scene).map((layer) => layer.id)).toEqual([DEFAULT_LAYER.id, 'top']);
|
|
95
|
+
expect(elementLayer(scene.nodes.a, sceneLayers(scene))).toBe(DEFAULT_LAYER.id);
|
|
96
|
+
expect(elementLayer(scene.nodes.b, sceneLayers(scene))).toBe('top');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('nodes paint by layer, then by their own order', ({ expect }) => {
|
|
100
|
+
const scene = layered();
|
|
101
|
+
// `b` is above `a` by layer even with the lower z.
|
|
102
|
+
const lowered = reduceIntent(scene, { kind: 'reorder', id: 'b', z: between(undefined, scene.nodes.a.z) });
|
|
103
|
+
expect(layerOrder(Object.values(lowered.nodes), sceneLayers(lowered)).map((node) => node.id)).toEqual(['a', 'b']);
|
|
104
|
+
// Moving the top layer to the bottom swaps them.
|
|
105
|
+
const moved = moveLayer(lowered, 'top', 0);
|
|
106
|
+
const reordered = moved ? reduceIntent(lowered, { kind: 'layer', layer: moved }) : lowered;
|
|
107
|
+
expect(layerOrder(Object.values(reordered.nodes), sceneLayers(reordered)).map((node) => node.id)).toEqual([
|
|
108
|
+
'b',
|
|
109
|
+
'a',
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('a hidden layer hides its elements and the links to them', ({ expect }) => {
|
|
114
|
+
const scene = layered();
|
|
115
|
+
const top = sceneLayers(scene)[1];
|
|
116
|
+
const hidden = reduceIntent(scene, { kind: 'layer', layer: { ...top, hidden: true } });
|
|
117
|
+
const visible = visibleScene(hidden);
|
|
118
|
+
expect(Object.keys(visible.nodes)).toEqual(['a']);
|
|
119
|
+
expect(Object.keys(visible.links)).toEqual([]);
|
|
120
|
+
expect(visibleScene(scene)).toBe(scene);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('removing a layer removes its elements; the last layer stays', ({ expect }) => {
|
|
124
|
+
const scene = reduceIntent(layered(), { kind: 'removeLayer', id: 'top' });
|
|
125
|
+
expect(Object.keys(scene.nodes)).toEqual(['a']);
|
|
126
|
+
expect(Object.keys(scene.links)).toEqual([]);
|
|
127
|
+
expect(reduceIntent(scene, { kind: 'removeLayer', id: DEFAULT_LAYER.id })).toBe(scene);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('merging a layer moves its elements onto the other and removes it', ({ expect }) => {
|
|
131
|
+
const scene = layered();
|
|
132
|
+
const merge = mergeLayersIntent(scene, ['top', DEFAULT_LAYER.id], DEFAULT_LAYER.id);
|
|
133
|
+
expect(merge).toBeDefined();
|
|
134
|
+
const merged = merge ? reduceIntent(scene, merge) : scene;
|
|
135
|
+
expect(sceneLayers(merged).map((layer) => layer.id)).toEqual([DEFAULT_LAYER.id]);
|
|
136
|
+
expect(Object.keys(merged.nodes).sort()).toEqual(['a', 'b']);
|
|
137
|
+
expect(Object.keys(merged.links)).toEqual(['ab']);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
// Scene layers: every node and link is on exactly one, the layers are ordered, and a hidden one is neither drawn nor
|
|
7
|
+
// hit. A scene that names no layers has one, implicitly, which the first layer edit makes real.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type Element,
|
|
12
|
+
type Intent,
|
|
13
|
+
type Layer,
|
|
14
|
+
type LayerId,
|
|
15
|
+
type Link,
|
|
16
|
+
type Node,
|
|
17
|
+
type Scene,
|
|
18
|
+
endpointNode,
|
|
19
|
+
} from '../model/types.ts';
|
|
20
|
+
import { between, sortByZ } from './order.ts';
|
|
21
|
+
|
|
22
|
+
/** The layer of a scene that names none: every element is on it. */
|
|
23
|
+
export const DEFAULT_LAYER: Layer = { id: 'layer', name: 'Layer 1', z: between() };
|
|
24
|
+
|
|
25
|
+
/** A scene's layers, bottom first; a scene with none has the implicit `DEFAULT_LAYER`. */
|
|
26
|
+
export const sceneLayers = (scene: Scene): Layer[] => {
|
|
27
|
+
const layers = sortByZ(Object.values(scene.layers ?? {}));
|
|
28
|
+
return layers.length > 0 ? layers : [DEFAULT_LAYER];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** The layer an element is on: its own, else (unset, or a layer the scene does not have) the bottom one. */
|
|
32
|
+
export const elementLayer = (element: Element, layers: readonly Layer[]): LayerId =>
|
|
33
|
+
element.layer !== undefined && layers.some((layer) => layer.id === element.layer)
|
|
34
|
+
? element.layer
|
|
35
|
+
: (layers[0]?.id ?? DEFAULT_LAYER.id);
|
|
36
|
+
|
|
37
|
+
/** The layer new elements go on: the active one when the scene has it, else the top layer. */
|
|
38
|
+
export const activeLayer = (scene: Scene, active: LayerId | undefined): LayerId => {
|
|
39
|
+
const layers = sceneLayers(scene);
|
|
40
|
+
return layers.find((layer) => layer.id === active)?.id ?? layers[layers.length - 1].id;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Nodes in paint order: by their layer's order, then by their own `z`. */
|
|
44
|
+
export const layerOrder = (nodes: readonly Node[], layers: readonly Layer[]): Node[] => {
|
|
45
|
+
const rank = new Map(layers.map((layer, index) => [layer.id, index]));
|
|
46
|
+
const rankOf = (node: Node) => rank.get(elementLayer(node, layers)) ?? 0;
|
|
47
|
+
return sortByZ(nodes).sort((left, right) => rankOf(left) - rankOf(right));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The scene as drawn and hit: without the elements of hidden layers, or the links to a node that is hidden. The same
|
|
52
|
+
* scene when no layer is hidden, so a memo over it holds.
|
|
53
|
+
*/
|
|
54
|
+
export const visibleScene = (scene: Scene): Scene => {
|
|
55
|
+
const layers = sceneLayers(scene);
|
|
56
|
+
const hidden = new Set(layers.filter((layer) => layer.hidden).map((layer) => layer.id));
|
|
57
|
+
if (hidden.size === 0) {
|
|
58
|
+
return scene;
|
|
59
|
+
}
|
|
60
|
+
const nodes = Object.fromEntries(
|
|
61
|
+
Object.entries(scene.nodes).filter(([, node]) => !hidden.has(elementLayer(node, layers))),
|
|
62
|
+
);
|
|
63
|
+
const shown = (link: Link) =>
|
|
64
|
+
!hidden.has(elementLayer(link, layers)) &&
|
|
65
|
+
[link.source, link.target].every((end) => {
|
|
66
|
+
const node = endpointNode(end);
|
|
67
|
+
return node === undefined || nodes[node] !== undefined;
|
|
68
|
+
});
|
|
69
|
+
const links = Object.fromEntries(Object.entries(scene.links).filter(([, link]) => shown(link)));
|
|
70
|
+
return { ...scene, nodes, links };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** The scene's layers as a record, the implicit layer made real so a new layer does not take its elements. */
|
|
74
|
+
const layerRecord = (scene: Scene): Record<LayerId, Layer> =>
|
|
75
|
+
scene.layers && Object.keys(scene.layers).length > 0 ? { ...scene.layers } : { [DEFAULT_LAYER.id]: DEFAULT_LAYER };
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The scene with its layers named and every element on one by name: an element with no layer, or one the scene does not
|
|
79
|
+
* have, takes the layer it is drawn on (`elementLayer`). The same scene when nothing changes, so a memo over it holds.
|
|
80
|
+
*/
|
|
81
|
+
export const pinLayers = (scene: Scene): Scene => {
|
|
82
|
+
const layers = sceneLayers(scene);
|
|
83
|
+
const pin = <T extends Element>(record: Record<string, T>): Record<string, T> | undefined => {
|
|
84
|
+
const loose = Object.values(record).filter((element) => element.layer !== elementLayer(element, layers));
|
|
85
|
+
return loose.length === 0
|
|
86
|
+
? undefined
|
|
87
|
+
: {
|
|
88
|
+
...record,
|
|
89
|
+
...Object.fromEntries(
|
|
90
|
+
loose.map((element) => [element.id, { ...element, layer: elementLayer(element, layers) }]),
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const named = scene.layers && Object.keys(scene.layers).length > 0;
|
|
95
|
+
const nodes = pin(scene.nodes);
|
|
96
|
+
const links = pin(scene.links);
|
|
97
|
+
return named && !nodes && !links
|
|
98
|
+
? scene
|
|
99
|
+
: { ...scene, layers: layerRecord(scene), nodes: nodes ?? scene.nodes, links: links ?? scene.links };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Applies a layer intent to the scene (`reduceIntent` delegates here). */
|
|
103
|
+
export const reduceLayerIntent = (scene: Scene, intent: Extract<Intent, { kind: 'layer' | 'removeLayer' }>): Scene => {
|
|
104
|
+
if (intent.kind === 'layer') {
|
|
105
|
+
// Pinned first: an element without a layer is on the bottom one, so a layer added below would otherwise take it.
|
|
106
|
+
const pinned = pinLayers(scene);
|
|
107
|
+
return { ...pinned, layers: { ...layerRecord(pinned), [intent.layer.id]: intent.layer } };
|
|
108
|
+
}
|
|
109
|
+
const layers = sceneLayers(scene);
|
|
110
|
+
if (layers.length < 2 || !layers.some((layer) => layer.id === intent.id)) {
|
|
111
|
+
return scene;
|
|
112
|
+
}
|
|
113
|
+
const { [intent.id]: _, ...rest } = layerRecord(scene);
|
|
114
|
+
// The elements on the removed layer go with it; `elementLayer` is read against the layers before the removal.
|
|
115
|
+
const keep = (element: Element) => elementLayer(element, layers) !== intent.id;
|
|
116
|
+
const nodes = Object.fromEntries(Object.entries(scene.nodes).filter(([, node]) => keep(node)));
|
|
117
|
+
const links = Object.fromEntries(
|
|
118
|
+
Object.entries(scene.links).filter(
|
|
119
|
+
([, link]) =>
|
|
120
|
+
keep(link) &&
|
|
121
|
+
[link.source, link.target].every((end) => {
|
|
122
|
+
const node = endpointNode(end);
|
|
123
|
+
return node === undefined || nodes[node] !== undefined;
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
return { ...scene, nodes, links, layers: rest };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** A new layer below the others, so it ends the top-first list, named after the first `Layer <n>` not taken. */
|
|
131
|
+
export const createLayer = (scene: Scene, id: LayerId): Layer => {
|
|
132
|
+
const layers = sceneLayers(scene);
|
|
133
|
+
const names = new Set(layers.map((layer) => layer.name));
|
|
134
|
+
let index = layers.length + 1;
|
|
135
|
+
while (names.has(`Layer ${index}`)) {
|
|
136
|
+
index++;
|
|
137
|
+
}
|
|
138
|
+
return { id, name: `Layer ${index}`, z: between(undefined, layers[0]?.z) };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** The layer moved to `index` in the bottom-first order. */
|
|
142
|
+
export const moveLayer = (scene: Scene, id: LayerId, index: number): Layer | undefined => {
|
|
143
|
+
const layers = sceneLayers(scene);
|
|
144
|
+
const layer = layers.find((candidate) => candidate.id === id);
|
|
145
|
+
if (!layer) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const others = layers.filter((candidate) => candidate.id !== id);
|
|
149
|
+
return { ...layer, z: between(others[index - 1]?.z, others[index]?.z) };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Merging layers into `into`, as one batch: the elements of every layer in `from` move onto `into`, keeping their own
|
|
154
|
+
* order, and those layers are removed. None unless `into` and at least one other of `from` are layers of the scene.
|
|
155
|
+
*/
|
|
156
|
+
export const mergeLayersIntent = (scene: Scene, from: readonly LayerId[], into: LayerId): Intent | undefined => {
|
|
157
|
+
const layers = sceneLayers(scene);
|
|
158
|
+
const ids = new Set(layers.map((layer) => layer.id));
|
|
159
|
+
const merged = new Set(from.filter((id) => id !== into && ids.has(id)));
|
|
160
|
+
if (!ids.has(into) || merged.size === 0) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
const moved = [...Object.values(scene.nodes), ...Object.values(scene.links)].filter((element) =>
|
|
164
|
+
merged.has(elementLayer(element, layers)),
|
|
165
|
+
);
|
|
166
|
+
return {
|
|
167
|
+
kind: 'batch',
|
|
168
|
+
intents: [
|
|
169
|
+
// The implicit layer is made real first, so the elements left on it keep it once the others are gone.
|
|
170
|
+
...(scene.layers ? [] : layers.map((layer): Intent => ({ kind: 'layer', layer }))),
|
|
171
|
+
...moved.map((element): Intent => ({ kind: 'update', id: element.id, values: { layer: into } })),
|
|
172
|
+
...[...merged].map((id): Intent => ({ kind: 'removeLayer', id })),
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
};
|
package/src/utils/parts.test.ts
CHANGED
|
@@ -2,52 +2,85 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
5
6
|
import { describe, test } from 'vitest';
|
|
6
7
|
|
|
8
|
+
import { type NodeRegistry, defaultNodeRegistry } from '../model/registry.ts';
|
|
9
|
+
import { nodeBase } from '../model/types.ts';
|
|
7
10
|
import { SceneBuilder } from './builder.ts';
|
|
8
|
-
import { isMultiline,
|
|
11
|
+
import { isMultiline, nodeParts, nodeTitle, partText, partValues } from './parts.ts';
|
|
9
12
|
import { frameClasses, hueClasses, resolveStyle } from './style.ts';
|
|
10
13
|
|
|
11
14
|
describe('parts', () => {
|
|
12
15
|
const box = { x: 0, y: 0, width: 256, height: 128 };
|
|
16
|
+
// A host type with a list part, the way a contributed type declares its text.
|
|
17
|
+
const ListNode = Schema.Struct({
|
|
18
|
+
type: Schema.Literal('list'),
|
|
19
|
+
...nodeBase,
|
|
20
|
+
title: Schema.String,
|
|
21
|
+
items: Schema.Array(Schema.String),
|
|
22
|
+
});
|
|
23
|
+
const registry: NodeRegistry = {
|
|
24
|
+
...defaultNodeRegistry,
|
|
25
|
+
list: {
|
|
26
|
+
...defaultNodeRegistry.rect,
|
|
27
|
+
type: 'list',
|
|
28
|
+
schema: ListNode,
|
|
29
|
+
parts: [{ field: 'title' }, { field: 'items', lines: true }],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
13
32
|
const {
|
|
14
33
|
scenes: [{ nodes }],
|
|
15
34
|
} = SceneBuilder.scene('s', [
|
|
16
35
|
SceneBuilder.rect('r', box),
|
|
17
|
-
SceneBuilder.
|
|
18
|
-
|
|
36
|
+
SceneBuilder.note('n', box),
|
|
37
|
+
SceneBuilder.node('list', 'l', box).properties({ title: 'Todo', items: ['buy milk'] }),
|
|
38
|
+
]).build(registry);
|
|
19
39
|
const rect = nodes.r;
|
|
20
|
-
const
|
|
40
|
+
const note = nodes.n;
|
|
41
|
+
const list = nodes.l;
|
|
21
42
|
|
|
22
|
-
test(
|
|
23
|
-
expect(partText(rect, 'label')).toBe('');
|
|
24
|
-
expect(partText(rect, '
|
|
25
|
-
expect(partText(
|
|
26
|
-
expect(partValues(rect, 'label', 'A')).toEqual({ label: 'A' });
|
|
27
|
-
expect(partValues(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
expect(
|
|
31
|
-
expect(
|
|
43
|
+
test("reads a part as text and writes it back as the node property, by the type's declared parts", ({ expect }) => {
|
|
44
|
+
expect(partText(registry, rect, 'label')).toBe('');
|
|
45
|
+
expect(partText(registry, rect, 'items')).toBeUndefined();
|
|
46
|
+
expect(partText(registry, list, 'items')).toBe('buy milk');
|
|
47
|
+
expect(partValues(registry, rect, 'label', 'A')).toEqual({ label: 'A' });
|
|
48
|
+
expect(partValues(registry, list, 'items', 'a\n\n b \n')).toEqual({ items: ['a', 'b'] });
|
|
49
|
+
expect(partValues(registry, rect, 'items', 'x')).toBeUndefined();
|
|
50
|
+
// The first part is the node's main text.
|
|
51
|
+
expect(nodeTitle(registry, list)).toBe('Todo');
|
|
52
|
+
expect(nodeTitle(registry, note)).toBe('Note');
|
|
32
53
|
});
|
|
33
54
|
|
|
34
|
-
test('lists and bodies are multi-line; labels
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
expect(
|
|
39
|
-
expect(partKey('other')).toBeUndefined();
|
|
55
|
+
test('lists and bodies are multi-line; labels are not', ({ expect }) => {
|
|
56
|
+
const [label] = nodeParts(registry, rect);
|
|
57
|
+
const [body] = nodeParts(registry, note);
|
|
58
|
+
const [, items] = nodeParts(registry, list);
|
|
59
|
+
expect([isMultiline(label), isMultiline(body), isMultiline(items)]).toEqual([false, true, true]);
|
|
40
60
|
});
|
|
41
61
|
|
|
42
62
|
test('frame classes follow the style', ({ expect }) => {
|
|
43
|
-
expect(frameClasses(rect, false)).toEqual(['bg-base-surface', '', 'border-separator', '', 'rounded-sm'
|
|
44
|
-
expect(frameClasses(rect, true)[2]).toBe('border-
|
|
45
|
-
expect(frameClasses(rect, false, true)[2]).toBe('border-
|
|
63
|
+
expect(frameClasses(rect, false)).toEqual(['bg-base-surface', '', 'border-separator', '', 'rounded-sm']);
|
|
64
|
+
expect(frameClasses(rect, true)[2]).toBe('border-focus');
|
|
65
|
+
expect(frameClasses(rect, false, true)[2]).toBe('border-focus/50');
|
|
46
66
|
const styled = { ...rect, style: { hue: 'teal', rounded: true, fill: false, border: false } };
|
|
47
|
-
expect(frameClasses(styled, false)).toEqual(['', 'text-teal-fg', 'border-transparent', '', 'rounded-2xl'
|
|
48
|
-
// A guide is dashed and unfilled whatever fill and border say
|
|
49
|
-
const guide = { ...rect, style: { guide: true, border: false
|
|
50
|
-
expect(frameClasses(guide, false)).toEqual(['', '', 'border-separator', 'border-dashed', 'rounded-sm'
|
|
67
|
+
expect(frameClasses(styled, false)).toEqual(['', 'text-teal-fg', 'border-transparent', '', 'rounded-2xl']);
|
|
68
|
+
// A guide is dashed and unfilled whatever fill and border say.
|
|
69
|
+
const guide = { ...rect, style: { guide: true, border: false } };
|
|
70
|
+
expect(frameClasses(guide, false)).toEqual(['', '', 'border-separator', 'border-dashed', 'rounded-sm']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('a scene shape showing its contents is opaque even in outline', ({ expect }) => {
|
|
74
|
+
const { scenes } = SceneBuilder.scene('s', [
|
|
75
|
+
SceneBuilder.scene('open')
|
|
76
|
+
.at(box)
|
|
77
|
+
.properties({ style: { hue: 'green', tone: 0 } }),
|
|
78
|
+
SceneBuilder.scene('closed')
|
|
79
|
+
.at(box)
|
|
80
|
+
.properties({ label: 'Closed', style: { hue: 'green', tone: 0 } }),
|
|
81
|
+
]).build();
|
|
82
|
+
expect(frameClasses(scenes[0].nodes.open, false)[0]).toBe('bg-base-surface');
|
|
83
|
+
expect(frameClasses(scenes[0].nodes.closed, false)[0]).toBe('bg-transparent');
|
|
51
84
|
});
|
|
52
85
|
|
|
53
86
|
test('every tone keeps the hue border; tone 0 drops the fill, unset is tone 2', ({ expect }) => {
|
|
@@ -56,9 +89,10 @@ describe('parts', () => {
|
|
|
56
89
|
text: '',
|
|
57
90
|
border: 'border-blue-border',
|
|
58
91
|
});
|
|
92
|
+
// Tones 1 to 3 run strongest to lightest.
|
|
59
93
|
expect(hueClasses('blue', 1)).toEqual({
|
|
60
|
-
surface: 'bg-blue-
|
|
61
|
-
text: 'text-
|
|
94
|
+
surface: 'bg-blue-500',
|
|
95
|
+
text: 'text-neutral-50',
|
|
62
96
|
border: 'border-blue-border',
|
|
63
97
|
});
|
|
64
98
|
expect(hueClasses('blue')).toEqual(hueClasses('blue', 2));
|
|
@@ -68,11 +102,11 @@ describe('parts', () => {
|
|
|
68
102
|
border: 'border-blue-border',
|
|
69
103
|
});
|
|
70
104
|
expect(hueClasses('blue', 3)).toEqual({
|
|
71
|
-
surface: 'bg-blue-
|
|
72
|
-
text: 'text-
|
|
105
|
+
surface: 'bg-blue-300',
|
|
106
|
+
text: 'text-blue-900',
|
|
73
107
|
border: 'border-blue-border',
|
|
74
108
|
});
|
|
75
|
-
// A hue the picker does not offer draws its stronger tones as
|
|
109
|
+
// A hue the picker does not offer draws its stronger tones as its role pair; no hue ignores the tone.
|
|
76
110
|
expect(hueClasses('lime', 3)).toEqual(hueClasses('lime'));
|
|
77
111
|
expect(hueClasses(undefined, 3)).toEqual(hueClasses(undefined));
|
|
78
112
|
});
|