@reekon-tools/boldr-cad 0.1.0 → 1.0.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 +81 -9
- 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 +5 -0
- package/dist/data/memory.js +2 -2
- package/dist/data/memory.js.map +1 -1
- package/dist/data/provider.d.ts +19 -7
- package/dist/data/sync.d.ts +7 -2
- package/dist/data/sync.js +115 -64
- 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 +19 -4
- package/dist/editor/keypad.js +39 -9
- 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 +36 -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 +167 -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 +5 -0
- 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 +46 -3
- package/dist/react/CadEditor.js +77 -20
- 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 +581 -96
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/elevation.js +1 -1
- 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 +22 -15
- 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 +10 -2
- package/dist/units/format.js +18 -3
- package/dist/units/format.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 +2 -2
- package/src/data/provider.ts +15 -7
- package/src/data/sync.ts +87 -50
- 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 +43 -11
- 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 +37 -6
- package/src/editor/project.ts +155 -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 +5 -0
- 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 +133 -30
- 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 +524 -92
- package/src/tools/elevation.ts +1 -1
- 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 +8 -13
- 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 +18 -3
- 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/dist/editor/elevation.js
CHANGED
|
@@ -1,475 +1,427 @@
|
|
|
1
|
-
import { afterEdit,
|
|
1
|
+
import { afterEdit, analyze, resolveOpeningsNow, undo } from '../app/edit';
|
|
2
2
|
import { ed } from '../app/state';
|
|
3
|
-
import {
|
|
3
|
+
import { DOOR_H, elevFrame, elevOpenings, openingHeights, roomSideOf, toU, wallHeight, WINDOW_H } from '../model/elevation';
|
|
4
|
+
import { faceEdit, faceKey, faceKeyParts } from '../model/faces';
|
|
5
|
+
import { wallFace } from '../model/wallfaces';
|
|
6
|
+
import { faceOverflow, forgetSynced, openFace, placedMarks, planOverflow, pushedOut, setPlacedMarks, syncFace, syncFaces } from '../model/facesolve';
|
|
4
7
|
import { clamp } from '../model/geom';
|
|
5
8
|
import { pushHistory } from '../model/history';
|
|
6
9
|
import { uid } from '../model/ids';
|
|
7
|
-
import { applySlide, openingSpan, pickBetween, sameRef, slideGroup } from '../model/openings';
|
|
8
|
-
import {
|
|
10
|
+
import { applySlide, edgeS, openingSpan, pickBetween, resolveOpenings, sameRef, slideGroup, STANDARD_OPENING_W } from '../model/openings';
|
|
11
|
+
import { sessionState } from '../model/session';
|
|
9
12
|
import { settings } from '../model/settings';
|
|
10
|
-
import { deleteSel, lineLen } from '../model/sketch';
|
|
11
|
-
import { SK } from '../model/store';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
13
|
+
import { deleteSel, gc, lineLen, restorePos, snapshotPos } from '../model/sketch';
|
|
14
|
+
import { planSK, SK } from '../model/store';
|
|
15
|
+
import { cabBox, previewChip } from '../render/elevation';
|
|
16
|
+
import { TH } from '../render/theme';
|
|
17
|
+
import { fitView, mainT, requestDraw, requestOverlayDraw, S, setWallBox } from '../render/view';
|
|
14
18
|
import { editElevationPosition, editOpeningHeight, editSill, editWallHeight } from '../tools/elevation';
|
|
15
19
|
import { editOpeningDim } from '../tools/openings';
|
|
20
|
+
import { cancelTool } from '../tools/registry';
|
|
16
21
|
import { fmtLen } from '../units';
|
|
17
|
-
import {
|
|
22
|
+
import { addCabinet, addHeightLine, CAB, cabinetSpot, deleteCabinets, dimensionFace, editCabinet, editCabinetDepth, snapObjectMove } from './face';
|
|
23
|
+
import { openKeypad, refreshModes, renderElevation, toast, updateCtx, updateHint, updateRail, updateTopBar } from './ports';
|
|
18
24
|
/*
|
|
19
|
-
The wall
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
the
|
|
25
|
+
The wall view: one straight wall seen straight on, edited with the plan's own tools. Opening a wall makes the
|
|
26
|
+
editor work on that wall face's sketch (model/faces.ts: SK() is the face) — its grid, lines, circles and arcs,
|
|
27
|
+
constraints, dimensions, selection and dragging are the plan's — with the plan's corners, floor, ceiling, doors and
|
|
28
|
+
windows as fixed anchors to snap and constrain to. On top of that, the wall view's own: doors and windows placed
|
|
29
|
+
and dragged on the wall (they're the plan's), cabinets, height lines, and the dimension chips doors, windows and
|
|
30
|
+
cabinets get (render/elevation.ts), each tapped to select and changed with the yellow +.
|
|
24
31
|
*/
|
|
25
|
-
/** The open wall
|
|
32
|
+
/** The open wall: which wall, from which side, and its frame (from the live plan). */
|
|
26
33
|
export const elev = sessionState(() => ({
|
|
27
34
|
lid: null,
|
|
28
35
|
flip: false,
|
|
29
|
-
view: null,
|
|
30
36
|
frame: null,
|
|
31
|
-
W: 0, H: 0,
|
|
32
|
-
hits: [],
|
|
33
|
-
/** the selected dimension (ringed in yellow; the bar's + opens its keypad) */
|
|
34
|
-
active: null,
|
|
35
|
-
/** the opening being dragged (outlined in yellow) */
|
|
36
|
-
dragOp: null,
|
|
37
|
-
/** selected openings, wall corners and drawn lines (tap; Shift/⌘-tap adds) — the bar's + dimensions them */
|
|
38
|
-
objs: [],
|
|
39
|
-
/** the active tool (the toolbar on the left) */
|
|
40
|
-
tool: 'select',
|
|
41
|
-
/** line / guide tool: the first point, [u, z], once placed */
|
|
42
|
-
lineStart: null,
|
|
43
|
-
/** where the pointer is (snapped), for previews while a drawing tool is active */
|
|
44
|
-
cursor: null,
|
|
45
37
|
}));
|
|
46
|
-
const objKey = (o) => (o.k === 'corner' ? 'corner:' + o.end : o.k + ':' + o.id);
|
|
47
38
|
/** Size of a new door/window placed in the wall view (as the plan's tool), inches. */
|
|
48
|
-
export const NEW_W =
|
|
39
|
+
export const NEW_W = STANDARD_OPENING_W;
|
|
49
40
|
export const elevOpen = () => elev.lid != null;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
/** Open the
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!elevFrame(
|
|
56
|
-
toast('Elevations work on straight walls. Turn the line into a wall first.');
|
|
41
|
+
/** The open wall, for the wall view to fit: along it from corner to corner, and from the floor to its height. */
|
|
42
|
+
const wallBox = () => (elev.frame ? { x0: elev.frame.face[0], y0: 0, x1: elev.frame.face[1], y1: elev.frame.H } : null);
|
|
43
|
+
/** Open the wall view of a straight wall, seen from the room side. */
|
|
44
|
+
/** Open a wall's view: from the side a room is on — or `side`, the face to the left (+1) or right (−1) of its A → B. */
|
|
45
|
+
export function openElevation(lid, side) {
|
|
46
|
+
if (!elevFrame(planSK(), lid)) {
|
|
47
|
+
toast('Elevations work on straight walls. Turn the line into a wall first.', { warn: true });
|
|
57
48
|
return;
|
|
58
49
|
}
|
|
50
|
+
cancelTool();
|
|
59
51
|
elev.lid = lid;
|
|
60
|
-
elev.flip =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
elev.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
52
|
+
elev.flip = side != null && side !== (roomSideOf(planSK(), lid) || 1);
|
|
53
|
+
enterFace();
|
|
54
|
+
}
|
|
55
|
+
/** Start editing the open wall's face (from the side `elev.flip` says). */
|
|
56
|
+
function enterFace() {
|
|
57
|
+
const f = elevFrame(planSK(), elev.lid, elev.flip);
|
|
58
|
+
elev.frame = f;
|
|
59
|
+
// (handed over here, not as this module loads: the view module is still loading then)
|
|
60
|
+
setWallBox(wallBox);
|
|
61
|
+
faceEdit.key = faceKey(f.lid, f.side);
|
|
62
|
+
openFace(planSK(), f.lid, f.side);
|
|
63
|
+
ed.mode = 'draft';
|
|
64
|
+
ed.tool = 'select';
|
|
65
|
+
ed.sel = [];
|
|
66
|
+
ed.hover = null;
|
|
67
|
+
ed.snapState = null;
|
|
68
|
+
ed.preview = null;
|
|
69
|
+
ed.boxSel = null;
|
|
70
|
+
analyze();
|
|
71
|
+
fitView();
|
|
72
|
+
refreshModes();
|
|
73
|
+
updateRail();
|
|
74
|
+
updateCtx();
|
|
75
|
+
updateHint();
|
|
76
|
+
updateTopBar();
|
|
77
|
+
renderElevation();
|
|
78
|
+
requestDraw();
|
|
79
|
+
}
|
|
80
|
+
/** Stop editing the face (a face with nothing on it isn't kept). */
|
|
81
|
+
function leaveFace() {
|
|
82
|
+
cancelTool();
|
|
83
|
+
faceEdit.key = null;
|
|
84
|
+
gc(planSK());
|
|
85
|
+
}
|
|
86
|
+
/** Open the wall a cabinet is on, from its side, with the cabinet selected (a tap on its footprint in the plan). */
|
|
87
|
+
export function openCabinet(key, cabId) {
|
|
88
|
+
const { lid, side } = faceKeyParts(key);
|
|
89
|
+
openElevation(lid);
|
|
90
|
+
if (!elevOpen())
|
|
91
|
+
return;
|
|
92
|
+
if (elev.frame && elev.frame.side !== side)
|
|
93
|
+
flipElevation();
|
|
94
|
+
if (SK().cabs?.[cabId]) {
|
|
95
|
+
ed.sel = [{ type: 'cab', id: cabId }];
|
|
96
|
+
updateCtx();
|
|
97
|
+
updateHint();
|
|
98
|
+
requestDraw();
|
|
99
|
+
}
|
|
100
|
+
toast('This cabinet’s wall — Back returns to the plan.');
|
|
101
|
+
}
|
|
102
|
+
/** Back to the plan, with the wall selected. */
|
|
69
103
|
export function closeElevation() {
|
|
70
104
|
if (!elevOpen())
|
|
71
105
|
return;
|
|
106
|
+
const lid = elev.lid;
|
|
107
|
+
leaveFace();
|
|
72
108
|
elev.lid = null;
|
|
73
109
|
elev.frame = null;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
ed.tool = 'select';
|
|
111
|
+
ed.hover = null;
|
|
112
|
+
ed.preview = null;
|
|
113
|
+
ed.sel = planSK().lines[lid] ? [{ type: 'line', id: lid }] : [];
|
|
114
|
+
analyze();
|
|
115
|
+
refreshModes();
|
|
116
|
+
updateRail();
|
|
117
|
+
updateCtx();
|
|
118
|
+
updateHint();
|
|
119
|
+
updateTopBar();
|
|
78
120
|
renderElevation();
|
|
79
121
|
requestDraw();
|
|
80
122
|
}
|
|
123
|
+
/** Look at the wall from its other side (each side has its own drawing). */
|
|
81
124
|
export function flipElevation() {
|
|
82
125
|
if (!elevOpen())
|
|
83
126
|
return;
|
|
127
|
+
leaveFace();
|
|
84
128
|
elev.flip = !elev.flip;
|
|
85
|
-
|
|
86
|
-
elev.active = null;
|
|
87
|
-
elev.objs = [];
|
|
88
|
-
elev.lineStart = null;
|
|
89
|
-
renderElevation();
|
|
129
|
+
enterFace();
|
|
90
130
|
}
|
|
91
131
|
/** Fit the wall to the view again. */
|
|
92
|
-
export function fitElevation() {
|
|
93
|
-
/** The open wall's frame, from the live
|
|
132
|
+
export function fitElevation() { fitView(); }
|
|
133
|
+
/** The open wall's frame, from the live plan. Closes the view if the wall is gone (or isn't a straight wall now). */
|
|
94
134
|
export function elevSyncFrame() {
|
|
95
135
|
if (!elevOpen())
|
|
96
136
|
return null;
|
|
97
|
-
const f = elevFrame(
|
|
137
|
+
const f = elevFrame(planSK(), elev.lid, elev.flip);
|
|
98
138
|
if (!f) {
|
|
99
139
|
closeElevation();
|
|
100
140
|
return null;
|
|
101
141
|
}
|
|
102
142
|
elev.frame = f;
|
|
143
|
+
// a room appeared or went on one side: the side "inside" is the other face now
|
|
144
|
+
const key = faceKey(f.lid, f.side);
|
|
145
|
+
if (faceEdit.key !== key) {
|
|
146
|
+
faceEdit.key = key;
|
|
147
|
+
openFace(planSK(), f.lid, f.side);
|
|
148
|
+
ed.sel = [];
|
|
149
|
+
analyze();
|
|
150
|
+
}
|
|
103
151
|
return f;
|
|
104
152
|
}
|
|
105
|
-
/**
|
|
106
|
-
* After drawing: keep the tappable chips, drop a selected dimension that no longer shows and selected things that
|
|
107
|
-
* are gone. Returns the selected dimension's chip, if any.
|
|
108
|
-
*/
|
|
109
|
-
export function elevAfterRender(hits) {
|
|
110
|
-
elev.hits = hits;
|
|
111
|
-
let on = elev.active ? hits.find(h => JSON.stringify(h.act) === elev.active) : undefined;
|
|
112
|
-
if (elev.active && !on) {
|
|
113
|
-
elev.active = null;
|
|
114
|
-
on = undefined;
|
|
115
|
-
}
|
|
116
|
-
const sk = SK(), lines = elev.frame ? elevLines(sk, elev.frame) : [];
|
|
117
|
-
elev.objs = elev.objs.filter(o => o.k === 'corner' || (o.k === 'op' ? !!sk.openings[o.id] : lines.some(l => l.id === o.id)));
|
|
118
|
-
return on;
|
|
119
|
-
}
|
|
120
|
-
/** Whether the trash has something to do: free or remove the selected dimension, or delete selected things. */
|
|
121
|
-
export const elevTrashEnabled = (on) => on ? !!storedDim(on.act) || !!lockedSill(on.act) : elev.objs.some(o => o.k !== 'corner');
|
|
122
153
|
/** The view's title: the wall's length, and which side it's seen from. */
|
|
123
154
|
export function elevTitle() {
|
|
124
|
-
const sk =
|
|
155
|
+
const sk = planSK(), f = elev.frame, hasRoom = roomSideOf(sk, elev.lid) !== 0;
|
|
125
156
|
return {
|
|
126
157
|
title: 'Wall ' + fmtLen(lineLen(sk, sk.lines[elev.lid])),
|
|
127
158
|
sub: hasRoom ? (f.inside ? 'Seen from inside' : 'Seen from outside') : 'Side ' + (elev.flip ? 2 : 1) + ' of 2',
|
|
128
159
|
};
|
|
129
160
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (o.type === 'door')
|
|
136
|
-
return locked ? 'This door is locked by a dimension. Change the dimension, or make it a reference, to move it.' : 'Drag the door to slide it along the floor.';
|
|
137
|
-
if (locked && o.sillLocked)
|
|
138
|
-
return 'This window is locked along the wall and in height. Select a blue dimension and press Delete to free it.';
|
|
139
|
-
if (o.sillLocked)
|
|
140
|
-
return 'This window’s height above the floor is locked — it only slides along the wall. Delete the blue sill dimension to free it.';
|
|
141
|
-
return locked ? 'This window is locked along the wall by a dimension — drag it to raise or lower it.' : 'Drag the ' + n + ' to move it along the wall or up and down.';
|
|
161
|
+
// ---- keeping what's drawn inside the wall ----
|
|
162
|
+
/** The open wall face's edges: across from corner to corner, up from the floor to the ceiling. Null outside the wall view. */
|
|
163
|
+
export function wallBounds() {
|
|
164
|
+
const f = elevOpen() ? elev.frame : null;
|
|
165
|
+
return f ? { x0: f.face[0], x1: f.face[1], y0: 0, y1: f.H } : null;
|
|
142
166
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const group = slideGroup(sk, op);
|
|
148
|
-
return { op, group, sill: openingHeights(o).sill, lockedH: !group, started: false, cx, cy };
|
|
167
|
+
/** A point brought inside the wall (as it is, outside the wall view). */
|
|
168
|
+
export function intoWall(x, y) {
|
|
169
|
+
const b = wallBounds();
|
|
170
|
+
return b ? [Math.min(Math.max(x, b.x0), b.x1), Math.min(Math.max(y, b.y0), b.y1)] : [x, y];
|
|
149
171
|
}
|
|
150
|
-
function moveDrag(d, du, dz) {
|
|
151
|
-
const sk = SK(), o = sk.openings[d.op], f = elev.frame, g = o && openingSpan(sk, o), l = o && sk.lines[o.line];
|
|
152
|
-
if (!o || !f || !g || !l)
|
|
153
|
-
return;
|
|
154
|
-
if (!d.started) {
|
|
155
|
-
const stuck = d.lockedH && (o.type === 'door' || !!o.sillLocked);
|
|
156
|
-
if (stuck) {
|
|
157
|
-
toast(dragHint(d.op));
|
|
158
|
-
elev.dragOp = null;
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (d.lockedH || o.sillLocked)
|
|
162
|
-
toast(dragHint(d.op));
|
|
163
|
-
d.started = true;
|
|
164
|
-
pushHistory();
|
|
165
|
-
elev.dragOp = d.op;
|
|
166
|
-
elev.active = null;
|
|
167
|
-
if (!elev.objs.some(x => x.k === 'op' && x.id === d.op))
|
|
168
|
-
elev.objs = [{ k: 'op', id: d.op }];
|
|
169
|
-
}
|
|
170
|
-
if (d.group)
|
|
171
|
-
applySlide(sk, d.group, f.mirror ? -du : du);
|
|
172
|
-
if (o.type === 'window' && !o.sillLocked) {
|
|
173
|
-
const { sill, head } = openingHeights(o), q = 1 / settings.denom;
|
|
174
|
-
o.sill = clamp(Math.round((d.sill + dz) / q) * q, 0, Math.max(0, wallHeight(l) - (head - sill)));
|
|
175
|
-
}
|
|
176
|
-
resolveOpeningsNow();
|
|
177
|
-
requestDraw();
|
|
178
|
-
renderElevation();
|
|
179
|
-
}
|
|
180
|
-
// ---- drawing tools ----
|
|
181
|
-
/** The wall view's toolbar: [tool, label, shortcut, icon]. */
|
|
182
|
-
export const ELEV_RAIL = [
|
|
183
|
-
['select', 'Select', 'V', 'select'], ['door', 'Door', 'B', 'door'], ['window', 'Window', 'N', 'window'],
|
|
184
|
-
['line', 'Line', 'L', 'line'], ['guide', 'Guide line', 'X', 'constr'], '|',
|
|
185
|
-
];
|
|
186
|
-
export const TOOL_HINT = {
|
|
187
|
-
select: '',
|
|
188
|
-
door: 'Tap the wall to place a 3\'-0" door. It sits on the floor where you tap.',
|
|
189
|
-
window: 'Tap the wall to place a 3\'-0" window, centered where you tap.',
|
|
190
|
-
line: 'Tap two points, or drag, to draw a line. It snaps to corners, openings, the floor and ceiling, and level/plumb.',
|
|
191
|
-
guide: 'Tap two points, or drag, to draw a dashed guide line.',
|
|
192
|
-
};
|
|
193
|
-
/** Switch the wall view's tool. */
|
|
194
|
-
export function setElevTool(t, quiet = false) {
|
|
195
|
-
elev.tool = t;
|
|
196
|
-
elev.lineStart = null;
|
|
197
|
-
elev.cursor = null;
|
|
198
|
-
if (t !== 'select') {
|
|
199
|
-
elev.active = null;
|
|
200
|
-
}
|
|
201
|
-
if (!quiet && TOOL_HINT[t])
|
|
202
|
-
toast(TOOL_HINT[t]);
|
|
203
|
-
renderElevation();
|
|
204
|
-
}
|
|
205
|
-
/** Esc in the wall view: cancel a line in progress, then leave the tool, then close the view. */
|
|
206
|
-
export function elevEscape() {
|
|
207
|
-
if (elev.lineStart) {
|
|
208
|
-
elev.lineStart = null;
|
|
209
|
-
renderElevation();
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (elev.tool !== 'select') {
|
|
213
|
-
setElevTool('select', true);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (elev.objs.length || elev.active) {
|
|
217
|
-
elev.objs = [];
|
|
218
|
-
elev.active = null;
|
|
219
|
-
renderElevation();
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
closeElevation();
|
|
223
|
-
}
|
|
224
|
-
/** Keyboard shortcuts for the wall view's tools. Returns true if the key was one. */
|
|
225
|
-
export function elevToolKey(k) {
|
|
226
|
-
const t = { v: 'select', b: 'door', n: 'window', l: 'line', x: 'guide' }[k.toLowerCase()];
|
|
227
|
-
if (!t)
|
|
228
|
-
return false;
|
|
229
|
-
setElevTool(t);
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
const toWorld = (x, y) => { const T = elevT(elev.view, elev.W, elev.H); return [(x - T.ox) / T.s, (T.oy - y) / T.s]; };
|
|
233
172
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
173
|
+
* How far what's drawn on the face reaches past the wall, inches (0: it's all inside): each point, and each circle's
|
|
174
|
+
* and arc's reach. Edits that make this grow are refused, so nothing is pushed out (a wall made shorter in the plan can
|
|
175
|
+
* leave something outside: it can still be edited, just not pushed further out).
|
|
237
176
|
*/
|
|
238
|
-
function
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
const ops = elevOpenings(sk, f), lines = elevLines(sk, f).map(l => elevLineUZ(f, l));
|
|
242
|
-
const pts = [[f.face[0], 0], [f.face[0], f.H], [f.face[1], 0], [f.face[1], f.H]];
|
|
243
|
-
for (const o of ops)
|
|
244
|
-
pts.push([o.u0, o.sill], [o.u0, o.head], [o.u1, o.sill], [o.u1, o.head]);
|
|
245
|
-
for (const [a, b] of lines)
|
|
246
|
-
pts.push(a, b, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]);
|
|
247
|
-
let best = null, bd = R;
|
|
248
|
-
for (const p of pts) {
|
|
249
|
-
const d = Math.hypot(p[0] - u, p[1] - z);
|
|
250
|
-
if (d < bd) {
|
|
251
|
-
bd = d;
|
|
252
|
-
best = p;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
if (best)
|
|
256
|
-
return { p: [best[0], best[1]], kind: 'pt' };
|
|
257
|
-
const us = [f.face[0], f.face[1], ...ops.flatMap(o => [o.u0, o.u1]), ...lines.flatMap(([a, b]) => [a[0], b[0]])];
|
|
258
|
-
const zs = [0, f.H, ...ops.flatMap(o => [o.sill, o.head]), ...lines.flatMap(([a, b]) => [a[1], b[1]])];
|
|
259
|
-
const near = (v, list) => list.reduce((m, x) => (Math.abs(x - v) < R && (m == null || Math.abs(x - v) < Math.abs(m - v)) ? x : m), null);
|
|
260
|
-
const q = 1 / settings.denom, su = near(u, us), sz = near(z, zs);
|
|
261
|
-
let pu = su ?? Math.round(u / q) * q, pz = sz ?? Math.round(z / q) * q, kind = su != null || sz != null ? 'edge' : null;
|
|
262
|
-
if (from) {
|
|
263
|
-
const du = Math.abs(pu - from[0]), dz = Math.abs(pz - from[1]), tan = Math.tan((6 * Math.PI) / 180);
|
|
264
|
-
if (sz == null && dz < du * tan) {
|
|
265
|
-
pz = from[1];
|
|
266
|
-
kind = 'hv';
|
|
267
|
-
}
|
|
268
|
-
else if (su == null && du < dz * tan) {
|
|
269
|
-
pu = from[0];
|
|
270
|
-
kind = 'hv';
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return { p: [pu, pz], kind };
|
|
274
|
-
}
|
|
275
|
-
/** Where a new door/window would go for a pointer at (u, z): centre s along the wall, and the window's sill. */
|
|
276
|
-
export function openingAt(type, u, z) {
|
|
277
|
-
const f = elev.frame, w = NEW_W;
|
|
278
|
-
if (f.L < w + 2)
|
|
279
|
-
return null;
|
|
280
|
-
const at = clamp(toU(f, u), w / 2, f.L - w / 2);
|
|
281
|
-
const q = 1 / settings.denom, sill = type === 'door' ? 0 : clamp(Math.round((z - WINDOW_H / 2) / q) * q, 0, Math.max(0, f.H - WINDOW_H));
|
|
282
|
-
return { at, sill };
|
|
177
|
+
export function wallOverflow(sk = SK()) {
|
|
178
|
+
const b = wallBounds();
|
|
179
|
+
return b ? faceOverflow(sk, b) : {};
|
|
283
180
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
181
|
+
/**
|
|
182
|
+
* How far things run past their walls, once an edit has been followed through: in the wall view, what's drawn on it;
|
|
183
|
+
* in the plan, the doors and windows (re-placed from their dimensions) and what's drawn on each wall face (brought up
|
|
184
|
+
* to date with the plan). Edits that make this grow are refused — a wall can't be made shorter than what's on it.
|
|
185
|
+
*/
|
|
186
|
+
export function overflow(sk = SK()) {
|
|
187
|
+
if (elevOpen())
|
|
188
|
+
return wallOverflow(sk);
|
|
189
|
+
if (sk !== planSK())
|
|
190
|
+
return {};
|
|
191
|
+
resolveOpenings(sk);
|
|
192
|
+
syncFaces(sk, true);
|
|
193
|
+
return planOverflow(sk);
|
|
194
|
+
}
|
|
195
|
+
export function fitState(sk = SK()) {
|
|
196
|
+
if (elevOpen() || sk !== planSK())
|
|
197
|
+
return { pos: snapshotPos(sk) };
|
|
198
|
+
return planFitState(sk);
|
|
199
|
+
}
|
|
200
|
+
/** Everything following a change to the plan through can change (see planFit), whichever view is open. */
|
|
201
|
+
export function planFitState(sk = planSK()) {
|
|
202
|
+
const pos = snapshotPos(sk);
|
|
203
|
+
const ops = {}, faces = {};
|
|
204
|
+
Object.assign(ops, snapOpenings(sk));
|
|
205
|
+
// (copied by hand: React Native has no structuredClone)
|
|
206
|
+
for (const [key, face] of Object.entries(sk.faces ?? {}))
|
|
207
|
+
faces[key] = { pos: snapshotPos(face), wall: face.wall && { u: [...face.wall.u], ends: [[...face.wall.ends[0]], [...face.wall.ends[1]]] } };
|
|
208
|
+
return { pos, ops, faces, marks: placedMarks() };
|
|
209
|
+
}
|
|
210
|
+
/** Each door and window as it is — its place, sill, width, dimensions: whatever an edit to it may change. */
|
|
211
|
+
function snapOpenings(sk) {
|
|
212
|
+
const out = {};
|
|
213
|
+
for (const [id, o] of Object.entries(sk.openings))
|
|
214
|
+
out[id] = JSON.stringify(o);
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
/** Put them back as they were — in place: what holds on to a door or window (a drag, a keypad) keeps the same one. */
|
|
218
|
+
function putOpenings(sk, snap) {
|
|
219
|
+
for (const [id, v] of Object.entries(snap)) {
|
|
220
|
+
const o = sk.openings[id];
|
|
221
|
+
if (!o)
|
|
222
|
+
continue;
|
|
223
|
+
const was = JSON.parse(v);
|
|
224
|
+
for (const k of Object.keys(o))
|
|
225
|
+
if (!(k in was))
|
|
226
|
+
delete o[k];
|
|
227
|
+
Object.assign(o, was);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export function restoreFit(sk, s) {
|
|
231
|
+
restorePos(sk, s.pos);
|
|
232
|
+
if (!s.ops)
|
|
288
233
|
return;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
234
|
+
putOpenings(sk, s.ops);
|
|
235
|
+
for (const [key, v] of Object.entries(s.faces ?? {})) {
|
|
236
|
+
const face = sk.faces?.[key];
|
|
237
|
+
if (!face)
|
|
238
|
+
continue;
|
|
239
|
+
restorePos(face, v.pos);
|
|
240
|
+
if (v.wall)
|
|
241
|
+
face.wall = v.wall;
|
|
242
|
+
else
|
|
243
|
+
delete face.wall;
|
|
244
|
+
}
|
|
245
|
+
setPlacedMarks(s.marks);
|
|
246
|
+
forgetSynced();
|
|
247
|
+
}
|
|
248
|
+
/** Limit a move of points (from where they started) so they all stay inside the wall, circles centred on them whole. */
|
|
249
|
+
export function keepInWall(sk, ids, start, dx, dy) {
|
|
250
|
+
const b = wallBounds();
|
|
251
|
+
if (!b || !ids.length)
|
|
252
|
+
return [dx, dy];
|
|
253
|
+
let lo = -Infinity, hi = Infinity, dlo = -Infinity, dhi = Infinity;
|
|
254
|
+
ids.forEach((id, i) => {
|
|
255
|
+
const r = Math.max(0, ...Object.values(sk.circles).filter(c => c.c === id).map(c => c.r)), p = start[i];
|
|
256
|
+
lo = Math.max(lo, b.x0 - (p.x - r));
|
|
257
|
+
hi = Math.min(hi, b.x1 - (p.x + r));
|
|
258
|
+
dlo = Math.max(dlo, b.y0 - (p.y - r));
|
|
259
|
+
dhi = Math.min(dhi, b.y1 - (p.y + r));
|
|
260
|
+
});
|
|
261
|
+
return [lo <= hi ? Math.min(Math.max(dx, lo), hi) : 0, dlo <= dhi ? Math.min(Math.max(dy, dlo), dhi) : 0];
|
|
296
262
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
263
|
+
export const PAST_WALL = 'That would run past the edge of the wall.';
|
|
264
|
+
/** A door or window moved or resized in a way that would shove what's tied to it (a cabinet) off its wall. */
|
|
265
|
+
export const PUSHES_OFF = 'That would push what’s tied to it past the edge of the wall.';
|
|
266
|
+
/**
|
|
267
|
+
* How far things run past their walls in the plan — doors and windows past their walls' ends, what's on each wall
|
|
268
|
+
* face past its corners, floor and ceiling — once a change to the plan has been followed through (the doors and
|
|
269
|
+
* windows re-placed from their dimensions, the faces brought up to date). Whichever view is open: a door or window
|
|
270
|
+
* changed from the wall view (its width, its distance from a corner) is the plan's.
|
|
271
|
+
*/
|
|
272
|
+
export function planFit(sk = planSK()) {
|
|
273
|
+
resolveOpenings(sk);
|
|
274
|
+
syncFaces(sk, true);
|
|
275
|
+
return planOverflow(sk);
|
|
309
276
|
}
|
|
310
|
-
/**
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
return best;
|
|
326
|
-
}
|
|
327
|
-
/** What's under a screen point: chips first, then drawn lines, then openings and corners. */
|
|
328
|
-
function pickAt(x, y) {
|
|
329
|
-
const h = hitAt(x, y);
|
|
330
|
-
if (h && h.act.k !== 'body' && h.act.k !== 'corner')
|
|
331
|
-
return h.act;
|
|
332
|
-
const l = lineAt(x, y);
|
|
333
|
-
if (l)
|
|
334
|
-
return { k: 'eline', id: l };
|
|
335
|
-
return h ? h.act : null;
|
|
336
|
-
}
|
|
337
|
-
/** Set a drawn line's length (its first end stays put). */
|
|
338
|
-
function editLineLength(id) {
|
|
339
|
-
const sk = SK(), f = elev.frame, ln = f && elevLines(sk, f).find(l => l.id === id);
|
|
340
|
-
if (!ln)
|
|
341
|
-
return;
|
|
342
|
-
const cur = Math.hypot(ln.b[0] - ln.a[0], ln.b[1] - ln.a[1]);
|
|
343
|
-
openKeypad({
|
|
344
|
-
title: ln.guide ? 'Guide line length' : 'Line length', initial: fmtLen(cur), selectAll: true, laser: true,
|
|
345
|
-
onDone: v => {
|
|
346
|
-
if (v <= 0)
|
|
347
|
-
return;
|
|
348
|
-
pushHistory();
|
|
349
|
-
const k = v / (cur || 1);
|
|
350
|
-
ln.b = [ln.a[0] + (ln.b[0] - ln.a[0]) * k, ln.a[1] + (ln.b[1] - ln.a[1]) * k];
|
|
351
|
-
afterEdit();
|
|
352
|
-
},
|
|
353
|
-
});
|
|
277
|
+
/**
|
|
278
|
+
* Make a change to the plan's doors and windows — a width, a distance entered — only if it pushes nothing on a wall
|
|
279
|
+
* past its edges (a cabinet tied to a window would otherwise be shoved off the wall). Taken back, and said, if it
|
|
280
|
+
* would. Returns whether it was made.
|
|
281
|
+
*/
|
|
282
|
+
export function withinWalls(change) {
|
|
283
|
+
const sk = planSK(), was = planFit(sk), held = planFitState(sk);
|
|
284
|
+
change();
|
|
285
|
+
if (!pushedOut(was, planFit(sk)))
|
|
286
|
+
return true;
|
|
287
|
+
restoreFit(sk, held);
|
|
288
|
+
planFit(sk);
|
|
289
|
+
toast(PUSHES_OFF, { warn: true });
|
|
290
|
+
return false;
|
|
354
291
|
}
|
|
355
292
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
293
|
+
* Slide a chain of doors and windows along their wall in the plan to `delta` from where it started, no further than
|
|
294
|
+
* what's drawn on the wall's faces can follow without being pushed off them (`base`: what ran past its wall when the
|
|
295
|
+
* slide began; `from`: the last slide that pushed nothing further out). The faces follow as it goes. Returns the
|
|
296
|
+
* slide made.
|
|
358
297
|
*/
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
298
|
+
export function slideWithinWalls(sk, group, delta, from, base) {
|
|
299
|
+
const held = planFitState(sk);
|
|
300
|
+
const tryAt = (v) => { applySlide(sk, group, v); return !pushedOut(base, planFit(sk)); };
|
|
301
|
+
if (tryAt(delta))
|
|
302
|
+
return delta;
|
|
303
|
+
let lo = 0, hi = 1;
|
|
304
|
+
for (let i = 0; i < 8; i++) {
|
|
305
|
+
const m = (lo + hi) / 2;
|
|
306
|
+
restoreFit(sk, held);
|
|
307
|
+
if (tryAt(from + (delta - from) * m))
|
|
308
|
+
lo = m;
|
|
365
309
|
else
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
310
|
+
hi = m;
|
|
311
|
+
}
|
|
312
|
+
// none of the way: back exactly as it was (solved again from there, the faces can land a hair apart — past the wall)
|
|
313
|
+
restoreFit(sk, held);
|
|
314
|
+
if (lo === 0)
|
|
315
|
+
return from;
|
|
316
|
+
const to = from + (delta - from) * lo;
|
|
317
|
+
tryAt(to);
|
|
318
|
+
return to;
|
|
319
|
+
}
|
|
320
|
+
/** In the plan: a wall made shorter than what's on it. */
|
|
321
|
+
export const TOO_SHORT = 'The wall can’t be shorter than what’s on it. Move or remove things on it first.';
|
|
322
|
+
/** The right message for an edit refused by overflow(). */
|
|
323
|
+
export const pastWallMsg = () => (elevOpen() ? PAST_WALL : TOO_SHORT);
|
|
324
|
+
// ---- the dimension chips ----
|
|
325
|
+
/**
|
|
326
|
+
* What can be tapped in the wall view, in canvas pixels, for tests: the doors' and windows' bodies, cabinets, the
|
|
327
|
+
* wall's corners (its end edges), and the dimension chips as last drawn (with their look).
|
|
328
|
+
*/
|
|
329
|
+
export function elevHits() {
|
|
330
|
+
const f = elev.frame;
|
|
331
|
+
if (!elevOpen() || !f)
|
|
332
|
+
return [];
|
|
333
|
+
const T = mainT(), out = [];
|
|
334
|
+
for (const o of elevOpenings(planSK(), f)) {
|
|
335
|
+
const [x0, y0] = S(T, o.u0, o.head), [x1, y1] = S(T, o.u1, o.sill);
|
|
336
|
+
out.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0, act: { k: 'body', op: o.id } });
|
|
337
|
+
}
|
|
338
|
+
for (const id of Object.keys(SK().cabs ?? {})) {
|
|
339
|
+
const b = cabBox(SK(), id);
|
|
340
|
+
if (!b)
|
|
341
|
+
continue;
|
|
342
|
+
const [x0, y0] = S(T, b.u0, b.z1), [x1, y1] = S(T, b.u1, b.z0);
|
|
343
|
+
out.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0, act: { k: 'cab', id } });
|
|
344
|
+
}
|
|
345
|
+
for (const [u, end] of [[f.face[0], f.leftEnd], [f.face[1], f.leftEnd === 'a' ? 'b' : 'a']]) {
|
|
346
|
+
const [x, yTop] = S(T, u, f.H), [, yBot] = S(T, u, 0);
|
|
347
|
+
out.push({ x: x - 10, y: yTop, w: 20, h: yBot - yTop, act: { k: 'corner', end } });
|
|
348
|
+
}
|
|
349
|
+
for (const h of ed.hitChips)
|
|
350
|
+
if (h.kind === 'elev')
|
|
351
|
+
out.push({ x: h.x, y: h.y, w: h.w, h: h.h, act: JSON.parse(h.id), look: h.part });
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
export const elevAct = (id) => JSON.parse(id);
|
|
355
|
+
/** A chip's name, as the bar shows it. */
|
|
356
|
+
export function elevChipName(a) {
|
|
357
|
+
if (a.k === 'gap')
|
|
358
|
+
return a.title;
|
|
359
|
+
if (a.k === 'len')
|
|
360
|
+
return 'Wall length · set in the floor plan';
|
|
361
|
+
if (a.k === 'wallH')
|
|
362
|
+
return 'Wall height';
|
|
363
|
+
if (a.k === 'pos')
|
|
364
|
+
return a.title;
|
|
365
|
+
if (a.k === 'fdim')
|
|
366
|
+
return 'Dimension';
|
|
367
|
+
if (a.k === 'depth')
|
|
368
|
+
return 'Cabinet depth · out from the wall';
|
|
369
|
+
const o = planSK().openings[a.op], n = o ? (o.type === 'door' ? 'Door' : 'Window') : '';
|
|
370
|
+
return a.k === 'w' ? n + ' width' : a.k === 'sill' ? n + ' sill height' : n + ' height';
|
|
371
|
+
}
|
|
372
|
+
/** The yellow + on a selected chip: its keypad. */
|
|
373
|
+
export function editElevChip(a) {
|
|
374
|
+
if (a.k === 'w')
|
|
375
|
+
editOpeningDim(a.op, 'w');
|
|
376
|
+
else if (a.k === 'sill')
|
|
377
|
+
editSill(a.op);
|
|
378
|
+
else if (a.k === 'h')
|
|
379
|
+
editOpeningHeight(a.op);
|
|
380
|
+
else if (a.k === 'wallH')
|
|
381
|
+
editWallHeight(elev.lid);
|
|
382
|
+
else if (a.k === 'pos')
|
|
383
|
+
editElevationPosition(a.op, a.edge, a.ref, a.title);
|
|
384
|
+
else if (a.k === 'gap')
|
|
385
|
+
dimensionFace(a.kind, a.a, a.b, a.title);
|
|
386
|
+
else if (a.k === 'depth')
|
|
387
|
+
editCabinetDepth(a.cab);
|
|
388
|
+
else if (a.k === 'len')
|
|
389
|
+
goToPlan();
|
|
377
390
|
}
|
|
378
|
-
/**
|
|
379
|
-
function objPlan() {
|
|
380
|
-
const sk = SK(), f = elev.frame, objs = elev.objs;
|
|
381
|
-
if (!f || !objs.length)
|
|
382
|
-
return null;
|
|
383
|
-
const ops = elevOpenings(sk, f), seen = (id) => ops.find(o => o.id === id);
|
|
384
|
-
const label = (id) => (sk.openings[id].type === 'door' ? 'Door' : 'Window');
|
|
385
|
-
const opsSel = objs.filter((o) => o.k === 'op').map(o => seen(o.id)).filter((o) => !!o);
|
|
386
|
-
const corners = objs.filter((o) => o.k === 'corner');
|
|
387
|
-
const lines = objs.filter((o) => o.k === 'line');
|
|
388
|
-
if (objs.length === 1 && lines.length === 1) {
|
|
389
|
-
const ln = elevLines(sk, f).find(l => l.id === lines[0].id);
|
|
390
|
-
if (!ln)
|
|
391
|
-
return null;
|
|
392
|
-
return { name: (ln.guide ? 'Guide line ' : 'Line ') + fmtLen(Math.hypot(ln.b[0] - ln.a[0], ln.b[1] - ln.a[1])), run: () => editLineLength(ln.id) };
|
|
393
|
-
}
|
|
394
|
-
if (lines.length)
|
|
395
|
-
return { name: objs.length + ' selected', run: () => toast('Select one line to set its length.') };
|
|
396
|
-
if (objs.length === 1 && opsSel.length === 1)
|
|
397
|
-
return { name: label(opsSel[0].id), run: () => editOpeningDim(opsSel[0].id, 'w') };
|
|
398
|
-
if (objs.length === 1 && corners.length === 1)
|
|
399
|
-
return { name: 'Corner', run: () => toast('Select a door or window too (Shift-tap), then + for the distance between them.') };
|
|
400
|
-
if (objs.length === 2 && opsSel.length === 2) {
|
|
401
|
-
const [l, r] = [...opsSel].sort((p, q) => p.u0 - q.u0), t = pickBetween(sk, l.id, l.e1, r.id, r.e0);
|
|
402
|
-
return { name: label(l.id) + ' + ' + label(r.id).toLowerCase(), run: () => editElevationPosition(t.op, t.edge, t.ref, 'Space between') };
|
|
403
|
-
}
|
|
404
|
-
if (objs.length === 2 && opsSel.length === 1 && corners.length === 1) {
|
|
405
|
-
const o = opsSel[0], end = corners[0].end, leftCorner = end === f.leftEnd;
|
|
406
|
-
const ref = { face: end, line: f.lid, side: f.side };
|
|
407
|
-
return { name: label(o.id) + ' + corner', run: () => editElevationPosition(o.id, leftCorner ? o.e0 : o.e1, ref, 'Distance from the corner') };
|
|
408
|
-
}
|
|
409
|
-
if (corners.length === 2 && objs.length === 2)
|
|
410
|
-
return { name: 'Both corners', run: () => toast('The wall’s length is set in the plan. Go back and use the wall’s +.') };
|
|
411
|
-
return { name: objs.length + ' selected', run: () => toast('Select two things — two openings, or an opening and a corner — to dimension between them.') };
|
|
412
|
-
}
|
|
413
|
-
/** The bar: the selected dimension, or the selected objects (with Height / Sill for a single opening). */
|
|
414
|
-
export function elevBar(dimAct) {
|
|
415
|
-
const plan = dimAct ? null : objPlan();
|
|
416
|
-
if (!dimAct && !plan)
|
|
417
|
-
return null;
|
|
418
|
-
if (dimAct && dimAct.k === 'len')
|
|
419
|
-
return { label: 'Wall length · set in the floor plan', plus: false, actions: [{ icon: 'back', label: 'Go to plan', run: goToPlan }] };
|
|
420
|
-
if (dimAct)
|
|
421
|
-
return { label: dimName(dimAct), plus: true, actions: [] };
|
|
422
|
-
const actions = [];
|
|
423
|
-
const one = elev.objs.length === 1 && elev.objs[0].k === 'op' ? SK().openings[elev.objs[0].id] : null;
|
|
424
|
-
if (one) {
|
|
425
|
-
const id = elev.objs[0].id;
|
|
426
|
-
actions.push({ icon: 'v', label: 'Height', run: () => editOpeningHeight(id) });
|
|
427
|
-
if (one.type === 'window')
|
|
428
|
-
actions.push({ icon: 'h', label: 'Sill', run: () => editSill(id) });
|
|
429
|
-
}
|
|
430
|
-
return { label: plan.name, plus: true, actions };
|
|
431
|
-
}
|
|
432
|
-
const dimName = (a) => {
|
|
433
|
-
const sk = SK(), o = 'op' in a ? sk.openings[a.op] : null, n = o ? (o.type === 'door' ? 'Door' : 'Window') : '';
|
|
434
|
-
return a.k === 'w' ? n + ' width' : a.k === 'sill' ? n + ' sill height' : a.k === 'h' ? n + ' height' : a.k === 'wallH' ? 'Wall height'
|
|
435
|
-
: a.k === 'pos' ? a.title : '';
|
|
436
|
-
};
|
|
437
|
-
/** The window whose locked sill height a selected chip shows, if any. */
|
|
391
|
+
/** The window whose locked sill height a chip shows, if any. */
|
|
438
392
|
function lockedSill(a) {
|
|
439
|
-
if (
|
|
393
|
+
if (a.k !== 'sill')
|
|
440
394
|
return null;
|
|
441
|
-
const o =
|
|
395
|
+
const o = planSK().openings[a.op];
|
|
442
396
|
return o && o.sillLocked ? o : null;
|
|
443
397
|
}
|
|
444
|
-
/** The stored door/window dimension a
|
|
398
|
+
/** The stored door/window dimension a position chip shows, if any. */
|
|
445
399
|
function storedDim(a) {
|
|
446
|
-
if (
|
|
400
|
+
if (a.k !== 'pos')
|
|
447
401
|
return null;
|
|
448
|
-
const o =
|
|
402
|
+
const o = planSK().openings[a.op], d = o && (o.dims || []).find(x => x.edge === a.edge && sameRef(x.ref, a.ref));
|
|
449
403
|
return d ? { op: a.op, d } : null;
|
|
450
404
|
}
|
|
451
|
-
/**
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
405
|
+
/** Whether the trash does something for a selected chip: frees a locked one, or removes a reference. */
|
|
406
|
+
export const elevChipDeletable = (a) => !!storedDim(a) || !!lockedSill(a);
|
|
407
|
+
/** The trash on a selected chip: a locked door/window dimension is freed (it stays as a reference), a reference is removed, a locked sill is freed. */
|
|
408
|
+
export function deleteElevChip(a) {
|
|
409
|
+
// a cabinet's entered depth: back to the standard one (grey), as an entered size is wiped
|
|
410
|
+
if (a.k === 'depth') {
|
|
411
|
+
const cb = SK().cabs?.[a.cab];
|
|
412
|
+
if (!cb)
|
|
413
|
+
return;
|
|
414
|
+
if (!cb.depthSet) {
|
|
415
|
+
toast('That’s the standard depth. Tap + to enter one.', { warn: true });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
460
418
|
pushHistory();
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (key && sk.elev?.[key])
|
|
464
|
-
sk.elev[key] = sk.elev[key].filter(l => !doomedLines.includes(l.id));
|
|
465
|
-
elev.objs = elev.objs.filter(o => o.k === 'corner');
|
|
466
|
-
ed.sel = ed.sel.filter(x => !(x.type === 'opening' && doomed.includes(x.id)));
|
|
419
|
+
cb.depth = CAB.d;
|
|
420
|
+
cb.depthSet = false;
|
|
467
421
|
afterEdit();
|
|
468
|
-
|
|
469
|
-
toast(what + ' deleted', { action: { label: 'Undo', fn: () => undo() } });
|
|
422
|
+
toast('Depth cleared — back to the standard ' + fmtLen(CAB.d) + '.', { action: { label: 'Undo', fn: () => undo() } });
|
|
470
423
|
return;
|
|
471
424
|
}
|
|
472
|
-
const a = elev.active ? JSON.parse(elev.active) : null, s = storedDim(a);
|
|
473
425
|
const ls = lockedSill(a);
|
|
474
426
|
if (ls) {
|
|
475
427
|
pushHistory();
|
|
@@ -478,12 +430,12 @@ export function deleteSelectedElevDim() {
|
|
|
478
430
|
toast('Sill height freed — drag the window to raise or lower it.');
|
|
479
431
|
return;
|
|
480
432
|
}
|
|
433
|
+
const s = storedDim(a);
|
|
481
434
|
if (!s) {
|
|
482
|
-
|
|
483
|
-
toast('Only locked dimensions and door/window position dimensions can be deleted here.');
|
|
435
|
+
toast('Only locked dimensions and door/window position dimensions can be deleted here.', { warn: true });
|
|
484
436
|
return;
|
|
485
437
|
}
|
|
486
|
-
const o =
|
|
438
|
+
const o = planSK().openings[s.op], name = o.type === 'door' ? 'door' : 'window';
|
|
487
439
|
pushHistory();
|
|
488
440
|
if (s.d.driving) {
|
|
489
441
|
s.d.driving = false;
|
|
@@ -492,258 +444,502 @@ export function deleteSelectedElevDim() {
|
|
|
492
444
|
}
|
|
493
445
|
else {
|
|
494
446
|
o.dims = (o.dims || []).filter(x => x !== s.d);
|
|
447
|
+
ed.sel = [];
|
|
495
448
|
afterEdit();
|
|
496
449
|
toast('Dimension removed');
|
|
497
450
|
}
|
|
498
451
|
}
|
|
499
452
|
/** Back to the floor plan with this wall selected, ready for its + (where its length is set). */
|
|
500
453
|
function goToPlan() {
|
|
501
|
-
const lid = elev.lid;
|
|
502
454
|
closeElevation();
|
|
503
|
-
if (!lid)
|
|
504
|
-
return;
|
|
505
|
-
ed.sel = [{ type: 'line', id: lid }];
|
|
506
|
-
updateCtx();
|
|
507
|
-
requestDraw();
|
|
508
455
|
toast('Tap + to change the wall’s length.');
|
|
509
456
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
457
|
+
// ---- doors and windows on the wall ----
|
|
458
|
+
/** Where a new door/window would go for a pointer at (u, z): centre s along the wall, and the window's sill. */
|
|
459
|
+
export function openingAt(type, u, z, tol = 0) {
|
|
460
|
+
const f = elev.frame, w = NEW_W;
|
|
461
|
+
if (f.L < w + 2)
|
|
462
|
+
return null;
|
|
463
|
+
// snapped (Object snapping) as a door or window dragged on the wall is: its sides to the corners and the other
|
|
464
|
+
// doors', windows' and cabinets' sides; a window's sill and head to the floor, the ceiling, cabinet tops and other
|
|
465
|
+
// windows' (a door sits on the floor)
|
|
466
|
+
const raw = type === 'door' ? 0 : z - WINDOW_H / 2, h = type === 'door' ? DOOR_H : WINDOW_H;
|
|
467
|
+
const [du, dz] = tol > 0 ? snapObjectMove({}, { u0: u - w / 2, u1: u + w / 2, z0: raw, z1: raw + h }, 0, 0, tol) : [0, 0];
|
|
468
|
+
const at = clamp(toU(f, u + du), w / 2, f.L - w / 2);
|
|
469
|
+
const q = 1 / settings.denom, sill = type === 'door' ? 0 : clamp(Math.round((raw + dz) / q) * q, 0, Math.max(0, f.H - WINDOW_H));
|
|
470
|
+
return { at, sill };
|
|
471
|
+
}
|
|
472
|
+
function placeOpening(type, u, z, tol = 0) {
|
|
473
|
+
const sk = planSK(), f = elev.frame, spot = openingAt(type, u, z, tol);
|
|
474
|
+
if (!spot) {
|
|
475
|
+
toast('That wall is too short for a ' + fmtLen(NEW_W) + ' ' + type + '.', { warn: true });
|
|
515
476
|
return;
|
|
516
477
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
editWallHeight(elev.lid);
|
|
525
|
-
else if (a.k === 'pos')
|
|
526
|
-
editElevationPosition(a.op, a.edge, a.ref, a.title);
|
|
527
|
-
else if (a.k === 'len')
|
|
528
|
-
goToPlan();
|
|
478
|
+
pushHistory();
|
|
479
|
+
const id = uid('o');
|
|
480
|
+
// a door swings toward the side you're looking from
|
|
481
|
+
sk.openings[id] = { line: f.lid, at: spot.at, w: NEW_W, wSet: false, type, flip: f.side !== 1, ...(type === 'window' ? { sill: spot.sill } : {}) };
|
|
482
|
+
// (placed, not selected: tap it to size it)
|
|
483
|
+
ed.sel = [];
|
|
484
|
+
afterEdit();
|
|
529
485
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
v.cu = u - (sx - elev.W / 2) / v.s;
|
|
542
|
-
v.cz = z + (sy - elev.H / 2) / v.s;
|
|
543
|
-
renderElevation();
|
|
486
|
+
const nameOf = (op) => (planSK().openings[op].type === 'door' ? 'door' : 'window');
|
|
487
|
+
/** What dragging an opening will do, given its locks. */
|
|
488
|
+
function dragHint(op) {
|
|
489
|
+
const sk = planSK(), o = sk.openings[op], locked = !slideGroup(sk, op), n = nameOf(op);
|
|
490
|
+
if (o.type === 'door')
|
|
491
|
+
return locked ? 'This door is locked by a dimension. Change the dimension, or make it a reference, to move it.' : 'Drag the door to slide it along the floor.';
|
|
492
|
+
if (locked && o.sillLocked)
|
|
493
|
+
return 'This window is locked along the wall and in height. Select a blue dimension and press Delete to free it.';
|
|
494
|
+
if (o.sillLocked)
|
|
495
|
+
return 'This window’s height above the floor is locked — it only slides along the wall. Delete the blue sill dimension to free it.';
|
|
496
|
+
return locked ? 'This window is locked along the wall by a dimension — drag it to raise or lower it.' : 'Drag the ' + n + ' to move it along the wall or up and down.';
|
|
544
497
|
}
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
export function elevPointerDown(e) {
|
|
552
|
-
const p = { x: e.x, y: e.y };
|
|
553
|
-
gp.set(e.id, p);
|
|
554
|
-
pinch = null;
|
|
555
|
-
mid = null;
|
|
556
|
-
down = gp.size === 1 ? { ...p, moved: false } : null;
|
|
557
|
-
panBtn = e.button === 1;
|
|
558
|
-
// a second finger while a press is drawing a line: it's a pinch, not a line
|
|
559
|
-
if (gp.size > 1 && pressStartedLine) {
|
|
560
|
-
elev.lineStart = null;
|
|
561
|
-
elev.cursor = null;
|
|
562
|
-
renderElevation();
|
|
563
|
-
}
|
|
564
|
-
pressStartedLine = false;
|
|
565
|
-
drag = null;
|
|
566
|
-
if (!down || panBtn || !elev.view)
|
|
567
|
-
return;
|
|
568
|
-
if (drawing()) {
|
|
569
|
-
// first point now (drag to draw), or the second point of a tap-tap line
|
|
570
|
-
const sp = snapUZ(p.x, p.y, elev.lineStart);
|
|
571
|
-
elev.cursor = sp;
|
|
572
|
-
if (!elev.lineStart) {
|
|
573
|
-
elev.lineStart = sp.p;
|
|
574
|
-
pressStartedLine = true;
|
|
575
|
-
renderElevation();
|
|
576
|
-
}
|
|
577
|
-
else
|
|
578
|
-
finishLine(sp.p);
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
if (elev.tool !== 'select')
|
|
582
|
-
return; // door/window: placed on release (a drag pans instead)
|
|
583
|
-
const h = hitAt(p.x, p.y);
|
|
584
|
-
drag = h && h.act.k === 'body' ? startDrag(h.act.op, e.cx ?? e.x, e.cy ?? e.y) : null;
|
|
498
|
+
export function startOpeningDrag(op) {
|
|
499
|
+
const sk = planSK(), o = sk.openings[op];
|
|
500
|
+
if (!o)
|
|
501
|
+
return null;
|
|
502
|
+
const group = slideGroup(sk, op), e = elev.frame ? elevOpenings(sk, elev.frame).find(x => x.id === op) : null;
|
|
503
|
+
return { op, group, sill: openingHeights(o).sill, lockedH: !group, started: false, box: e ? { u0: e.u0, u1: e.u1, z0: e.sill, z1: e.head } : null };
|
|
585
504
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
if (!elev.view)
|
|
602
|
-
return;
|
|
603
|
-
if (down && !down.moved) {
|
|
604
|
-
if (Math.hypot(p.x - down.x, p.y - down.y) < 6) {
|
|
605
|
-
if (drawing()) {
|
|
606
|
-
elev.cursor = snapUZ(p.x, p.y, elev.lineStart);
|
|
607
|
-
renderElevation();
|
|
608
|
-
}
|
|
609
|
-
return;
|
|
505
|
+
/**
|
|
506
|
+
* Move a drag to (du, dz) inches from where it started, snapping to corners and the other objects (within `tol`
|
|
507
|
+
* inches). Returns false if it can't move at all (a locked door).
|
|
508
|
+
*/
|
|
509
|
+
export function moveOpeningDrag(d, du, dz, tol = 0) {
|
|
510
|
+
if (d.box && tol > 0)
|
|
511
|
+
[du, dz] = snapObjectMove({ op: d.op }, d.box, du, dz, tol);
|
|
512
|
+
const sk = planSK(), o = sk.openings[d.op], f = elev.frame, g = o && openingSpan(sk, o), l = o && sk.lines[o.line];
|
|
513
|
+
if (!o || !f || !g || !l)
|
|
514
|
+
return false;
|
|
515
|
+
if (!d.started) {
|
|
516
|
+
if (d.lockedH && (o.type === 'door' || !!o.sillLocked)) {
|
|
517
|
+
toast(dragHint(d.op), { warn: true });
|
|
518
|
+
return false;
|
|
610
519
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (drawing() && gp.size === 1 && !panBtn) {
|
|
615
|
-
elev.cursor = snapUZ(p.x, p.y, elev.lineStart);
|
|
616
|
-
renderElevation();
|
|
617
|
-
return;
|
|
520
|
+
if (d.lockedH || o.sillLocked)
|
|
521
|
+
toast(dragHint(d.op), { warn: true });
|
|
522
|
+
d.started = true;
|
|
618
523
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
524
|
+
const face = faceEdit.key ? SK() : null;
|
|
525
|
+
/** The opening at (u, z) from where it started, and what's drawn on the face following it (cabinets tied to it). */
|
|
526
|
+
const place = (u, z) => {
|
|
527
|
+
// (a slide is measured from where the chain started)
|
|
528
|
+
if (d.group)
|
|
529
|
+
applySlide(sk, d.group, f.mirror ? -u : u);
|
|
530
|
+
if (o.type === 'window' && !o.sillLocked) {
|
|
531
|
+
// kept between the floor and the ceiling — but one already past them (pushed there before) isn't yanked back
|
|
532
|
+
// at the first touch: that dragged whatever's tied to it along, through the floor
|
|
533
|
+
const { sill, head } = openingHeights(o), q = 1 / settings.denom;
|
|
534
|
+
o.sill = clamp(Math.round((d.sill + z) / q) * q, Math.min(0, d.sill), Math.max(0, wallHeight(l) - (head - sill), d.sill));
|
|
625
535
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
endDrag();
|
|
636
|
-
drag = null;
|
|
637
|
-
} // a second finger: stop dragging, pinch instead
|
|
638
|
-
if (gp.size === 1) {
|
|
639
|
-
elev.view.cu -= (p.x - prev.x) / elev.view.s;
|
|
640
|
-
elev.view.cz += (p.y - prev.y) / elev.view.s;
|
|
641
|
-
renderElevation();
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
const [a, b] = [...gp.values()], d = Math.hypot(a.x - b.x, a.y - b.y);
|
|
645
|
-
if (!pinch) {
|
|
646
|
-
pinch = { d, s: elev.view.s };
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
zoomElevAt((a.x + b.x) / 2, (a.y + b.y) / 2, (pinch.s * (d / pinch.d)) / elev.view.s);
|
|
650
|
-
}
|
|
651
|
-
export function elevPointerUp(e) {
|
|
652
|
-
const tapped = down && !down.moved && gp.size === 1;
|
|
653
|
-
gp.delete(e.id);
|
|
654
|
-
pinch = null;
|
|
655
|
-
if (panBtn) {
|
|
656
|
-
panBtn = false;
|
|
657
|
-
down = null;
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
if (drawing()) {
|
|
661
|
-
// a drag from the first point draws the line on release; a tap waits for the second tap
|
|
662
|
-
if (pressStartedLine && down?.moved && elev.lineStart)
|
|
663
|
-
finishLine(snapUZ(e.x, e.y, elev.lineStart).p);
|
|
664
|
-
pressStartedLine = false;
|
|
665
|
-
down = null;
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
if ((elev.tool === 'door' || elev.tool === 'window') && tapped && elev.view) {
|
|
669
|
-
const [u, z] = toWorld(down.x, down.y);
|
|
670
|
-
if (z < -2 || z > (elev.frame?.H ?? 0) + 2)
|
|
671
|
-
toast('Tap on the wall to place it.');
|
|
672
|
-
else
|
|
673
|
-
placeOpening(elev.tool, u, z);
|
|
674
|
-
down = null;
|
|
675
|
-
return;
|
|
536
|
+
resolveOpeningsNow();
|
|
537
|
+
// what's drawn on the face follows the opening's corners as it moves
|
|
538
|
+
if (faceEdit.key)
|
|
539
|
+
syncFace(sk, faceEdit.key);
|
|
540
|
+
};
|
|
541
|
+
if (!face) {
|
|
542
|
+
place(du, dz);
|
|
543
|
+
requestDraw();
|
|
544
|
+
return true;
|
|
676
545
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
546
|
+
// what's tied to it (a cabinet a set distance below a window) can't be pushed off the wall — past its floor,
|
|
547
|
+
// ceiling or ends: the opening goes as far as that allows and stops there, as a cabinet dragged to the edge does
|
|
548
|
+
d.base ??= wallOverflow(face);
|
|
549
|
+
const from = d.last ?? { du: 0, dz: 0 }, held = holdFace(sk, face);
|
|
550
|
+
const at = (t) => ({ du: from.du + (du - from.du) * t, dz: from.dz + (dz - from.dz) * t });
|
|
551
|
+
const fits = () => !pushedOut(d.base, wallOverflow(face));
|
|
552
|
+
place(du, dz);
|
|
553
|
+
let to = { du, dz };
|
|
554
|
+
if (!fits()) {
|
|
555
|
+
let lo = 0, hi = 1;
|
|
556
|
+
for (let i = 0; i < 8; i++) {
|
|
557
|
+
const m = (lo + hi) / 2;
|
|
558
|
+
putBack(sk, face, held);
|
|
559
|
+
place(at(m).du, at(m).dz);
|
|
560
|
+
if (fits())
|
|
561
|
+
lo = m;
|
|
562
|
+
else
|
|
563
|
+
hi = m;
|
|
684
564
|
}
|
|
565
|
+
putBack(sk, face, held);
|
|
566
|
+
// (none of the way: left exactly as it was — solved again, it can land a hair apart)
|
|
567
|
+
to = at(lo);
|
|
568
|
+
if (lo > 0)
|
|
569
|
+
place(to.du, to.dz);
|
|
685
570
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
down = null;
|
|
690
|
-
}
|
|
691
|
-
export function elevPointerCancel(e) {
|
|
692
|
-
gp.delete(e.id);
|
|
693
|
-
pinch = null;
|
|
694
|
-
if (drag) {
|
|
695
|
-
endDrag();
|
|
696
|
-
drag = null;
|
|
697
|
-
}
|
|
698
|
-
if (pressStartedLine) {
|
|
699
|
-
elev.lineStart = null;
|
|
700
|
-
pressStartedLine = false;
|
|
701
|
-
}
|
|
571
|
+
d.last = to;
|
|
572
|
+
requestDraw();
|
|
573
|
+
return true;
|
|
702
574
|
}
|
|
703
|
-
/**
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
elev.cursor = null;
|
|
707
|
-
renderElevation();
|
|
708
|
-
}
|
|
575
|
+
/** The doors' and windows' places in the plan and what's drawn on the face, to put back if a move is taken back. */
|
|
576
|
+
function holdFace(sk, face) {
|
|
577
|
+
return { ops: snapOpenings(sk), pos: snapshotPos(face), marks: placedMarks() };
|
|
709
578
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
579
|
+
function putBack(sk, face, h) {
|
|
580
|
+
putOpenings(sk, h.ops);
|
|
581
|
+
restorePos(face, h.pos);
|
|
582
|
+
setPlacedMarks(h.marks);
|
|
583
|
+
forgetSynced();
|
|
715
584
|
}
|
|
716
585
|
/**
|
|
717
|
-
*
|
|
718
|
-
*
|
|
586
|
+
* Where a door or window sliding along its wall in the plan can snap its edges: the wall's ends, its corners on both
|
|
587
|
+
* sides, and the edges of the other doors and windows on it — not those sliding with it (locked to it in a chain).
|
|
588
|
+
* Empty when Object snapping is off.
|
|
589
|
+
*/
|
|
590
|
+
/**
|
|
591
|
+
* Where a door or window placed on wall `lid` in the plan snaps its edges (Object snapping): the wall's ends, its
|
|
592
|
+
* corners on both sides, and the edges of the doors and windows already on it — as one slid along it does.
|
|
593
|
+
*/
|
|
594
|
+
export function placeTargets(sk, lid) {
|
|
595
|
+
const l = sk.lines[lid];
|
|
596
|
+
if (!l || !settings.layers.objSnap)
|
|
597
|
+
return [];
|
|
598
|
+
const A = sk.points[l.a], B = sk.points[l.b], out = [0, Math.hypot(B.x - A.x, B.y - A.y)];
|
|
599
|
+
for (const side of [1, -1]) {
|
|
600
|
+
const wf = wallFace(sk, lid, side);
|
|
601
|
+
if (wf)
|
|
602
|
+
out.push(wf.s0, wf.s1);
|
|
603
|
+
}
|
|
604
|
+
for (const x of Object.values(sk.openings)) {
|
|
605
|
+
if (x.line !== lid)
|
|
606
|
+
continue;
|
|
607
|
+
const g = openingSpan(sk, x);
|
|
608
|
+
if (g)
|
|
609
|
+
out.push(edgeS(x, g.w, 's0'), edgeS(x, g.w, 's1'));
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
export function slideTargets(sk, op, group) {
|
|
614
|
+
const o = sk.openings[op], l = o && sk.lines[o.line];
|
|
615
|
+
if (!o || !l || !settings.layers.objSnap)
|
|
616
|
+
return [];
|
|
617
|
+
// which move with it: slide the chain a little and see
|
|
618
|
+
const before = Object.fromEntries(Object.entries(sk.openings).map(([id, x]) => [id, x.at]));
|
|
619
|
+
applySlide(sk, group, group.hi > 0.25 ? 0.25 : group.lo < -0.25 ? -0.25 : (group.hi || group.lo));
|
|
620
|
+
resolveOpeningsNow();
|
|
621
|
+
const moving = new Set(Object.keys(sk.openings).filter(id => Math.abs(sk.openings[id].at - before[id]) > 1e-9));
|
|
622
|
+
applySlide(sk, group, 0);
|
|
623
|
+
resolveOpeningsNow();
|
|
624
|
+
const A = sk.points[l.a], B = sk.points[l.b], out = [0, Math.hypot(B.x - A.x, B.y - A.y)];
|
|
625
|
+
for (const side of [1, -1]) {
|
|
626
|
+
const wf = wallFace(sk, o.line, side);
|
|
627
|
+
if (wf)
|
|
628
|
+
out.push(wf.s0, wf.s1);
|
|
629
|
+
}
|
|
630
|
+
for (const [id, x] of Object.entries(sk.openings)) {
|
|
631
|
+
if (x.line !== o.line || moving.has(id) || id === op)
|
|
632
|
+
continue;
|
|
633
|
+
const g = openingSpan(sk, x);
|
|
634
|
+
if (g)
|
|
635
|
+
out.push(edgeS(x, g.w, 's0'), edgeS(x, g.w, 's1'));
|
|
636
|
+
}
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
639
|
+
/** A slide `delta` of a door or window (centre `at0`, width `w`, when the drag began) pulled so an edge meets the nearest target within `tol`. */
|
|
640
|
+
export function snapSlide(at0, w, delta, targets, tol) {
|
|
641
|
+
let best = null;
|
|
642
|
+
for (const e of [at0 + delta - w / 2, at0 + delta + w / 2])
|
|
643
|
+
for (const t of targets)
|
|
644
|
+
if (Math.abs(t - e) < tol && (best == null || Math.abs(t - e) < Math.abs(best)))
|
|
645
|
+
best = t - e;
|
|
646
|
+
return delta + (best ?? 0);
|
|
647
|
+
}
|
|
648
|
+
// ---- the wall tools ----
|
|
649
|
+
/** A ghost of what a placing tool would put at the pointer. */
|
|
650
|
+
function ghost(c, T, u0, z0, w, h, label) {
|
|
651
|
+
const [x0, y0] = S(T, u0, z0 + h), [x1, y1] = S(T, u0 + w, z0);
|
|
652
|
+
c.save();
|
|
653
|
+
c.fillStyle = 'rgba(0,102,255,.08)';
|
|
654
|
+
c.fillRect(x0, y0, x1 - x0, y1 - y0);
|
|
655
|
+
c.strokeStyle = TH.blue;
|
|
656
|
+
c.lineWidth = 1.5;
|
|
657
|
+
c.setLineDash([6, 4]);
|
|
658
|
+
c.strokeRect(x0, y0, x1 - x0, y1 - y0);
|
|
659
|
+
c.restore();
|
|
660
|
+
if (label)
|
|
661
|
+
previewChip(c, label, (x0 + x1) / 2, y0 - 16);
|
|
662
|
+
}
|
|
663
|
+
/** On the wall (with a little slack), or not. */
|
|
664
|
+
const onWall = (u, z) => { const f = elev.frame; return !!f && z >= -2 && z <= f.H + 2 && u >= f.face[0] - 2 && u <= f.face[1] + 2; };
|
|
665
|
+
/** A tool that does something where the wall is tapped: a ghost follows the pointer, a tap acts (a drag doesn't). */
|
|
666
|
+
/**
|
|
667
|
+
* A tool that places something where the wall is tapped, a ghost of it following the pointer first. `tol`: how near
|
|
668
|
+
* (inches) it snaps to what's on the wall — a finger's reach or a mouse's, as a drag snaps (Object snapping).
|
|
719
669
|
*/
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
670
|
+
function placingTool(o) {
|
|
671
|
+
const tolOf = (ev) => (ev.touch ? 16 : 10) / mainT().s;
|
|
672
|
+
return {
|
|
673
|
+
st: {},
|
|
674
|
+
reset() { this.st = {}; ed.preview = null; },
|
|
675
|
+
hover(ev) {
|
|
676
|
+
const tol = tolOf(ev);
|
|
677
|
+
ed.preview = onWall(ev.wx, ev.wy) ? (c, T) => o.preview(c, T, ev.wx, ev.wy, tol) : null;
|
|
678
|
+
requestOverlayDraw();
|
|
679
|
+
},
|
|
680
|
+
down(ev) { this.st = { down: ev, far: false }; this.hover(ev); },
|
|
681
|
+
move(ev) {
|
|
682
|
+
const st = this.st;
|
|
683
|
+
if (st.down && Math.hypot(ev.sx - st.down.sx, ev.sy - st.down.sy) > (ev.touch ? 12 : 6))
|
|
684
|
+
st.far = true;
|
|
685
|
+
this.hover(ev);
|
|
686
|
+
},
|
|
687
|
+
up(ev) {
|
|
688
|
+
const st = this.st;
|
|
689
|
+
this.st = {};
|
|
690
|
+
if (ev.touch)
|
|
691
|
+
ed.preview = null;
|
|
692
|
+
requestOverlayDraw();
|
|
693
|
+
if (!st.down || st.far)
|
|
694
|
+
return;
|
|
695
|
+
if (!onWall(ev.wx, ev.wy)) {
|
|
696
|
+
toast('Tap on the wall to place it.', { warn: true });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
o.place(ev.wx, ev.wy, tolOf(ev));
|
|
700
|
+
},
|
|
701
|
+
hint: () => o.hint,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
const openingTool = (type) => placingTool({
|
|
705
|
+
preview(c, T, u, z, tol) {
|
|
706
|
+
const spot = openingAt(type, u, z, tol), f = elev.frame;
|
|
707
|
+
if (!spot || !f)
|
|
708
|
+
return;
|
|
709
|
+
const a = toU(f, spot.at - NEW_W / 2), b = toU(f, spot.at + NEW_W / 2);
|
|
710
|
+
ghost(c, T, Math.min(a, b), spot.sill, NEW_W, type === 'door' ? DOOR_H : WINDOW_H);
|
|
711
|
+
},
|
|
712
|
+
place: (u, z, tol) => placeOpening(type, u, z, tol),
|
|
713
|
+
hint: type === 'door' ? 'Tap the wall to place a 3\'-0" door. It sits on the floor where you tap.' : 'Tap the wall to place a 3\'-0" window, centered where you tap.',
|
|
714
|
+
});
|
|
715
|
+
export const wallDoorTool = openingTool('door');
|
|
716
|
+
export const wallWindowTool = openingTool('window');
|
|
717
|
+
export const cabinetTool = placingTool({
|
|
718
|
+
preview(c, T, u, z) { const spot = cabinetSpot(u, z); if (spot)
|
|
719
|
+
ghost(c, T, spot.u0, spot.z0, CAB.w, CAB.h, fmtLen(CAB.w) + ' × ' + fmtLen(CAB.h)); },
|
|
720
|
+
place: (u, z) => { addCabinet(u, z); },
|
|
721
|
+
hint: 'Tap the wall to place a cabinet (24" wide, 34½" high, 24" deep) — anywhere, like a window. Tap it, then one of its dimensions, to set it.',
|
|
722
|
+
});
|
|
723
|
+
export const heightTool = placingTool({
|
|
724
|
+
preview(c, T, _u, z) {
|
|
725
|
+
const f = elev.frame;
|
|
726
|
+
if (!f)
|
|
727
|
+
return;
|
|
728
|
+
const q = 1 / settings.denom, h = clamp(Math.round(z / q) * q, 0, f.H), [x0, y] = S(T, f.face[0], h), [x1] = S(T, f.face[1], h);
|
|
729
|
+
c.save();
|
|
730
|
+
c.strokeStyle = TH.blue;
|
|
731
|
+
c.lineWidth = 1.5;
|
|
732
|
+
c.setLineDash([6, 4]);
|
|
733
|
+
c.beginPath();
|
|
734
|
+
c.moveTo(x0, y);
|
|
735
|
+
c.lineTo(x1, y);
|
|
736
|
+
c.stroke();
|
|
737
|
+
c.restore();
|
|
738
|
+
previewChip(c, fmtLen(h), x1 - 40, y - 16);
|
|
739
|
+
},
|
|
740
|
+
place: (_u, z) => {
|
|
741
|
+
const q = 1 / settings.denom, start = clamp(Math.round(z / q) * q, 0, elev.frame.H);
|
|
742
|
+
openKeypad({ title: 'Height line', sub: 'Floor to the line — it runs across the whole wall', initial: fmtLen(start), selectAll: true, laser: true, onDone: v => { addHeightLine(v); } });
|
|
743
|
+
},
|
|
744
|
+
hint: 'Tap the wall at a height, then enter it: a level line across the wall (a counter, a rail).',
|
|
745
|
+
});
|
|
746
|
+
/** The wall view's toolbar: the plan's drawing tools, then what goes on a wall. (The height line tool is kept off it for now.) */
|
|
747
|
+
export const WALL_RAIL = [
|
|
748
|
+
['select', 'Select', 'V'], ['line', 'Line', 'L'], ['freehand', 'Draw to shape', 'S'], ['rect', 'Rectangle', 'R'], ['circle', 'Circle', 'C'], ['arc', 'Arc', 'A'], '|',
|
|
749
|
+
['dim', 'Dimension', 'D'], ['wdoor', 'Door', 'B'], ['wwindow', 'Window', 'N'], ['cabinet', 'Cabinet', 'K'], '|', ['constr', 'Construction lines', 'X', 'toggle'], '|',
|
|
750
|
+
['undo', 'Undo', '⌘Z', 'act'], ['redo', 'Redo', '⇧⌘Z', 'act'],
|
|
751
|
+
];
|
|
752
|
+
/** Tool letters in the wall view. */
|
|
753
|
+
export const WALL_KEYS = { v: 'select', l: 'line', s: 'freehand', r: 'rect', c: 'circle', a: 'arc', d: 'dim', t: 'trim', o: 'offset', b: 'wdoor', n: 'wwindow', k: 'cabinet' };
|
|
754
|
+
/** The icon for a wall tool whose id isn't an icon's name. */
|
|
755
|
+
export const WALL_ICON = { wdoor: 'door', wwindow: 'window', height: 'hline' };
|
|
756
|
+
// ---- what's under the pointer, beyond the face's own geometry ----
|
|
757
|
+
/** A cabinet (on it, or within `slack` inches of its outline) or a door/window body under a world point on the face (cabinets first). */
|
|
758
|
+
export function wallBodyAt(wx, wy, slack = 0) {
|
|
759
|
+
const face = SK(), f = elev.frame;
|
|
760
|
+
if (!f)
|
|
761
|
+
return null;
|
|
762
|
+
// (inside it: right on its edge picks the edge, as a window's)
|
|
763
|
+
for (const id of Object.keys(face.cabs ?? {}).reverse()) {
|
|
764
|
+
const b = cabBox(face, id);
|
|
765
|
+
if (b && wx > b.u0 && wx < b.u1 && wy > b.z0 && wy < b.z1)
|
|
766
|
+
return { type: 'cab', id };
|
|
767
|
+
}
|
|
768
|
+
for (const o of elevOpenings(planSK(), f))
|
|
769
|
+
if (wx > o.u0 && wx < o.u1 && wy > o.sill && wy < o.head)
|
|
770
|
+
return { type: 'opening', id: o.id };
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
/** Two doors/windows, or one and a wall end: the + sets the distance between them (the plan's door/window dimension). */
|
|
774
|
+
function wallPair() {
|
|
775
|
+
if (ed.sel.length !== 2)
|
|
776
|
+
return null;
|
|
777
|
+
const ops = ed.sel.filter(x => x.type === 'opening').map(x => x.id);
|
|
778
|
+
const ends = ed.sel.filter(x => x.type === 'line' && (x.id === '@ea' || x.id === '@eb')).map(x => x.id.slice(2));
|
|
779
|
+
if (ops.length === 2)
|
|
780
|
+
return { op: ops[0], other: { op: ops[1] } };
|
|
781
|
+
if (ops.length === 1 && ends.length === 1)
|
|
782
|
+
return { op: ops[0], other: { end: ends[0] } };
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
/** The bar for what's selected on the wall that the plan doesn't know (a door or window, a cabinet, a chip). Null: the plan's bar. */
|
|
786
|
+
export function wallBar() {
|
|
787
|
+
if (!elevOpen())
|
|
788
|
+
return null;
|
|
789
|
+
const pair = wallPair();
|
|
790
|
+
if (pair) {
|
|
791
|
+
const name = (id) => (planSK().openings[id]?.type === 'door' ? 'Door' : 'Window');
|
|
792
|
+
const label = name(pair.op) + ' + ' + ('op' in pair.other ? name(pair.other.op).toLowerCase() : 'corner');
|
|
793
|
+
return { label, items: [{ ic: 'dim', label: 'Distance', act: 'wallDist' }], plus: true };
|
|
794
|
+
}
|
|
795
|
+
if (ed.sel.length !== 1)
|
|
796
|
+
return null;
|
|
797
|
+
const s = ed.sel[0], sk = planSK();
|
|
798
|
+
if (s.type === 'elev') {
|
|
799
|
+
const a = elevAct(s.id);
|
|
800
|
+
return { label: elevChipName(a), items: a.k === 'len' ? [{ ic: 'back', label: 'Go to plan', act: 'wallGoPlan' }] : [], plus: a.k !== 'len' };
|
|
801
|
+
}
|
|
802
|
+
// a door, window or cabinet picked as a whole: its sizes to set, by name (no + — it isn't one dimension)
|
|
803
|
+
if (s.type === 'opening') {
|
|
804
|
+
const o = sk.openings[s.id];
|
|
805
|
+
if (!o)
|
|
806
|
+
return null;
|
|
807
|
+
return { label: openingLabel(o), items: OPENING_SIZES, plus: false };
|
|
808
|
+
}
|
|
809
|
+
if (s.type === 'cab') {
|
|
810
|
+
const face = SK(), b = cabBox(face, s.id), cb = face.cabs[s.id];
|
|
811
|
+
return {
|
|
812
|
+
label: 'Cabinet' + (b ? ' ' + fmtLen(b.u1 - b.u0) + ' L × ' + fmtLen(b.z1 - b.z0) + ' H × ' + fmtLen(cb.depth) + ' D' : ''),
|
|
813
|
+
items: [{ ic: 'v', label: 'Height', act: 'cabH' }, { ic: 'h', label: 'Length', act: 'cabW' }, { ic: 'thick', label: 'Depth', act: 'cabD' }], plus: false,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
const face = SK();
|
|
817
|
+
if (s.type === 'line' && face.lines[s.id]?.anchor)
|
|
818
|
+
return { label: anchorName(s.id), items: [], plus: false };
|
|
819
|
+
if (s.type === 'line' && face.lines[s.id]?.cab)
|
|
820
|
+
return { label: 'Cabinet edge', items: [], plus: false };
|
|
821
|
+
if (s.type === 'point' && face.points[s.id]?.anchor)
|
|
822
|
+
return { label: 'Corner · set by the plan', items: [], plus: false };
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
/** What one of the plan's edges on the face is. */
|
|
826
|
+
function anchorName(id) {
|
|
827
|
+
if (id === '@floor')
|
|
828
|
+
return 'Floor';
|
|
829
|
+
if (id === '@ceil')
|
|
830
|
+
return 'Ceiling';
|
|
831
|
+
if (id === '@ea' || id === '@eb')
|
|
832
|
+
return 'Wall corner';
|
|
833
|
+
const op = planSK().openings[id.split(':')[1]];
|
|
834
|
+
return (op?.type === 'door' ? 'Door' : 'Window') + ' edge';
|
|
835
|
+
}
|
|
836
|
+
/** A door's or window's sizes to set, in the bar (the wall view's and the plan's): its height and its length. */
|
|
837
|
+
export const OPENING_SIZES = [{ ic: 'v', label: 'Height', act: 'opHeight' }, { ic: 'h', label: 'Length', act: 'opLength' }];
|
|
838
|
+
/** What the bar calls a door or window: its kind and sizes, length by height ("Door 3'-0" L × 6'-8" H"). */
|
|
839
|
+
export function openingLabel(o) {
|
|
840
|
+
const { sill, head } = openingHeights(o);
|
|
841
|
+
return (o.type === 'door' ? 'Door ' : 'Window ') + fmtLen(o.w) + ' L × ' + fmtLen(head - sill) + ' H';
|
|
842
|
+
}
|
|
843
|
+
/** Run a bar action for the wall view's things (a door's or window's sizes work in the plan too). Returns false if it isn't one. */
|
|
844
|
+
export function wallAction(act) {
|
|
845
|
+
const s = ed.sel[0];
|
|
846
|
+
switch (act) {
|
|
847
|
+
case 'opHeight':
|
|
848
|
+
editOpeningHeight(s.id);
|
|
849
|
+
return true;
|
|
850
|
+
case 'opLength':
|
|
851
|
+
editOpeningDim(s.id, 'w');
|
|
852
|
+
return true;
|
|
853
|
+
case 'cabW':
|
|
854
|
+
editCabinet(s.id, 'w');
|
|
855
|
+
return true;
|
|
856
|
+
case 'opSill':
|
|
857
|
+
editSill(s.id);
|
|
858
|
+
return true;
|
|
859
|
+
case 'cabH':
|
|
860
|
+
editCabinet(s.id, 'h');
|
|
861
|
+
return true;
|
|
862
|
+
case 'cabD':
|
|
863
|
+
editCabinetDepth(s.id);
|
|
864
|
+
return true;
|
|
865
|
+
case 'cabZ':
|
|
866
|
+
editCabinet(s.id, 'z');
|
|
867
|
+
return true;
|
|
868
|
+
case 'wallGoPlan':
|
|
869
|
+
goToPlan();
|
|
870
|
+
return true;
|
|
871
|
+
case 'wallDist':
|
|
872
|
+
wallPlus();
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
/** The yellow + for wall things. Returns false if the plan's + should handle the selection. */
|
|
878
|
+
export function wallPlus() {
|
|
879
|
+
if (!elevOpen())
|
|
880
|
+
return false;
|
|
881
|
+
const pair = wallPair(), f = elev.frame;
|
|
882
|
+
if (pair) {
|
|
883
|
+
const ops = elevOpenings(planSK(), f), a = ops.find(o => o.id === pair.op);
|
|
884
|
+
if (!a)
|
|
885
|
+
return true;
|
|
886
|
+
if ('op' in pair.other) {
|
|
887
|
+
const otherId = pair.other.op, b = ops.find(o => o.id === otherId);
|
|
888
|
+
if (!b)
|
|
889
|
+
return true;
|
|
890
|
+
const [l, r] = a.u0 <= b.u0 ? [a, b] : [b, a], t = pickBetween(planSK(), l.id, l.e1, r.id, r.e0);
|
|
891
|
+
editElevationPosition(t.op, t.edge, t.ref, 'Space between');
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
const end = pair.other.end, ref = { face: end, line: f.lid, side: f.side };
|
|
895
|
+
editElevationPosition(a.id, end === f.leftEnd ? a.e0 : a.e1, ref, 'Distance from the corner');
|
|
896
|
+
}
|
|
724
897
|
return true;
|
|
725
898
|
}
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
899
|
+
if (ed.sel.length !== 1)
|
|
900
|
+
return false;
|
|
901
|
+
const s = ed.sel[0];
|
|
902
|
+
if (s.type === 'elev') {
|
|
903
|
+
editElevChip(elevAct(s.id));
|
|
731
904
|
return true;
|
|
732
905
|
}
|
|
733
|
-
if (
|
|
734
|
-
|
|
906
|
+
if (s.type === 'opening') {
|
|
907
|
+
editOpeningDim(s.id, 'w');
|
|
735
908
|
return true;
|
|
736
909
|
}
|
|
737
|
-
if (
|
|
738
|
-
|
|
910
|
+
if (s.type === 'cab') {
|
|
911
|
+
editCabinet(s.id, 'w');
|
|
739
912
|
return true;
|
|
740
913
|
}
|
|
741
|
-
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Delete in the wall view: a selected chip's dimension is freed or removed; doors and windows go from the plan,
|
|
918
|
+
* cabinets from the face, the rest as in the plan. The plan's anchors stay. Returns false outside the wall view.
|
|
919
|
+
*/
|
|
920
|
+
export function wallDelete() {
|
|
921
|
+
if (!elevOpen())
|
|
922
|
+
return false;
|
|
923
|
+
if (ed.sel.length === 1 && ed.sel[0].type === 'elev') {
|
|
924
|
+
deleteElevChip(elevAct(ed.sel[0].id));
|
|
742
925
|
return true;
|
|
743
|
-
|
|
744
|
-
|
|
926
|
+
}
|
|
927
|
+
const face = SK();
|
|
928
|
+
const anchor = (x) => (x.type === 'line' && !!face.lines[x.id]?.anchor) || (x.type === 'point' && !!face.points[x.id]?.anchor);
|
|
929
|
+
const items = ed.sel.filter(x => !anchor(x) && x.type !== 'elev');
|
|
930
|
+
if (!items.length) {
|
|
931
|
+
toast('The wall’s edges and corners come from the plan — they can’t be deleted here.', { warn: true });
|
|
745
932
|
return true;
|
|
746
933
|
}
|
|
747
|
-
|
|
934
|
+
const one = items.length === 1 ? items[0] : null, op = one?.type === 'opening' ? planSK().openings[one.id] : null;
|
|
935
|
+
const what = one?.type === 'cab' ? 'Cabinet deleted' : op ? (op.type === 'door' ? 'Door' : 'Window') + ' deleted' : 'Deleted';
|
|
936
|
+
pushHistory();
|
|
937
|
+
deleteSel(planSK(), items.filter(x => x.type === 'opening'));
|
|
938
|
+
deleteCabinets(items.filter(x => x.type === 'cab').map(x => x.id));
|
|
939
|
+
deleteSel(face, items.filter(x => x.type !== 'opening' && x.type !== 'cab'));
|
|
940
|
+
ed.sel = [];
|
|
941
|
+
afterEdit();
|
|
942
|
+
toast(what, { action: { label: 'Undo', fn: () => undo() } });
|
|
943
|
+
return true;
|
|
748
944
|
}
|
|
749
945
|
//# sourceMappingURL=elevation.js.map
|