@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/src/react/Toast.tsx
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { Animated, Text, View } from 'react-native';
|
|
3
|
+
import { notify, ui, useEditorKey } from './store';
|
|
3
4
|
import { C, FONT, shadow } from './theme';
|
|
5
|
+
import { Tap, usePopup } from './motion';
|
|
6
|
+
import { createSheet } from './corners';
|
|
4
7
|
|
|
5
8
|
/** A short message above the bottom bar, sometimes with an action (Undo). */
|
|
6
9
|
export function Toast({ phone }: { phone: boolean }) {
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
useEditorKey(() => (ui.toast ? String(ui.toast.id) : ''));
|
|
11
|
+
// (it rises in, and fades as it goes, still saying what it said)
|
|
12
|
+
const open = !!ui.toast, last = useRef(ui.toast), pop = usePopup(open, { y: 8 }, 'bottom center');
|
|
13
|
+
if (ui.toast) last.current = ui.toast;
|
|
14
|
+
const t = last.current;
|
|
15
|
+
if (!pop.present || !t) return null;
|
|
9
16
|
return (
|
|
10
17
|
<View style={[s.wrap, { bottom: phone ? 160 : 96 }]}>
|
|
11
|
-
<View style={[s.toast, t.warn && s.warn]} accessibilityRole="alert" testID=
|
|
18
|
+
<Animated.View style={[s.toast, t.warn && s.warn, pop.style]} pointerEvents={open ? 'auto' : 'none'} accessibilityRole="alert" testID={open ? 'toast' : undefined} key={t.id}>
|
|
12
19
|
<Text style={s.text}>{t.msg}</Text>
|
|
13
20
|
{t.action && (
|
|
14
|
-
<
|
|
21
|
+
<Tap accessibilityRole="button" onPress={() => { const fn = t.action!.fn; ui.toast = null; notify(); fn(); }}>
|
|
15
22
|
<Text style={s.action}>{t.action.label}</Text>
|
|
16
|
-
</
|
|
23
|
+
</Tap>
|
|
17
24
|
)}
|
|
18
|
-
</View>
|
|
25
|
+
</Animated.View>
|
|
19
26
|
</View>
|
|
20
27
|
);
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
const s =
|
|
30
|
+
const s = createSheet({
|
|
24
31
|
wrap: { pointerEvents: 'box-none', position: 'absolute', left: 16, right: 16, alignItems: 'center', zIndex: 70 },
|
|
25
32
|
toast: { backgroundColor: C.dark, borderWidth: 1, borderColor: C.g2, borderRadius: 12, paddingVertical: 11, paddingHorizontal: 14, flexDirection: 'row', alignItems: 'center', gap: 14, maxWidth: 520, ...shadow },
|
|
26
33
|
warn: { borderLeftWidth: 4, borderLeftColor: C.wred },
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Animated, useWindowDimensions, View } from 'react-native';
|
|
3
|
+
import { TIP_TOP, topLayout } from './topRow';
|
|
4
|
+
import { RichText } from './RichText';
|
|
5
|
+
import { ui, useEditorKey } from './store';
|
|
6
|
+
import { C, FONT, shadow } from './theme';
|
|
7
|
+
import { TOGGLE_EASE } from './motion';
|
|
8
|
+
import { NATIVE } from './easing';
|
|
9
|
+
import { createSheet } from './corners';
|
|
10
|
+
|
|
11
|
+
/** How long the note stays, and how it comes and goes. */
|
|
12
|
+
const SHOW_MS = 2600, IN_MS = 160, OUT_MS = 240;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the tool just picked from the rail does, in a line, just under the Annotate / Draft tabs: it fades in, stays a
|
|
16
|
+
* moment and goes by itself; picking another tool replaces it at once.
|
|
17
|
+
*/
|
|
18
|
+
export function ToolTip({ phone }: { phone: boolean }) {
|
|
19
|
+
const { width, height } = useWindowDimensions();
|
|
20
|
+
useEditorKey(() => String(ui.toolTip?.seq ?? '') + '|' + topLayout(width, height, phone).below);
|
|
21
|
+
const tip = ui.toolTip, v = useRef(new Animated.Value(0)).current, [shown, setShown] = useState<{ text: string; seq: number } | null>(null);
|
|
22
|
+
const slide = useRef(v.interpolate({ inputRange: [0, 1], outputRange: [-4, 0] })).current;
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!tip) return;
|
|
25
|
+
setShown(tip);
|
|
26
|
+
v.setValue(0);
|
|
27
|
+
const go = (to: number, ms: number) => Animated.timing(v, { toValue: to, duration: ms, easing: TOGGLE_EASE, useNativeDriver: NATIVE });
|
|
28
|
+
go(1, IN_MS).start();
|
|
29
|
+
const t = setTimeout(() => go(0, OUT_MS).start(({ finished }) => { if (finished) setShown(s => (s?.seq === tip.seq ? null : s)); }), SHOW_MS);
|
|
30
|
+
return () => clearTimeout(t);
|
|
31
|
+
}, [tip?.seq]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
32
|
+
// (always mounted, see-through when idle: a view put on screen as its fade starts can be detached for a moment by
|
|
33
|
+
// React's strict mode, which stops the fade part-way)
|
|
34
|
+
return (
|
|
35
|
+
<View pointerEvents="none" style={[s.wrap, { top: topLayout(width, height, phone).below ? TIP_TOP.below : TIP_TOP.row }]}>
|
|
36
|
+
<Animated.View testID={shown ? 'toolTip' : undefined} accessibilityLiveRegion="polite" aria-hidden={!shown}
|
|
37
|
+
style={[s.tip, !shown && s.idle, { opacity: v, transform: [{ translateY: slide }] }]}>
|
|
38
|
+
{shown ? <RichText html={shown.text} style={s.text} /> : null}
|
|
39
|
+
</Animated.View>
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const s = createSheet({
|
|
45
|
+
wrap: { position: 'absolute', left: 12, right: 12, alignItems: 'center', zIndex: 29 },
|
|
46
|
+
tip: { maxWidth: 460, backgroundColor: C.g1, borderRadius: 999, paddingVertical: 8, paddingHorizontal: 14, borderWidth: 1, borderColor: C.g2, ...shadow },
|
|
47
|
+
text: { fontFamily: FONT, fontSize: 13, color: C.lg2, textAlign: 'center' },
|
|
48
|
+
idle: { borderWidth: 0, padding: 0, boxShadow: 'none' },
|
|
49
|
+
});
|
package/src/react/TopBar.tsx
CHANGED
|
@@ -1,75 +1,133 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Text, useWindowDimensions, View } from 'react-native';
|
|
3
|
+
import { closeElevation, elev, elevOpen, elevTitle, flipElevation } from '../editor/elevation';
|
|
4
4
|
import { setMode } from '../editor/mode';
|
|
5
5
|
import { openDrawer } from '../editor/ports';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { close3d, open3d } from '../editor/view3d';
|
|
7
|
+
import { command3d, is3d } from '../editor/view3dState';
|
|
8
|
+
import { ed, type Mode } from '../app/state';
|
|
8
9
|
import { page, store } from '../model/store';
|
|
9
10
|
import { fitView } from '../render/view';
|
|
10
11
|
import { CircleButton, Pill } from './Buttons';
|
|
11
12
|
import { Icon } from './Icon';
|
|
12
13
|
import { reactPorts } from './ports';
|
|
13
|
-
import {
|
|
14
|
+
import { manualSave, saveState } from './saving';
|
|
15
|
+
import { notify, ui, useEditorKey } from './store';
|
|
14
16
|
import { C, FONT, shadow } from './theme';
|
|
17
|
+
import { Segmented, Tap } from './motion';
|
|
18
|
+
import { createSheet } from './corners';
|
|
19
|
+
import { setTopRoom, TABS_TOP, topKey, topLayout } from './topRow';
|
|
15
20
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The top bar: back, the project and page, view controls, the host's own buttons, settings, and Save. In the wall
|
|
23
|
+
* view: back to the plan, the wall and the side it's seen from, and Flip to look from the other side.
|
|
24
|
+
*/
|
|
25
|
+
export function TopBar({ onBack, headerRight, phone, readOnly }: {
|
|
26
|
+
onBack?: () => void; headerRight?: ReactNode | ((buttonSize: number) => ReactNode); phone: boolean; readOnly?: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
const { width, height } = useWindowDimensions(), size = phone ? 42 : 46;
|
|
29
|
+
useEditorKey(() => {
|
|
30
|
+
const w = elevOpen() && !!elev.frame, title = w ? elevTitle() : null;
|
|
31
|
+
return [w, title?.title, title?.sub, store.project.name, page().name, ui.menu, saveState(), manualSave(), is3d(), topKey(topLayout(width, height, phone, size))].join('|');
|
|
32
|
+
});
|
|
33
|
+
const wall = elevOpen() && !!elev.frame, t = wall ? elevTitle() : null, crumb = topLayout(width, height, phone, size).crumb;
|
|
19
34
|
return (
|
|
20
35
|
<View style={s.top}>
|
|
21
|
-
<View style={s.side}>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
<View style={[s.side, wall && { flexShrink: 1 }]}>
|
|
37
|
+
{wall ? (
|
|
38
|
+
<>
|
|
39
|
+
<CircleButton icon="back" label="Back to the plan" size={size} onPress={closeElevation} testID="elevClose" />
|
|
40
|
+
{/* on a phone the wall's name gets the row below (where Draft / Annotate sit on the plan) */}
|
|
41
|
+
<View style={[s.crumb, s.wallCrumb, { height: size }, phone && s.wallCrumbPhone]} accessibilityLabel="Wall elevation" testID="elev">
|
|
42
|
+
<Text numberOfLines={1} style={s.wallTitle} testID="elevTitle">{t!.title}</Text>
|
|
43
|
+
<Text numberOfLines={1} style={s.wallSub} testID="elevSub">{t!.sub}</Text>
|
|
44
|
+
</View>
|
|
45
|
+
</>
|
|
46
|
+
) : (
|
|
47
|
+
<CircleButton icon="back" label={onBack ? 'Back' : 'Project pages'} size={size} onPress={onBack ?? (() => openDrawer('pages'))} testID="btnPages" />
|
|
48
|
+
)}
|
|
49
|
+
{/* (where there's room: beside the tabs, or with the tabs below it, up to the buttons) */}
|
|
50
|
+
{!phone && !wall && crumb > 0 && (
|
|
51
|
+
<Tap accessibilityRole="button" accessibilityLabel="Open pages" testID="crumb" onPress={() => openDrawer('pages')} style={[s.crumb, { maxWidth: crumb }]}>
|
|
25
52
|
<Text numberOfLines={1} style={s.crumbProj}>{store.project.name}</Text>
|
|
26
53
|
<Text style={s.crumbSep}>/</Text>
|
|
27
54
|
<Text numberOfLines={1} style={s.crumbPage}>{page().name}</Text>
|
|
28
|
-
</
|
|
55
|
+
</Tap>
|
|
29
56
|
)}
|
|
30
57
|
</View>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{
|
|
58
|
+
{/* (how wide the buttons are decides whether the tabs fit between them and the crumb: the wall view's own aren't
|
|
59
|
+
the plan's, and its tabs are hidden) */}
|
|
60
|
+
<View style={[s.side, phone && { gap: 6 }]} onLayout={e => { if (!elevOpen()) setTopRoom('right', e.nativeEvent.layout.width); }}>
|
|
61
|
+
{wall && (
|
|
62
|
+
<Pill kind="grey" accessibilityLabel="Flip: look from the other side" testID="elevFlip" onPress={flipElevation} style={[{ height: size, minWidth: 0 }, phone && { paddingHorizontal: 12 }]}>
|
|
63
|
+
<Icon name="flip" />{!phone && <Text style={s.flipText}>Flip</Text>}
|
|
64
|
+
</Pill>
|
|
65
|
+
)}
|
|
66
|
+
{/* (on a phone, Fit is the first item in •••: the row has no room for it) */}
|
|
67
|
+
{!phone && <CircleButton icon="fit" label="Fit to screen" size={size} onPress={() => (is3d() ? command3d('fit') : fitView())} testID="btnFit" />}
|
|
68
|
+
{/* the stack: what's shown (the Layers menu), open or closed — the plan's layers, so not in 3D */}
|
|
69
|
+
{!(wall && phone) && !is3d() && (
|
|
70
|
+
<CircleButton icon="layers" label="Layers" size={size} iconColor={ui.menu === 'layers' ? C.yellow : '#fff'}
|
|
71
|
+
onPress={() => { ui.menu = ui.menu === 'layers' ? false : 'layers'; ui.palette = false; ui.shapeMenu = null; notify(); }} testID="btnLayers" />
|
|
72
|
+
)}
|
|
73
|
+
{/* the room: the project panel, pages and spaces */}
|
|
74
|
+
{!wall && <CircleButton icon="spaces" label="Pages and spaces" size={size} onPress={() => openDrawer('pages')} testID="btnProject" />}
|
|
75
|
+
{typeof headerRight === 'function' ? headerRight(size) : headerRight}
|
|
76
|
+
{/* •••: exports, files and settings */}
|
|
77
|
+
<CircleButton icon="dots" label="Share, settings and more" size={size} iconColor={ui.menu === 'more' ? C.yellow : '#fff'}
|
|
78
|
+
onPress={() => { ui.menu = ui.menu === 'more' ? false : 'more'; ui.palette = false; ui.shapeMenu = null; notify(); }} testID="btnMore" />
|
|
79
|
+
{!readOnly && (
|
|
80
|
+
<Pill accessibilityLabel="Save" testID="btnSave" style={{ height: size }}
|
|
81
|
+
disabled={saveState() === 'saving'} onPress={() => reactPorts.save()}>
|
|
82
|
+
<Icon name="save" color={C.dark} />
|
|
83
|
+
{/* saving manually: what Save will do, or has done */}
|
|
84
|
+
{manualSave() && !phone && (
|
|
85
|
+
<Text style={s.saveText}>{saveState() === 'saving' ? 'Saving…' : saveState() === 'saved' ? 'Saved' : 'Save'}</Text>
|
|
86
|
+
)}
|
|
87
|
+
</Pill>
|
|
88
|
+
)}
|
|
42
89
|
</View>
|
|
43
90
|
</View>
|
|
44
91
|
);
|
|
45
92
|
}
|
|
46
93
|
|
|
47
|
-
|
|
94
|
+
const TABS: [Mode | '3d', string][] = [['annotate', 'Annotate'], ['draft', 'Draft'], ['3d', '3D']];
|
|
95
|
+
|
|
96
|
+
/** Annotate / Draft / 3D: centred in the top row, or on a row of their own under it (see topRow.ts). */
|
|
48
97
|
export function ModeTabs({ phone }: { phone: boolean }) {
|
|
98
|
+
const { width, height } = useWindowDimensions();
|
|
99
|
+
useEditorKey(() => (is3d() ? '3d' : ed.mode) + '|' + topKey(topLayout(width, height, phone)));
|
|
100
|
+
const at = topLayout(width, height, phone);
|
|
101
|
+
// (straight over: no line under the tabs saying how to get about in 3D)
|
|
102
|
+
const pick = (m: Mode | '3d') => {
|
|
103
|
+
if (m !== '3d') { close3d(); setMode(m); return; }
|
|
104
|
+
open3d();
|
|
105
|
+
};
|
|
49
106
|
return (
|
|
50
|
-
<View style={[s.
|
|
51
|
-
{(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<Pressable key={m} accessibilityRole="tab" accessibilityState={{ selected: on }} accessibilityLabel={m === 'draft' ? 'Draft' : 'Annotate'}
|
|
55
|
-
onPress={() => setMode(m)} style={[s.mode, on && { backgroundColor: '#fff' }]}>
|
|
56
|
-
<Text style={[s.modeText, on && { color: C.dark }]}>{m === 'draft' ? 'Draft' : 'Annotate'}</Text>
|
|
57
|
-
</Pressable>
|
|
58
|
-
);
|
|
59
|
-
})}
|
|
107
|
+
<View pointerEvents="box-none" style={[s.modesAt, at.left == null ? s.modesCentred : { left: at.left }, { top: at.below ? TABS_TOP.below : TABS_TOP.row }]}
|
|
108
|
+
onLayout={e => setTopRoom('tabs', e.nativeEvent.layout.width)}>
|
|
109
|
+
<Segmented role="tab" value={is3d() ? '3d' : ed.mode} options={TABS} onPick={pick}
|
|
110
|
+
style={s.modes} optionStyle={s.mode} textStyle={s.modeText} onText={C.dark} offText={C.lg1} />
|
|
60
111
|
</View>
|
|
61
112
|
);
|
|
62
113
|
}
|
|
63
114
|
|
|
64
|
-
const s =
|
|
115
|
+
const s = createSheet({
|
|
116
|
+
wallCrumb: { flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center', gap: 0, flexShrink: 1, minWidth: 0 },
|
|
117
|
+
wallCrumbPhone: { position: 'absolute', top: 54, left: 0, maxWidth: 260 },
|
|
118
|
+
wallTitle: { fontFamily: FONT, fontSize: 14, fontWeight: '700', color: '#fff' },
|
|
119
|
+
wallSub: { fontFamily: FONT, fontSize: 12, color: C.lg1 },
|
|
120
|
+
flipText: { fontFamily: FONT, fontSize: 14, fontWeight: '600', color: '#fff', marginLeft: 6 },
|
|
121
|
+
saveText: { fontFamily: FONT, fontSize: 14, fontWeight: '600', color: C.dark, marginLeft: 6 },
|
|
65
122
|
top: { pointerEvents: 'box-none', position: 'absolute', top: 12, left: 12, right: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10, zIndex: 30 },
|
|
66
123
|
side: { pointerEvents: 'box-none', flexDirection: 'row', alignItems: 'center', gap: 8, minWidth: 0 },
|
|
67
124
|
crumb: { height: 46, borderRadius: 24, backgroundColor: C.g1, paddingHorizontal: 16, flexDirection: 'row', alignItems: 'center', gap: 7, maxWidth: 420, ...shadow },
|
|
68
125
|
crumbProj: { fontFamily: FONT, fontSize: 14, color: C.lg1, flexShrink: 1 },
|
|
69
126
|
crumbSep: { fontFamily: FONT, fontSize: 14, color: C.g4 },
|
|
70
127
|
crumbPage: { fontFamily: FONT, fontSize: 14, color: '#fff', fontWeight: '600', flexShrink: 1 },
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
128
|
+
modesAt: { position: 'absolute', zIndex: 31 },
|
|
129
|
+
modesCentred: { alignSelf: 'center' },
|
|
130
|
+
modes: { flexDirection: 'row', backgroundColor: C.g1, borderRadius: 24, padding: 4, height: 46, ...shadow },
|
|
131
|
+
mode: { paddingHorizontal: 18, borderRadius: 999, justifyContent: 'center' },
|
|
74
132
|
modeText: { fontFamily: FONT, fontSize: 14, fontWeight: '600', color: C.lg1 },
|
|
75
133
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
4
|
+
import { DURATION, type TweenTo } from './easing';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
TweenView on a device (see Tween.tsx): its box eases on the UI thread with Reanimated, so a rail growing longer or a
|
|
8
|
+
segmented control's highlight sliding never waits for the JavaScript thread. Same curve as the rest of the chrome.
|
|
9
|
+
*/
|
|
10
|
+
const EASE_UI = Easing.bezier(0.2, 0, 0, 1);
|
|
11
|
+
|
|
12
|
+
export function TweenView({ to, duration = DURATION, style, ...rest }: ViewProps & { to: TweenTo; duration?: number }) {
|
|
13
|
+
const left = useSharedValue(to.left ?? 0), top = useSharedValue(to.top ?? 0);
|
|
14
|
+
const width = useSharedValue(to.width ?? 0), height = useSharedValue(to.height ?? 0);
|
|
15
|
+
const hasL = to.left != null, hasT = to.top != null, hasW = to.width != null, hasH = to.height != null;
|
|
16
|
+
// (each set at once the first time it's given, as in a browser — one given after the first render too)
|
|
17
|
+
const given = useRef({ left: hasL, top: hasT, width: hasW, height: hasH }).current;
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const cfg = { duration, easing: EASE_UI };
|
|
20
|
+
const go = (k: keyof typeof given, v: { value: number }, to: number | undefined) => {
|
|
21
|
+
if (to == null) return;
|
|
22
|
+
v.value = given[k] ? withTiming(to, cfg) : to;
|
|
23
|
+
given[k] = true;
|
|
24
|
+
};
|
|
25
|
+
go('left', left, to.left); go('top', top, to.top); go('width', width, to.width); go('height', height, to.height);
|
|
26
|
+
}, [to.left, to.top, to.width, to.height]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
27
|
+
const box = useAnimatedStyle(() => {
|
|
28
|
+
const o: { left?: number; top?: number; width?: number; height?: number } = {};
|
|
29
|
+
if (hasL) o.left = left.value;
|
|
30
|
+
if (hasT) o.top = top.value;
|
|
31
|
+
if (hasW) o.width = width.value;
|
|
32
|
+
if (hasH) o.height = height.value;
|
|
33
|
+
return o;
|
|
34
|
+
});
|
|
35
|
+
return <Animated.View {...rest} style={[style, box]} />;
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, type ViewProps } from 'react-native';
|
|
3
|
+
import { DURATION, EASE, type TweenTo } from './easing';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
A view whose size and place ease to each new value: what a transform can't do — a rail growing longer, the highlight
|
|
7
|
+
of a segmented control moving to a wider option. In a browser JavaScript draws every frame anyway; on a device the
|
|
8
|
+
same view runs on the UI thread (Tween.native.tsx), so it never waits for the JavaScript thread.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const KEYS = ['left', 'top', 'width', 'height'] as const;
|
|
12
|
+
|
|
13
|
+
/** `to`: the box it eases to (set at once the first time); `duration` in ms. */
|
|
14
|
+
export function TweenView({ to, duration = DURATION, style, ...rest }: ViewProps & { to: TweenTo; duration?: number }) {
|
|
15
|
+
const vals = useRef<Partial<Record<(typeof KEYS)[number], Animated.Value>>>({}).current;
|
|
16
|
+
for (const k of KEYS) if (to[k] != null && !vals[k]) vals[k] = new Animated.Value(to[k]!);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
for (const k of KEYS) {
|
|
19
|
+
const v = vals[k];
|
|
20
|
+
if (v && to[k] != null) Animated.timing(v, { toValue: to[k]!, duration, easing: EASE, useNativeDriver: false }).start();
|
|
21
|
+
}
|
|
22
|
+
}, [to.left, to.top, to.width, to.height]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
23
|
+
return <Animated.View {...rest} style={[style, vals]} />;
|
|
24
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ComponentType } from 'react';
|
|
2
|
+
import { PixelRatio, StyleSheet, View, type LayoutChangeEvent, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
4
|
+
import type { WebGLRenderer } from 'three';
|
|
5
|
+
import { toast } from '../editor/ports';
|
|
6
|
+
import { view3dKey } from '../editor/view3d';
|
|
7
|
+
import { rendererOn, sizeOn } from '../view3d/viewer';
|
|
8
|
+
import { useEditorKey } from './store';
|
|
9
|
+
import { View3DHost } from './View3DHost';
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
The 3D tab on a device: an expo-gl view over the plan (which stays as it was underneath), drawn by three.js on the
|
|
13
|
+
JavaScript thread only when something changed. One finger turns the model, two move it and pinch to zoom, a tap
|
|
14
|
+
picks what's under it, a double tap frames everything.
|
|
15
|
+
|
|
16
|
+
Sized from its layout at the screen's pixel ratio, never from gl.drawingBufferWidth: expo-gl sets that once, as the
|
|
17
|
+
view is made, and leaves it as the device turns (iOS resizes the buffer itself as the view lays out; Android's
|
|
18
|
+
surface follows the view).
|
|
19
|
+
|
|
20
|
+
The GL view is made only once this view has a size, as react-three-fiber makes its own: resized after it's made,
|
|
21
|
+
expo-gl (iOS) deletes the framebuffer three.js draws to, makes another and binds framebuffers behind three.js's back,
|
|
22
|
+
and three.js keeps track of what it thinks is bound. So each resize (a device turned) binds the default framebuffer
|
|
23
|
+
afresh (sizeOn — not three.js's resetState(), which reads gl.canvas: expo-gl's context has none, and it threw), and
|
|
24
|
+
the frames just after are drawn again, in case one went while expo-gl was making its buffers — this view draws
|
|
25
|
+
only when something changes, so a frame lost stays lost. Anything that goes wrong making or sizing it is said in a
|
|
26
|
+
toast, never thrown: thrown from a GL view's callbacks, it takes the app down.
|
|
27
|
+
|
|
28
|
+
Left, the tab's view is kept (hidden, untouchable, drawing nothing: `active`), so coming back is at once — made
|
|
29
|
+
again each time, its GL context, renderer and shaders took a pause every time the tab opened. CadEditor lets it go
|
|
30
|
+
after a while away.
|
|
31
|
+
|
|
32
|
+
expo-gl comes from the host app (BOLDR has it, for its layout viewer); its types aren't installed here, so what's
|
|
33
|
+
used of it is written out below.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** expo-gl's context: WebGL 2, plus the call that puts a drawn frame on the screen. */
|
|
37
|
+
interface ExpoGL extends WebGL2RenderingContext { endFrameEXP(): void }
|
|
38
|
+
interface GLViewProps { style?: StyleProp<ViewStyle>; onContextCreate: (gl: ExpoGL) => void; msaaSamples?: number }
|
|
39
|
+
declare const require: (id: 'expo-gl') => { GLView: ComponentType<GLViewProps> };
|
|
40
|
+
const { GLView } = require('expo-gl');
|
|
41
|
+
|
|
42
|
+
export function View3D({ theme, active = true }: { theme: 'light' | 'dark'; active?: boolean }) {
|
|
43
|
+
const host = useRef<View3DHost | null>(null), renderer = useRef<WebGLRenderer | null>(null), context = useRef<ExpoGL | null>(null);
|
|
44
|
+
const size = useRef({ w: 0, h: 0 }), themeNow = useRef(theme), failed = useRef(false);
|
|
45
|
+
// (the GL view waits for this view's size: see above)
|
|
46
|
+
const [laidOut, setLaidOut] = useState(false);
|
|
47
|
+
themeNow.current = theme;
|
|
48
|
+
const key = useEditorKey(view3dKey);
|
|
49
|
+
const drop = () => { host.current?.dispose(); renderer.current?.dispose(); host.current = null; renderer.current = null; context.current = null; };
|
|
50
|
+
useEffect(() => drop, []);
|
|
51
|
+
// (hidden, it follows nothing: shown again, it catches up with the plan at once)
|
|
52
|
+
const activeNow = useRef(active);
|
|
53
|
+
activeNow.current = active;
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
host.current?.setVisible(active);
|
|
56
|
+
if (active) host.current?.sync(theme);
|
|
57
|
+
}, [key, theme, active]);
|
|
58
|
+
|
|
59
|
+
/** What went wrong making or sizing the view, in a toast (once): see above. */
|
|
60
|
+
const fail = (e: unknown) => {
|
|
61
|
+
console.error('3D view:', e);
|
|
62
|
+
if (failed.current) return;
|
|
63
|
+
failed.current = true;
|
|
64
|
+
toast('The 3D view couldn’t start (' + (e instanceof Error ? e.message : String(e)) + '). Leave the tab and open it again to retry.', { warn: true });
|
|
65
|
+
};
|
|
66
|
+
/** The drawing as big as the view (in points, at the screen's pixel density): again as the device turns. */
|
|
67
|
+
const fit = () => {
|
|
68
|
+
const { w, h } = size.current;
|
|
69
|
+
if (!renderer.current || !context.current || w < 1 || h < 1) return;
|
|
70
|
+
try {
|
|
71
|
+
sizeOn(renderer.current, context.current, w, h, PixelRatio.get());
|
|
72
|
+
host.current?.resize(w, h);
|
|
73
|
+
host.current?.settle();
|
|
74
|
+
} catch (e) { fail(e); }
|
|
75
|
+
};
|
|
76
|
+
const onContextCreate = (gl: ExpoGL) => {
|
|
77
|
+
drop();
|
|
78
|
+
try {
|
|
79
|
+
renderer.current = rendererOn(gl);
|
|
80
|
+
context.current = gl;
|
|
81
|
+
// (the GL view is made after this view's first layout: its size is known)
|
|
82
|
+
host.current = new View3DHost(renderer.current, themeNow.current, () => gl.endFrameEXP());
|
|
83
|
+
host.current.setVisible(activeNow.current);
|
|
84
|
+
fit();
|
|
85
|
+
host.current.sync(themeNow.current);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
drop();
|
|
88
|
+
fail(e);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const onLayout = (e: LayoutChangeEvent) => {
|
|
92
|
+
const { width, height } = e.nativeEvent.layout;
|
|
93
|
+
size.current = { w: width, h: height };
|
|
94
|
+
if (width >= 1 && height >= 1) setLaidOut(true);
|
|
95
|
+
fit();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const gesture = useMemo(() => {
|
|
99
|
+
// the chrome steps aside while either moves the camera: a pinch with its fingers spreading evenly needn't move
|
|
100
|
+
// their middle far enough to start the pan
|
|
101
|
+
let panning = false, pinching = false;
|
|
102
|
+
const moving = () => host.current?.moving(panning || pinching);
|
|
103
|
+
// one finger turns the model, two move it (and pinch to zoom, at the same time)
|
|
104
|
+
const pan = Gesture.Pan().runOnJS(true).minPointers(1).maxPointers(2).averageTouches(true)
|
|
105
|
+
.onStart(() => { panning = true; moving(); })
|
|
106
|
+
.onChange(e => { if (e.numberOfPointers >= 2) host.current?.pan(e.changeX, e.changeY); else host.current?.orbit(e.changeX, e.changeY); })
|
|
107
|
+
.onFinalize(() => { panning = false; moving(); });
|
|
108
|
+
const pinch = Gesture.Pinch().runOnJS(true)
|
|
109
|
+
.onStart(() => { pinching = true; moving(); })
|
|
110
|
+
.onChange(e => { if (e.scaleChange > 0) host.current?.zoom(1 / e.scaleChange, e.focalX, e.focalY); })
|
|
111
|
+
.onFinalize(() => { pinching = false; moving(); });
|
|
112
|
+
// a tap is told at once, and the host makes a second one soon after a double tap: a double-tap gesture would hold
|
|
113
|
+
// every single tap back until it failed (half a second)
|
|
114
|
+
const tap = Gesture.Tap().runOnJS(true).maxDuration(400).onEnd((e, ok) => { if (ok) host.current?.tapAt(e.x, e.y); });
|
|
115
|
+
return Gesture.Race(Gesture.Simultaneous(pan, pinch), tap);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<GestureDetector gesture={gesture}>
|
|
120
|
+
<View style={[s.fill, !active && s.away]} pointerEvents={active ? 'auto' : 'none'} aria-hidden={!active}
|
|
121
|
+
onLayout={onLayout} collapsable={false} testID="view3d" accessibilityLabel="3D view of the plan">
|
|
122
|
+
{laidOut && <GLView style={s.fill} onContextCreate={onContextCreate} msaaSamples={4} />}
|
|
123
|
+
</View>
|
|
124
|
+
</GestureDetector>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// (no zIndex: it's drawn after the plan and before the chrome, which stays on top. Spelt out: React Native 0.86
|
|
129
|
+
// dropped StyleSheet.absoluteFillObject, and with it undefined the view got no style — the screen's width, no height —
|
|
130
|
+
// so the 3D tab showed nothing over the plan.)
|
|
131
|
+
const s = StyleSheet.create({ fill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, away: { opacity: 0 } });
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { WebGLRenderer } from 'three';
|
|
3
|
+
import { toast } from '../editor/ports';
|
|
4
|
+
import { view3dKey } from '../editor/view3d';
|
|
5
|
+
import { useEditorKey } from './store';
|
|
6
|
+
import { View3DHost } from './View3DHost';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
The 3D tab in a browser: a WebGL <canvas> over the plan (which stays as it was underneath). A finger or the mouse
|
|
10
|
+
drags to turn the model; two fingers, the right or middle button, or Shift and a drag move it; a pinch, the wheel or
|
|
11
|
+
a trackpad's pinch zooms toward the pointer. A tap picks what's under it; a double tap frames everything.
|
|
12
|
+
|
|
13
|
+
Left, the tab's canvas is kept (hidden, untouchable, drawing nothing: `active`), so coming back is at once — made
|
|
14
|
+
again each time, its WebGL context, renderer and shaders took a pause every time the tab opened. CadEditor lets it
|
|
15
|
+
go after a while away.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Moving less than this (points) between press and release is a tap (a double tap is the host's: View3DHost.tapAt). */
|
|
19
|
+
const TAP_SLOP = 6;
|
|
20
|
+
|
|
21
|
+
export function View3D({ theme, active = true }: { theme: 'light' | 'dark'; active?: boolean }) {
|
|
22
|
+
const ref = useRef<HTMLCanvasElement>(null), host = useRef<View3DHost | null>(null), activeNow = useRef(active);
|
|
23
|
+
activeNow.current = active;
|
|
24
|
+
const key = useEditorKey(view3dKey);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const cv = ref.current!;
|
|
27
|
+
let renderer: WebGLRenderer;
|
|
28
|
+
try {
|
|
29
|
+
renderer = new WebGLRenderer({ canvas: cv, antialias: true, powerPreference: 'high-performance' });
|
|
30
|
+
} catch {
|
|
31
|
+
toast('3D needs WebGL, which this browser has turned off.', { warn: true });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
|
|
35
|
+
const h = new View3DHost(renderer, theme);
|
|
36
|
+
host.current = h;
|
|
37
|
+
h.setVisible(activeNow.current);
|
|
38
|
+
// (only for a new size: setting a canvas's size makes its drawing buffer again and clears it, the same size or not —
|
|
39
|
+
// and the observer reports the size just set as it starts)
|
|
40
|
+
let was = { w: -1, h: -1 };
|
|
41
|
+
const size = () => {
|
|
42
|
+
const w = cv.clientWidth, ht = cv.clientHeight;
|
|
43
|
+
if (w === was.w && ht === was.h) return;
|
|
44
|
+
was = { w, h: ht };
|
|
45
|
+
renderer.setSize(w, ht, false);
|
|
46
|
+
h.resize(w, ht);
|
|
47
|
+
};
|
|
48
|
+
size();
|
|
49
|
+
h.sync(theme);
|
|
50
|
+
const ro = new ResizeObserver(size);
|
|
51
|
+
ro.observe(cv);
|
|
52
|
+
|
|
53
|
+
// the pointers down, and what they're doing
|
|
54
|
+
const ac = new AbortController(), on = { signal: ac.signal };
|
|
55
|
+
const pts = new Map<number, { x: number; y: number }>();
|
|
56
|
+
let mode: 'none' | 'orbit' | 'pan' | 'pinch' = 'none', moved = false, downAt = 0, start = { x: 0, y: 0 };
|
|
57
|
+
let pinch = { d: 0, x: 0, y: 0 };
|
|
58
|
+
const at = (e: PointerEvent | WheelEvent) => { const r = cv.getBoundingClientRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; };
|
|
59
|
+
const two = () => {
|
|
60
|
+
const [a, b] = [...pts.values()];
|
|
61
|
+
return { d: Math.hypot(b.x - a.x, b.y - a.y), x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
62
|
+
};
|
|
63
|
+
cv.addEventListener('pointerdown', e => {
|
|
64
|
+
cv.focus({ preventScroll: true });
|
|
65
|
+
try { cv.setPointerCapture(e.pointerId); } catch { /* not capturable (synthetic) */ }
|
|
66
|
+
const p = at(e);
|
|
67
|
+
pts.set(e.pointerId, p);
|
|
68
|
+
if (pts.size === 1) {
|
|
69
|
+
mode = e.button === 1 || e.button === 2 || e.shiftKey ? 'pan' : 'orbit';
|
|
70
|
+
moved = false; downAt = performance.now(); start = p;
|
|
71
|
+
} else if (pts.size === 2) {
|
|
72
|
+
// (two fingers never tap)
|
|
73
|
+
mode = 'pinch'; moved = true; pinch = two();
|
|
74
|
+
h.moving(true);
|
|
75
|
+
}
|
|
76
|
+
}, on);
|
|
77
|
+
cv.addEventListener('pointermove', e => {
|
|
78
|
+
const prev = pts.get(e.pointerId);
|
|
79
|
+
if (!prev) return;
|
|
80
|
+
const p = at(e);
|
|
81
|
+
pts.set(e.pointerId, p);
|
|
82
|
+
if (mode === 'pinch' && pts.size >= 2) {
|
|
83
|
+
const now = two();
|
|
84
|
+
if (pinch.d > 0 && now.d > 0) h.zoom(pinch.d / now.d, now.x, now.y);
|
|
85
|
+
h.pan(now.x - pinch.x, now.y - pinch.y);
|
|
86
|
+
pinch = now;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (mode !== 'orbit' && mode !== 'pan') return;
|
|
90
|
+
if (!moved) {
|
|
91
|
+
if (Math.hypot(p.x - start.x, p.y - start.y) < TAP_SLOP) return;
|
|
92
|
+
moved = true;
|
|
93
|
+
h.moving(true);
|
|
94
|
+
}
|
|
95
|
+
if (mode === 'orbit') h.orbit(p.x - prev.x, p.y - prev.y);
|
|
96
|
+
else h.pan(p.x - prev.x, p.y - prev.y);
|
|
97
|
+
}, on);
|
|
98
|
+
const up = (e: PointerEvent, cancel: boolean) => {
|
|
99
|
+
if (!pts.has(e.pointerId)) return;
|
|
100
|
+
pts.delete(e.pointerId);
|
|
101
|
+
if (!cancel && (mode === 'orbit' || mode === 'pan') && !moved && performance.now() - downAt < 600) {
|
|
102
|
+
const p = at(e);
|
|
103
|
+
h.tapAt(p.x, p.y);
|
|
104
|
+
}
|
|
105
|
+
if (pts.size === 1 && mode === 'pinch') { mode = 'orbit'; return; } // (one finger left: it turns the model)
|
|
106
|
+
if (!pts.size) { if (moved) h.moving(false); mode = 'none'; }
|
|
107
|
+
};
|
|
108
|
+
cv.addEventListener('pointerup', e => up(e, false), on);
|
|
109
|
+
cv.addEventListener('pointercancel', e => up(e, true), on);
|
|
110
|
+
cv.addEventListener('contextmenu', e => e.preventDefault(), on);
|
|
111
|
+
// (a browser can take the GL context back — Safari does in the background — and three.js makes it again: draw then)
|
|
112
|
+
cv.addEventListener('webglcontextrestored', () => h.redraw(), on);
|
|
113
|
+
cv.addEventListener('wheel', e => {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
const p = at(e), d = e.deltaY || e.deltaX, unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 400 : 1;
|
|
116
|
+
// a trackpad's pinch arrives as the wheel with Ctrl held, in smaller steps
|
|
117
|
+
h.zoom(Math.exp(d * unit * (e.ctrlKey ? 0.01 : 0.0015)), p.x, p.y);
|
|
118
|
+
}, { passive: false, signal: ac.signal });
|
|
119
|
+
return () => { ac.abort(); ro.disconnect(); h.dispose(); renderer.dispose(); host.current = null; };
|
|
120
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
121
|
+
// (hidden, it follows nothing: shown again, it catches up with the plan at once)
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
host.current?.setVisible(active);
|
|
124
|
+
if (active) host.current?.sync(theme);
|
|
125
|
+
}, [key, theme, active]);
|
|
126
|
+
return (
|
|
127
|
+
<canvas ref={ref} tabIndex={-1} aria-label="3D view of the plan" aria-hidden={!active} data-testid="view3d" id="cv3d"
|
|
128
|
+
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', touchAction: 'none', display: 'block', outline: 'none',
|
|
129
|
+
visibility: active ? 'visible' : 'hidden', pointerEvents: active ? 'auto' : 'none' }} />
|
|
130
|
+
);
|
|
131
|
+
}
|