@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/parts.ts
CHANGED
|
@@ -3,65 +3,87 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
//
|
|
6
|
-
// Text parts of a node: the
|
|
7
|
-
//
|
|
6
|
+
// Text parts of a node: the properties a node type renders as text and lets the user edit in place. Each
|
|
7
|
+
// type declares its parts in its registry definition, so a type a host adds edits the same way as a built-in
|
|
8
|
+
// one. A part is named by the property it edits; a `lines` part reads and writes a list one entry per line.
|
|
8
9
|
//
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { nodeDef } from '../model/node-def.ts';
|
|
12
|
+
import { type NodeRegistry } from '../model/registry.ts';
|
|
13
|
+
import { type Node, type NodeValues } from '../model/types.ts';
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
/** A part's name: the node property it edits. */
|
|
16
|
+
export type PartKey = string;
|
|
17
|
+
|
|
18
|
+
/** A text property of a node type, as its definition declares it. */
|
|
19
|
+
export type PartField = {
|
|
20
|
+
/** The node property the part edits. */
|
|
21
|
+
field: PartKey;
|
|
22
|
+
/** A list of strings, one per line; implies `multiline`. */
|
|
23
|
+
lines?: boolean;
|
|
24
|
+
/** Enter breaks the line and Mod-Enter commits, rather than Enter committing. */
|
|
25
|
+
multiline?: boolean;
|
|
26
|
+
};
|
|
13
27
|
|
|
14
28
|
/** A part edited in place: which one, and how the editor hands its result back. */
|
|
15
29
|
export type PartEditing = {
|
|
16
30
|
part: PartKey;
|
|
31
|
+
multiline: boolean;
|
|
17
32
|
commit: (text: string) => void;
|
|
18
33
|
cancel: () => void;
|
|
19
34
|
};
|
|
20
35
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
/** The part a `data-part` attribute names, if it names one. */
|
|
24
|
-
export const partKey = (value: string | null | undefined): PartKey | undefined => PARTS.find((part) => part === value);
|
|
25
|
-
|
|
26
|
-
const LINES: readonly PartKey[] = ['text', 'attributes', 'methods'];
|
|
27
|
-
|
|
28
|
-
/** Enter inserts a line in a multi-line part; Mod-Enter commits. */
|
|
29
|
-
export const isMultiline = (part: PartKey): boolean => LINES.includes(part);
|
|
30
|
-
|
|
31
|
-
const lines = (text: string): string[] =>
|
|
36
|
+
const toLines = (text: string): string[] =>
|
|
32
37
|
text
|
|
33
38
|
.split('\n')
|
|
34
39
|
.map((line) => line.trim())
|
|
35
40
|
.filter((line) => line.length > 0);
|
|
36
41
|
|
|
37
|
-
/** The
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
/** The core base's one part: the label a node of an unknown type is edited by (see `BaseNode`). */
|
|
43
|
+
const BASE_PARTS: readonly PartField[] = [{ field: 'label' }];
|
|
44
|
+
|
|
45
|
+
/** The parts a node's type declares; a type the registry does not know has the core base's label. */
|
|
46
|
+
export const nodeParts = (registry: NodeRegistry, node: Node): readonly PartField[] => {
|
|
47
|
+
const def = nodeDef(registry, node);
|
|
48
|
+
return def ? (def.parts ?? []) : BASE_PARTS;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const partOf = (registry: NodeRegistry, node: Node, part: PartKey): PartField | undefined =>
|
|
52
|
+
nodeParts(registry, node).find(({ field }) => field === part);
|
|
53
|
+
|
|
54
|
+
/** Whether the part takes several lines. */
|
|
55
|
+
export const isMultiline = (part: PartField): boolean => part.lines === true || part.multiline === true;
|
|
56
|
+
|
|
57
|
+
/** The part's text, or none when the node's type has no such part. */
|
|
58
|
+
export const partText = (registry: NodeRegistry, node: Node, part: PartKey): string | undefined => {
|
|
59
|
+
const field = partOf(registry, node, part);
|
|
60
|
+
if (!field) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const value: unknown = Reflect.get(node, field.field);
|
|
64
|
+
if (field.lines) {
|
|
65
|
+
return Array.isArray(value) ? value.filter((line) => typeof line === 'string').join('\n') : '';
|
|
50
66
|
}
|
|
67
|
+
return typeof value === 'string' ? value : '';
|
|
51
68
|
};
|
|
52
69
|
|
|
53
|
-
/** The `update` values that set the part's text, or none when the node type has no such part. */
|
|
54
|
-
export const partValues = (node: Node, part: PartKey, text: string): NodeValues | undefined => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
case 'text':
|
|
59
|
-
return isNoteNode(node) ? { text } : undefined;
|
|
60
|
-
case 'name':
|
|
61
|
-
return isClassNode(node) ? { name: text.trim() } : undefined;
|
|
62
|
-
case 'attributes':
|
|
63
|
-
return isClassNode(node) ? { attributes: lines(text) } : undefined;
|
|
64
|
-
case 'methods':
|
|
65
|
-
return isClassNode(node) ? { methods: lines(text) } : undefined;
|
|
70
|
+
/** The `update` values that set the part's text, or none when the node's type has no such part. */
|
|
71
|
+
export const partValues = (registry: NodeRegistry, node: Node, part: PartKey, text: string): NodeValues | undefined => {
|
|
72
|
+
const field = partOf(registry, node, part);
|
|
73
|
+
if (!field) {
|
|
74
|
+
return undefined;
|
|
66
75
|
}
|
|
76
|
+
return { [field.field]: field.lines ? toLines(text) : text };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** A node's main text: its first part's, which a diagram or a DSL export shows as the node's name. */
|
|
80
|
+
export const nodeTitle = (registry: NodeRegistry, node: Node): string | undefined => {
|
|
81
|
+
const [first] = nodeParts(registry, node);
|
|
82
|
+
return first ? partText(registry, node, first.field) : undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** The node with its main text (its first part) set; a type without parts is returned as is. */
|
|
86
|
+
export const withTitle = <N extends Node>(registry: NodeRegistry, node: N, text: string): N => {
|
|
87
|
+
const [first] = nodeParts(registry, node);
|
|
88
|
+
return first ? { ...node, ...partValues(registry, node, first.field, text) } : node;
|
|
67
89
|
};
|
package/src/utils/ports.ts
CHANGED
|
@@ -128,8 +128,8 @@ const distance2 = (left: Point, right: Point) => (left.x - right.x) ** 2 + (left
|
|
|
128
128
|
* source end only leaves a port that accepts `out`, the target end only lands on one that accepts `in`.
|
|
129
129
|
*/
|
|
130
130
|
export const pairPorts = (source: PortTerminal, target: PortTerminal): PortPair | undefined => {
|
|
131
|
-
const sources =
|
|
132
|
-
const targets =
|
|
131
|
+
const sources = portCandidates(source, 'out');
|
|
132
|
+
const targets = portCandidates(target, 'in');
|
|
133
133
|
if (sources.length === 0 || targets.length === 0) {
|
|
134
134
|
return undefined;
|
|
135
135
|
}
|
|
@@ -156,7 +156,7 @@ export const nearestPort = (
|
|
|
156
156
|
): Port | undefined => {
|
|
157
157
|
let best: Port | undefined;
|
|
158
158
|
let bestDistance = Infinity;
|
|
159
|
-
for (const port of
|
|
159
|
+
for (const port of portCandidates(terminal, direction)) {
|
|
160
160
|
const value = distance2(portPoint(terminal.bounds, port), point);
|
|
161
161
|
if (value < bestDistance) {
|
|
162
162
|
bestDistance = value;
|
|
@@ -167,7 +167,10 @@ export const nearestPort = (
|
|
|
167
167
|
};
|
|
168
168
|
|
|
169
169
|
/** The ports an end may use: those accepting its direction, narrowed to the pinned one when it is among them. */
|
|
170
|
-
const
|
|
170
|
+
export const portCandidates = (
|
|
171
|
+
{ ports, port }: PortTerminal,
|
|
172
|
+
direction: Exclude<PortDirection, 'any'>,
|
|
173
|
+
): readonly Port[] => {
|
|
171
174
|
const allowed = ports.filter((candidate) => portAccepts(candidate, direction));
|
|
172
175
|
if (port === undefined) {
|
|
173
176
|
return allowed;
|
|
@@ -7,7 +7,7 @@ import { describe, test } from 'vitest';
|
|
|
7
7
|
|
|
8
8
|
import { getPropertySignatures } from '@dxos/effect/SchemaAST';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { EllipseNode, LineLink, NoteNode, RectNode } from '../model/types.ts';
|
|
11
11
|
import { commonSchema, mergeValues, patchValues } from './properties.ts';
|
|
12
12
|
|
|
13
13
|
const names = (schema: Schema.Codec<any, any> | undefined) =>
|
|
@@ -24,12 +24,20 @@ describe('properties', () => {
|
|
|
24
24
|
expect(rectEllipse).toContain('style');
|
|
25
25
|
expect(rectEllipse).not.toContain('type');
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
expect(
|
|
29
|
-
expect(
|
|
27
|
+
const rectNote = names(commonSchema([RectNode, NoteNode]));
|
|
28
|
+
expect(rectNote).toContain('style');
|
|
29
|
+
expect(rectNote).not.toContain('label');
|
|
30
30
|
|
|
31
|
-
// A node and a link share only what both declare:
|
|
32
|
-
|
|
31
|
+
// A node and a link share only what both declare: `locked`, `layer`, `class` and `style` (and the hidden `id`/`z`),
|
|
32
|
+
// the style narrowed to the common base both kinds' styles extend.
|
|
33
|
+
const shared = commonSchema([RectNode, LineLink]);
|
|
34
|
+
expect(names(shared)).toEqual(['id', 'z', 'locked', 'layer', 'class', 'style']);
|
|
35
|
+
const style = shared && getPropertySignatures(shared.ast).find((property) => property.name === 'style');
|
|
36
|
+
const struct = style?.type._tag === 'Union' ? style.type.types.find((type) => type._tag === 'Objects') : undefined;
|
|
37
|
+
expect(struct?._tag === 'Objects' && struct.propertySignatures.map((property) => property.name)).toEqual([
|
|
38
|
+
'hue',
|
|
39
|
+
'lineStyle',
|
|
40
|
+
]);
|
|
33
41
|
expect(commonSchema([Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.String })])).toBeUndefined();
|
|
34
42
|
});
|
|
35
43
|
|
|
@@ -52,6 +60,15 @@ describe('properties', () => {
|
|
|
52
60
|
expect([...mixed].sort()).toEqual(['center.x', 'style.hue']);
|
|
53
61
|
});
|
|
54
62
|
|
|
63
|
+
test('an instance value (a reference) is kept whole, not merged field by field', ({ expect }) => {
|
|
64
|
+
class Reference {
|
|
65
|
+
constructor(readonly uri: string) {}
|
|
66
|
+
}
|
|
67
|
+
const reference = new Reference('echo://space/object');
|
|
68
|
+
const { values } = mergeValues([{ drawing: reference }], ['drawing']);
|
|
69
|
+
expect(values.drawing).toBe(reference);
|
|
70
|
+
});
|
|
71
|
+
|
|
55
72
|
test('a patch writes only the changed paths and keeps the rest of a nested value', ({ expect }) => {
|
|
56
73
|
const element = { label: 'A', style: { hue: 'red', rounded: true }, center: { x: 0, y: 16 } };
|
|
57
74
|
const values = { label: 'B', style: { hue: 'blue', rounded: false }, center: { x: 48, y: 0 } };
|
package/src/utils/properties.ts
CHANGED
|
@@ -8,14 +8,20 @@
|
|
|
8
8
|
//
|
|
9
9
|
|
|
10
10
|
import * as Schema from 'effect/Schema';
|
|
11
|
-
import
|
|
11
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
12
12
|
|
|
13
|
-
import { getChecks, getPropertySignatures
|
|
13
|
+
import { getChecks, getPropertySignatures } from '@dxos/effect/SchemaAST';
|
|
14
14
|
|
|
15
15
|
type Values = Readonly<Record<string, unknown>>;
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/** A record of fields to merge into; an instance (an ECHO `Ref`, a date) is a value, compared and kept whole. */
|
|
18
|
+
const isPlainObject = (value: unknown): value is Values => {
|
|
19
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const prototype = Object.getPrototypeOf(value);
|
|
23
|
+
return prototype === Object.prototype || prototype === null;
|
|
24
|
+
};
|
|
19
25
|
|
|
20
26
|
const isOptional = (ast: SchemaAST.AST) => ast.context?.isOptional ?? false;
|
|
21
27
|
|
|
@@ -76,24 +82,68 @@ const sameList = (left: readonly SchemaAST.AST[], right: readonly SchemaAST.AST[
|
|
|
76
82
|
left.length === right.length && left.every((type, index) => sameType(type, right[index]));
|
|
77
83
|
|
|
78
84
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
85
|
+
* What two property types share: the type itself when they are the same, and for two structs (a node's style and a
|
|
86
|
+
* link's, say) the struct of the fields they declare alike, recursively; none when they share nothing.
|
|
87
|
+
*/
|
|
88
|
+
const commonType = (left: SchemaAST.AST, right: SchemaAST.AST): SchemaAST.AST | undefined => {
|
|
89
|
+
if (sameType(left, right)) {
|
|
90
|
+
return left;
|
|
91
|
+
}
|
|
92
|
+
// An optional struct is a union with `undefined`: intersect member by member.
|
|
93
|
+
if (left._tag === 'Union' && right._tag === 'Union' && left.types.length === right.types.length) {
|
|
94
|
+
const types = left.types.map((type, index) => commonType(type, right.types[index]));
|
|
95
|
+
return types.every((type) => type !== undefined)
|
|
96
|
+
? new SchemaAST.Union(
|
|
97
|
+
types.flatMap((type) => (type ? [type] : [])),
|
|
98
|
+
left.options,
|
|
99
|
+
left.annotations,
|
|
100
|
+
left.checks,
|
|
101
|
+
left.encoding,
|
|
102
|
+
left.context,
|
|
103
|
+
left.encodingChecks,
|
|
104
|
+
)
|
|
105
|
+
: undefined;
|
|
106
|
+
}
|
|
107
|
+
if (left._tag !== 'Objects' || right._tag !== 'Objects' || isOptional(left) !== isOptional(right)) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const shared = left.propertySignatures.flatMap((property) => {
|
|
111
|
+
const other = right.propertySignatures.find((candidate) => candidate.name === property.name);
|
|
112
|
+
const type = other && commonType(property.type, other.type);
|
|
113
|
+
return type ? [new SchemaAST.PropertySignature(property.name, type)] : [];
|
|
114
|
+
});
|
|
115
|
+
return shared.length > 0
|
|
116
|
+
? new SchemaAST.Objects(
|
|
117
|
+
shared,
|
|
118
|
+
left.indexSignatures,
|
|
119
|
+
left.annotations,
|
|
120
|
+
left.checks,
|
|
121
|
+
left.encoding,
|
|
122
|
+
left.context,
|
|
123
|
+
left.encodingChecks,
|
|
124
|
+
)
|
|
125
|
+
: undefined;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The properties every schema declares alike, as one struct over the first schema's declarations, a struct
|
|
130
|
+
* property narrowed to the fields every schema's declares alike; `undefined` when they share none. One schema is
|
|
131
|
+
* its own common schema.
|
|
81
132
|
*/
|
|
82
133
|
export const commonSchema = (schemas: readonly Schema.Codec<any, any>[]): Schema.Codec<any, any> | undefined => {
|
|
83
134
|
const [first, ...rest] = schemas;
|
|
84
135
|
if (!first) {
|
|
85
136
|
return undefined;
|
|
86
137
|
}
|
|
87
|
-
const shared = getPropertySignatures(first.ast)
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return shared.length > 0 ? Schema.make<Schema.Codec<any, any>>(pick(first.ast, shared)) : undefined;
|
|
138
|
+
const shared = getPropertySignatures(first.ast).flatMap((property) => {
|
|
139
|
+
let type: SchemaAST.AST | undefined = property.type;
|
|
140
|
+
for (const schema of rest) {
|
|
141
|
+
const other = getPropertySignatures(schema.ast).find((candidate) => candidate.name === property.name);
|
|
142
|
+
type = type && other ? commonType(type, other.type) : undefined;
|
|
143
|
+
}
|
|
144
|
+
return type ? [new SchemaAST.PropertySignature(property.name, type)] : [];
|
|
145
|
+
});
|
|
146
|
+
return shared.length > 0 ? Schema.make<Schema.Codec<any, any>>(new SchemaAST.Objects(shared, [])) : undefined;
|
|
97
147
|
};
|
|
98
148
|
|
|
99
149
|
const equal = (left: unknown, right: unknown): boolean => {
|
package/src/utils/route.test.ts
CHANGED
|
@@ -5,9 +5,18 @@
|
|
|
5
5
|
import { describe, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { defaultNodeRegistry } from '../model/registry.ts';
|
|
8
|
-
import { type Link } from '../model/types.ts';
|
|
8
|
+
import { type Link, type Scene } from '../model/types.ts';
|
|
9
9
|
import { SceneBuilder } from './builder.ts';
|
|
10
|
-
import {
|
|
10
|
+
import { DEFAULT_LATTICE } from './lattice.ts';
|
|
11
|
+
import {
|
|
12
|
+
insertIndex,
|
|
13
|
+
linkGeometry,
|
|
14
|
+
linkPath,
|
|
15
|
+
sceneLinkGeometry,
|
|
16
|
+
sideToward,
|
|
17
|
+
smartPoints,
|
|
18
|
+
splinePath,
|
|
19
|
+
} from './route.ts';
|
|
11
20
|
|
|
12
21
|
const from = { point: { x: 0, y: 0 }, side: 'e' as const };
|
|
13
22
|
const to = { point: { x: 300, y: 0 }, side: 'w' as const };
|
|
@@ -98,4 +107,44 @@ describe('route', () => {
|
|
|
98
107
|
expect(half?.target).toEqual({ point: { x: 256, y: 64 }, side: 'e' });
|
|
99
108
|
expect(half?.source.side).toBe('w');
|
|
100
109
|
});
|
|
110
|
+
|
|
111
|
+
test('a route runs straight to a free end rather than turning to its side', ({ expect }) => {
|
|
112
|
+
const port = { point: { x: 0, y: 0 }, side: 'e' as const };
|
|
113
|
+
const free = { point: { x: 100, y: 37 }, side: 'w' as const, free: true };
|
|
114
|
+
// A curve's control point at a free end is the end itself, and a smart link has no stub there.
|
|
115
|
+
expect(
|
|
116
|
+
linkPath({ type: 'curve', id: 'c', z: 'a', source: { node: 'n' }, target: { point: free.point } }, port, free),
|
|
117
|
+
).toMatch(/, 100 37, 100 37$/);
|
|
118
|
+
expect(smartPoints(port, free)).toEqual([port.point, { x: 32, y: 0 }, free.point]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('on a lattice a smart link takes the ports whose gutter route bends least', ({ expect }) => {
|
|
122
|
+
// MESH on (4, 1) and EDGE on (3, 4), as the Composer overview has them: the nearest ports (MESH's bottom-left,
|
|
123
|
+
// EDGE's top-right) need two bends round EDGE's corner; down the free column and in from the east needs one.
|
|
124
|
+
const box = (id: string, col: number, row: number) => ({
|
|
125
|
+
type: 'rect',
|
|
126
|
+
id,
|
|
127
|
+
z: 'a0',
|
|
128
|
+
center: { x: col * 384, y: row * 192 },
|
|
129
|
+
size: { width: 256, height: 128 },
|
|
130
|
+
});
|
|
131
|
+
const link: Link = { type: 'smart', id: 'link', z: 'a1', source: { node: 'mesh' }, target: { node: 'edge' } };
|
|
132
|
+
const scene: Scene = {
|
|
133
|
+
id: 'root',
|
|
134
|
+
nodes: { mesh: box('mesh', 4, 1), edge: box('edge', 3, 4) },
|
|
135
|
+
links: { link },
|
|
136
|
+
};
|
|
137
|
+
const [geometry] = sceneLinkGeometry(scene, defaultNodeRegistry, [link], DEFAULT_LATTICE);
|
|
138
|
+
expect([geometry.source.side, geometry.source.point, geometry.target.side, geometry.target.point]).toEqual([
|
|
139
|
+
's',
|
|
140
|
+
{ x: 1536, y: 256 },
|
|
141
|
+
'e',
|
|
142
|
+
{ x: 1280, y: 768 },
|
|
143
|
+
]);
|
|
144
|
+
// One bend: a single rounded corner between the two straight runs.
|
|
145
|
+
expect(geometry.path.match(/Q/g)).toHaveLength(1);
|
|
146
|
+
// The same choice for the link on its own, so hit-testing and handles agree with what is drawn.
|
|
147
|
+
const single = linkGeometry(scene, defaultNodeRegistry, link, DEFAULT_LATTICE);
|
|
148
|
+
expect([single?.source.side, single?.target.side]).toEqual(['s', 'e']);
|
|
149
|
+
});
|
|
101
150
|
});
|
package/src/utils/route.ts
CHANGED
|
@@ -25,9 +25,17 @@ import {
|
|
|
25
25
|
type Side,
|
|
26
26
|
isPointEndpoint,
|
|
27
27
|
} from '../model/types.ts';
|
|
28
|
-
import { gutterRoute } from './gutter-route.ts';
|
|
28
|
+
import { gutterRoute, gutterRouteWithCost } from './gutter-route.ts';
|
|
29
29
|
import { type LatticeSpec } from './lattice.ts';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
type PortTerminal,
|
|
32
|
+
nearestPort,
|
|
33
|
+
nodePorts,
|
|
34
|
+
pairPorts,
|
|
35
|
+
portCandidates,
|
|
36
|
+
portPoint,
|
|
37
|
+
sideNormal,
|
|
38
|
+
} from './ports.ts';
|
|
31
39
|
import { nodeBounds } from './shapes.ts';
|
|
32
40
|
|
|
33
41
|
const MIN_TANGENT = 40;
|
|
@@ -42,7 +50,11 @@ const CORNER_RADIUS = DEFAULT_GRID;
|
|
|
42
50
|
/** How far a smart link's stub leaves its port, in scene px (half a major cell). */
|
|
43
51
|
const SMART_STUB = MAJOR_GRID / 2;
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
/**
|
|
54
|
+
* A resolved link end. `free` marks a point end (one being dragged, or left on empty canvas): it has no port to
|
|
55
|
+
* leave along, so a route runs straight to it rather than turning to its side's normal.
|
|
56
|
+
*/
|
|
57
|
+
export type RouteEnd = { point: Point; side: Side; free?: boolean };
|
|
46
58
|
|
|
47
59
|
const pt = ({ x, y }: Point) => `${x} ${y}`;
|
|
48
60
|
|
|
@@ -52,12 +64,12 @@ const curveControls = (from: RouteEnd, to: RouteEnd): [Point, Point] => {
|
|
|
52
64
|
const dx = to.point.x - from.point.x;
|
|
53
65
|
const dy = to.point.y - from.point.y;
|
|
54
66
|
const tangent = Math.max(MIN_TANGENT, Math.hypot(dx, dy) * TANGENT_RATIO);
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
{ x:
|
|
59
|
-
|
|
60
|
-
];
|
|
67
|
+
// A free end takes no tangent: its control point sits on the end, so the curve arrives straight at it.
|
|
68
|
+
const control = (end: RouteEnd): Point => {
|
|
69
|
+
const normal = sideNormal(end.side);
|
|
70
|
+
return end.free ? end.point : { x: end.point.x + normal.x * tangent, y: end.point.y + normal.y * tangent };
|
|
71
|
+
};
|
|
72
|
+
return [control(from), control(to)];
|
|
61
73
|
};
|
|
62
74
|
|
|
63
75
|
/** Cubic Bézier whose control points sit `tangent` px out along each port's normal. */
|
|
@@ -122,7 +134,14 @@ export const linkPath = (link: Link, from: RouteEnd, to: RouteEnd, lattice?: Lat
|
|
|
122
134
|
case 'spline':
|
|
123
135
|
return splinePath([from.point, ...link.points, to.point]);
|
|
124
136
|
case 'smart':
|
|
125
|
-
|
|
137
|
+
// A free end (one being dragged) takes no gutter: the route runs straight to it.
|
|
138
|
+
return splinePath(
|
|
139
|
+
(lattice && !from.free && !to.free && gutterRoute(lattice.nodes, lattice.spec, from, to)) ||
|
|
140
|
+
smartPoints(from, to),
|
|
141
|
+
);
|
|
142
|
+
default:
|
|
143
|
+
// A link type this engine does not know (saved by a newer one) draws as the core base: a straight line.
|
|
144
|
+
return linePath(from.point, to.point);
|
|
126
145
|
}
|
|
127
146
|
};
|
|
128
147
|
|
|
@@ -135,14 +154,12 @@ export type LatticeRoute = { spec: LatticeSpec; nodes: readonly Node[] };
|
|
|
135
154
|
* ports imply rather than a straight line cutting across their own nodes.
|
|
136
155
|
*/
|
|
137
156
|
export const smartPoints = (from: RouteEnd, to: RouteEnd): Point[] => {
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
to.point,
|
|
145
|
-
];
|
|
157
|
+
// A free end has no stub: the route runs straight to it.
|
|
158
|
+
const stub = (end: RouteEnd): Point[] => {
|
|
159
|
+
const normal = sideNormal(end.side);
|
|
160
|
+
return end.free ? [] : [{ x: end.point.x + normal.x * SMART_STUB, y: end.point.y + normal.y * SMART_STUB }];
|
|
161
|
+
};
|
|
162
|
+
return [from.point, ...stub(from), ...stub(to).reverse(), to.point];
|
|
146
163
|
};
|
|
147
164
|
|
|
148
165
|
export type LinkGeometry = { link: Link; path: string; source: RouteEnd; target: RouteEnd };
|
|
@@ -165,6 +182,90 @@ const routeEnd = (terminal: PortTerminal, port: Port): RouteEnd => ({
|
|
|
165
182
|
side: port.side,
|
|
166
183
|
});
|
|
167
184
|
|
|
185
|
+
/** Port pairs searched per tier of fewest possible bends. */
|
|
186
|
+
const PAIRS_PER_TIER = 3;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The fewest bends any right-angled route between two ports could take: none when they face each other on one line,
|
|
190
|
+
* one when their sides are perpendicular and each points toward the corner they share, otherwise at least two.
|
|
191
|
+
*/
|
|
192
|
+
const leastBends = (from: RouteEnd, to: RouteEnd): number => {
|
|
193
|
+
const out = sideNormal(from.side);
|
|
194
|
+
// A route arrives at its target moving against the target side's normal.
|
|
195
|
+
const arrive = sideNormal(to.side);
|
|
196
|
+
const [dx, dy] = [to.point.x - from.point.x, to.point.y - from.point.y];
|
|
197
|
+
const ahead = (normal: Point, x: number, y: number) => normal.x * x + normal.y * y > 0;
|
|
198
|
+
if (out.x === -arrive.x && out.y === -arrive.y) {
|
|
199
|
+
const aligned = out.x !== 0 ? dy === 0 : dx === 0;
|
|
200
|
+
return aligned && ahead(out, dx, dy) ? 0 : 2;
|
|
201
|
+
}
|
|
202
|
+
if (out.x * arrive.x + out.y * arrive.y === 0) {
|
|
203
|
+
// The corner is where the source's line meets the target's.
|
|
204
|
+
const corner = out.x !== 0 ? { x: to.point.x, y: from.point.y } : { x: from.point.x, y: to.point.y };
|
|
205
|
+
const first = ahead(out, corner.x - from.point.x, corner.y - from.point.y);
|
|
206
|
+
const second = ahead({ x: -arrive.x, y: -arrive.y }, to.point.x - corner.x, to.point.y - corner.y);
|
|
207
|
+
return first && second ? 1 : 2;
|
|
208
|
+
}
|
|
209
|
+
return 2;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The ends and gutter route of a smart link between two nodes on a lattice: of the pairs of side-centre ports (or the
|
|
214
|
+
* pinned ones), the one whose route is cheapest, fewest bends first, so a link leaves straight down a free column
|
|
215
|
+
* rather than from the nearest port round a corner. Pairs are tried nearest first, so a tie keeps the nearest.
|
|
216
|
+
*/
|
|
217
|
+
const latticeEnds = (
|
|
218
|
+
scene: Scene,
|
|
219
|
+
registry: NodeRegistry,
|
|
220
|
+
link: Link,
|
|
221
|
+
route: LatticeRoute,
|
|
222
|
+
): { from: RouteEnd; to: RouteEnd; points: Point[] } | undefined => {
|
|
223
|
+
if (link.type !== 'smart' || isPointEndpoint(link.source) || isPointEndpoint(link.target)) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const source = terminalOf(scene, registry, link.source);
|
|
227
|
+
const target = terminalOf(scene, registry, link.target);
|
|
228
|
+
if (!source || !target) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
// A side's centre, where a side has one; a pinned port or a custom layout keeps what it offers.
|
|
232
|
+
const centres = (ports: readonly Port[]) => {
|
|
233
|
+
const middle = ports.filter((port) => port.offset === 0.5);
|
|
234
|
+
return middle.length > 0 ? middle : ports;
|
|
235
|
+
};
|
|
236
|
+
const sources = centres(portCandidates(source, 'out'));
|
|
237
|
+
const targets = centres(portCandidates(target, 'in'));
|
|
238
|
+
// Fewest possible bends first, then nearest: the search stops once no pair left could bend less than the best found.
|
|
239
|
+
const pairs = sources
|
|
240
|
+
.flatMap((from) => targets.map((to) => ({ from: routeEnd(source, from), to: routeEnd(target, to) })))
|
|
241
|
+
.map((pair) => ({
|
|
242
|
+
...pair,
|
|
243
|
+
least: leastBends(pair.from, pair.to),
|
|
244
|
+
distance: Math.hypot(pair.from.point.x - pair.to.point.x, pair.from.point.y - pair.to.point.y),
|
|
245
|
+
}))
|
|
246
|
+
.sort((left, right) => left.least - right.least || left.distance - right.distance);
|
|
247
|
+
let best: { from: RouteEnd; to: RouteEnd; points: Point[]; bends: number } | undefined;
|
|
248
|
+
// Within a tier only the nearest few are searched: a farther pair seldom routes better, and each search has a cost.
|
|
249
|
+
const tried = new Map<number, number>();
|
|
250
|
+
for (const pair of pairs) {
|
|
251
|
+
if (best && pair.least >= best.bends) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
const count = tried.get(pair.least) ?? 0;
|
|
255
|
+
if (count >= PAIRS_PER_TIER) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
tried.set(pair.least, count + 1);
|
|
259
|
+
const routed = gutterRouteWithCost(route.nodes, route.spec, pair.from, pair.to);
|
|
260
|
+
// The route's points are its ends and its corners.
|
|
261
|
+
const bends = routed ? routed.points.length - 2 : Infinity;
|
|
262
|
+
if (routed && (!best || bends < best.bends)) {
|
|
263
|
+
best = { from: pair.from, to: pair.to, points: routed.points, bends };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return best;
|
|
267
|
+
};
|
|
268
|
+
|
|
168
269
|
/**
|
|
169
270
|
* Resolve a link's ends and route it by its type: two node ends take the automatic (or pinned) port pair,
|
|
170
271
|
* a node end facing a free point takes the port nearest that point, and two free points face each other.
|
|
@@ -175,12 +276,16 @@ export const linkGeometry = (
|
|
|
175
276
|
link: Link,
|
|
176
277
|
lattice?: LatticeSpec,
|
|
177
278
|
): LinkGeometry | undefined => {
|
|
279
|
+
const route = lattice ? { spec: lattice, nodes: Object.values(scene.nodes) } : undefined;
|
|
280
|
+
const routed = route && latticeEnds(scene, registry, link, route);
|
|
281
|
+
if (routed) {
|
|
282
|
+
return { link, path: splinePath(routed.points), source: routed.from, target: routed.to };
|
|
283
|
+
}
|
|
178
284
|
const ends = resolveEnds(scene, registry, link.source, link.target);
|
|
179
285
|
if (!ends) {
|
|
180
286
|
return undefined;
|
|
181
287
|
}
|
|
182
288
|
const [from, to] = ends;
|
|
183
|
-
const route = lattice ? { spec: lattice, nodes: Object.values(scene.nodes) } : undefined;
|
|
184
289
|
return { link, path: linkPath(link, from, to, route), source: from, target: to };
|
|
185
290
|
};
|
|
186
291
|
|
|
@@ -193,8 +298,8 @@ const resolveEnds = (
|
|
|
193
298
|
if (isPointEndpoint(source)) {
|
|
194
299
|
if (isPointEndpoint(target)) {
|
|
195
300
|
return [
|
|
196
|
-
{ point: source.point, side: sideToward(source.point, target.point) },
|
|
197
|
-
{ point: target.point, side: sideToward(target.point, source.point) },
|
|
301
|
+
{ point: source.point, side: sideToward(source.point, target.point), free: true },
|
|
302
|
+
{ point: target.point, side: sideToward(target.point, source.point), free: true },
|
|
198
303
|
];
|
|
199
304
|
}
|
|
200
305
|
const terminal = terminalOf(scene, registry, target);
|
|
@@ -203,7 +308,7 @@ const resolveEnds = (
|
|
|
203
308
|
return undefined;
|
|
204
309
|
}
|
|
205
310
|
const to = routeEnd(terminal, port);
|
|
206
|
-
return [{ point: source.point, side: sideToward(source.point, to.point) }, to];
|
|
311
|
+
return [{ point: source.point, side: sideToward(source.point, to.point), free: true }, to];
|
|
207
312
|
}
|
|
208
313
|
if (isPointEndpoint(target)) {
|
|
209
314
|
const terminal = terminalOf(scene, registry, source);
|
|
@@ -212,7 +317,7 @@ const resolveEnds = (
|
|
|
212
317
|
return undefined;
|
|
213
318
|
}
|
|
214
319
|
const from = routeEnd(terminal, port);
|
|
215
|
-
return [from, { point: target.point, side: sideToward(target.point, from.point) }];
|
|
320
|
+
return [from, { point: target.point, side: sideToward(target.point, from.point), free: true }];
|
|
216
321
|
}
|
|
217
322
|
const sourceTerminal = terminalOf(scene, registry, source);
|
|
218
323
|
const targetTerminal = terminalOf(scene, registry, target);
|
|
@@ -249,7 +354,12 @@ export const sceneLinkGeometry = (
|
|
|
249
354
|
const node = isPointEndpoint(end) ? undefined : scene.nodes[end.node];
|
|
250
355
|
return node ? rect(nodeBounds(node)) : undefined;
|
|
251
356
|
};
|
|
357
|
+
const route = { spec: lattice, nodes };
|
|
252
358
|
const routed = links.flatMap((link) => {
|
|
359
|
+
const chosen = latticeEnds(scene, registry, link, route);
|
|
360
|
+
if (chosen) {
|
|
361
|
+
return [{ link, from: chosen.from, to: chosen.to, points: chosen.points }];
|
|
362
|
+
}
|
|
253
363
|
const ends = resolveEnds(scene, registry, link.source, link.target);
|
|
254
364
|
if (!ends) {
|
|
255
365
|
return [];
|