@reekon-tools/boldr-cad 0.1.0 → 1.1.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 +99 -16
- package/dist/app/edit.d.ts +12 -1
- package/dist/app/edit.js +96 -11
- package/dist/app/edit.js.map +1 -1
- package/dist/app/state.d.ts +31 -4
- package/dist/app/state.js +31 -2
- package/dist/app/state.js.map +1 -1
- package/dist/data/memory.d.ts +9 -1
- package/dist/data/memory.js +13 -8
- package/dist/data/memory.js.map +1 -1
- package/dist/data/provider.d.ts +30 -10
- package/dist/data/sync.d.ts +14 -3
- package/dist/data/sync.js +133 -66
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.d.ts +1 -2
- package/dist/editor/actions.js +391 -113
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/confirm.d.ts +23 -0
- package/dist/editor/confirm.js +35 -0
- package/dist/editor/confirm.js.map +1 -0
- package/dist/editor/elevation.d.ts +191 -90
- package/dist/editor/elevation.js +812 -616
- package/dist/editor/elevation.js.map +1 -1
- package/dist/editor/face.d.ts +73 -0
- package/dist/editor/face.js +269 -0
- package/dist/editor/face.js.map +1 -0
- package/dist/editor/files.d.ts +6 -0
- package/dist/editor/files.js +50 -4
- package/dist/editor/files.js.map +1 -1
- package/dist/editor/gestures.d.ts +18 -3
- package/dist/editor/gestures.js +145 -30
- package/dist/editor/gestures.js.map +1 -1
- package/dist/editor/keypad.d.ts +20 -5
- package/dist/editor/keypad.js +62 -15
- package/dist/editor/keypad.js.map +1 -1
- package/dist/editor/keys.js +36 -4
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/laser.d.ts +20 -4
- package/dist/editor/laser.js +104 -21
- package/dist/editor/laser.js.map +1 -1
- package/dist/editor/linkActions.d.ts +49 -0
- package/dist/editor/linkActions.js +210 -0
- package/dist/editor/linkActions.js.map +1 -0
- package/dist/editor/linkFollow.d.ts +2 -0
- package/dist/editor/linkFollow.js +53 -0
- package/dist/editor/linkFollow.js.map +1 -0
- package/dist/editor/linkWrite.d.ts +13 -0
- package/dist/editor/linkWrite.js +84 -0
- package/dist/editor/linkWrite.js.map +1 -0
- package/dist/editor/links.d.ts +84 -0
- package/dist/editor/links.js +130 -0
- package/dist/editor/links.js.map +1 -0
- package/dist/editor/markup.d.ts +29 -3
- package/dist/editor/markup.js +95 -10
- package/dist/editor/markup.js.map +1 -1
- package/dist/editor/mode.d.ts +40 -2
- package/dist/editor/mode.js +71 -4
- package/dist/editor/mode.js.map +1 -1
- package/dist/editor/ports.d.ts +38 -6
- package/dist/editor/ports.js +10 -1
- package/dist/editor/ports.js.map +1 -1
- package/dist/editor/project.d.ts +38 -6
- package/dist/editor/project.js +170 -27
- package/dist/editor/project.js.map +1 -1
- package/dist/editor/selection.d.ts +10 -0
- package/dist/editor/selection.js +62 -8
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/status.js +3 -0
- package/dist/editor/status.js.map +1 -1
- package/dist/editor/tooltips.d.ts +5 -0
- package/dist/editor/tooltips.js +48 -0
- package/dist/editor/tooltips.js.map +1 -0
- package/dist/editor/view3d.d.ts +17 -0
- package/dist/editor/view3d.js +135 -0
- package/dist/editor/view3d.js.map +1 -0
- package/dist/editor/view3dState.d.ts +39 -0
- package/dist/editor/view3dState.js +44 -0
- package/dist/editor/view3dState.js.map +1 -0
- package/dist/export/dxf.js +1 -1
- package/dist/export/dxf.js.map +1 -1
- package/dist/export/pdf.js +17 -12
- package/dist/export/pdf.js.map +1 -1
- package/dist/export/plan.d.ts +23 -6
- package/dist/export/plan.js +127 -44
- package/dist/export/plan.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model/constraints.js +15 -4
- package/dist/model/constraints.js.map +1 -1
- package/dist/model/demo.js +3 -3
- package/dist/model/demo.js.map +1 -1
- package/dist/model/elevation.d.ts +6 -9
- package/dist/model/elevation.js +5 -13
- package/dist/model/elevation.js.map +1 -1
- package/dist/model/faces.d.ts +58 -0
- package/dist/model/faces.js +197 -0
- package/dist/model/faces.js.map +1 -0
- package/dist/model/facesolve.d.ts +72 -0
- package/dist/model/facesolve.js +369 -0
- package/dist/model/facesolve.js.map +1 -0
- package/dist/model/history.d.ts +3 -2
- package/dist/model/history.js +17 -11
- package/dist/model/history.js.map +1 -1
- package/dist/model/images.d.ts +18 -0
- package/dist/model/images.js +26 -0
- package/dist/model/images.js.map +1 -0
- package/dist/model/links.d.ts +46 -0
- package/dist/model/links.js +73 -0
- package/dist/model/links.js.map +1 -0
- package/dist/model/markup.d.ts +46 -0
- package/dist/model/markup.js +71 -0
- package/dist/model/markup.js.map +1 -0
- package/dist/model/measurement.d.ts +11 -0
- package/dist/model/measurement.js +24 -0
- package/dist/model/measurement.js.map +1 -0
- package/dist/model/openings.d.ts +4 -0
- package/dist/model/openings.js +4 -0
- package/dist/model/openings.js.map +1 -1
- package/dist/model/planGeom.d.ts +72 -2
- package/dist/model/planGeom.js +184 -10
- package/dist/model/planGeom.js.map +1 -1
- package/dist/model/project.js +3 -2
- package/dist/model/project.js.map +1 -1
- package/dist/model/projectfile.js +2 -1
- package/dist/model/projectfile.js.map +1 -1
- package/dist/model/rooms.js +2 -2
- package/dist/model/rooms.js.map +1 -1
- package/dist/model/settings.d.ts +6 -0
- package/dist/model/settings.js +2 -2
- package/dist/model/settings.js.map +1 -1
- package/dist/model/sketch.d.ts +14 -0
- package/dist/model/sketch.js +143 -24
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/smooth.d.ts +6 -0
- package/dist/model/smooth.js +52 -0
- package/dist/model/smooth.js.map +1 -0
- package/dist/model/store.d.ts +12 -0
- package/dist/model/store.js +35 -3
- package/dist/model/store.js.map +1 -1
- package/dist/model/types.d.ts +173 -8
- package/dist/model/wallfaces.d.ts +32 -0
- package/dist/model/wallfaces.js +133 -20
- package/dist/model/wallfaces.js.map +1 -1
- package/dist/model/wallshape.d.ts +15 -0
- package/dist/model/wallshape.js +89 -0
- package/dist/model/wallshape.js.map +1 -0
- package/dist/react/BottomBar.d.ts +9 -5
- package/dist/react/BottomBar.js +213 -24
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/Buttons.d.ts +13 -2
- package/dist/react/Buttons.js +37 -5
- package/dist/react/Buttons.js.map +1 -1
- package/dist/react/CadEditor.d.ts +54 -4
- package/dist/react/CadEditor.js +79 -21
- package/dist/react/CadEditor.js.map +1 -1
- package/dist/react/ConfirmSheet.d.ts +4 -0
- package/dist/react/ConfirmSheet.js +31 -0
- package/dist/react/ConfirmSheet.js.map +1 -0
- package/dist/react/ConflictSheet.d.ts +2 -1
- package/dist/react/ConflictSheet.js +16 -8
- package/dist/react/ConflictSheet.js.map +1 -1
- package/dist/react/Drawer.d.ts +8 -2
- package/dist/react/Drawer.js +167 -38
- package/dist/react/Drawer.js.map +1 -1
- package/dist/react/Duck.d.ts +13 -0
- package/dist/react/Duck.js +45 -0
- package/dist/react/Duck.js.map +1 -0
- package/dist/react/Empty3D.d.ts +2 -0
- package/dist/react/Empty3D.js +39 -0
- package/dist/react/Empty3D.js.map +1 -0
- package/dist/react/FitScroll.d.ts +6 -0
- package/dist/react/FitScroll.js +14 -0
- package/dist/react/FitScroll.js.map +1 -0
- package/dist/react/Icon.native.d.ts +6 -0
- package/dist/react/Icon.native.js +4 -0
- package/dist/react/Icon.native.js.map +1 -1
- package/dist/react/Icon.web.d.ts +6 -0
- package/dist/react/Icon.web.js +4 -0
- package/dist/react/Icon.web.js.map +1 -1
- package/dist/react/KeypadSheet.d.ts +9 -2
- package/dist/react/KeypadSheet.js +174 -42
- package/dist/react/KeypadSheet.js.map +1 -1
- package/dist/react/LaserTray.js +8 -5
- package/dist/react/LaserTray.js.map +1 -1
- package/dist/react/MarkupPanel.d.ts +8 -0
- package/dist/react/MarkupPanel.js +67 -0
- package/dist/react/MarkupPanel.js.map +1 -0
- package/dist/react/PageThumb.native.d.ts +7 -2
- package/dist/react/PageThumb.native.js +19 -7
- package/dist/react/PageThumb.native.js.map +1 -1
- package/dist/react/PageThumb.web.d.ts +3 -2
- package/dist/react/PageThumb.web.js +4 -4
- package/dist/react/PageThumb.web.js.map +1 -1
- package/dist/react/PlanCanvas.native.d.ts +1 -1
- package/dist/react/PlanCanvas.native.js +134 -36
- package/dist/react/PlanCanvas.native.js.map +1 -1
- package/dist/react/PlanCanvas.web.js +16 -5
- package/dist/react/PlanCanvas.web.js.map +1 -1
- package/dist/react/Popovers.d.ts +12 -6
- package/dist/react/Popovers.js +53 -26
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/Rail.d.ts +24 -0
- package/dist/react/Rail.js +324 -34
- package/dist/react/Rail.js.map +1 -1
- package/dist/react/ShapeMenu.d.ts +7 -0
- package/dist/react/ShapeMenu.js +58 -0
- package/dist/react/ShapeMenu.js.map +1 -0
- package/dist/react/Sheet.d.ts +14 -4
- package/dist/react/Sheet.js +54 -9
- package/dist/react/Sheet.js.map +1 -1
- package/dist/react/Slider.d.ts +18 -0
- package/dist/react/Slider.js +27 -0
- package/dist/react/Slider.js.map +1 -0
- package/dist/react/Toast.js +14 -6
- package/dist/react/Toast.js.map +1 -1
- package/dist/react/ToolTip.d.ts +7 -0
- package/dist/react/ToolTip.js +43 -0
- package/dist/react/ToolTip.js.map +1 -0
- package/dist/react/TopBar.d.ts +6 -3
- package/dist/react/TopBar.js +48 -17
- package/dist/react/TopBar.js.map +1 -1
- package/dist/react/Tween.d.ts +7 -0
- package/dist/react/Tween.js +26 -0
- package/dist/react/Tween.js.map +1 -0
- package/dist/react/Tween.native.d.ts +6 -0
- package/dist/react/Tween.native.js +43 -0
- package/dist/react/Tween.native.js.map +1 -0
- package/dist/react/View3D.native.d.ts +4 -0
- package/dist/react/View3D.native.js +103 -0
- package/dist/react/View3D.native.js.map +1 -0
- package/dist/react/View3D.web.d.ts +4 -0
- package/dist/react/View3D.web.js +151 -0
- package/dist/react/View3D.web.js.map +1 -0
- package/dist/react/View3DHost.d.ts +143 -0
- package/dist/react/View3DHost.js +377 -0
- package/dist/react/View3DHost.js.map +1 -0
- package/dist/react/corners.d.ts +7 -0
- package/dist/react/corners.js +51 -0
- package/dist/react/corners.js.map +1 -0
- package/dist/react/easing.d.ts +15 -0
- package/dist/react/easing.js +10 -0
- package/dist/react/easing.js.map +1 -0
- package/dist/react/host.d.ts +7 -2
- package/dist/react/host.js +8 -3
- package/dist/react/host.js.map +1 -1
- package/dist/react/hostKeypad.d.ts +56 -0
- package/dist/react/hostKeypad.js +5 -0
- package/dist/react/hostKeypad.js.map +1 -0
- package/dist/react/motion.d.ts +77 -0
- package/dist/react/motion.js +173 -0
- package/dist/react/motion.js.map +1 -0
- package/dist/react/ports.js +90 -8
- package/dist/react/ports.js.map +1 -1
- package/dist/react/saving.d.ts +15 -0
- package/dist/react/saving.js +76 -0
- package/dist/react/saving.js.map +1 -0
- package/dist/react/store.d.ts +38 -1
- package/dist/react/store.js +31 -1
- package/dist/react/store.js.map +1 -1
- package/dist/react/theme.d.ts +10 -0
- package/dist/react/theme.js +8 -0
- package/dist/react/theme.js.map +1 -1
- package/dist/react/topRow.d.ts +22 -0
- package/dist/react/topRow.js +61 -0
- package/dist/react/topRow.js.map +1 -0
- package/dist/react/touch.d.ts +6 -0
- package/dist/react/touch.js +9 -0
- package/dist/react/touch.js.map +1 -0
- package/dist/render/ctx.d.ts +8 -0
- package/dist/render/draw.d.ts +33 -1
- package/dist/render/draw.js +593 -215
- package/dist/render/draw.js.map +1 -1
- package/dist/render/elevation.d.ts +65 -43
- package/dist/render/elevation.js +414 -140
- package/dist/render/elevation.js.map +1 -1
- package/dist/render/figmaIcons.d.ts +12 -0
- package/dist/render/figmaIcons.js +21 -0
- package/dist/render/figmaIcons.js.map +1 -0
- package/dist/render/hit.d.ts +22 -2
- package/dist/render/hit.js +168 -25
- package/dist/render/hit.js.map +1 -1
- package/dist/render/icons.js +69 -10
- package/dist/render/icons.js.map +1 -1
- package/dist/render/linkTile.d.ts +15 -0
- package/dist/render/linkTile.js +369 -0
- package/dist/render/linkTile.js.map +1 -0
- package/dist/render/markup.d.ts +11 -0
- package/dist/render/markup.js +112 -0
- package/dist/render/markup.js.map +1 -0
- package/dist/render/navigate.d.ts +88 -0
- package/dist/render/navigate.js +100 -0
- package/dist/render/navigate.js.map +1 -0
- package/dist/render/snap.d.ts +20 -1
- package/dist/render/snap.js +118 -25
- package/dist/render/snap.js.map +1 -1
- package/dist/render/squircle.d.ts +8 -0
- package/dist/render/squircle.js +71 -0
- package/dist/render/squircle.js.map +1 -0
- package/dist/render/view.d.ts +115 -2
- package/dist/render/view.js +244 -19
- package/dist/render/view.js.map +1 -1
- package/dist/skia/SkiaDrawCtx.d.ts +30 -2
- package/dist/skia/SkiaDrawCtx.js +198 -43
- package/dist/skia/SkiaDrawCtx.js.map +1 -1
- package/dist/skia/host.d.ts +18 -2
- package/dist/skia/host.js +46 -1
- package/dist/skia/host.js.map +1 -1
- package/dist/solver/equations.js +37 -2
- package/dist/solver/equations.js.map +1 -1
- package/dist/solver/gauss-newton.d.ts +17 -0
- package/dist/solver/gauss-newton.js +144 -36
- package/dist/solver/gauss-newton.js.map +1 -1
- package/dist/solver/linalg.d.ts +45 -2
- package/dist/solver/linalg.js +164 -10
- package/dist/solver/linalg.js.map +1 -1
- package/dist/solver/types.d.ts +5 -0
- package/dist/tools/annotate.d.ts +57 -1
- package/dist/tools/annotate.js +64 -43
- package/dist/tools/annotate.js.map +1 -1
- package/dist/tools/chamfer.d.ts +12 -0
- package/dist/tools/chamfer.js +114 -0
- package/dist/tools/chamfer.js.map +1 -0
- package/dist/tools/corner.d.ts +38 -0
- package/dist/tools/corner.js +99 -0
- package/dist/tools/corner.js.map +1 -0
- package/dist/tools/dim.d.ts +86 -9
- package/dist/tools/dim.js +582 -97
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/elevation.js +5 -5
- package/dist/tools/elevation.js.map +1 -1
- package/dist/tools/fillet.d.ts +9 -0
- package/dist/tools/fillet.js +86 -0
- package/dist/tools/fillet.js.map +1 -0
- package/dist/tools/freehand.d.ts +3 -2
- package/dist/tools/freehand.js +19 -12
- package/dist/tools/freehand.js.map +1 -1
- package/dist/tools/index.js +14 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/line.d.ts +8 -3
- package/dist/tools/line.js +66 -30
- package/dist/tools/line.js.map +1 -1
- package/dist/tools/modify.d.ts +13 -0
- package/dist/tools/modify.js +105 -25
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/openings.js +25 -18
- package/dist/tools/openings.js.map +1 -1
- package/dist/tools/registry.d.ts +8 -0
- package/dist/tools/registry.js +42 -2
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +77 -3
- package/dist/tools/select.js +560 -74
- package/dist/tools/select.js.map +1 -1
- package/dist/tools/shapes.d.ts +14 -0
- package/dist/tools/shapes.js +154 -75
- package/dist/tools/shapes.js.map +1 -1
- package/dist/units/format.d.ts +11 -3
- package/dist/units/format.js +27 -6
- package/dist/units/format.js.map +1 -1
- package/dist/units/parse.d.ts +2 -1
- package/dist/units/parse.js +5 -4
- package/dist/units/parse.js.map +1 -1
- package/dist/units/prefs.d.ts +9 -1
- package/dist/units/prefs.js +6 -0
- package/dist/units/prefs.js.map +1 -1
- package/dist/view3d/build.d.ts +35 -0
- package/dist/view3d/build.js +630 -0
- package/dist/view3d/build.js.map +1 -0
- package/dist/view3d/camera.d.ts +60 -0
- package/dist/view3d/camera.js +84 -0
- package/dist/view3d/camera.js.map +1 -0
- package/dist/view3d/fatLines.d.ts +11 -0
- package/dist/view3d/fatLines.js +83 -0
- package/dist/view3d/fatLines.js.map +1 -0
- package/dist/view3d/lines.d.ts +17 -0
- package/dist/view3d/lines.js +271 -0
- package/dist/view3d/lines.js.map +1 -0
- package/dist/view3d/mesh.d.ts +51 -0
- package/dist/view3d/mesh.js +101 -0
- package/dist/view3d/mesh.js.map +1 -0
- package/dist/view3d/poly.d.ts +16 -0
- package/dist/view3d/poly.js +109 -0
- package/dist/view3d/poly.js.map +1 -0
- package/dist/view3d/viewer.d.ts +86 -0
- package/dist/view3d/viewer.js +350 -0
- package/dist/view3d/viewer.js.map +1 -0
- package/package.json +15 -2
- package/src/app/edit.ts +83 -12
- package/src/app/main.ts +11 -5
- package/src/app/state.ts +38 -5
- package/src/data/memory.ts +15 -10
- package/src/data/provider.ts +25 -10
- package/src/data/sync.ts +107 -53
- package/src/editor/actions.ts +292 -83
- package/src/editor/confirm.ts +58 -0
- package/src/editor/elevation.ts +698 -478
- package/src/editor/face.ts +227 -0
- package/src/editor/files.ts +40 -4
- package/src/editor/gestures.ts +147 -31
- package/src/editor/keypad.ts +67 -18
- package/src/editor/keys.ts +25 -5
- package/src/editor/laser.ts +98 -20
- package/src/editor/linkActions.ts +194 -0
- package/src/editor/linkFollow.ts +43 -0
- package/src/editor/linkWrite.ts +77 -0
- package/src/editor/links.ts +181 -0
- package/src/editor/markup.ts +92 -8
- package/src/editor/mode.ts +83 -7
- package/src/editor/ports.ts +39 -6
- package/src/editor/project.ts +157 -26
- package/src/editor/selection.ts +54 -8
- package/src/editor/status.ts +2 -0
- package/src/editor/tooltips.ts +47 -0
- package/src/editor/view3d.ts +119 -0
- package/src/editor/view3dState.ts +61 -0
- package/src/export/dxf.ts +1 -1
- package/src/export/pdf.ts +15 -7
- package/src/export/plan.ts +119 -35
- package/src/index.ts +7 -1
- package/src/input/keyboard.ts +2 -4
- package/src/model/constraints.ts +13 -4
- package/src/model/demo.ts +3 -3
- package/src/model/elevation.ts +6 -16
- package/src/model/faces.ts +153 -0
- package/src/model/facesolve.ts +322 -0
- package/src/model/history.ts +19 -11
- package/src/model/images.ts +35 -0
- package/src/model/links.ts +77 -0
- package/src/model/markup.ts +80 -0
- package/src/model/measurement.ts +26 -0
- package/src/model/openings.ts +6 -0
- package/src/model/planGeom.ts +174 -8
- package/src/model/project.ts +3 -2
- package/src/model/projectfile.ts +2 -1
- package/src/model/rooms.ts +1 -1
- package/src/model/settings.ts +8 -2
- package/src/model/sketch.ts +101 -14
- package/src/model/smooth.ts +51 -0
- package/src/model/store.ts +35 -4
- package/src/model/types.ts +170 -8
- package/src/model/wallfaces.ts +126 -17
- package/src/model/wallshape.ts +97 -0
- package/src/react/BottomBar.tsx +263 -46
- package/src/react/Buttons.tsx +50 -9
- package/src/react/CadEditor.tsx +142 -34
- package/src/react/ConfirmSheet.tsx +38 -0
- package/src/react/ConflictSheet.tsx +19 -12
- package/src/react/Drawer.tsx +213 -110
- package/src/react/Duck.tsx +50 -0
- package/src/react/Empty3D.tsx +32 -0
- package/src/react/FitScroll.tsx +19 -0
- package/src/react/Icon.native.tsx +5 -0
- package/src/react/Icon.web.tsx +5 -0
- package/src/react/KeypadSheet.tsx +253 -60
- package/src/react/LaserTray.tsx +12 -8
- package/src/react/MarkupPanel.tsx +150 -0
- package/src/react/PageThumb.native.tsx +18 -7
- package/src/react/PageThumb.web.tsx +4 -4
- package/src/react/PlanCanvas.native.tsx +151 -34
- package/src/react/PlanCanvas.web.tsx +15 -5
- package/src/react/Popovers.tsx +116 -51
- package/src/react/Rail.tsx +360 -27
- package/src/react/ShapeMenu.tsx +66 -0
- package/src/react/Sheet.tsx +56 -11
- package/src/react/Slider.tsx +48 -0
- package/src/react/Toast.tsx +16 -9
- package/src/react/ToolTip.tsx +49 -0
- package/src/react/TopBar.tsx +97 -39
- package/src/react/Tween.native.tsx +36 -0
- package/src/react/Tween.tsx +24 -0
- package/src/react/View3D.native.tsx +131 -0
- package/src/react/View3D.web.tsx +131 -0
- package/src/react/View3DHost.ts +367 -0
- package/src/react/corners.ts +52 -0
- package/src/react/easing.ts +14 -0
- package/src/react/host.ts +8 -3
- package/src/react/hostKeypad.ts +66 -0
- package/src/react/motion.tsx +239 -0
- package/src/react/ports.ts +78 -9
- package/src/react/saving.ts +80 -0
- package/src/react/store.ts +35 -3
- package/src/react/theme.ts +8 -0
- package/src/react/topRow.ts +71 -0
- package/src/react/touch.ts +10 -0
- package/src/render/ctx.ts +8 -0
- package/src/render/draw.ts +437 -175
- package/src/render/elevation.ts +349 -143
- package/src/render/figmaIcons.ts +22 -0
- package/src/render/hit.ts +140 -21
- package/src/render/icons.ts +69 -10
- package/src/render/linkTile.ts +297 -0
- package/src/render/markup.ts +91 -0
- package/src/render/navigate.ts +114 -0
- package/src/render/snap.ts +105 -22
- package/src/render/squircle.ts +73 -0
- package/src/render/view.ts +255 -19
- package/src/skia/SkiaDrawCtx.ts +163 -41
- package/src/skia/host.ts +46 -1
- package/src/solver/equations.ts +37 -2
- package/src/solver/gauss-newton.ts +125 -28
- package/src/solver/linalg.ts +141 -5
- package/src/solver/types.ts +5 -0
- package/src/styles.css +91 -42
- package/src/tools/annotate.ts +58 -29
- package/src/tools/chamfer.ts +103 -0
- package/src/tools/corner.ts +99 -0
- package/src/tools/dim.ts +525 -93
- package/src/tools/elevation.ts +5 -5
- package/src/tools/fillet.ts +82 -0
- package/src/tools/freehand.ts +20 -16
- package/src/tools/index.ts +17 -4
- package/src/tools/line.ts +59 -23
- package/src/tools/modify.ts +86 -24
- package/src/tools/openings.ts +11 -16
- package/src/tools/registry.ts +27 -2
- package/src/tools/select.ts +424 -75
- package/src/tools/shapes.ts +119 -58
- package/src/ui/ctx.ts +2 -1
- package/src/ui/domPorts.ts +23 -4
- package/src/ui/drawer.ts +6 -26
- package/src/ui/elevation.ts +10 -101
- package/src/ui/keypad.ts +8 -14
- package/src/ui/laser.ts +2 -2
- package/src/ui/menu.ts +63 -17
- package/src/ui/noteinput.ts +3 -3
- package/src/ui/opacity.ts +108 -0
- package/src/ui/palette.ts +67 -12
- package/src/ui/rail.ts +21 -7
- package/src/ui/topbar.ts +4 -14
- package/src/units/format.ts +24 -6
- package/src/units/parse.ts +4 -3
- package/src/units/prefs.ts +13 -1
- package/src/view3d/build.ts +607 -0
- package/src/view3d/camera.ts +113 -0
- package/src/view3d/fatLines.ts +83 -0
- package/src/view3d/lines.ts +258 -0
- package/src/view3d/mesh.ts +111 -0
- package/src/view3d/poly.ts +104 -0
- package/src/view3d/viewer.ts +348 -0
- package/dist/react/ElevCanvas.native.d.ts +0 -3
- package/dist/react/ElevCanvas.native.js +0 -58
- package/dist/react/ElevCanvas.native.js.map +0 -1
- package/dist/react/ElevCanvas.web.d.ts +0 -4
- package/dist/react/ElevCanvas.web.js +0 -44
- package/dist/react/ElevCanvas.web.js.map +0 -1
- package/dist/react/ElevationSheet.d.ts +0 -2
- package/dist/react/ElevationSheet.js +0 -38
- package/dist/react/ElevationSheet.js.map +0 -1
- package/dist/render/elevationView.d.ts +0 -3
- package/dist/render/elevationView.js +0 -100
- package/dist/render/elevationView.js.map +0 -1
- package/src/react/ElevCanvas.native.tsx +0 -68
- package/src/react/ElevCanvas.web.tsx +0 -42
- package/src/react/ElevationSheet.tsx +0 -88
- package/src/render/elevationView.ts +0 -73
package/src/model/planGeom.ts
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
import { clamp, hyp, norm2, pointInPoly } from './geom';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { clamp, hyp, norm2, normAng, pointInPoly } from './geom';
|
|
2
|
+
import { roomGeom } from './rooms';
|
|
3
|
+
import { frameMemo, roomValid } from './sketch';
|
|
4
|
+
import type { Arc, Constraint, LineKind, Sketch, XY } from './types';
|
|
5
|
+
import { faceCorner } from './wallfaces';
|
|
6
|
+
import { getUnitPrefs, isMetric, type Unit } from '../units';
|
|
4
7
|
|
|
5
8
|
/*
|
|
6
9
|
Plan geometry shared by drawing, hit testing, snapping and the tools: grid steps, where a door/window sits on
|
|
7
10
|
its wall, arc angles, and which side of a wall faces out. Pure (no view, no canvas).
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The grid at a zoom (`s` px per inch): its finest lines (`minor`, what snapping uses) and its heavier ones (`major`),
|
|
15
|
+
* in inches. It follows the zoom in the drawing's units — 1/16", 1/8" … 6", 1', 2', 5'; or 1, 2, 5, 10 mm … — as fine
|
|
16
|
+
* as keeps its lines at least GRID_MIN_PX apart: what's drawn is exactly what's snapped to, spaced so each line can be
|
|
17
|
+
* picked out with a finger.
|
|
18
|
+
*/
|
|
19
|
+
export const GRID_MIN_PX = 16;
|
|
20
|
+
const IMPERIAL = [1 / 16, 1 / 8, 1 / 4, 1 / 2, 1, 2, 6, 12, 24, 60, 120, 240, 600, 1200, 2400, 6000];
|
|
21
|
+
const IMPERIAL_MAJOR = [12, 60, 120, 600, 1200, 6000];
|
|
22
|
+
const MM = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000].map(v => v / 25.4);
|
|
23
|
+
const MM_MAJOR = [10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000].map(v => v / 25.4);
|
|
24
|
+
|
|
25
|
+
export function gridSteps(s: number, unit: Unit = getUnitPrefs().unit): { minor: number; major: number } {
|
|
26
|
+
const metric = isMetric(unit), list = metric ? MM : IMPERIAL;
|
|
27
|
+
const minor = list.find(v => v * s >= GRID_MIN_PX) || list[list.length - 1];
|
|
28
|
+
const major = (metric ? MM_MAJOR : IMPERIAL_MAJOR).find(v => v >= minor * 4 - 1e-9) || minor * 5;
|
|
14
29
|
return { minor, major };
|
|
15
30
|
}
|
|
16
31
|
|
|
@@ -47,6 +62,157 @@ export function outsideSide(sk: Sketch, lid: string): number {
|
|
|
47
62
|
const l = sk.lines[lid], a = sk.points[l.a], b = sk.points[l.b];
|
|
48
63
|
const n = norm2(-(b.y - a.y), b.x - a.x);
|
|
49
64
|
const m = { x: (a.x + b.x) / 2 + n[0] * 12, y: (a.y + b.y) / 2 + n[1] * 12 };
|
|
50
|
-
|
|
65
|
+
// (asked for every door and window on every frame: the rooms' outlines, and the boxes around them, are worked out
|
|
66
|
+
// once a frame — a room the box rules out isn't looked at more closely)
|
|
67
|
+
const rooms = frameMemo(sk, 'roomOutlines', () => Object.values(sk.rooms).filter(r => roomValid(sk, r)).map(r => {
|
|
68
|
+
const poly = r.loop.map(p => sk.points[p]), xs = poly.map(p => p.x), ys = poly.map(p => p.y);
|
|
69
|
+
return { poly, x0: Math.min(...xs), x1: Math.max(...xs), y0: Math.min(...ys), y1: Math.max(...ys) };
|
|
70
|
+
}));
|
|
71
|
+
for (const r of rooms) if (m.x >= r.x0 && m.x <= r.x1 && m.y >= r.y0 && m.y <= r.y1 && pointInPoly(m, r.poly)) return -1;
|
|
51
72
|
return 1;
|
|
52
73
|
}
|
|
74
|
+
|
|
75
|
+
/** A wall's own record — a straight one's line, a curved one's circle or arc — when `id` is a wall (not a guide). */
|
|
76
|
+
export function wallOf(sk: Sketch, id: string): { kind?: LineKind; thick?: number; constr?: boolean } | null {
|
|
77
|
+
const o = sk.lines[id] ?? sk.circles[id] ?? sk.arcs[id];
|
|
78
|
+
return o && o.kind === 'wall' && !o.constr ? o : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** How thick a wall is (a curved one with none set: 4½", as it's drawn); 0 for a line. */
|
|
82
|
+
export const wallThick = (o: { kind?: LineKind; thick?: number }): number => (o.kind === 'wall' ? o.thick || 4.5 : 0);
|
|
83
|
+
|
|
84
|
+
/** A circle's or arc's centre and centreline radius; an arc's angles too (it runs counter-clockwise from a to b). */
|
|
85
|
+
export function curveFrame(sk: Sketch, id: string): { o: XY; r: number; arc?: { t0: number; t1: number } } | null {
|
|
86
|
+
const ci = sk.circles[id];
|
|
87
|
+
if (ci) { const o = sk.points[ci.c]; return o ? { o, r: ci.r } : null; }
|
|
88
|
+
const a = sk.arcs[id];
|
|
89
|
+
if (!a || !sk.points[a.c] || !sk.points[a.a] || !sk.points[a.b]) return null;
|
|
90
|
+
const g = arcAngles(sk, a);
|
|
91
|
+
return { o: g.o, r: g.r, arc: { t0: g.t0, t1: g.t1 } };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The space against one side of a wall, if any (a point just past its face there is inside it): its id. A straight
|
|
96
|
+
* wall's sides are the left (+1) and right (−1) of its A → B; a curved one's, its outer (+1) and inner (−1) faces —
|
|
97
|
+
* nothing is inside a circle (a space has corners: it can't be bounded by one).
|
|
98
|
+
*/
|
|
99
|
+
export function spaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null {
|
|
100
|
+
const l = sk.lines[id];
|
|
101
|
+
if (!l) return curveSpaceBeside(sk, id, side);
|
|
102
|
+
const a = sk.points[l.a], b = sk.points[l.b];
|
|
103
|
+
if (!a || !b) return null;
|
|
104
|
+
const n = norm2(-(b.y - a.y), b.x - a.x), k = side * ((l.kind === 'wall' ? l.thick / 2 : 0) + 1);
|
|
105
|
+
const m = { x: (a.x + b.x) / 2 + n[0] * k, y: (a.y + b.y) / 2 + n[1] * k };
|
|
106
|
+
for (const rid in sk.rooms) {
|
|
107
|
+
const r = sk.rooms[rid];
|
|
108
|
+
if (roomValid(sk, r) && pointInPoly(m, r.loop.map(p => sk.points[p]))) return rid;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function curveSpaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null {
|
|
114
|
+
const f = curveFrame(sk, id), w = sk.circles[id] ?? sk.arcs[id];
|
|
115
|
+
if (!f || !w || (side === -1 && !f.arc)) return null;
|
|
116
|
+
// (just past the face at the middle of the arc, or anywhere around a circle)
|
|
117
|
+
const at = f.arc ? f.arc.t0 + normAng(f.arc.t1 - f.arc.t0) / 2 : 0, rr = f.r + side * (wallThick(w) / 2 + 1);
|
|
118
|
+
if (rr <= 0) return null;
|
|
119
|
+
const m = { x: f.o.x + Math.cos(at) * rr, y: f.o.y + Math.sin(at) * rr };
|
|
120
|
+
// (a space's outline as it's drawn: its arcs as arcs, not chords)
|
|
121
|
+
for (const rid in sk.rooms) {
|
|
122
|
+
const r = sk.rooms[rid];
|
|
123
|
+
if (roomValid(sk, r) && pointInPoly(m, roomGeom(sk, r).poly)) return rid;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The face a wall is measured on unless one is picked (a straight wall's length, a curved one's diameter): the side a
|
|
130
|
+
* space is on — the side it was tapped on (`tapped`), when there are spaces both sides — or none (its centreline) when
|
|
131
|
+
* there's no space beside it, or it isn't a wall.
|
|
132
|
+
*/
|
|
133
|
+
export function defaultFace(sk: Sketch, id: string, tapped?: 1 | -1): 1 | -1 | undefined {
|
|
134
|
+
if (!wallOf(sk, id)) return undefined;
|
|
135
|
+
const left = spaceBeside(sk, id, 1), right = spaceBeside(sk, id, -1);
|
|
136
|
+
if (left && right) return tapped ?? 1;
|
|
137
|
+
return left ? 1 : right ? -1 : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What one face of a wall is called. A straight wall's: its space's side between two spaces, inside or outside one —
|
|
142
|
+
* or, with no space against it (or one space either side of it: a wall standing in a room; or two spaces of one name),
|
|
143
|
+
* which way it faces on the page (Top, Bottom, Left, Right). A curved wall's: Outer or Inner, whatever is beside it.
|
|
144
|
+
*/
|
|
145
|
+
export function faceName(sk: Sketch, lid: string, side: 1 | -1): string {
|
|
146
|
+
if (!sk.lines[lid]) return side === 1 ? 'Outer' : 'Inner';
|
|
147
|
+
const here = spaceBeside(sk, lid, side), there = spaceBeside(sk, lid, side === 1 ? -1 : 1);
|
|
148
|
+
if (here && there) { if (sk.rooms[here].name !== sk.rooms[there].name) return sk.rooms[here].name + ' side'; }
|
|
149
|
+
else if (here) return 'Inside';
|
|
150
|
+
else if (there) return 'Outside';
|
|
151
|
+
const l = sk.lines[lid], a = sk.points[l.a], b = sk.points[l.b];
|
|
152
|
+
// (the face's outward direction: the left of A → B for +1; y is up)
|
|
153
|
+
const nx = -(b.y - a.y) * side, ny = (b.x - a.x) * side;
|
|
154
|
+
return Math.abs(ny) >= Math.abs(nx) ? (ny > 0 ? 'Top' : 'Bottom') : nx > 0 ? 'Right' : 'Left';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Where a length dimension's two ends are: its points — or, measured on walls' faces, the faces': along a wall, its
|
|
159
|
+
* face's two corners (what the number measures); between two walls, the foot on each face. Null if it isn't a length
|
|
160
|
+
* dimension or what it measures is gone.
|
|
161
|
+
*/
|
|
162
|
+
export function dimEnds(sk: Sketch, c: Constraint): { a: XY; b: XY } | null {
|
|
163
|
+
const P = (id?: string) => (id ? sk.points[id] : undefined);
|
|
164
|
+
if (c.type === 'dist') {
|
|
165
|
+
const a = P(c.a), b = P(c.b);
|
|
166
|
+
if (!a || !b) return null;
|
|
167
|
+
const w = c.face && c.line ? sk.lines[c.line] : undefined;
|
|
168
|
+
if (w && w.kind === 'wall' && (!c.kind || c.kind === 'al')) {
|
|
169
|
+
const fa = faceCorner(sk, c.line!, c.face!, 'a'), fb = faceCorner(sk, c.line!, c.face!, 'b');
|
|
170
|
+
if (fa && fb) return c.a === w.a ? { a: fa, b: fb } : { a: fb, b: fa };
|
|
171
|
+
}
|
|
172
|
+
return { a, b };
|
|
173
|
+
}
|
|
174
|
+
if (c.type === 'pldist') {
|
|
175
|
+
const l = c.l ? sk.lines[c.l] : undefined, p = P(c.p);
|
|
176
|
+
if (!l || !p) return null;
|
|
177
|
+
const A = sk.points[l.a], B = sk.points[l.b], len2 = (B.x - A.x) ** 2 + (B.y - A.y) ** 2 || 1;
|
|
178
|
+
const t = ((p.x - A.x) * (B.x - A.x) + (p.y - A.y) * (B.y - A.y)) / len2;
|
|
179
|
+
const f = { x: A.x + (B.x - A.x) * t, y: A.y + (B.y - A.y) * t };
|
|
180
|
+
if (!c.faces) return { a: p, b: f };
|
|
181
|
+
// each end moved onto its face: toward the other for a face toward it, away for a far face
|
|
182
|
+
const d = hyp(p.x - f.x, p.y - f.y), L = Math.sqrt(len2);
|
|
183
|
+
const [ux, uy] = d > 1e-9 ? [(p.x - f.x) / d, (p.y - f.y) / d] : [-(B.y - A.y) / L, (B.x - A.x) / L];
|
|
184
|
+
const th = (id?: string) => { const w = id ? sk.lines[id] : undefined; return w && w.kind === 'wall' ? w.thick : 0; };
|
|
185
|
+
const kl = (c.faces[0] * th(c.l)) / 2, kp = (c.faces[1] * th(c.l2)) / 2;
|
|
186
|
+
return { a: { x: p.x - ux * kp, y: p.y - uy * kp }, b: { x: f.x + ux * kl, y: f.y + uy * kl } };
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Where a diameter (or radius) dimension's arrows go: its curve's centre, and the radius it measures at — the
|
|
193
|
+
* centreline's, or on a curved wall's face, that face's.
|
|
194
|
+
*/
|
|
195
|
+
export function dimRadius(sk: Sketch, c: Constraint): { o: XY; r: number } | null {
|
|
196
|
+
const f = c.c ? curveFrame(sk, c.c) : null, w = c.c ? sk.circles[c.c] ?? sk.arcs[c.c] : undefined;
|
|
197
|
+
if (!f || !w) return null;
|
|
198
|
+
return { o: f.o, r: f.r + (c.face && w.kind === 'wall' ? (c.face * wallThick(w)) / 2 : 0) };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** A wall's edge as said to people: 'Inside edge', 'Outside edge', 'Kitchen side', 'Top edge', 'Inner edge'. */
|
|
202
|
+
export function edgeLabel(sk: Sketch, lid: string, side: 1 | -1): string {
|
|
203
|
+
const n = faceName(sk, lid, side);
|
|
204
|
+
return n.endsWith(' side') ? n : n + ' edge';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A wall edge's selection id, and back: '<line id>|1' or '<line id>|-1'. */
|
|
208
|
+
export const edgeId = (lid: string, side: 1 | -1): string => lid + '|' + side;
|
|
209
|
+
export function edgeOf(id: string): [string, 1 | -1] {
|
|
210
|
+
const i = id.lastIndexOf('|');
|
|
211
|
+
return [id.slice(0, i), id.slice(i + 1) === '-1' ? -1 : 1];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Which side of line `lid` a point is on: +1 the left of its A → B, −1 the right. */
|
|
215
|
+
export function sideOf(sk: Sketch, lid: string, p: XY): 1 | -1 {
|
|
216
|
+
const l = sk.lines[lid], a = sk.points[l.a], b = sk.points[l.b];
|
|
217
|
+
return (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x) >= 0 ? 1 : -1;
|
|
218
|
+
}
|
package/src/model/project.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeImages } from './images';
|
|
1
2
|
import { newPage } from './demo';
|
|
2
3
|
import type { Page, Project } from './types';
|
|
3
4
|
|
|
@@ -6,9 +7,9 @@ import type { Page, Project } from './types';
|
|
|
6
7
|
* name appears in more than one level, it's prefixed with the level's name ("Level 2 · Floor plan").
|
|
7
8
|
*/
|
|
8
9
|
export function migrateProject(p: { name: string; pages?: Page[]; spaces?: { name: string; pages: Page[] }[] }): Project {
|
|
9
|
-
if (p.pages) return { name: p.name, pages: p.pages };
|
|
10
|
+
if (p.pages) return { name: p.name, pages: p.pages.map(normalizeImages) };
|
|
10
11
|
const spaces = p.spaces || [], count: Record<string, number> = {};
|
|
11
12
|
for (const s of spaces) for (const pg of s.pages) count[pg.name] = (count[pg.name] || 0) + 1;
|
|
12
|
-
const pages = spaces.flatMap(s => s.pages.map(pg => (count[pg.name] > 1 ? { ...pg, name: s.name + ' · ' + pg.name } : pg)));
|
|
13
|
+
const pages = spaces.flatMap(s => s.pages.map(pg => normalizeImages(count[pg.name] > 1 ? { ...pg, name: s.name + ' · ' + pg.name } : pg)));
|
|
13
14
|
return { name: p.name, pages: pages.length ? pages : [newPage('Floor plan')] };
|
|
14
15
|
}
|
package/src/model/projectfile.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeImages } from './images';
|
|
1
2
|
import { migrateProject } from './project';
|
|
2
3
|
import type { Project } from './types';
|
|
3
4
|
|
|
@@ -38,7 +39,7 @@ export function projectFromFile(text: string): Project {
|
|
|
38
39
|
for (const k of ['circles', 'arcs', 'cons', 'rooms', 'openings']) if (!sk[k] || typeof sk[k] !== 'object') sk[k] = {};
|
|
39
40
|
if (!Array.isArray(sk.notes)) sk.notes = [];
|
|
40
41
|
if (!sk.seq) sk.seq = { room: 1 };
|
|
41
|
-
pg
|
|
42
|
+
normalizeImages(pg);
|
|
42
43
|
pg.view = pg.view ?? null;
|
|
43
44
|
}
|
|
44
45
|
return p;
|
package/src/model/rooms.ts
CHANGED
|
@@ -171,7 +171,7 @@ export function roomGeom(sk: Sketch, r: Room): RoomGeom {
|
|
|
171
171
|
* leave it out for loops of straight lines. Null if it's under 1 sq ft.
|
|
172
172
|
*/
|
|
173
173
|
export function maybeRoom(sk: Sketch, loop: string[], edges?: string[]): string | null {
|
|
174
|
-
if (loop.length < 2) return null;
|
|
174
|
+
if (loop.length < 2 || 'cabs' in sk) return null; // spaces are the plan's: not on a wall face
|
|
175
175
|
const es = edges || roomEdges(sk, { loop, name: '' });
|
|
176
176
|
if (!es) return null;
|
|
177
177
|
if (Math.abs(polyArea(outline(sk, loop, es).pts)) < 144) return null;
|
package/src/model/settings.ts
CHANGED
|
@@ -8,11 +8,17 @@ export interface Layers {
|
|
|
8
8
|
print: boolean;
|
|
9
9
|
rooms: boolean;
|
|
10
10
|
annot: boolean;
|
|
11
|
+
/** cabinets, doors and windows snap to corners and each other as they're placed and dragged */
|
|
12
|
+
objSnap: boolean;
|
|
13
|
+
/** cabinets' footprints in the plan */
|
|
14
|
+
cabinets: boolean;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export interface Settings extends UnitPrefs {
|
|
14
18
|
snapGrid: boolean;
|
|
15
19
|
autoCons: boolean;
|
|
20
|
+
/** picking a tool says in a line what it does (under the tabs, for a moment) */
|
|
21
|
+
toolTips: boolean;
|
|
16
22
|
showCons: boolean;
|
|
17
23
|
theme: 'system' | 'light' | 'dark';
|
|
18
24
|
welcomed: boolean;
|
|
@@ -21,10 +27,10 @@ export interface Settings extends UnitPrefs {
|
|
|
21
27
|
wallH: number;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export const defaultLayers = (): Layers => ({ underlay: true, construction: true, dims: true, print: true, rooms: true, annot: true });
|
|
30
|
+
export const defaultLayers = (): Layers => ({ underlay: true, construction: true, dims: true, print: true, rooms: true, annot: true, cabinets: true, objSnap: true });
|
|
25
31
|
|
|
26
32
|
export const settings = sessionState((): Settings => ({
|
|
27
|
-
unit: 'ftin', denom: 16, snapGrid: false, autoCons: true, showCons: true, theme: 'system', welcomed: false,
|
|
33
|
+
unit: 'ftin', denom: 16, snapGrid: false, autoCons: true, toolTips: true, showCons: true, theme: 'system', welcomed: false,
|
|
28
34
|
layers: defaultLayers(), wallH: 96,
|
|
29
35
|
}));
|
|
30
36
|
|
package/src/model/sketch.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { hyp } from './geom';
|
|
2
|
+
import { faceEdit, faceEmpty, faceKeyParts, gcCabinets } from './faces';
|
|
2
3
|
import { uid } from './ids';
|
|
3
4
|
import { pruneOpeningDims } from './openings';
|
|
4
|
-
import type { Arc, Circle, Constraint, Line, LineKind, Pt, Room, SelItem, Sketch } from './types';
|
|
5
|
+
import type { Arc, Circle, Constraint, Line, LineKind, Pt, Room, SelItem, Sketch, WallFace } from './types';
|
|
5
6
|
import { onSessionReset } from './session';
|
|
6
7
|
|
|
7
8
|
export function emptySketch(): Sketch {
|
|
@@ -23,9 +24,12 @@ export function addPoint(sk: Sketch, x: number, y: number, extra?: Partial<Pt>):
|
|
|
23
24
|
return id;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/** A new wall's thickness, inches (4 1/2": a stud wall with drywall both sides), until it's changed. */
|
|
28
|
+
export const WALL_THICK = 4.5;
|
|
29
|
+
|
|
26
30
|
export function addLine(sk: Sketch, a: string, b: string, props: { kind?: LineKind; constr?: boolean; thick?: number } = {}): string {
|
|
27
31
|
const id = uid('l');
|
|
28
|
-
sk.lines[id] = { a, b, kind: props.kind || 'line', constr: !!props.constr, thick: props.thick ||
|
|
32
|
+
sk.lines[id] = { a, b, kind: props.kind || 'line', constr: !!props.constr, thick: props.thick || WALL_THICK };
|
|
29
33
|
return id;
|
|
30
34
|
}
|
|
31
35
|
|
|
@@ -72,14 +76,17 @@ export function gc(sk: Sketch): void {
|
|
|
72
76
|
for (const k of REFKEYS) {
|
|
73
77
|
const v = c[k];
|
|
74
78
|
if (v != null && typeof v === 'string' && !refExists(sk, v)) {
|
|
75
|
-
if (k === 'line')
|
|
79
|
+
if (k === 'line') {
|
|
80
|
+
c.line = null;
|
|
81
|
+
// (measured on its wall's face, the wall gone: end to end, as it is now — nothing moves)
|
|
82
|
+
if (c.face) { delete c.face; const a = sk.points[c.a!], b = sk.points[c.b!]; if (a && b && c.driving !== false) c.value = Math.hypot(b.x - a.x, b.y - a.y); }
|
|
83
|
+
}
|
|
76
84
|
else { delete sk.cons[id]; changed = true; break; }
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
if (sk.cons[id] && (c.type === 'h' || c.type === 'v' || c.type === 'dist') && c.a === c.b) { delete sk.cons[id]; changed = true; }
|
|
80
88
|
}
|
|
81
89
|
for (const id in sk.openings) if (!sk.lines[sk.openings[id].line]) { delete sk.openings[id]; changed = true; }
|
|
82
|
-
if (sk.elev) for (const k in sk.elev) if (!sk.lines[k.split('|')[0]] || !sk.elev[k].length) delete sk.elev[k];
|
|
83
90
|
if (pruneOpeningDims(sk)) changed = true;
|
|
84
91
|
const used = new Set<string>();
|
|
85
92
|
for (const id in sk.lines) { used.add(sk.lines[id].a); used.add(sk.lines[id].b); }
|
|
@@ -88,13 +95,70 @@ export function gc(sk: Sketch): void {
|
|
|
88
95
|
for (const id in sk.openings) for (const d of sk.openings[id].dims || []) if ('pt' in d.ref) used.add(d.ref.pt);
|
|
89
96
|
for (const id in sk.points) {
|
|
90
97
|
const p = sk.points[id];
|
|
91
|
-
if (!used.has(id) && !p.origin && !p.datum && !pinnedPoints.has(id)) { delete sk.points[id]; changed = true; }
|
|
98
|
+
if (!used.has(id) && !p.origin && !p.datum && !p.anchor && !pinnedPoints.has(id)) { delete sk.points[id]; changed = true; }
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
|
-
|
|
101
|
+
// (each room's edges looked up by their ends, not by scanning every line for each edge: this runs after every edit)
|
|
102
|
+
withLineIndex(sk, () => { for (const id in sk.rooms) if (!roomValid(sk, sk.rooms[id])) delete sk.rooms[id]; });
|
|
103
|
+
if (sk.faces) gcFaces(sk);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Wall faces: each goes with its wall, and one with nothing drawn on it isn't kept (unless it's open); each is cleaned up as a sketch. */
|
|
107
|
+
function gcFaces(sk: Sketch): void {
|
|
108
|
+
for (const key in sk.faces!) {
|
|
109
|
+
const face = sk.faces![key];
|
|
110
|
+
if (!sk.lines[faceKeyParts(key).lid]) { delete sk.faces![key]; continue; }
|
|
111
|
+
gc(face);
|
|
112
|
+
gcCabinets(face);
|
|
113
|
+
if (key !== faceEdit.key && faceEmpty(face)) delete sk.faces![key];
|
|
114
|
+
}
|
|
115
|
+
if (!Object.keys(sk.faces!).length) delete sk.faces;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
While a frame is drawn the sketch can't change, so its lines are indexed by their two ends once for the frame
|
|
120
|
+
(withLineIndex) rather than scanned on every lookup: drawing asks which lines join two points for every room edge,
|
|
121
|
+
for every door and window (which side of its wall faces out) and for every H/V badge — rooms × edges × lines per
|
|
122
|
+
opening — which on a 20-room plan was most of a frame's time.
|
|
123
|
+
*/
|
|
124
|
+
/** each end → the other end → the lines joining them (one list for both ways round) */
|
|
125
|
+
let lineIndex: { sk: Sketch; pairs: Map<string, Map<string, string[]>>; memo: Map<string, unknown> } | null = null;
|
|
126
|
+
|
|
127
|
+
/** Run `fn` with `sk`'s lines indexed by their ends (nothing in `fn` may change the sketch). */
|
|
128
|
+
export function withLineIndex<T>(sk: Sketch, fn: () => T): T {
|
|
129
|
+
if (lineIndex) return fn();
|
|
130
|
+
const pairs = new Map<string, Map<string, string[]>>();
|
|
131
|
+
const from = (p: string) => pairs.get(p) ?? pairs.set(p, new Map()).get(p)!;
|
|
132
|
+
for (const id in sk.lines) {
|
|
133
|
+
const l = sk.lines[id];
|
|
134
|
+
let ids = pairs.get(l.a)?.get(l.b);
|
|
135
|
+
if (!ids) { ids = []; from(l.a).set(l.b, ids); from(l.b).set(l.a, ids); }
|
|
136
|
+
ids.push(id);
|
|
137
|
+
}
|
|
138
|
+
lineIndex = { sk, pairs, memo: new Map() };
|
|
139
|
+
try { return fn(); } finally { lineIndex = null; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Inside withLineIndex, what `compute` gives is worked out once for the frame; outside it, every time. */
|
|
143
|
+
export function frameMemo<T>(sk: Sketch, key: string, compute: () => T): T {
|
|
144
|
+
if (!lineIndex || lineIndex.sk !== sk) return compute();
|
|
145
|
+
const memo = lineIndex.memo;
|
|
146
|
+
if (!memo.has(key)) memo.set(key, compute());
|
|
147
|
+
return memo.get(key) as T;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The first line joining two points, if any (linesBetween(…)[0], without making a list for it). */
|
|
151
|
+
export function lineBetween(sk: Sketch, a: string, b: string): string | undefined {
|
|
152
|
+
if (lineIndex && lineIndex.sk === sk) return lineIndex.pairs.get(a)?.get(b)?.[0];
|
|
153
|
+
for (const id in sk.lines) {
|
|
154
|
+
const l = sk.lines[id];
|
|
155
|
+
if ((l.a === a && l.b === b) || (l.a === b && l.b === a)) return id;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
95
158
|
}
|
|
96
159
|
|
|
97
160
|
export function linesBetween(sk: Sketch, a: string, b: string): string[] {
|
|
161
|
+
if (lineIndex && lineIndex.sk === sk) return lineIndex.pairs.get(a)?.get(b)?.slice() ?? [];
|
|
98
162
|
const out: string[] = [];
|
|
99
163
|
for (const id in sk.lines) {
|
|
100
164
|
const l = sk.lines[id];
|
|
@@ -113,18 +177,20 @@ export function roomValid(sk: Sketch, r: Room): boolean {
|
|
|
113
177
|
const id = r.edges[i], l = sk.lines[id], ar = sk.arcs[id];
|
|
114
178
|
const ends = l ? [l.a, l.b] : ar ? [ar.a, ar.b] : null;
|
|
115
179
|
if (!ends || !((ends[0] === a && ends[1] === b) || (ends[0] === b && ends[1] === a))) return false;
|
|
116
|
-
} else if (!
|
|
180
|
+
} else if (!lineBetween(sk, a, b)) return false;
|
|
117
181
|
}
|
|
118
182
|
return true;
|
|
119
183
|
}
|
|
120
184
|
|
|
121
185
|
export function deleteSel(sk: Sketch, items: SelItem[]): void {
|
|
186
|
+
const cabCorners = new Set(Object.values((sk as WallFace).cabs ?? {}).flatMap(cb => [cb.bl, cb.br, cb.tr, cb.tl]));
|
|
122
187
|
for (const it of items) {
|
|
123
|
-
|
|
124
|
-
|
|
188
|
+
// (a cabinet's corner goes with its cabinet, not on its own)
|
|
189
|
+
if (it.type === 'point') { const p = sk.points[it.id]; if (p && !p.origin && !p.anchor && !cabCorners.has(it.id)) delete sk.points[it.id]; }
|
|
190
|
+
else if (it.type === 'line') { if (!sk.lines[it.id]?.anchor && !sk.lines[it.id]?.cab) delete sk.lines[it.id]; } // (a cabinet's edge goes with it)
|
|
125
191
|
else if (it.type === 'circle') delete sk.circles[it.id];
|
|
126
192
|
else if (it.type === 'arc') delete sk.arcs[it.id];
|
|
127
|
-
else if (it.type === 'cons' || it.type === 'dim') delete sk.cons[it.id];
|
|
193
|
+
else if (it.type === 'cons' || it.type === 'dim') { if (!sk.cons[it.id]?.anchor) delete sk.cons[it.id]; }
|
|
128
194
|
else if (it.type === 'room') delete sk.rooms[it.id];
|
|
129
195
|
else if (it.type === 'opening') delete sk.openings[it.id];
|
|
130
196
|
else if (it.type === 'odim') {
|
|
@@ -136,9 +202,26 @@ export function deleteSel(sk: Sketch, items: SelItem[]): void {
|
|
|
136
202
|
gc(sk);
|
|
137
203
|
}
|
|
138
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Join two points: one takes the other's place (a shared id) — or, two of a wall face's anchors (named by the plan,
|
|
207
|
+
* so neither can go), they're held together where they are (level and plumb with each other, hidden). False if they
|
|
208
|
+
* can't be (two the plan fixes).
|
|
209
|
+
*/
|
|
210
|
+
export function joinPoints(sk: Sketch, keep: string, drop: string): boolean {
|
|
211
|
+
const A = sk.points[keep], B = sk.points[drop];
|
|
212
|
+
if (!A || !B || keep === drop) return false;
|
|
213
|
+
if (!(A.anchor && B.anchor)) return mergePoints(sk, keep, drop);
|
|
214
|
+
if (A.fixed && B.fixed) return false;
|
|
215
|
+
addCons(sk, { type: 'h', a: keep, b: drop, elev: true });
|
|
216
|
+
addCons(sk, { type: 'v', a: keep, b: drop, elev: true });
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
139
220
|
/** Replace every reference to `drop` with `keep`. Coincidence = shared id. */
|
|
140
221
|
export function mergePoints(sk: Sketch, keep: string, drop: string): boolean {
|
|
141
222
|
if (keep === drop || !sk.points[keep] || !sk.points[drop]) return false;
|
|
223
|
+
// a wall face's anchor (set by the plan) is the one that stays
|
|
224
|
+
if (sk.points[drop].anchor) { if (sk.points[keep].anchor) return false; [keep, drop] = [drop, keep]; }
|
|
142
225
|
for (const id of linesBetween(sk, keep, drop)) delete sk.lines[id];
|
|
143
226
|
const rep = (v: string) => (v === drop ? keep : v);
|
|
144
227
|
for (const id in sk.lines) { const l = sk.lines[id]; l.a = rep(l.a); l.b = rep(l.b); }
|
|
@@ -150,6 +233,8 @@ export function mergePoints(sk: Sketch, keep: string, drop: string): boolean {
|
|
|
150
233
|
}
|
|
151
234
|
for (const id in sk.rooms) sk.rooms[id].loop = sk.rooms[id].loop.map(rep);
|
|
152
235
|
for (const id in sk.openings) for (const d of sk.openings[id].dims || []) if ('pt' in d.ref) d.ref.pt = rep(d.ref.pt);
|
|
236
|
+
// a wall face's cabinets: a corner joined to another point is that point now (else the cabinet would lose a corner and go)
|
|
237
|
+
for (const cb of Object.values((sk as WallFace).cabs ?? {})) { cb.bl = rep(cb.bl); cb.br = rep(cb.br); cb.tr = rep(cb.tr); cb.tl = rep(cb.tl); }
|
|
153
238
|
if (sk.points[drop].fixed) sk.points[keep].fixed = true;
|
|
154
239
|
delete sk.points[drop];
|
|
155
240
|
// drop duplicate lines
|
|
@@ -194,19 +279,21 @@ export function restorePos(sk: Sketch, s: PosSnapshot): void {
|
|
|
194
279
|
|
|
195
280
|
/** Every line and arc joined end to end with the given ones (through shared points), including them. */
|
|
196
281
|
export function connectedGeometry(sk: Sketch, lineIds: string[], arcIds: string[] = []): { lines: string[]; arcs: string[] } {
|
|
282
|
+
// on a wall face, the plan's corners and edges aren't part of a shape: nothing joins through them
|
|
197
283
|
const pts = new Set<string>(), lines = new Set(lineIds), arcs = new Set(arcIds);
|
|
198
|
-
|
|
199
|
-
|
|
284
|
+
const join = (p: string) => { if (!sk.points[p]?.anchor) pts.add(p); };
|
|
285
|
+
lineIds.forEach(id => { const l = sk.lines[id]; if (l) { join(l.a); join(l.b); } });
|
|
286
|
+
arcIds.forEach(id => { const a = sk.arcs[id]; if (a) { join(a.a); join(a.b); } });
|
|
200
287
|
let grow = true;
|
|
201
288
|
while (grow) {
|
|
202
289
|
grow = false;
|
|
203
290
|
for (const id in sk.lines) {
|
|
204
291
|
const l = sk.lines[id];
|
|
205
|
-
if (!lines.has(id) && (pts.has(l.a) || pts.has(l.b))) { lines.add(id);
|
|
292
|
+
if (!lines.has(id) && !l.anchor && !l.cab && (pts.has(l.a) || pts.has(l.b))) { lines.add(id); join(l.a); join(l.b); grow = true; }
|
|
206
293
|
}
|
|
207
294
|
for (const id in sk.arcs) {
|
|
208
295
|
const a = sk.arcs[id];
|
|
209
|
-
if (!arcs.has(id) && (pts.has(a.a) || pts.has(a.b))) { arcs.add(id);
|
|
296
|
+
if (!arcs.has(id) && (pts.has(a.a) || pts.has(a.b))) { arcs.add(id); join(a.a); join(a.b); grow = true; }
|
|
210
297
|
}
|
|
211
298
|
}
|
|
212
299
|
return { lines: [...lines], arcs: [...arcs] };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
type P = [number, number];
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A pen stroke is drawn as a smooth curve through the points it was sampled at, not straight from one to the next —
|
|
5
|
+
which shows a corner at every sample wherever they're far apart (a quick stroke, a finger's coarser samples, or just
|
|
6
|
+
zoomed in). Centripetal Catmull-Rom, as cubic Béziers: it passes through every point, doesn't loop or overshoot where
|
|
7
|
+
the spacing changes, and is the same curve at any zoom — so the screen, a tap on it and an export all agree.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The curve through `pts` as one cubic Bézier per span, [first control, second control, end], from the first point. */
|
|
11
|
+
export function smoothSpans(pts: P[]): [P, P, P][] {
|
|
12
|
+
// (a repeated point makes no span)
|
|
13
|
+
const q: P[] = [];
|
|
14
|
+
for (const p of pts) { const l = q[q.length - 1]; if (!l || p[0] !== l[0] || p[1] !== l[1]) q.push(p); }
|
|
15
|
+
const n = q.length, out: [P, P, P][] = [];
|
|
16
|
+
// past each end, the point mirrored through it: the curve leaves along its first span and arrives along its last
|
|
17
|
+
const at = (i: number): P =>
|
|
18
|
+
i < 0 ? [2 * q[0][0] - q[1][0], 2 * q[0][1] - q[1][1]] : i >= n ? [2 * q[n - 1][0] - q[n - 2][0], 2 * q[n - 1][1] - q[n - 2][1]] : q[i];
|
|
19
|
+
// (centripetal: each span weighs in by the square root of its length)
|
|
20
|
+
const w = (a: P, b: P) => Math.sqrt(Math.hypot(b[0] - a[0], b[1] - a[1]));
|
|
21
|
+
for (let i = 0; i + 1 < n; i++) {
|
|
22
|
+
const p0 = at(i - 1), p1 = q[i], p2 = q[i + 1], p3 = at(i + 2);
|
|
23
|
+
const a = w(p0, p1), b = w(p1, p2), c = w(p2, p3);
|
|
24
|
+
const k1 = 3 * a * (a + b), k2 = 3 * c * (c + b), m1 = 2 * a * a + 3 * a * b + b * b, m2 = 2 * c * c + 3 * c * b + b * b;
|
|
25
|
+
out.push([
|
|
26
|
+
[(a * a * p2[0] - b * b * p0[0] + m1 * p1[0]) / k1, (a * a * p2[1] - b * b * p0[1] + m1 * p1[1]) / k1],
|
|
27
|
+
[(c * c * p1[0] - b * b * p3[0] + m2 * p2[0]) / k2, (c * c * p1[1] - b * b * p3[1] + m2 * p2[1]) / k2],
|
|
28
|
+
p2,
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The same curve as straight pieces, never more than `tol` off it — for whatever draws only lines (an export, a tap). */
|
|
35
|
+
export function smoothPolyline(pts: P[], tol: number): P[] {
|
|
36
|
+
if (pts.length < 3) return pts.slice();
|
|
37
|
+
const out: P[] = [pts[0]];
|
|
38
|
+
let from = pts[0];
|
|
39
|
+
for (const [c1, c2, to] of smoothSpans(pts)) {
|
|
40
|
+
// how many pieces keep within `tol` (Wang’s bound, from how sharply the controls turn; the cap only stops a runaway)
|
|
41
|
+
const dd = Math.max(Math.hypot(from[0] - 2 * c1[0] + c2[0], from[1] - 2 * c1[1] + c2[1]), Math.hypot(c1[0] - 2 * c2[0] + to[0], c1[1] - 2 * c2[1] + to[1]));
|
|
42
|
+
const k = Math.min(256, Math.max(1, Math.ceil(Math.sqrt((0.75 * dd) / tol))));
|
|
43
|
+
for (let j = 1; j < k; j++) {
|
|
44
|
+
const t = j / k, s = 1 - t, A = s * s * s, B = 3 * s * s * t, C = 3 * s * t * t, D = t * t * t;
|
|
45
|
+
out.push([A * from[0] + B * c1[0] + C * c2[0] + D * to[0], A * from[1] + B * c1[1] + C * c2[1] + D * to[1]]);
|
|
46
|
+
}
|
|
47
|
+
out.push(to);
|
|
48
|
+
from = to;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
package/src/model/store.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { seedProject } from './demo';
|
|
|
2
2
|
import { deep } from './ids';
|
|
3
3
|
import { defaultLayers, settings } from './settings';
|
|
4
4
|
import { migrateProject } from './project';
|
|
5
|
-
import
|
|
5
|
+
import { faceEdit, faceOf } from './faces';
|
|
6
|
+
import type { Page, Project, Sketch, WallFace } from './types';
|
|
6
7
|
import { onSessionReset, sessionState } from './session';
|
|
7
8
|
|
|
8
9
|
export { migrateProject };
|
|
@@ -18,16 +19,29 @@ export interface Persistence {
|
|
|
18
19
|
load(): void;
|
|
19
20
|
/** Save the project now. Returns false if images had to be left out. */
|
|
20
21
|
save(): boolean;
|
|
22
|
+
/** It keeps where each page was viewed from, so a pan or zoom alone is worth saving (not so a drawing synced to a job). */
|
|
23
|
+
keepsView?: boolean;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export const store = sessionState((): { project: Project; curPageId: string } => ({ project: null as unknown as Project, curPageId: '' }));
|
|
24
27
|
|
|
25
28
|
export const allPages = (): Page[] => store.project.pages;
|
|
26
29
|
export const page = (): Page => allPages().find(p => p.id === store.curPageId)!;
|
|
27
|
-
|
|
30
|
+
/** The page's floor plan (whatever is being edited). */
|
|
31
|
+
export const planSK = (): Sketch => page().sk;
|
|
32
|
+
/** The sketch being edited: the floor plan, or the wall face open in the wall view (see model/faces.ts faceEdit). */
|
|
33
|
+
export const SK = (): Sketch => (faceEdit.key ? faceOf(page().sk, faceEdit.key, true) : page().sk);
|
|
34
|
+
/** Whether a wall face is being edited (the wall view is open). */
|
|
35
|
+
export const editingFace = (): boolean => faceEdit.key != null;
|
|
36
|
+
/** Put back a copy of the sketch being edited (a change that couldn't be made). */
|
|
37
|
+
export function replaceSK(next: Sketch): void {
|
|
38
|
+
if (faceEdit.key) page().sk.faces![faceEdit.key] = next as WallFace;
|
|
39
|
+
else page().sk = next;
|
|
40
|
+
}
|
|
28
41
|
|
|
29
42
|
/** This browser's localStorage (the default). */
|
|
30
43
|
export const localPersistence: Persistence = {
|
|
44
|
+
keepsView: true,
|
|
31
45
|
load() {
|
|
32
46
|
try {
|
|
33
47
|
const raw = localStorage.getItem(STORE_KEY);
|
|
@@ -50,7 +64,7 @@ export const localPersistence: Persistence = {
|
|
|
50
64
|
} catch {
|
|
51
65
|
try {
|
|
52
66
|
const p = deep(store.project);
|
|
53
|
-
p.pages.forEach(pg => (pg.
|
|
67
|
+
p.pages.forEach(pg => (pg.underlays = []));
|
|
54
68
|
localStorage.setItem(STORE_KEY, payload(p));
|
|
55
69
|
} catch { /* nothing more we can do */ }
|
|
56
70
|
return false;
|
|
@@ -75,9 +89,26 @@ export function load(): void {
|
|
|
75
89
|
|
|
76
90
|
let saveT: ReturnType<typeof setTimeout> | undefined;
|
|
77
91
|
onSessionReset(() => clearTimeout(saveT));
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether nothing is being done on the drawing just now (no finger or pointer down): a save waits for that — saving
|
|
95
|
+
* takes the JavaScript thread a moment, felt as a hitch in a drag or a stroke started just after an edit. (Never for
|
|
96
|
+
* long: a lift that was lost mustn't hold saving back.)
|
|
97
|
+
*/
|
|
98
|
+
let idle: () => boolean = () => true;
|
|
99
|
+
export function setSaveIdle(fn: (() => boolean) | null): void { idle = fn ?? (() => true); }
|
|
100
|
+
const SAVE_MS = 700, WAIT_MS = 300, WAITS = 10;
|
|
101
|
+
|
|
78
102
|
export function saveSoon(): void {
|
|
79
103
|
clearTimeout(saveT);
|
|
80
|
-
|
|
104
|
+
let waits = 0;
|
|
105
|
+
const attempt = () => { if (!idle() && waits++ < WAITS) saveT = setTimeout(attempt, WAIT_MS); else saveNow(); };
|
|
106
|
+
saveT = setTimeout(attempt, SAVE_MS);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The view moved (a pan or a zoom): saved only where that's kept (see Persistence.keepsView). */
|
|
110
|
+
export function saveViewSoon(): void {
|
|
111
|
+
if (persistence.keepsView) saveSoon();
|
|
81
112
|
}
|
|
82
113
|
|
|
83
114
|
/** Save now. Returns false if underlays had to be left out. */
|