@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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { SceneBuilder } from './builder.ts';
|
|
8
|
+
import { sceneOptions } from './scenes.ts';
|
|
9
|
+
|
|
10
|
+
describe('sceneOptions', () => {
|
|
11
|
+
const box = (x: number) => ({ x, y: 0, width: 200, height: 100 });
|
|
12
|
+
const { scenes: list } = SceneBuilder.scene('root', [
|
|
13
|
+
SceneBuilder.scene('a', [SceneBuilder.scene('x').at(box(0)).properties({ label: 'Inner' })])
|
|
14
|
+
.name('A')
|
|
15
|
+
.at(box(0)),
|
|
16
|
+
SceneBuilder.scene('b').name('B').at(box(300)),
|
|
17
|
+
]).build();
|
|
18
|
+
const scenes = Object.fromEntries(list.map((scene) => [scene.id, scene]));
|
|
19
|
+
|
|
20
|
+
test('lists every scene a shape may open, named or by the label of a shape that opens it', ({ expect }) => {
|
|
21
|
+
expect(sceneOptions(scenes, ['root'])).toEqual([
|
|
22
|
+
{ value: 'a', label: 'A' },
|
|
23
|
+
{ value: 'b', label: 'B' },
|
|
24
|
+
{ value: 'x', label: 'Inner' },
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('never offers a scene that would open the path again', ({ expect }) => {
|
|
29
|
+
// Inside `a`: neither `a` nor the root, which opens `a`, may be opened from it; `b` and `x` may.
|
|
30
|
+
expect(sceneOptions(scenes, ['root', 'a']).map((option) => option.value)).toEqual(['b', 'x']);
|
|
31
|
+
// Inside `x`: `a` opens `x`, so it is out too.
|
|
32
|
+
expect(sceneOptions(scenes, ['root', 'a', 'x']).map((option) => option.value)).toEqual(['b']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('a host narrows the candidates', ({ expect }) => {
|
|
36
|
+
expect(sceneOptions(scenes, ['root'], (id) => id !== 'x').map((option) => option.value)).toEqual(['a', 'b']);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type SceneMap } from '../model/store.ts';
|
|
6
|
+
import { type Scene, type SceneId, isFrameNode } from '../model/types.ts';
|
|
7
|
+
|
|
8
|
+
/** A scene a scene shape may open, as the properties panel lists it. */
|
|
9
|
+
export type SceneOption = { value: SceneId; label: string };
|
|
10
|
+
|
|
11
|
+
/** The scenes the shapes of `scene` open. */
|
|
12
|
+
const childScenes = (scene: Scene | undefined): SceneId[] =>
|
|
13
|
+
scene ? Object.values(scene.nodes).flatMap((node) => (isFrameNode(node) ? [node.scene] : [])) : [];
|
|
14
|
+
|
|
15
|
+
/** Whether `from`, or any scene it opens however deep, is one of `targets`. */
|
|
16
|
+
const reaches = (scenes: SceneMap, from: SceneId, targets: ReadonlySet<SceneId>): boolean => {
|
|
17
|
+
const seen = new Set<SceneId>();
|
|
18
|
+
const pending = [from];
|
|
19
|
+
for (let id = pending.pop(); id !== undefined; id = pending.pop()) {
|
|
20
|
+
if (targets.has(id)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (!seen.has(id)) {
|
|
24
|
+
seen.add(id);
|
|
25
|
+
pending.push(...childScenes(scenes[id]));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The scenes a shape in the scene at the head of `path` may open: any scene of the store, shared by as many shapes
|
|
33
|
+
* as open it, except one that would open the path again, which would nest the scene in itself without end. An
|
|
34
|
+
* unnamed scene reads as the label of a shape that opens it.
|
|
35
|
+
*/
|
|
36
|
+
export const sceneOptions = (
|
|
37
|
+
scenes: SceneMap,
|
|
38
|
+
path: readonly SceneId[],
|
|
39
|
+
filter: (id: SceneId) => boolean = () => true,
|
|
40
|
+
): SceneOption[] => {
|
|
41
|
+
const ancestors = new Set(path);
|
|
42
|
+
const labels = new Map<SceneId, string>();
|
|
43
|
+
for (const scene of Object.values(scenes)) {
|
|
44
|
+
for (const node of Object.values(scene.nodes)) {
|
|
45
|
+
if (isFrameNode(node) && node.label && !labels.has(node.scene)) {
|
|
46
|
+
labels.set(node.scene, node.label);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return Object.values(scenes)
|
|
51
|
+
.filter((scene) => filter(scene.id) && !reaches(scenes, scene.id, ancestors))
|
|
52
|
+
.map((scene) => ({ value: scene.id, label: scene.name ?? labels.get(scene.id) ?? scene.id }))
|
|
53
|
+
.sort((left, right) => left.label.localeCompare(right.label));
|
|
54
|
+
};
|
package/src/utils/shapes.test.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, test } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { type SplineLink } from '../model/types.ts';
|
|
8
|
+
import { cloneShape, createNode, flipLink } from './shapes.ts';
|
|
8
9
|
|
|
9
10
|
describe('shapes', () => {
|
|
10
11
|
test('a cloned shape keeps the look and size but not the text, identity or place', ({ expect }) => {
|
|
@@ -15,7 +16,7 @@ describe('shapes', () => {
|
|
|
15
16
|
style: { hue: 'teal', rounded: true },
|
|
16
17
|
};
|
|
17
18
|
const fresh = createNode({ type: 'rect', id: 'b', z: 'a1', center: { x: 400, y: 0 }, size: source.size });
|
|
18
|
-
expect(cloneShape(source, fresh)).toEqual({
|
|
19
|
+
expect(cloneShape(source, fresh, [{ field: 'label' }])).toEqual({
|
|
19
20
|
type: 'rect',
|
|
20
21
|
id: 'b',
|
|
21
22
|
z: 'a1',
|
|
@@ -26,13 +27,37 @@ describe('shapes', () => {
|
|
|
26
27
|
});
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
test('a cloned portal opens onto its own scene, and a
|
|
30
|
-
const portal = createNode({ type: '
|
|
31
|
-
const copy = cloneShape(portal, createNode({ type: '
|
|
30
|
+
test('a cloned portal opens onto its own scene, and a note gets its type text', ({ expect }) => {
|
|
31
|
+
const portal = createNode({ type: 'frame', id: 'p', z: 'a0', center: { x: 0, y: 0 }, scene: 'scene:p' });
|
|
32
|
+
const copy = cloneShape(portal, createNode({ type: 'frame', id: 'q', z: 'a1', center: { x: 0, y: 0 } }));
|
|
32
33
|
expect(copy).toMatchObject({ id: 'q', scene: 'q' });
|
|
33
34
|
|
|
34
|
-
const named = { ...createNode({ type: '
|
|
35
|
-
const fresh = createNode({ type: '
|
|
36
|
-
expect(cloneShape(named, fresh)).toMatchObject({ id: 'd',
|
|
35
|
+
const named = { ...createNode({ type: 'note', id: 'c', z: 'a0', center: { x: 0, y: 0 } }), text: 'Mine' };
|
|
36
|
+
const fresh = createNode({ type: 'note', id: 'd', z: 'a1', center: { x: 0, y: 0 } });
|
|
37
|
+
expect(cloneShape(named, fresh, [{ field: 'text', multiline: true }])).toMatchObject({ id: 'd', text: 'Note' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('flipping a link swaps its ends, keeps its markers and reverses a spline', ({ expect }) => {
|
|
41
|
+
const link: SplineLink = {
|
|
42
|
+
type: 'spline',
|
|
43
|
+
id: 'l',
|
|
44
|
+
z: 'a',
|
|
45
|
+
source: { node: 'a', port: 'e1' },
|
|
46
|
+
target: { point: { x: 100, y: 0 } },
|
|
47
|
+
ends: { end: 'arrow' },
|
|
48
|
+
points: [
|
|
49
|
+
{ x: 10, y: 0 },
|
|
50
|
+
{ x: 20, y: 5 },
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
const flipped = flipLink(link);
|
|
54
|
+
expect(flipped).toEqual({
|
|
55
|
+
source: { point: { x: 100, y: 0 } },
|
|
56
|
+
target: { node: 'a', port: 'e1' },
|
|
57
|
+
points: [
|
|
58
|
+
{ x: 20, y: 5 },
|
|
59
|
+
{ x: 10, y: 0 },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
37
62
|
});
|
|
38
63
|
});
|
package/src/utils/shapes.ts
CHANGED
|
@@ -12,19 +12,18 @@ import {
|
|
|
12
12
|
type Bounds,
|
|
13
13
|
type BuiltinNode,
|
|
14
14
|
type BuiltinNodeType,
|
|
15
|
+
DEFAULT_GRID,
|
|
15
16
|
type Endpoint,
|
|
16
17
|
type Link,
|
|
17
18
|
type LinkEnds,
|
|
18
19
|
type LinkType,
|
|
20
|
+
type LinkValues,
|
|
21
|
+
MAJOR_GRID_RATIO,
|
|
19
22
|
type Node,
|
|
20
23
|
type Point,
|
|
21
24
|
type Size,
|
|
22
|
-
isClassNode,
|
|
23
|
-
isEllipseNode,
|
|
24
|
-
isNoteNode,
|
|
25
|
-
isRectNode,
|
|
26
25
|
} from '../model/types.ts';
|
|
27
|
-
import {
|
|
26
|
+
import { type PartField } from './parts.ts';
|
|
28
27
|
|
|
29
28
|
/** Axis-aligned frame of a node: its size centred on its centre. */
|
|
30
29
|
export const nodeBounds = (node: Node): Bounds => ({
|
|
@@ -43,24 +42,44 @@ export const resizeNode = <N extends Node>(node: N, bounds: Bounds): N => ({
|
|
|
43
42
|
size: { width: bounds.width, height: bounds.height },
|
|
44
43
|
});
|
|
45
44
|
|
|
46
|
-
/**
|
|
47
|
-
export const
|
|
45
|
+
/** Scene px of one nominal unit (a major grid cell) at the default grid. */
|
|
46
|
+
export const DEFAULT_CELL = DEFAULT_GRID * MAJOR_GRID_RATIO;
|
|
48
47
|
|
|
48
|
+
/** A nominal size (in major grid cells) in scene px, for a drawing whose major cell is `cell` px. */
|
|
49
|
+
export const nominalSize = (size: Size, cell: number = DEFAULT_CELL): Size => ({
|
|
50
|
+
width: size.width * cell,
|
|
51
|
+
height: size.height * cell,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The built-in types' sizes when created, in nominal units: one unit is the drawing's major grid cell, so a
|
|
56
|
+
* new shape scales with the drawing's grid.
|
|
57
|
+
*/
|
|
58
|
+
export const NOMINAL_SIZES: Record<BuiltinNodeType, Size> = {
|
|
59
|
+
rect: { width: 2, height: 1 },
|
|
60
|
+
ellipse: { width: 2, height: 2 },
|
|
61
|
+
note: { width: 4, height: 2 },
|
|
62
|
+
frame: { width: 4, height: 2 },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** The built-in types' sizes in scene px, for fixtures and imports, whose layouts are written against them. */
|
|
49
66
|
export const DEFAULT_SIZES: Record<BuiltinNodeType, Size> = {
|
|
50
|
-
rect:
|
|
51
|
-
ellipse:
|
|
52
|
-
class: DEFAULT_SHAPE_SIZE,
|
|
67
|
+
rect: { width: 256, height: 256 },
|
|
68
|
+
ellipse: { width: 256, height: 256 },
|
|
53
69
|
note: { width: 256, height: 128 },
|
|
54
|
-
|
|
70
|
+
frame: { width: 512, height: 256 },
|
|
55
71
|
};
|
|
56
72
|
|
|
73
|
+
/** The bounding box a new basic shape gets at the default grid. */
|
|
74
|
+
export const DEFAULT_SHAPE_SIZE: Size = DEFAULT_SIZES.rect;
|
|
75
|
+
|
|
57
76
|
export type CreateNodeProps = {
|
|
58
77
|
type: BuiltinNodeType;
|
|
59
78
|
id: string;
|
|
60
79
|
z: string;
|
|
61
80
|
center: Point;
|
|
62
81
|
size?: Size;
|
|
63
|
-
/** A
|
|
82
|
+
/** A frame's child scene id. */
|
|
64
83
|
scene?: string;
|
|
65
84
|
};
|
|
66
85
|
|
|
@@ -79,11 +98,9 @@ export const createNode = ({
|
|
|
79
98
|
return { type, id, z, center, size };
|
|
80
99
|
case 'ellipse':
|
|
81
100
|
return { type, id, z, center, size };
|
|
82
|
-
case 'class':
|
|
83
|
-
return { type, id, z, center, size, name: 'Class', attributes: ['id: string'], methods: ['save(): void'] };
|
|
84
101
|
case 'note':
|
|
85
102
|
return { type, id, z, center, size, text: 'Note' };
|
|
86
|
-
case '
|
|
103
|
+
case 'frame':
|
|
87
104
|
return { type, id, z, center, size, scene: scene ?? id };
|
|
88
105
|
}
|
|
89
106
|
};
|
|
@@ -95,23 +112,12 @@ const OWN_FIELDS = new Set(['id', 'type', 'z', 'center', 'scene']);
|
|
|
95
112
|
* A new shape like `source` (its size, look and ports) built on `fresh`, the type's own new node at the
|
|
96
113
|
* new place: it keeps its own identity and child scene, and the type's text rather than the source's.
|
|
97
114
|
*/
|
|
98
|
-
export const cloneShape = (source: Node, fresh: Node): Node =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
export const withLabel = <N extends Node>(node: N, label: string): N => {
|
|
105
|
-
if (isRectNode(node) || isEllipseNode(node)) {
|
|
106
|
-
return { ...node, label };
|
|
107
|
-
}
|
|
108
|
-
if (isClassNode(node)) {
|
|
109
|
-
return { ...node, name: label };
|
|
110
|
-
}
|
|
111
|
-
if (isNoteNode(node)) {
|
|
112
|
-
return { ...node, text: label };
|
|
113
|
-
}
|
|
114
|
-
return node;
|
|
115
|
+
export const cloneShape = (source: Node, fresh: Node, parts: readonly PartField[] = []): Node => {
|
|
116
|
+
const text = new Set(parts.map(({ field }) => field));
|
|
117
|
+
return {
|
|
118
|
+
...fresh,
|
|
119
|
+
...Object.fromEntries(Object.entries(source).filter(([key]) => !OWN_FIELDS.has(key) && !text.has(key))),
|
|
120
|
+
};
|
|
115
121
|
};
|
|
116
122
|
|
|
117
123
|
export type CreateLinkProps = {
|
|
@@ -122,13 +128,12 @@ export type CreateLinkProps = {
|
|
|
122
128
|
target: Endpoint;
|
|
123
129
|
/** Where a new spline gets its first control point; ignored by the other types. */
|
|
124
130
|
midpoint?: Point;
|
|
125
|
-
directed?: boolean;
|
|
126
131
|
ends?: LinkEnds;
|
|
127
132
|
};
|
|
128
133
|
|
|
129
134
|
/** A new link of `type`; a spline starts with one control point so it is editable at once. */
|
|
130
|
-
export const createLink = ({ type, id, z, source, target, midpoint,
|
|
131
|
-
const base = { id, z, source, target, ...(
|
|
135
|
+
export const createLink = ({ type, id, z, source, target, midpoint, ends }: CreateLinkProps): Link => {
|
|
136
|
+
const base = { id, z, source, target, ...(ends ? { ends } : {}) };
|
|
132
137
|
switch (type) {
|
|
133
138
|
case 'line':
|
|
134
139
|
case 'curve':
|
|
@@ -138,3 +143,14 @@ export const createLink = ({ type, id, z, source, target, midpoint, directed, en
|
|
|
138
143
|
return { type, ...base, points: midpoint ? [midpoint] : [] };
|
|
139
144
|
}
|
|
140
145
|
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The `update` values that reverse a link: source and target swap while the markers stay at the start and
|
|
149
|
+
* end, so an arrow comes to point the other way; a spline's control points run the other way, so the drawn
|
|
150
|
+
* path keeps its shape.
|
|
151
|
+
*/
|
|
152
|
+
export const flipLink = (link: Link): LinkValues => ({
|
|
153
|
+
source: link.target,
|
|
154
|
+
target: link.source,
|
|
155
|
+
...(link.type === 'spline' ? { points: [...link.points].reverse() } : {}),
|
|
156
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { type StyleMap } from '../model/types.ts';
|
|
8
|
+
import { SceneBuilder } from './builder.ts';
|
|
9
|
+
import { classedLink, classedNode, frameClasses, splitClassEdit } from './style.ts';
|
|
10
|
+
|
|
11
|
+
describe('style classes', () => {
|
|
12
|
+
const styles: StyleMap = {
|
|
13
|
+
warn: { id: 'warn', name: 'Warning', style: { hue: 'red', lineStyle: 'dashed', rounded: true } },
|
|
14
|
+
};
|
|
15
|
+
const box = { x: 0, y: 0, width: 200, height: 100 };
|
|
16
|
+
const {
|
|
17
|
+
scenes: [{ nodes, links }],
|
|
18
|
+
} = SceneBuilder.scene('s', [
|
|
19
|
+
SceneBuilder.rect('a', box).properties({ class: 'warn', style: { hue: 'blue' } }),
|
|
20
|
+
SceneBuilder.rect('b', { ...box, x: 400 }).properties({ class: 'gone' }),
|
|
21
|
+
SceneBuilder.link('line', 'a', 'b')
|
|
22
|
+
.id('l')
|
|
23
|
+
.properties({ class: 'warn', style: { lineStyle: 'solid' } }),
|
|
24
|
+
]).build();
|
|
25
|
+
|
|
26
|
+
test('an element takes its class look under its own', ({ expect }) => {
|
|
27
|
+
expect(classedNode(nodes.a, styles).style).toEqual({ hue: 'blue', lineStyle: 'dashed', rounded: true });
|
|
28
|
+
// A link takes only the common base of the class's style: its colour and line style.
|
|
29
|
+
expect(classedLink(links.l, styles).style).toEqual({ hue: 'red', lineStyle: 'solid' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a node's line style draws its border", ({ expect }) => {
|
|
33
|
+
expect(frameClasses(classedNode(nodes.a, styles), false)).toEqual(expect.arrayContaining(['border-dashed']));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('a missing class leaves the element as it is', ({ expect }) => {
|
|
37
|
+
expect(classedNode(nodes.b, styles)).toBe(nodes.b);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('an edit of a classed look goes to the class, and the element stops overriding it', ({ expect }) => {
|
|
41
|
+
// A shows blue over the class's red; the user picks green and turns rounding off.
|
|
42
|
+
const shown = classedNode(nodes.a, styles).style;
|
|
43
|
+
const edited = { hue: 'green', rounded: false };
|
|
44
|
+
expect(splitClassEdit(shown, edited, styles.warn.style ?? {}, nodes.a.style)).toEqual({
|
|
45
|
+
classLook: { hue: 'green', lineStyle: 'dashed', rounded: false },
|
|
46
|
+
own: {},
|
|
47
|
+
});
|
|
48
|
+
// An unchanged field stays where it was: A keeps its own blue, the class its red.
|
|
49
|
+
expect(splitClassEdit(shown, { ...shown, rounded: false }, styles.warn.style ?? {}, nodes.a.style)).toEqual({
|
|
50
|
+
classLook: { hue: 'red', lineStyle: 'dashed', rounded: false },
|
|
51
|
+
own: { hue: 'blue' },
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/utils/style.ts
CHANGED
|
@@ -7,7 +7,20 @@
|
|
|
7
7
|
// every hue is spelled out here rather than composed from the hue name.
|
|
8
8
|
//
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type HorizontalAlign,
|
|
12
|
+
type Link,
|
|
13
|
+
type Node,
|
|
14
|
+
type NodeStyle,
|
|
15
|
+
type NodeTone,
|
|
16
|
+
STYLE_HUES,
|
|
17
|
+
type StyleHue,
|
|
18
|
+
type StyleMap,
|
|
19
|
+
type VerticalAlign,
|
|
20
|
+
isEllipseNode,
|
|
21
|
+
isFrameNode,
|
|
22
|
+
showsContents,
|
|
23
|
+
} from '../model/types.ts';
|
|
11
24
|
|
|
12
25
|
export type HueClasses = { surface: string; text: string; border: string };
|
|
13
26
|
|
|
@@ -32,14 +45,10 @@ const HUES: Record<string, HueClasses> = {
|
|
|
32
45
|
rose: { surface: 'bg-rose-surface', text: 'text-rose-fg', border: 'border-rose-border' },
|
|
33
46
|
};
|
|
34
47
|
|
|
35
|
-
/** The hues the style picker offers, neutral first; any theme hue still renders, at its `medium` tone. */
|
|
36
|
-
export const STYLE_HUES = ['neutral', 'red', 'orange', 'amber', 'green', 'teal', 'blue', 'violet', 'pink'] as const;
|
|
37
|
-
export type StyleHue = (typeof STYLE_HUES)[number];
|
|
38
|
-
|
|
39
48
|
export const TONES: readonly NodeTone[] = [0, 1, 2, 3];
|
|
40
49
|
|
|
41
50
|
/** What each tone is called, for labels. */
|
|
42
|
-
export const TONE_NAMES: Record<NodeTone, string> = { 0: 'outline', 1: '
|
|
51
|
+
export const TONE_NAMES: Record<NodeTone, string> = { 0: 'outline', 1: 'strong', 2: 'medium', 3: 'light' };
|
|
43
52
|
|
|
44
53
|
/** The tone a hue without one draws at: the look a hue had before tones. */
|
|
45
54
|
export const DEFAULT_TONE: NodeTone = 2;
|
|
@@ -47,56 +56,56 @@ export const DEFAULT_TONE: NodeTone = 2;
|
|
|
47
56
|
type ToneClasses = { surface: string; text: string };
|
|
48
57
|
|
|
49
58
|
/**
|
|
50
|
-
* The fills
|
|
51
|
-
*
|
|
59
|
+
* The fills either side of a hue's `surface` role (its 400): the scale's 500 under light text (tone 1), and its
|
|
60
|
+
* 300 under its darkest text (tone 3). Tone 2 is the role pair in `HUES`; tone 0 has no fill.
|
|
52
61
|
*/
|
|
53
|
-
const TONE_FILLS: Record<StyleHue, Record<
|
|
62
|
+
const TONE_FILLS: Record<StyleHue, Record<'strong' | 'light', ToneClasses>> = {
|
|
54
63
|
neutral: {
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
light: { surface: 'bg-neutral-300', text: 'text-neutral-900' },
|
|
65
|
+
strong: { surface: 'bg-neutral-500', text: 'text-neutral-50' },
|
|
57
66
|
},
|
|
58
67
|
red: {
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
light: { surface: 'bg-red-300', text: 'text-red-900' },
|
|
69
|
+
strong: { surface: 'bg-red-500', text: 'text-neutral-50' },
|
|
61
70
|
},
|
|
62
71
|
orange: {
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
light: { surface: 'bg-orange-300', text: 'text-orange-900' },
|
|
73
|
+
strong: { surface: 'bg-orange-500', text: 'text-neutral-50' },
|
|
65
74
|
},
|
|
66
75
|
amber: {
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
light: { surface: 'bg-amber-300', text: 'text-amber-900' },
|
|
77
|
+
strong: { surface: 'bg-amber-500', text: 'text-neutral-50' },
|
|
69
78
|
},
|
|
70
79
|
green: {
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
light: { surface: 'bg-green-300', text: 'text-green-900' },
|
|
81
|
+
strong: { surface: 'bg-green-500', text: 'text-neutral-50' },
|
|
73
82
|
},
|
|
74
83
|
teal: {
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
light: { surface: 'bg-teal-300', text: 'text-teal-900' },
|
|
85
|
+
strong: { surface: 'bg-teal-500', text: 'text-neutral-50' },
|
|
86
|
+
},
|
|
87
|
+
sky: {
|
|
88
|
+
light: { surface: 'bg-sky-300', text: 'text-sky-900' },
|
|
89
|
+
strong: { surface: 'bg-sky-500', text: 'text-neutral-50' },
|
|
77
90
|
},
|
|
78
91
|
blue: {
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
light: { surface: 'bg-blue-300', text: 'text-blue-900' },
|
|
93
|
+
strong: { surface: 'bg-blue-500', text: 'text-neutral-50' },
|
|
81
94
|
},
|
|
82
95
|
violet: {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
pink: {
|
|
87
|
-
1: { surface: 'bg-pink-200', text: 'text-pink-900' },
|
|
88
|
-
3: { surface: 'bg-pink-bg', text: 'text-neutral-50' },
|
|
96
|
+
light: { surface: 'bg-violet-300', text: 'text-violet-900' },
|
|
97
|
+
strong: { surface: 'bg-violet-500', text: 'text-neutral-50' },
|
|
89
98
|
},
|
|
90
99
|
};
|
|
91
100
|
|
|
92
|
-
const isStyleHue = (hue: string): hue is StyleHue => STYLE_HUES.some((candidate) => candidate === hue);
|
|
101
|
+
const isStyleHue = (hue: string | undefined): hue is StyleHue => STYLE_HUES.some((candidate) => candidate === hue);
|
|
93
102
|
|
|
94
103
|
/** The default look: the base surface, the base text and the separator border. */
|
|
95
104
|
const DEFAULT: HueClasses = { surface: 'bg-base-surface', text: '', border: 'border-separator' };
|
|
96
105
|
|
|
97
106
|
/**
|
|
98
107
|
* The classes a hue at a tone draws with. Every tone keeps the hue's border; tone 0 drops the fill and
|
|
99
|
-
* keeps the default text, and a tone a hue has no fills for draws as `medium
|
|
108
|
+
* keeps the default text, and a tone a hue has no fills for draws as `medium` (its role pair).
|
|
100
109
|
*/
|
|
101
110
|
export const hueClasses = (hue: string | undefined, tone: NodeTone = DEFAULT_TONE): HueClasses => {
|
|
102
111
|
const base = (hue && HUES[hue]) || DEFAULT;
|
|
@@ -107,11 +116,38 @@ export const hueClasses = (hue: string | undefined, tone: NodeTone = DEFAULT_TON
|
|
|
107
116
|
return { surface: 'bg-transparent', text: '', border: base.border };
|
|
108
117
|
}
|
|
109
118
|
if ((tone === 1 || tone === 3) && isStyleHue(hue)) {
|
|
110
|
-
return { ...TONE_FILLS[hue][tone], border: base.border };
|
|
119
|
+
return { ...TONE_FILLS[hue][tone === 1 ? 'strong' : 'light'], border: base.border };
|
|
111
120
|
}
|
|
112
121
|
return base;
|
|
113
122
|
};
|
|
114
123
|
|
|
124
|
+
/** `base` with every field `own` sets over it; an `undefined` field of `own` is unset, so it leaves the base's. */
|
|
125
|
+
const overlay = <T extends object>(base: T | undefined, own: T | undefined): T | undefined => {
|
|
126
|
+
if (!base || !own) {
|
|
127
|
+
return own ?? base;
|
|
128
|
+
}
|
|
129
|
+
const merged = { ...base };
|
|
130
|
+
for (const [key, value] of Object.entries(own)) {
|
|
131
|
+
if (value !== undefined) {
|
|
132
|
+
Reflect.set(merged, key, value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return merged;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** A node as drawn: its class's style under its own (`StyleClass`). */
|
|
139
|
+
export const classedNode = (node: Node, styles: StyleMap | undefined): Node => {
|
|
140
|
+
const style = node.class ? styles?.[node.class]?.style : undefined;
|
|
141
|
+
return style ? { ...node, style: overlay(style, node.style) } : node;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** A link as drawn: its class's colour and line style under its own. */
|
|
145
|
+
export const classedLink = (link: Link, styles: StyleMap | undefined): Link => {
|
|
146
|
+
const style = link.class ? styles?.[link.class]?.style : undefined;
|
|
147
|
+
// A link draws only the common base (`LineStyle`) of its class's style.
|
|
148
|
+
return style ? { ...link, style: overlay({ hue: style.hue, lineStyle: style.lineStyle }, link.style) } : link;
|
|
149
|
+
};
|
|
150
|
+
|
|
115
151
|
/**
|
|
116
152
|
* A node's style with the defaults the frame draws spelled out: an unset `fill` or `border` is drawn, so
|
|
117
153
|
* the properties panel must show it as on rather than as an unset (off) toggle.
|
|
@@ -124,24 +160,101 @@ export const resolveStyle = (style: NodeStyle = {}): NodeStyle => ({
|
|
|
124
160
|
|
|
125
161
|
/**
|
|
126
162
|
* Frame classes for a node: fill and text colour, border and corner radius from its style; a guide is
|
|
127
|
-
* dashed and unfilled
|
|
163
|
+
* dashed and unfilled.
|
|
128
164
|
*/
|
|
129
165
|
export const frameClasses = (node: Node, selected: boolean, hovered = false): string[] => {
|
|
130
166
|
const style = resolveStyle(node.style);
|
|
131
167
|
const hue = hueClasses(style.hue, style.tone);
|
|
132
168
|
const filled = style.fill && !style.guide;
|
|
169
|
+
// A scene shape drawing its contents is a window onto another canvas, so it is opaque even in outline: the
|
|
170
|
+
// grid behind it would read as part of the child scene.
|
|
171
|
+
const opaque = isFrameNode(node) && showsContents(node);
|
|
172
|
+
const clear = !filled || hue.surface === 'bg-transparent';
|
|
173
|
+
const surface = opaque && clear ? 'bg-base-surface' : filled ? hue.surface : '';
|
|
133
174
|
return [
|
|
134
|
-
|
|
135
|
-
hue.
|
|
175
|
+
surface,
|
|
176
|
+
// A guide's hue marks its edge (a group's backdrop); its title reads in the default text colour.
|
|
177
|
+
style.guide ? '' : hue.text,
|
|
136
178
|
selected
|
|
137
|
-
? 'border-
|
|
179
|
+
? 'border-focus'
|
|
138
180
|
: hovered
|
|
139
|
-
? 'border-
|
|
140
|
-
:
|
|
181
|
+
? 'border-focus/50'
|
|
182
|
+
: // A frame always shows its edge: it bounds a scene or an embedded object, which has none of its own.
|
|
183
|
+
style.border === false && !style.guide && !isFrameNode(node)
|
|
141
184
|
? 'border-transparent'
|
|
142
185
|
: hue.border,
|
|
143
|
-
style.guide ? 'border-dashed' : '',
|
|
186
|
+
style.guide || style.lineStyle === 'dashed' ? 'border-dashed' : style.lineStyle === 'dotted' ? 'border-dotted' : '',
|
|
144
187
|
isEllipseNode(node) ? 'rounded-[50%]' : style.rounded ? 'rounded-2xl' : 'rounded-sm',
|
|
145
|
-
style.className ?? '',
|
|
146
188
|
];
|
|
147
189
|
};
|
|
190
|
+
|
|
191
|
+
export type LineClasses = { stroke: string; fill: string };
|
|
192
|
+
|
|
193
|
+
/** A link's stroke and its end markers' fill, in the hue's border colour so a link matches a node of its hue. */
|
|
194
|
+
const LINE_CLASSES: Record<StyleHue, LineClasses> = {
|
|
195
|
+
neutral: { stroke: 'stroke-neutral-border', fill: 'fill-neutral-border' },
|
|
196
|
+
red: { stroke: 'stroke-red-border', fill: 'fill-red-border' },
|
|
197
|
+
orange: { stroke: 'stroke-orange-border', fill: 'fill-orange-border' },
|
|
198
|
+
amber: { stroke: 'stroke-amber-border', fill: 'fill-amber-border' },
|
|
199
|
+
green: { stroke: 'stroke-green-border', fill: 'fill-green-border' },
|
|
200
|
+
teal: { stroke: 'stroke-teal-border', fill: 'fill-teal-border' },
|
|
201
|
+
sky: { stroke: 'stroke-sky-border', fill: 'fill-sky-border' },
|
|
202
|
+
blue: { stroke: 'stroke-blue-border', fill: 'fill-blue-border' },
|
|
203
|
+
violet: { stroke: 'stroke-violet-border', fill: 'fill-violet-border' },
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** The default line: the grey every link drew before lines took a hue. */
|
|
207
|
+
const DEFAULT_LINE: LineClasses = { stroke: 'stroke-neutral-500', fill: 'fill-neutral-500' };
|
|
208
|
+
|
|
209
|
+
export const lineClasses = (hue: string | undefined): LineClasses =>
|
|
210
|
+
isStyleHue(hue) ? LINE_CLASSES[hue] : DEFAULT_LINE;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Splits an edit of a classed element's look (`edited`, against what the panel `shown`): every field that changed
|
|
214
|
+
* goes to the class, so each element of the class derives it, and the element drops its own value for that field,
|
|
215
|
+
* which would otherwise hide the class's.
|
|
216
|
+
*/
|
|
217
|
+
export const splitClassEdit = <T extends object>(
|
|
218
|
+
shown: object | undefined,
|
|
219
|
+
edited: unknown,
|
|
220
|
+
classLook: T,
|
|
221
|
+
own: T | undefined,
|
|
222
|
+
): { classLook: T; own: T | undefined } => {
|
|
223
|
+
const nextClass = { ...classLook };
|
|
224
|
+
const nextOwn = own ? { ...own } : undefined;
|
|
225
|
+
if (typeof edited === 'object' && edited !== null) {
|
|
226
|
+
for (const [key, value] of Object.entries(edited)) {
|
|
227
|
+
if (Reflect.get(shown ?? {}, key) !== value) {
|
|
228
|
+
Reflect.set(nextClass, key, value);
|
|
229
|
+
if (nextOwn) {
|
|
230
|
+
Reflect.deleteProperty(nextOwn, key);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { classLook: nextClass, own: nextOwn };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const HORIZONTAL_CLASSES: Record<HorizontalAlign, string> = {
|
|
239
|
+
left: 'items-start text-left',
|
|
240
|
+
center: 'items-center text-center',
|
|
241
|
+
right: 'items-end text-right',
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const VERTICAL_CLASSES: Record<VerticalAlign, string> = {
|
|
245
|
+
top: 'justify-start',
|
|
246
|
+
middle: 'justify-center',
|
|
247
|
+
bottom: 'justify-end',
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Classes placing a text part in its shape by the style's alignment, else by the type's own (`defaults`): the part
|
|
252
|
+
* is a column, so `justify` places it down the shape and `items` across it.
|
|
253
|
+
*/
|
|
254
|
+
export const alignClasses = (
|
|
255
|
+
style: NodeStyle | undefined,
|
|
256
|
+
defaults: { horizontal: HorizontalAlign; vertical: VerticalAlign },
|
|
257
|
+
): string =>
|
|
258
|
+
`flex flex-col ${HORIZONTAL_CLASSES[style?.alignHorizontal ?? defaults.horizontal]} ${
|
|
259
|
+
VERTICAL_CLASSES[style?.alignVertical ?? defaults.vertical]
|
|
260
|
+
}`;
|
|
@@ -8,7 +8,7 @@ import { solve } from '../model/projections/constrained.ts';
|
|
|
8
8
|
import { layoutGraph } from '../model/projections/dynamic.ts';
|
|
9
9
|
import { MAJOR_GRID, type Point, type Scene } from '../model/types.ts';
|
|
10
10
|
import { nodeBounds } from './shapes.ts';
|
|
11
|
-
import {
|
|
11
|
+
import { createModelSceneTree, createSceneTree } from './testing.ts';
|
|
12
12
|
|
|
13
13
|
/** Every edge of every node lies on the grid, which is what move and resize snap to. */
|
|
14
14
|
const offGrid = (scene: Scene): string[] =>
|
|
@@ -42,8 +42,8 @@ describe('initial layouts conform to the major grid', () => {
|
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
test('
|
|
46
|
-
const { scenes, root } =
|
|
45
|
+
test('model scene tree fixture', ({ expect }) => {
|
|
46
|
+
const { scenes, root } = createModelSceneTree();
|
|
47
47
|
expect(scenes.map(({ id }) => id)).toContain(root);
|
|
48
48
|
// Three levels: the root, its two subsystems, and one leaf under each.
|
|
49
49
|
expect(scenes).toHaveLength(5);
|