@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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { V3 } from './mesh';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The 3D view's camera: it circles a point (the target), `dist` away from it, turned `az` round from due south (−y,
|
|
5
|
+
looking north: the plan the right way up) and raised `el` above level. Plan inches, z up. Plain numbers, so the
|
|
6
|
+
gestures' maths is tested without a screen:
|
|
7
|
+
- a drag turns it (the model follows the finger), up and down tips it;
|
|
8
|
+
- a pan slides it across the screen;
|
|
9
|
+
- a pinch or the wheel zooms toward the point under the fingers or the pointer;
|
|
10
|
+
- fit frames everything at the angle it's at; home is the corner view; top looks straight down.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface Orbit { x: number; y: number; z: number; dist: number; az: number; el: number }
|
|
14
|
+
|
|
15
|
+
/** The camera's vertical field of view (radians). */
|
|
16
|
+
export const FOV = (45 * Math.PI) / 180;
|
|
17
|
+
/** How low and high it can be tipped: just above level, and all but straight down. */
|
|
18
|
+
export const EL_MIN = 0.03, EL_MAX = Math.PI / 2 - 1e-4;
|
|
19
|
+
/** The corner view: from the south-west, looking down at about 35°. */
|
|
20
|
+
export const HOME_AZ = -0.62, HOME_EL = 0.62;
|
|
21
|
+
|
|
22
|
+
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
|
|
23
|
+
|
|
24
|
+
/** Where the camera is. */
|
|
25
|
+
export function eye(o: Orbit): V3 {
|
|
26
|
+
const ce = Math.cos(o.el);
|
|
27
|
+
return [o.x + o.dist * ce * Math.sin(o.az), o.y - o.dist * ce * Math.cos(o.az), o.z + o.dist * Math.sin(o.el)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The camera's right, up and forward (toward the target), unit length. */
|
|
31
|
+
export function axes(o: Orbit): { right: V3; up: V3; fwd: V3 } {
|
|
32
|
+
const ce = Math.cos(o.el), se = Math.sin(o.el), sa = Math.sin(o.az), ca = Math.cos(o.az);
|
|
33
|
+
return { right: [ca, sa, 0], up: [-se * sa, se * ca, ce], fwd: [-ce * sa, ce * ca, -se] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A drag of dx, dy screen pixels on a view `viewH` tall: a full turn for a drag the view's height. */
|
|
37
|
+
export function orbitBy(o: Orbit, dx: number, dy: number, viewH: number): Orbit {
|
|
38
|
+
const k = (2 * Math.PI) / Math.max(1, viewH);
|
|
39
|
+
return { ...o, az: o.az - dx * k, el: clamp(o.el + dy * k, EL_MIN, EL_MAX) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** World inches per screen pixel at the target, on a view `viewH` tall. */
|
|
43
|
+
export const perPixel = (o: Orbit, viewH: number): number => (2 * o.dist * Math.tan(FOV / 2)) / Math.max(1, viewH);
|
|
44
|
+
|
|
45
|
+
/** The model slid by dx, dy screen pixels (it follows the fingers). */
|
|
46
|
+
export function panBy(o: Orbit, dx: number, dy: number, viewH: number): Orbit {
|
|
47
|
+
const { right, up } = axes(o), k = perPixel(o, viewH);
|
|
48
|
+
return { ...o, x: o.x - (right[0] * dx - up[0] * dy) * k, y: o.y - (right[1] * dx - up[1] * dy) * k, z: o.z - (right[2] * dx - up[2] * dy) * k };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Closer by `f` (under 1 zooms in) toward the screen point at nx, ny (−1 to 1 across and up the view, `aspect` wide):
|
|
53
|
+
* that point stays where it is on the screen. The distance stays within [lo, hi].
|
|
54
|
+
*/
|
|
55
|
+
export function zoomAt(o: Orbit, f: number, nx: number, ny: number, aspect: number, lo: number, hi: number): Orbit {
|
|
56
|
+
const dist = clamp(o.dist * f, lo, hi), k = 1 - dist / o.dist, t = Math.tan(FOV / 2);
|
|
57
|
+
const { right, up } = axes(o), ox = nx * t * aspect * o.dist * k, oy = ny * t * o.dist * k;
|
|
58
|
+
return { ...o, dist, x: o.x + right[0] * ox + up[0] * oy, y: o.y + right[1] * ox + up[1] * oy, z: o.z + right[2] * ox + up[2] * oy };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The part of the view to frame in, −1 to 1 across (x) and up (y): all of it, or what the chrome around it leaves.
|
|
63
|
+
*/
|
|
64
|
+
export interface Window3 { x0: number; x1: number; y0: number; y1: number }
|
|
65
|
+
const WHOLE: Window3 = { x0: -1, x1: 1, y0: -1, y1: 1 };
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Framing box min–max at the angle `o` looks from, on a view `aspect` wide: everything in, with a margin, in the
|
|
69
|
+
* middle of `win`.
|
|
70
|
+
*/
|
|
71
|
+
export function fit(o: Orbit, min: V3, max: V3, aspect: number, win: Window3 = WHOLE): Orbit {
|
|
72
|
+
const c: V3 = [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2];
|
|
73
|
+
const { right, up, fwd } = axes(o), margin = 1.1, t = Math.tan(FOV / 2), a = Math.max(0.1, aspect);
|
|
74
|
+
const tv = (t * (win.y1 - win.y0)) / 2 / margin, th = (t * a * (win.x1 - win.x0)) / 2 / margin;
|
|
75
|
+
let dist = 1;
|
|
76
|
+
for (const x of [min[0], max[0]]) {
|
|
77
|
+
for (const y of [min[1], max[1]]) {
|
|
78
|
+
for (const z of [min[2], max[2]]) {
|
|
79
|
+
const p = [x - c[0], y - c[1], z - c[2]], dot = (v: V3) => p[0] * v[0] + p[1] * v[1] + p[2] * v[2];
|
|
80
|
+
// the camera is `dist` back along fwd from the middle: each corner must be within the view at its depth
|
|
81
|
+
const depth = dot(fwd);
|
|
82
|
+
dist = Math.max(dist, Math.abs(dot(right)) / th - depth, Math.abs(dot(up)) / tv - depth);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// the box's middle to the window's: the camera slides the other way
|
|
87
|
+
const sr = (-(win.x0 + win.x1) / 2) * t * a * dist, su = (-(win.y0 + win.y1) / 2) * t * dist;
|
|
88
|
+
return { ...o, x: c[0] + right[0] * sr + up[0] * su, y: c[1] + right[1] * sr + up[1] * su, z: c[2] + right[2] * sr + up[2] * su, dist };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The corner view of box min–max. */
|
|
92
|
+
export const home = (min: V3, max: V3, aspect: number, win?: Window3): Orbit => fit({ x: 0, y: 0, z: 0, dist: 1, az: HOME_AZ, el: HOME_EL }, min, max, aspect, win);
|
|
93
|
+
|
|
94
|
+
/** Straight down on box min–max, the plan the right way up. */
|
|
95
|
+
export const topDown = (min: V3, max: V3, aspect: number, win?: Window3): Orbit => fit({ x: 0, y: 0, z: 0, dist: 1, az: 0, el: EL_MAX }, min, max, aspect, win);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Part of the way (t from 0 to 1) from camera `a` to camera `b`, eased at both ends: turning the short way round,
|
|
99
|
+
* zooming evenly (by ratio, not inches), its target sliding between theirs. For the rail's views and a double tap, which
|
|
100
|
+
* glide there rather than jump, so it's still clear what's being looked at.
|
|
101
|
+
*/
|
|
102
|
+
export function between(a: Orbit, b: Orbit, t: number): Orbit {
|
|
103
|
+
if (t >= 1) return { ...b };
|
|
104
|
+
const k = t <= 0 ? 0 : t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2, mix = (p: number, q: number) => p + (q - p) * k;
|
|
105
|
+
const turn = Math.atan2(Math.sin(b.az - a.az), Math.cos(b.az - a.az));
|
|
106
|
+
return { x: mix(a.x, b.x), y: mix(a.y, b.y), z: mix(a.z, b.z), dist: a.dist * (b.dist / a.dist) ** k, az: a.az + turn * k, el: mix(a.el, b.el) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** How near and far the camera may zoom for a model of box min–max. */
|
|
110
|
+
export function zoomLimits(min: V3, max: V3): [number, number] {
|
|
111
|
+
const size = Math.max(1, Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]));
|
|
112
|
+
return [12, Math.max(240, size * 8)];
|
|
113
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { BufferAttribute, BufferGeometry, Color, DoubleSide, Mesh, ShaderMaterial, Vector2 } from 'three';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The 3D view's outlines, drawn the same width on the screen however near or far — each segment a thin quad turned to
|
|
5
|
+
face the screen, in a small shader. Only three.js's core: its add-on lines brought in a second copy of three.js
|
|
6
|
+
where a host resolves `three/examples/...` from this package's own install (BOLDR Web's dev server did), and two
|
|
7
|
+
copies in one scene draw nothing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const VERTEX = /* glsl */ `
|
|
11
|
+
uniform vec2 resolution;
|
|
12
|
+
uniform float linewidth;
|
|
13
|
+
attribute vec3 other;
|
|
14
|
+
attribute float side;
|
|
15
|
+
void main() {
|
|
16
|
+
vec4 a = modelViewMatrix * vec4(position, 1.0);
|
|
17
|
+
vec4 b = modelViewMatrix * vec4(other, 1.0);
|
|
18
|
+
// an end behind the camera comes forward along the line to just inside the near plane (as three.js's LineMaterial)
|
|
19
|
+
float nearZ = -0.5 * projectionMatrix[3][2] / projectionMatrix[2][2];
|
|
20
|
+
if (a.z > nearZ && b.z < nearZ) a.xyz = mix(b.xyz, a.xyz, (nearZ - b.z) / (a.z - b.z));
|
|
21
|
+
if (b.z > nearZ && a.z < nearZ) b.xyz = mix(a.xyz, b.xyz, (nearZ - a.z) / (b.z - a.z));
|
|
22
|
+
vec4 ca = projectionMatrix * a;
|
|
23
|
+
vec4 cb = projectionMatrix * b;
|
|
24
|
+
vec2 d = (cb.xy / cb.w - ca.xy / ca.w) * resolution;
|
|
25
|
+
float len = length(d);
|
|
26
|
+
vec2 n = len > 1e-6 ? vec2(-d.y, d.x) / len : vec2(0.0, 1.0);
|
|
27
|
+
// half the width to each side (a pixel is 2 / resolution across clip space)
|
|
28
|
+
ca.xy += n * side * linewidth / resolution * ca.w;
|
|
29
|
+
gl_Position = ca;
|
|
30
|
+
}`;
|
|
31
|
+
|
|
32
|
+
const FRAGMENT = /* glsl */ `
|
|
33
|
+
uniform vec3 color;
|
|
34
|
+
void main() {
|
|
35
|
+
gl_FragColor = vec4(color, 1.0);
|
|
36
|
+
}`;
|
|
37
|
+
|
|
38
|
+
/** Outlines `width` points wide: set the view's size (points) with setLineView, and their colour with setLineColor. */
|
|
39
|
+
export function lineMaterial(width: number): ShaderMaterial {
|
|
40
|
+
return new ShaderMaterial({
|
|
41
|
+
uniforms: { color: { value: new Color() }, resolution: { value: new Vector2(1, 1) }, linewidth: { value: width } },
|
|
42
|
+
vertexShader: VERTEX, fragmentShader: FRAGMENT, side: DoubleSide,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const setLineView = (m: ShaderMaterial, w: number, h: number): void => { (m.uniforms.resolution.value as Vector2).set(w, h); };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The outlines' colour, as written (#rrggbb): the shader draws it as it is, with none of three.js's colour management
|
|
50
|
+
* (which its own materials go through, both ways).
|
|
51
|
+
*/
|
|
52
|
+
export function setLineColor(m: ShaderMaterial, hex: string): void {
|
|
53
|
+
const v = parseInt(hex.slice(1), 16);
|
|
54
|
+
(m.uniforms.color.value as Color).setRGB(((v >> 16) & 255) / 255, ((v >> 8) & 255) / 255, (v & 255) / 255);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Line segments (x, y, z of each end, three.js's axes) as screen-facing quads for lineMaterial. */
|
|
58
|
+
export function linesMesh(pts: ArrayLike<number>, m: ShaderMaterial): Mesh {
|
|
59
|
+
const n = Math.floor(pts.length / 6), pos = new Float32Array(n * 12), other = new Float32Array(n * 12), side = new Float32Array(n * 4);
|
|
60
|
+
const idx = n * 4 > 65535 ? new Uint32Array(n * 6) : new Uint16Array(n * 6);
|
|
61
|
+
for (let i = 0; i < n; i++) {
|
|
62
|
+
const s = i * 6, v = i * 12;
|
|
63
|
+
// two corners at each end, either side of the line: A, A, B, B — each looking along it to the other end
|
|
64
|
+
for (let k = 0; k < 3; k++) {
|
|
65
|
+
const a = pts[s + k], b = pts[s + 3 + k];
|
|
66
|
+
pos[v + k] = a; pos[v + 3 + k] = a; pos[v + 6 + k] = b; pos[v + 9 + k] = b;
|
|
67
|
+
other[v + k] = b; other[v + 3 + k] = b; other[v + 6 + k] = a; other[v + 9 + k] = a;
|
|
68
|
+
}
|
|
69
|
+
// (looking back along it from B, its sides swap: −1 there is the side +1 is at A)
|
|
70
|
+
side.set([1, -1, -1, 1], i * 4);
|
|
71
|
+
idx.set([i * 4, i * 4 + 1, i * 4 + 2, i * 4 + 1, i * 4 + 3, i * 4 + 2], i * 6);
|
|
72
|
+
}
|
|
73
|
+
const g = new BufferGeometry();
|
|
74
|
+
g.setAttribute('position', new BufferAttribute(pos, 3));
|
|
75
|
+
g.setAttribute('other', new BufferAttribute(other, 3));
|
|
76
|
+
g.setAttribute('side', new BufferAttribute(side, 1));
|
|
77
|
+
g.setIndex(new BufferAttribute(idx, 1));
|
|
78
|
+
const mesh = new Mesh(g, m);
|
|
79
|
+
// (drawn whatever the camera sees: the quads reach a little past the points)
|
|
80
|
+
mesh.frustumCulled = false;
|
|
81
|
+
mesh.matrixAutoUpdate = false;
|
|
82
|
+
return mesh;
|
|
83
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { XY } from '../model/types';
|
|
2
|
+
import { area, clipBand, clipHalf, tidy } from './poly';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The 3D view's outlines: a line wherever the walls' surface turns a corner — their tops' edges, the corners up their
|
|
6
|
+
sides, where they meet the floor, round each opening — and none where it runs on flat (across a mitre, where a
|
|
7
|
+
taller wall's corner carries on over a shorter one, between two walls of one height).
|
|
8
|
+
|
|
9
|
+
The walls seen from above are a set of cells that don't overlap (each wall's outline, cut across at its openings and
|
|
10
|
+
where a joint rises with a taller wall), each solid between some heights. A line runs:
|
|
11
|
+
- along a cell's edge, at a height where the solid on the two sides of that edge, just above and below it, doesn't
|
|
12
|
+
make a flat surface (a top, a side or the floor's level);
|
|
13
|
+
- up a corner of a cell, between heights where the solid around that corner point, seen from above, isn't a
|
|
14
|
+
straight edge (or nothing, or all round).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A cell of the walls seen from above (counter-clockwise), and the heights it's solid between ([bottom, top], in order). */
|
|
18
|
+
export interface Cell3 { poly: XY[]; iv: [number, number][] }
|
|
19
|
+
|
|
20
|
+
const EPS = 1e-6, TAU = 2 * Math.PI;
|
|
21
|
+
/** Corners straighter than this (radians) aren't drawn: a curved wall is drawn as gently angled stretches. */
|
|
22
|
+
const STRAIGHT = (10 * Math.PI) / 180;
|
|
23
|
+
|
|
24
|
+
/** Intervals in order, touching and overlapping ones joined, empty ones gone. */
|
|
25
|
+
export function mergeIv(iv: [number, number][]): [number, number][] {
|
|
26
|
+
const s = iv.filter(([a, b]) => b - a > EPS).sort((p, q) => p[0] - q[0]), out: [number, number][] = [];
|
|
27
|
+
for (const [a, b] of s) {
|
|
28
|
+
const last = out[out.length - 1];
|
|
29
|
+
if (last && a <= last[1] + EPS) last[1] = Math.max(last[1], b);
|
|
30
|
+
else out.push([a, b]);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const solid = (iv: [number, number][], z: number) => iv.some(([a, b]) => z > a && z < b);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Cell `c` split across a band (within `half` of the line through `o` along unit `u`): what's inside it solid on up to
|
|
39
|
+
* `top`, the parts either side as they were.
|
|
40
|
+
*/
|
|
41
|
+
export function raiseIn(c: Cell3, o: XY, u: XY, half: number, top: number): Cell3[] {
|
|
42
|
+
const inside = tidy(clipBand(c.poly, o, u, half));
|
|
43
|
+
if (inside.length < 3 || area(inside) < 1e-6) return [c];
|
|
44
|
+
const nx = -u.y, ny = u.x, k = nx * o.x + ny * o.y, hi = c.iv.length ? c.iv[c.iv.length - 1][1] : 0;
|
|
45
|
+
const out: Cell3[] = [{ poly: inside, iv: mergeIv([...c.iv, [hi, Math.max(hi, top)]]) }];
|
|
46
|
+
for (const side of [tidy(clipHalf(c.poly, -nx, -ny, -(k + half))), tidy(clipHalf(c.poly, nx, ny, k - half))]) {
|
|
47
|
+
if (side.length >= 3 && area(side) > 1e-6) out.push({ poly: side, iv: c.iv });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether there's an edge at a height along a cell's side, from what's solid just above and below it in the cell (ca,
|
|
54
|
+
* cb) and across the side (na, nb): not when nothing or everything is, nor when the solid's face there is flat.
|
|
55
|
+
*/
|
|
56
|
+
function crease(ca: boolean, cb: boolean, na: boolean, nb: boolean): boolean {
|
|
57
|
+
const n = +ca + +cb + +na + +nb;
|
|
58
|
+
if (n === 0 || n === 4) return false;
|
|
59
|
+
if (n === 2 && ((ca && cb) || (na && nb) || (cb && nb) || (ca && na))) return false;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether wedges of solid round a point (start angle and span each) make a corner there, seen from above. */
|
|
64
|
+
function corner(ws: { start: number; span: number }[]): boolean {
|
|
65
|
+
const total = ws.reduce((s, w) => s + w.span, 0);
|
|
66
|
+
if (total < 1e-3 || total > TAU - 1e-2) return false;
|
|
67
|
+
// the wedges that run on into each other, joined
|
|
68
|
+
const s = ws.map(w => ({ start: ((w.start % TAU) + TAU) % TAU, span: w.span })).sort((p, q) => p.start - q.start), runs: { start: number; span: number }[] = [];
|
|
69
|
+
for (const w of s) {
|
|
70
|
+
const last = runs[runs.length - 1];
|
|
71
|
+
if (last && Math.abs(last.start + last.span - w.start) < 1e-3) last.span += w.span;
|
|
72
|
+
else runs.push({ ...w });
|
|
73
|
+
}
|
|
74
|
+
if (runs.length > 1 && Math.abs(runs[runs.length - 1].start + runs[runs.length - 1].span - TAU - runs[0].start) < 1e-3) {
|
|
75
|
+
const last = runs.pop()!;
|
|
76
|
+
runs[0] = { start: last.start, span: last.span + runs[0].span };
|
|
77
|
+
}
|
|
78
|
+
return !(runs.length === 1 && Math.abs(runs[0].span - Math.PI) < STRAIGHT);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const near = (p: XY, q: XY) => Math.abs(p.x - q.x) < 1e-5 && Math.abs(p.y - q.y) < 1e-5;
|
|
82
|
+
|
|
83
|
+
/** How far round something to look for what's near it: a touch more than `near` allows. */
|
|
84
|
+
const REACH = 2e-5;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Items found by where they are: each is kept in the squares (`size` inches across) its box covers, so what might meet a
|
|
88
|
+
* box is found in the squares that box covers, not by asking everything. Asking every cell edge about every other made
|
|
89
|
+
* the outlines cost the square of the plan: most of the build on a big one, and a phone builds again as the cutaway
|
|
90
|
+
* turns.
|
|
91
|
+
*/
|
|
92
|
+
class Grid {
|
|
93
|
+
private readonly at = new Map<number, number[]>();
|
|
94
|
+
private readonly seen: Uint32Array;
|
|
95
|
+
private stamp = 0;
|
|
96
|
+
constructor(private readonly size: number, count: number) { this.seen = new Uint32Array(count); }
|
|
97
|
+
|
|
98
|
+
/** Item `id` (0 up to the count) covers x0 to x1, y0 to y1. */
|
|
99
|
+
add(id: number, x0: number, y0: number, x1: number, y1: number): void {
|
|
100
|
+
const s = this.size;
|
|
101
|
+
for (let i = Math.floor(x0 / s), i1 = Math.floor(x1 / s); i <= i1; i++) {
|
|
102
|
+
for (let j = Math.floor(y0 / s), j1 = Math.floor(y1 / s); j <= j1; j++) {
|
|
103
|
+
const k = square(i, j), list = this.at.get(k);
|
|
104
|
+
if (list) list.push(id);
|
|
105
|
+
else this.at.set(k, [id]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The items whose boxes might meet x0 to x1, y0 to y1: each once, in the order of their ids (as a scan would meet them). */
|
|
111
|
+
near(x0: number, y0: number, x1: number, y1: number): number[] {
|
|
112
|
+
const s = this.size, out: number[] = [];
|
|
113
|
+
this.stamp++;
|
|
114
|
+
for (let i = Math.floor(x0 / s), i1 = Math.floor(x1 / s); i <= i1; i++) {
|
|
115
|
+
for (let j = Math.floor(y0 / s), j1 = Math.floor(y1 / s); j <= j1; j++) {
|
|
116
|
+
const list = this.at.get(square(i, j));
|
|
117
|
+
if (list) for (const id of list) if (this.seen[id] !== this.stamp) { this.seen[id] = this.stamp; out.push(id); }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out.sort((p, q) => p - q);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A grid square's key (plans reach nowhere near 2^20 squares from the origin). */
|
|
125
|
+
const square = (i: number, j: number) => (i + 1048576) * 2097152 + (j + 1048576);
|
|
126
|
+
|
|
127
|
+
/** The box round points `ps`: x0, y0, x1, y1. */
|
|
128
|
+
function boxOf(ps: XY[]): [number, number, number, number] {
|
|
129
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
130
|
+
for (const p of ps) { x0 = Math.min(x0, p.x); y0 = Math.min(y0, p.y); x1 = Math.max(x1, p.x); y1 = Math.max(y1, p.y); }
|
|
131
|
+
return [x0, y0, x1, y1];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Squares of the grids the outlines are worked out on: about a wall's length, and an inch for points. */
|
|
135
|
+
const EDGE_SQ = 24, POINT_SQ = 1;
|
|
136
|
+
|
|
137
|
+
/** The outlines of `cells`, as line segments: x, y, z of one end, then the other, for each. */
|
|
138
|
+
export function cellLines(cells: Cell3[]): number[] {
|
|
139
|
+
const out: number[] = [];
|
|
140
|
+
const seg = (a: XY, za: number, b: XY, zb: number) => out.push(a.x, a.y, za, b.x, b.y, zb);
|
|
141
|
+
const edges = cells.flatMap((c, ci) => c.poly.map((a, i) => ({ ci, a, b: c.poly[(i + 1) % c.poly.length] })));
|
|
142
|
+
const byEdge = new Grid(EDGE_SQ, edges.length);
|
|
143
|
+
edges.forEach((e, i) => byEdge.add(i, Math.min(e.a.x, e.b.x), Math.min(e.a.y, e.b.y), Math.max(e.a.x, e.b.x), Math.max(e.a.y, e.b.y)));
|
|
144
|
+
|
|
145
|
+
// along the cells' sides
|
|
146
|
+
for (const e of edges) {
|
|
147
|
+
const dx = e.b.x - e.a.x, dy = e.b.y - e.a.y, L2 = dx * dx + dy * dy, L = Math.sqrt(L2);
|
|
148
|
+
if (L < EPS) continue;
|
|
149
|
+
// the stretches of it another cell's side runs along, the other way (the cell across it)
|
|
150
|
+
const shared: { t0: number; t1: number; ci: number }[] = [];
|
|
151
|
+
const x0 = Math.min(e.a.x, e.b.x) - REACH, y0 = Math.min(e.a.y, e.b.y) - REACH, x1 = Math.max(e.a.x, e.b.x) + REACH, y1 = Math.max(e.a.y, e.b.y) + REACH;
|
|
152
|
+
for (const k of byEdge.near(x0, y0, x1, y1)) {
|
|
153
|
+
const f = edges[k];
|
|
154
|
+
if (f.ci === e.ci) continue;
|
|
155
|
+
const off = (p: XY) => Math.abs((p.x - e.a.x) * dy - (p.y - e.a.y) * dx) / L;
|
|
156
|
+
if (off(f.a) > 1e-5 || off(f.b) > 1e-5 || (f.b.x - f.a.x) * dx + (f.b.y - f.a.y) * dy >= 0) continue;
|
|
157
|
+
const ta = ((f.a.x - e.a.x) * dx + (f.a.y - e.a.y) * dy) / L2, tb = ((f.b.x - e.a.x) * dx + (f.b.y - e.a.y) * dy) / L2;
|
|
158
|
+
const t0 = Math.max(0, Math.min(ta, tb)), t1 = Math.min(1, Math.max(ta, tb));
|
|
159
|
+
if (t1 - t0 > 1e-9) shared.push({ t0, t1, ci: f.ci });
|
|
160
|
+
}
|
|
161
|
+
const cuts = [...new Set([0, 1, ...shared.flatMap(s => [s.t0, s.t1])])].sort((p, q) => p - q);
|
|
162
|
+
const c = cells[e.ci];
|
|
163
|
+
for (let k = 0; k + 1 < cuts.length; k++) {
|
|
164
|
+
const t0 = cuts[k], t1 = cuts[k + 1];
|
|
165
|
+
if (t1 - t0 < 1e-9) continue;
|
|
166
|
+
const m = (t0 + t1) / 2, across = shared.find(s => s.t0 <= m && s.t1 >= m), n = across ? cells[across.ci] : null;
|
|
167
|
+
// (each stretch once: from the cell with nothing across it, or the first of the two)
|
|
168
|
+
if (across && across.ci < e.ci) continue;
|
|
169
|
+
const P = { x: e.a.x + dx * t0, y: e.a.y + dy * t0 }, Q = { x: e.a.x + dx * t1, y: e.a.y + dy * t1 };
|
|
170
|
+
const zs = [...new Set([...c.iv.flat(), ...(n ? n.iv.flat() : [])])];
|
|
171
|
+
for (const z of zs) {
|
|
172
|
+
const up = z + 1e-4, down = z - 1e-4;
|
|
173
|
+
if (crease(solid(c.iv, up), solid(c.iv, down), !!n && solid(n.iv, up), !!n && solid(n.iv, down))) seg(P, z, Q, z);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// up the corners
|
|
179
|
+
const points: XY[] = [], byPoint = new Grid(POINT_SQ, edges.length);
|
|
180
|
+
for (const c of cells) {
|
|
181
|
+
for (const p of c.poly) {
|
|
182
|
+
if (byPoint.near(p.x - REACH, p.y - REACH, p.x + REACH, p.y + REACH).some(k => near(p, points[k]))) continue;
|
|
183
|
+
byPoint.add(points.length, p.x, p.y, p.x, p.y);
|
|
184
|
+
points.push(p);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const byCell = new Grid(EDGE_SQ, cells.length);
|
|
188
|
+
cells.forEach((c, i) => { const [x0, y0, x1, y1] = boxOf(c.poly); byCell.add(i, x0, y0, x1, y1); });
|
|
189
|
+
for (const v of points) {
|
|
190
|
+
const ws: { start: number; span: number; iv: [number, number][] }[] = [];
|
|
191
|
+
for (const ci of byCell.near(v.x - REACH, v.y - REACH, v.x + REACH, v.y + REACH)) {
|
|
192
|
+
const c = cells[ci], n = c.poly.length, j = c.poly.findIndex(p => near(p, v));
|
|
193
|
+
if (j >= 0) {
|
|
194
|
+
const prev = c.poly[(j + n - 1) % n], next = c.poly[(j + 1) % n];
|
|
195
|
+
const start = Math.atan2(next.y - v.y, next.x - v.x), end = Math.atan2(prev.y - v.y, prev.x - v.x);
|
|
196
|
+
const span = (((end - start) % TAU) + TAU) % TAU;
|
|
197
|
+
if (span > 1e-6) ws.push({ start, span, iv: c.iv });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// (on one of its sides, not at a corner: a half turn of it)
|
|
201
|
+
for (let i = 0; i < n; i++) {
|
|
202
|
+
const a = c.poly[i], b = c.poly[(i + 1) % n], dx = b.x - a.x, dy = b.y - a.y, L2 = dx * dx + dy * dy;
|
|
203
|
+
if (L2 < EPS) continue;
|
|
204
|
+
const t = ((v.x - a.x) * dx + (v.y - a.y) * dy) / L2, off = Math.abs((v.x - a.x) * dy - (v.y - a.y) * dx) / Math.sqrt(L2);
|
|
205
|
+
if (off < 1e-5 && t > 1e-6 && t < 1 - 1e-6) { ws.push({ start: Math.atan2(dy, dx), span: Math.PI, iv: c.iv }); break; }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const zs = [...new Set(ws.flatMap(w => w.iv.flat()))].sort((p, q) => p - q);
|
|
209
|
+
let from: number | null = null;
|
|
210
|
+
for (let k = 0; k + 1 < zs.length; k++) {
|
|
211
|
+
const mid = (zs[k] + zs[k + 1]) / 2, on = corner(ws.filter(w => solid(w.iv, mid)));
|
|
212
|
+
if (on && from == null) from = zs[k];
|
|
213
|
+
if (!on && from != null) { seg(v, from, v, zs[k]); from = null; }
|
|
214
|
+
}
|
|
215
|
+
if (from != null) seg(v, from, v, zs[zs.length - 1]);
|
|
216
|
+
}
|
|
217
|
+
return simplify(out);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Lines closer together than this (inches) read as one: where many walls meet, only one is drawn. */
|
|
221
|
+
const CLOSE = 1;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Fewer lines where many walls meet: of corners up the walls standing closer than CLOSE, one; and none of the scraps
|
|
225
|
+
* of edge shorter than that between them.
|
|
226
|
+
*/
|
|
227
|
+
function simplify(segs: number[]): number[] {
|
|
228
|
+
const out: number[] = [], up: number[][] = [];
|
|
229
|
+
for (let i = 0; i < segs.length; i += 6) {
|
|
230
|
+
const [ax, ay, az, bx, by, bz] = segs.slice(i, i + 6);
|
|
231
|
+
if (Math.abs(ax - bx) < 1e-6 && Math.abs(ay - by) < 1e-6) { up.push([ax, ay, Math.min(az, bz), Math.max(az, bz)]); continue; }
|
|
232
|
+
if (Math.abs(az - bz) < 1e-6 && Math.hypot(bx - ax, by - ay) < CLOSE / 2) continue;
|
|
233
|
+
out.push(ax, ay, az, bx, by, bz);
|
|
234
|
+
}
|
|
235
|
+
// the longest first: a short corner beside a longer one is the one let go
|
|
236
|
+
up.sort((p, q) => q[3] - q[2] - (p[3] - p[2]));
|
|
237
|
+
const kept: number[][] = [], byKept = new Grid(CLOSE, up.length);
|
|
238
|
+
for (const v of up) {
|
|
239
|
+
const twin = byKept.near(v[0] - CLOSE, v[1] - CLOSE, v[0] + CLOSE, v[1] + CLOSE)
|
|
240
|
+
.some(i => { const k = kept[i]; return Math.hypot(k[0] - v[0], k[1] - v[1]) < CLOSE && Math.min(k[3], v[3]) - Math.max(k[2], v[2]) > 0.5 * (v[3] - v[2]); });
|
|
241
|
+
if (twin) continue;
|
|
242
|
+
byKept.add(kept.length, v[0], v[1], v[0], v[1]);
|
|
243
|
+
kept.push(v);
|
|
244
|
+
out.push(v[0], v[1], v[2], v[0], v[1], v[3]);
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A box's twelve edges (a cabinet: `poly` its four corners), from z0 to z1, and round it at each of `rings`. */
|
|
250
|
+
export function boxLines(poly: XY[], z0: number, z1: number, rings: number[] = []): number[] {
|
|
251
|
+
const out: number[] = [];
|
|
252
|
+
for (let i = 0; i < poly.length; i++) {
|
|
253
|
+
const a = poly[i], b = poly[(i + 1) % poly.length];
|
|
254
|
+
for (const z of [z0, z1, ...rings]) out.push(a.x, a.y, z, b.x, b.y, z);
|
|
255
|
+
out.push(a.x, a.y, z0, a.x, a.y, z1);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { XY } from '../model/types';
|
|
2
|
+
import { triangulate } from './poly';
|
|
3
|
+
|
|
4
|
+
/** What a surface is drawn with. 'pick': not drawn, only there to be tapped (a doorway). */
|
|
5
|
+
export type Mat3 = 'wall' | 'top' | 'floor' | 'cab' | 'counter' | 'glass' | 'door' | 'frame' | 'pick';
|
|
6
|
+
export const MATS: readonly Mat3[] = ['wall', 'top', 'floor', 'cab', 'counter', 'glass', 'door', 'frame', 'pick'];
|
|
7
|
+
|
|
8
|
+
/** What a part is, as a tap picks it: a straight wall, a curved one, a space's floor, a cabinet, a door or window. */
|
|
9
|
+
export type PartKind = 'wall' | 'curve' | 'floor' | 'cab' | 'opening';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One thing in the 3D view, as triangles grouped by what they're drawn with. Plan inches: x and y as on the plan, z up
|
|
13
|
+
* from the floor.
|
|
14
|
+
*/
|
|
15
|
+
export interface Part3 {
|
|
16
|
+
kind: PartKind;
|
|
17
|
+
id: string;
|
|
18
|
+
/** x, y, z of each vertex */
|
|
19
|
+
pos: number[];
|
|
20
|
+
/** the surface's normal at each vertex */
|
|
21
|
+
nrm: number[];
|
|
22
|
+
/** triangles, as vertex index triples, for each material */
|
|
23
|
+
tris: Partial<Record<Mat3, number[]>>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type V3 = [number, number, number];
|
|
27
|
+
|
|
28
|
+
/** Makes a part's triangles: flat faces, polygons laid flat, and upright prisms. */
|
|
29
|
+
export class PartBuilder implements Part3 {
|
|
30
|
+
pos: number[] = [];
|
|
31
|
+
nrm: number[] = [];
|
|
32
|
+
tris: Partial<Record<Mat3, number[]>> = {};
|
|
33
|
+
constructor(readonly kind: PartKind, readonly id: string) {}
|
|
34
|
+
|
|
35
|
+
private vert(p: V3, n: V3): number {
|
|
36
|
+
this.pos.push(p[0], p[1], p[2]);
|
|
37
|
+
this.nrm.push(n[0], n[1], n[2]);
|
|
38
|
+
return this.pos.length / 3 - 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private tri(mat: Mat3, a: number, b: number, c: number): void {
|
|
42
|
+
(this.tris[mat] ??= []).push(a, b, c);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A flat four-cornered face that faces `n`: its corners in order around it, either way round (it's turned to face
|
|
47
|
+
* n). `ns`: a normal for each corner instead, for a curved surface shaded smooth.
|
|
48
|
+
*/
|
|
49
|
+
quad(mat: Mat3, a: V3, b: V3, c: V3, d: V3, n: V3, ns?: [V3, V3, V3, V3]): void {
|
|
50
|
+
const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2], vx = d[0] - a[0], vy = d[1] - a[1], vz = d[2] - a[2];
|
|
51
|
+
const cx = uy * vz - uz * vy, cy = uz * vx - ux * vz, cz = ux * vy - uy * vx;
|
|
52
|
+
let pts: V3[] = [a, b, c, d], nn: V3[] = ns ?? [n, n, n, n];
|
|
53
|
+
// (counter-clockwise, seen from where it faces)
|
|
54
|
+
if (cx * n[0] + cy * n[1] + cz * n[2] < 0) { pts = [a, d, c, b]; nn = [nn[0], nn[3], nn[2], nn[1]]; }
|
|
55
|
+
const i = pts.map((p, k) => this.vert(p, nn[k]));
|
|
56
|
+
this.tri(mat, i[0], i[1], i[2]);
|
|
57
|
+
this.tri(mat, i[0], i[2], i[3]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A flat convex face with corners `pts` in order round it (some may lie along its sides: the corners of the faces
|
|
62
|
+
* beside it, so the two meet without a seam), facing n: triangles fanned from its middle.
|
|
63
|
+
*/
|
|
64
|
+
fan(mat: Mat3, pts: V3[], n: V3): void {
|
|
65
|
+
if (pts.length < 3) return;
|
|
66
|
+
const c: V3 = [0, 0, 0];
|
|
67
|
+
let nx = 0, ny = 0, nz = 0;
|
|
68
|
+
for (let i = 0; i < pts.length; i++) {
|
|
69
|
+
const a = pts[i], b = pts[(i + 1) % pts.length];
|
|
70
|
+
for (let k = 0; k < 3; k++) c[k] += a[k] / pts.length;
|
|
71
|
+
// (the way round the corners run: Newell's normal)
|
|
72
|
+
nx += (a[1] - b[1]) * (a[2] + b[2]); ny += (a[2] - b[2]) * (a[0] + b[0]); nz += (a[0] - b[0]) * (a[1] + b[1]);
|
|
73
|
+
}
|
|
74
|
+
const ring = nx * n[0] + ny * n[1] + nz * n[2] < 0 ? [...pts].reverse() : pts;
|
|
75
|
+
const ic = this.vert(c, n), is = ring.map(p => this.vert(p, n));
|
|
76
|
+
for (let i = 0; i < is.length; i++) this.tri(mat, ic, is[i], is[(i + 1) % is.length]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Polygon `poly` (counter-clockwise) laid flat at height z, facing up or down. */
|
|
80
|
+
cap(mat: Mat3, poly: XY[], z: number, up = true): void {
|
|
81
|
+
const t = triangulate(poly);
|
|
82
|
+
if (!t.length) return;
|
|
83
|
+
const n: V3 = [0, 0, up ? 1 : -1], base = this.pos.length / 3;
|
|
84
|
+
for (const p of poly) this.vert([p.x, p.y, z], n);
|
|
85
|
+
for (const [a, b, c] of t) {
|
|
86
|
+
if (up) this.tri(mat, base + a, base + b, base + c);
|
|
87
|
+
else this.tri(mat, base + a, base + c, base + b);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* An upright prism: polygon `poly` (counter-clockwise) from z0 up to z1 — its sides, its top (`top`: what it's
|
|
93
|
+
* drawn with; null, no top) and its underside (by default only when it's off the floor).
|
|
94
|
+
*/
|
|
95
|
+
prism(poly: XY[], z0: number, z1: number, side: Mat3, top: Mat3 | null, bottom: Mat3 | null = z0 > 1e-6 ? side : null): void {
|
|
96
|
+
if (z1 - z0 < 1e-6 || poly.length < 3) return;
|
|
97
|
+
for (let i = 0, n = poly.length; i < n; i++) {
|
|
98
|
+
const a = poly[i], b = poly[(i + 1) % n], dx = b.x - a.x, dy = b.y - a.y, L = Math.hypot(dx, dy);
|
|
99
|
+
if (L < 1e-9) continue;
|
|
100
|
+
// outward is to the right of a counter-clockwise outline
|
|
101
|
+
this.quad(side, [a.x, a.y, z0], [b.x, b.y, z0], [b.x, b.y, z1], [a.x, a.y, z1], [dy / L, -dx / L, 0]);
|
|
102
|
+
}
|
|
103
|
+
if (top) this.cap(top, poly, z1, true);
|
|
104
|
+
if (bottom) this.cap(bottom, poly, z0, false);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Whether nothing was made. */
|
|
108
|
+
get empty(): boolean {
|
|
109
|
+
return !this.pos.length;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { XY } from '../model/types';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Flat polygons for the 3D view: clipping one to a half-plane (a wall cut at a door's edge, a wall's share of a corner)
|
|
5
|
+
and cutting one into triangles (a floor, a wall's top). Plan inches, y up; counter-clockwise is positive.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Points closer than this (inches) are the same point. */
|
|
9
|
+
const EPS = 1e-6;
|
|
10
|
+
|
|
11
|
+
/** Signed area: positive when the points run counter-clockwise. */
|
|
12
|
+
export function area(p: XY[]): number {
|
|
13
|
+
let a = 0;
|
|
14
|
+
for (let i = 0, n = p.length; i < n; i++) {
|
|
15
|
+
const q = p[i], r = p[(i + 1) % n];
|
|
16
|
+
a += q.x * r.y - r.x * q.y;
|
|
17
|
+
}
|
|
18
|
+
return a / 2;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The points running counter-clockwise, without repeats (a point the same as the one before it). */
|
|
22
|
+
export function tidy(p: XY[]): XY[] {
|
|
23
|
+
const out: XY[] = [];
|
|
24
|
+
for (const q of p) {
|
|
25
|
+
const last = out[out.length - 1];
|
|
26
|
+
if (!last || Math.abs(last.x - q.x) > EPS || Math.abs(last.y - q.y) > EPS) out.push({ x: q.x, y: q.y });
|
|
27
|
+
}
|
|
28
|
+
while (out.length > 1 && Math.abs(out[0].x - out[out.length - 1].x) <= EPS && Math.abs(out[0].y - out[out.length - 1].y) <= EPS) out.pop();
|
|
29
|
+
return area(out) < 0 ? out.reverse() : out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The part of `p` where n · point ≤ c (one side of a line), as a polygon — empty when none of it is. */
|
|
33
|
+
export function clipHalf(p: XY[], nx: number, ny: number, c: number): XY[] {
|
|
34
|
+
const out: XY[] = [];
|
|
35
|
+
for (let i = 0, n = p.length; i < n; i++) {
|
|
36
|
+
const a = p[i], b = p[(i + 1) % n];
|
|
37
|
+
const da = nx * a.x + ny * a.y - c, db = nx * b.x + ny * b.y - c;
|
|
38
|
+
if (da <= 0) out.push(a);
|
|
39
|
+
if ((da < 0 && db > 0) || (da > 0 && db < 0)) {
|
|
40
|
+
const t = da / (da - db);
|
|
41
|
+
out.push({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The part of `p` within `half` of the line through `o` along unit `u` (a wall's band, its thickness wide). */
|
|
48
|
+
export function clipBand(p: XY[], o: XY, u: XY, half: number): XY[] {
|
|
49
|
+
const nx = -u.y, ny = u.x, c = nx * o.x + ny * o.y;
|
|
50
|
+
return clipHalf(clipHalf(p, nx, ny, c + half), -nx, -ny, -c + half);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The part of `p` from `s0` to `s1` along unit `u` measured from `o` (a wall between two cuts across it). */
|
|
54
|
+
export function clipSpan(p: XY[], o: XY, u: XY, s0: number, s1: number): XY[] {
|
|
55
|
+
const c = u.x * o.x + u.y * o.y;
|
|
56
|
+
return clipHalf(clipHalf(p, u.x, u.y, c + s1), -u.x, -u.y, -(c + s0));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether `p` is inside triangle a b c (counter-clockwise), off its edges. */
|
|
60
|
+
function inside(p: XY, a: XY, b: XY, c: XY): boolean {
|
|
61
|
+
const s = (q: XY, r: XY) => (r.x - q.x) * (p.y - q.y) - (r.y - q.y) * (p.x - q.x);
|
|
62
|
+
return s(a, b) > EPS && s(b, c) > EPS && s(c, a) > EPS;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Triangles covering polygon `p` (counter-clockwise, no repeats — see tidy), as index triples into it, each
|
|
67
|
+
* counter-clockwise. Ear clipping: a floor's outline is a simple polygon, convex or not.
|
|
68
|
+
*/
|
|
69
|
+
export function triangulate(p: XY[]): [number, number, number][] {
|
|
70
|
+
const idx = p.map((_, i) => i), tris: [number, number, number][] = [];
|
|
71
|
+
const cross = (i0: number, i1: number, i2: number) => {
|
|
72
|
+
const a = p[i0], b = p[i1], c = p[i2];
|
|
73
|
+
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
74
|
+
};
|
|
75
|
+
let guard = 0;
|
|
76
|
+
while (idx.length > 3 && guard++ < 100000) {
|
|
77
|
+
let cut = false;
|
|
78
|
+
for (let i = 0; i < idx.length; i++) {
|
|
79
|
+
const i0 = idx[(i + idx.length - 1) % idx.length], i1 = idx[i], i2 = idx[(i + 1) % idx.length];
|
|
80
|
+
if (cross(i0, i1, i2) <= EPS) continue;
|
|
81
|
+
let blocked = false;
|
|
82
|
+
for (const j of idx) {
|
|
83
|
+
if (j !== i0 && j !== i1 && j !== i2 && inside(p[j], p[i0], p[i1], p[i2])) { blocked = true; break; }
|
|
84
|
+
}
|
|
85
|
+
if (blocked) continue;
|
|
86
|
+
tris.push([i0, i1, i2]);
|
|
87
|
+
idx.splice(i, 1);
|
|
88
|
+
cut = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (cut) continue;
|
|
92
|
+
// no ear left (points in a row, or rounding): drop the flattest corner, keeping it if it covers anything
|
|
93
|
+
let best = 0, flat = Infinity;
|
|
94
|
+
for (let i = 0; i < idx.length; i++) {
|
|
95
|
+
const c = Math.abs(cross(idx[(i + idx.length - 1) % idx.length], idx[i], idx[(i + 1) % idx.length]));
|
|
96
|
+
if (c < flat) { flat = c; best = i; }
|
|
97
|
+
}
|
|
98
|
+
const i0 = idx[(best + idx.length - 1) % idx.length], i1 = idx[best], i2 = idx[(best + 1) % idx.length];
|
|
99
|
+
if (cross(i0, i1, i2) > EPS) tris.push([i0, i1, i2]);
|
|
100
|
+
idx.splice(best, 1);
|
|
101
|
+
}
|
|
102
|
+
if (idx.length === 3 && cross(idx[0], idx[1], idx[2]) > EPS) tris.push([idx[0], idx[1], idx[2]]);
|
|
103
|
+
return tris;
|
|
104
|
+
}
|