@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/Rail.tsx
CHANGED
|
@@ -1,45 +1,378 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toolTip } from '../editor/tooltips';
|
|
2
|
+
import { notify, ui, useEditorKey } from './store';
|
|
3
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Animated, Dimensions, Platform, ScrollView, StyleSheet, View } from 'react-native';
|
|
2
5
|
import { redo, undo } from '../app/edit';
|
|
3
6
|
import { ed } from '../app/state';
|
|
4
|
-
import { RAIL,
|
|
5
|
-
import {
|
|
7
|
+
import { groupTools, RAIL, railIcon, railItems, TOGGLES, TOOL_GROUPS, toolLabel, type RailItem } from '../editor/mode';
|
|
8
|
+
import { markupTarget } from '../editor/markup';
|
|
9
|
+
import { elevOpen } from '../editor/elevation';
|
|
10
|
+
import { openPalette } from '../editor/ports';
|
|
6
11
|
import { setTool } from '../tools/registry';
|
|
12
|
+
import { addLinks, LINK_PLUS_LABEL, linkPlusVisible } from '../editor/linkActions';
|
|
13
|
+
import { canLink } from '../editor/links';
|
|
7
14
|
import { hovered } from './Buttons';
|
|
8
15
|
import { Icon } from './Icon';
|
|
9
16
|
import { C, shadow } from './theme';
|
|
17
|
+
import { EASE, DURATION, Tap, TOGGLE_EASE, useEased } from './motion';
|
|
18
|
+
import { NATIVE } from './easing';
|
|
19
|
+
import { TweenView } from './Tween';
|
|
20
|
+
import { createSheet } from './corners';
|
|
21
|
+
|
|
22
|
+
/** A rail button's icon, fading from white to dark as it's selected (two icons, crossfaded). */
|
|
23
|
+
const RailIcon = memo(function RailIcon({ name, size, on }: { name: string; size: number; on: boolean }) {
|
|
24
|
+
const t = useEased(on), off = useMemo(() => t.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }), [t]);
|
|
25
|
+
return (
|
|
26
|
+
<View style={{ width: size, height: size }}>
|
|
27
|
+
<Animated.View style={[StyleSheet.absoluteFill, { opacity: off }]}><Icon name={name} size={size} color="#fff" /></Animated.View>
|
|
28
|
+
<Animated.View style={[StyleSheet.absoluteFill, { opacity: t }]}><Icon name={name} size={size} color={C.dark} /></Animated.View>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The selected tool's white disc: it glides from tool to tool (and fades when the tool isn't on the rail), moved by a
|
|
35
|
+
* transform so the glide runs on the native driver. Where it goes is worked out from the rail's own layout, not
|
|
36
|
+
* measured: a button that moves without changing size reports no new layout (react-native-web), which left the disc
|
|
37
|
+
* between buttons.
|
|
38
|
+
*/
|
|
39
|
+
function useGlide(top: number | null) {
|
|
40
|
+
const y = useRef(new Animated.Value(0)).current, o = useRef(new Animated.Value(0)).current, seen = useRef(false);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const go = (v: Animated.Value, to: number) => Animated.timing(v, { toValue: to, duration: DURATION, easing: EASE, useNativeDriver: NATIVE }).start();
|
|
43
|
+
if (top == null) { go(o, 0); return; }
|
|
44
|
+
if (!seen.current) { y.setValue(top); o.setValue(1); seen.current = true; return; }
|
|
45
|
+
go(y, top); go(o, 1);
|
|
46
|
+
}, [top]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
47
|
+
return { y, o };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* INSET: the space around the buttons, the same at the ends as at the sides, so the end buttons sit concentric in the
|
|
52
|
+
* pill. GAP: the space between buttons — kept, not spread to fill a tall screen (less only where the screen is short).
|
|
53
|
+
*/
|
|
54
|
+
const BTN = 40, MIN_BTN = 32, DIV = 11, INSET = 6, PAD = 2 * INSET, GAP = 5;
|
|
55
|
+
/**
|
|
56
|
+
* On a tablet the rail is bigger than on a computer (44 pt buttons, easy to hit with a finger, the same eighth of a
|
|
57
|
+
* button apart) and keeps its proportions as it sizes: dividers, padding and icons follow the buttons. It's sized for
|
|
58
|
+
* the room it has in landscape and stays that size in portrait, so it looks the same whichever way the tablet is held:
|
|
59
|
+
* where landscape is short the space between buttons tightens first, to a twelfth of a button at least, and then the
|
|
60
|
+
* whole rail scales down — 44 pt buttons on a 13" iPad Pro, 41 pt on an 11" iPad, 36 pt on an iPad mini. A tablet's
|
|
61
|
+
* rail never scrolls: TABLET_MIN_BTN is below what the smallest iPad needs.
|
|
62
|
+
*/
|
|
63
|
+
const TABLET_BTN = 44, TABLET_MIN_BTN = 30;
|
|
64
|
+
const PER_BTN = { gapMax: 0.12, gapMin: 0.08, div: 0.275, pad: (2 * INSET) / BTN, icon: 0.6 };
|
|
65
|
+
|
|
66
|
+
export interface TabletRail { size: number; gap: number; div: number; pad: number; icon: number }
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The room a tablet's rail is sized for: its room in landscape — what it has when held that way, and in portrait less
|
|
70
|
+
* the difference between the window's sides. From one layout (the window read with the room): the same either way the
|
|
71
|
+
* tablet is held, so the rail doesn't change as it's turned.
|
|
72
|
+
*/
|
|
73
|
+
export function landscapeRoom(height: number, winW: number, winH: number): number {
|
|
74
|
+
return height - Math.max(0, winH - Math.min(winW, winH));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The tablet rail for `height` of room: 44 pt buttons an eighth apart, tighter gaps then smaller to fit. */
|
|
78
|
+
export function tabletRailLayout(height: number, buttons: number, dividers: number): TabletRail {
|
|
79
|
+
const spaces = buttons + dividers - 1;
|
|
80
|
+
// height per point of button size, before the gaps: the buttons, the dividers and the padding
|
|
81
|
+
const base = buttons + dividers * PER_BTN.div + PER_BTN.pad;
|
|
82
|
+
let size = TABLET_BTN, gap = PER_BTN.gapMax;
|
|
83
|
+
if (height) {
|
|
84
|
+
const room = height / TABLET_BTN - base; // what's left for gaps at full size, per point of button
|
|
85
|
+
if (room < spaces * PER_BTN.gapMax) {
|
|
86
|
+
gap = Math.max(PER_BTN.gapMin, room / spaces);
|
|
87
|
+
if (room < spaces * PER_BTN.gapMin) size = Math.floor(height / (base + spaces * gap));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
size = Math.max(TABLET_MIN_BTN, Math.min(TABLET_BTN, size));
|
|
91
|
+
return {
|
|
92
|
+
size,
|
|
93
|
+
gap: Math.round(size * gap),
|
|
94
|
+
div: Math.round(size * PER_BTN.div),
|
|
95
|
+
pad: Math.round(size * PER_BTN.pad),
|
|
96
|
+
icon: Math.round(size * PER_BTN.icon),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Buttons and the space between them for a rail `height` tall: on a computer GAP apart, closer on a short screen, and
|
|
102
|
+
* on a shorter one still the buttons shrink to fit before it has to scroll. Phones keep the compact rail, scrolling
|
|
103
|
+
* when it must.
|
|
104
|
+
*/
|
|
105
|
+
export function railLayout(height: number, buttons: number, dividers: number, phone: boolean): { size: number; gap: number } {
|
|
106
|
+
if (phone || !height || buttons < 2) return { size: BTN, gap: 0 };
|
|
107
|
+
const natural = buttons * BTN + dividers * DIV + PAD;
|
|
108
|
+
if (height < natural) {
|
|
109
|
+
return { size: Math.max(MIN_BTN, Math.floor((height - dividers * DIV - PAD) / buttons)), gap: 0 };
|
|
110
|
+
}
|
|
111
|
+
return { size: BTN, gap: Math.min(GAP, Math.floor((height - natural) / (buttons + dividers - 1))) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The rail's room on a tablet or a computer: from 30 under the top bar's buttons (their 58) to 14 above the bottom.
|
|
116
|
+
* The rail hangs from the top of it, whatever its tools and whichever way a tablet is held: its first button (Select)
|
|
117
|
+
* never moves, and a shorter rail just ends sooner.
|
|
118
|
+
*/
|
|
119
|
+
const RAIL_TOP = 58 + 30, RAIL_BOTTOM = 14;
|
|
120
|
+
|
|
121
|
+
/** How long the rail takes to change from one tool set to another (switching between Draft and Annotate). */
|
|
122
|
+
const MORPH = 240, FADE_OUT = 130, FADE_IN_AT = 70;
|
|
123
|
+
/** Annotate's + sits this far under the rail, as wide as the rail is. */
|
|
124
|
+
const PLUS_GAP = 10;
|
|
125
|
+
|
|
126
|
+
/** A value that follows `to`, easing to each new one (set at once the first time), on the native driver. */
|
|
127
|
+
function useMorphed(to: number): Animated.Value {
|
|
128
|
+
const v = useRef(new Animated.Value(to)).current;
|
|
129
|
+
useEffect(() => { Animated.timing(v, { toValue: to, duration: MORPH, easing: EASE, useNativeDriver: NATIVE }).start(); }, [to, v]);
|
|
130
|
+
return v;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What one place on the rail shows, all as plain values: a divider (`id` null), or a tool, toggle or action. A place
|
|
135
|
+
* re-renders only when one of them changes — picking a tool redraws the two places whose selection changed, not the
|
|
136
|
+
* whole rail (each of which took its share of a phone's frame).
|
|
137
|
+
*/
|
|
138
|
+
interface SlotProps {
|
|
139
|
+
y: number; appear: boolean; leaving?: boolean;
|
|
140
|
+
id: string | null; label: string; kind?: 'toggle' | 'act' | 'group'; tool: string; on: boolean;
|
|
141
|
+
size: number; icon: number; unit: number; div: number;
|
|
142
|
+
/** the markup colour's dot (the Color place) */
|
|
143
|
+
dot: string | null;
|
|
144
|
+
/** where a shared place's menu opens: beside it */
|
|
145
|
+
menuX: number; menuY: number;
|
|
146
|
+
onAddImage: () => void;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* One place on the rail. As the tool set changes it glides to where it now sits; a place new to this tool set fades
|
|
151
|
+
* (and grows a touch) in, and one that's gone fades out — so switching modes, the rail morphs rather than redraws.
|
|
152
|
+
* It moves by a transform, on the native driver.
|
|
153
|
+
*/
|
|
154
|
+
const RailSlot = memo(function RailSlot(p: SlotProps) {
|
|
155
|
+
const top = useMorphed(p.y), vis = useRef(new Animated.Value(p.appear ? 0 : 1)).current;
|
|
156
|
+
const scale = useMemo(() => vis.interpolate({ inputRange: [0, 1], outputRange: [0.88, 1] }), [vis]);
|
|
157
|
+
// what goes clears quickly, and what comes fades in just behind it, so the two never sit on top of each other
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
// (fades ease in and out: a fast start reads as a blink on something appearing or going)
|
|
160
|
+
Animated.timing(vis, p.leaving ? { toValue: 0, duration: FADE_OUT, easing: TOGGLE_EASE, useNativeDriver: NATIVE }
|
|
161
|
+
: { toValue: 1, duration: MORPH - FADE_IN_AT, delay: p.appear ? FADE_IN_AT : 0, easing: TOGGLE_EASE, useNativeDriver: NATIVE }).start();
|
|
162
|
+
}, [p.leaving, vis]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
163
|
+
return (
|
|
164
|
+
<Animated.View pointerEvents={p.leaving ? 'none' : 'box-none'} aria-hidden={p.leaving || undefined}
|
|
165
|
+
style={[s.slot, { opacity: vis, transform: [{ translateY: top }, { scale }] }]}>
|
|
166
|
+
{p.id == null ? <View style={[s.div, { width: 28 * p.unit, marginVertical: (p.div - 1) / 2 }]} /> : <RailEntry {...p} />}
|
|
167
|
+
</Animated.View>
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A tool, toggle or action on the rail, and what a press does. A place several tools share draws with the one it shows
|
|
173
|
+
* on a tap, and opens the menu of them all on a long press; with a mouse (a browser on a computer) a click opens the
|
|
174
|
+
* menu instead — a mouse has no long press to find. A browser on a phone or an iPad keeps the touch behaviour.
|
|
175
|
+
*/
|
|
176
|
+
const MENU_ON_CLICK = Platform.OS === 'web' && typeof matchMedia === 'function' && matchMedia('(hover: hover) and (pointer: fine)').matches;
|
|
177
|
+
function RailEntry({ id, label, kind, tool, on, size, icon, unit, dot, menuX, menuY, onAddImage, leaving }: SlotProps) {
|
|
178
|
+
const group = kind === 'group';
|
|
179
|
+
const press = () => {
|
|
180
|
+
if (group && MENU_ON_CLICK) {
|
|
181
|
+
// a second click closes it again
|
|
182
|
+
if (ui.shapeMenu?.group === id) { ui.shapeMenu = null; notify(); } else openMenu();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
ui.shapeMenu = null;
|
|
186
|
+
if (kind === 'toggle') TOGGLES[id!]?.flip();
|
|
187
|
+
else if (kind === 'act') {
|
|
188
|
+
if (id === 'undo') undo(); else if (id === 'redo') redo(); else if (id === 'image') onAddImage(); else if (id === 'color') openPalette();
|
|
189
|
+
} else setTool(tool);
|
|
190
|
+
// what it does, in a line under the tabs (for a tool or a toggle; not Undo, a colour, an action)
|
|
191
|
+
const t = kind === 'act' ? null : toolTip(tool, label);
|
|
192
|
+
if (t) ui.toolTip = { text: t, seq: (ui.toolTip?.seq ?? 0) + 1 };
|
|
193
|
+
notify();
|
|
194
|
+
};
|
|
195
|
+
// the menu of the place's tools, beside the rail level with it
|
|
196
|
+
const openMenu = () => {
|
|
197
|
+
setTool(tool);
|
|
198
|
+
ui.menu = false; ui.palette = false;
|
|
199
|
+
ui.shapeMenu = { x: menuX, y: menuY, group: id! };
|
|
200
|
+
notify();
|
|
201
|
+
};
|
|
202
|
+
return (
|
|
203
|
+
<RailButton id={id!} label={label} on={on} toggle={kind === 'toggle'} size={size} onPress={press} testID={leaving ? undefined : 'tool-' + id}
|
|
204
|
+
onLongPress={group ? openMenu : undefined} hint={group ? (MENU_ON_CLICK ? 'Opens the other shapes' : 'Touch and hold for the other shapes') : undefined}>
|
|
205
|
+
{id === 'color'
|
|
206
|
+
? <View style={[s.dot, { width: 20 * unit, height: 20 * unit, borderRadius: 10 * unit, backgroundColor: dot ?? undefined }]} />
|
|
207
|
+
: <RailIcon name={railIcon(tool)} size={icon} on={on} />}
|
|
208
|
+
</RailButton>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** A rail item's place: a tool keeps its place across tool sets when it's the same tool (Select / Select markup). */
|
|
213
|
+
const slotKey = (it: RailItem, divider: number): string => (it === '|' ? 'd' + divider : it[0] === 'aselect' ? 'select' : it[0]);
|
|
214
|
+
interface Placed { key: string; y: number; it: RailItem }
|
|
10
215
|
|
|
11
216
|
/** The tool rail on the left: the current mode's tools, toggles and actions. */
|
|
12
217
|
export function Rail({ phone, onAddImage }: { phone: boolean; onAddImage?: () => void }) {
|
|
13
|
-
|
|
218
|
+
// its tools (the mode's or the wall view's), the one in use, the toggles, the markup colour, the shape
|
|
219
|
+
useEditorKey(() => [ed.mode, elevOpen(), ed.tool, ed.drawConstr, ed.drawWalls, markupTarget().color, JSON.stringify(ui.picked), canLink(), linkPlusVisible()].join('|'));
|
|
220
|
+
// (shapes share one place: Annotate's always, Draft's on a phone, which has no room for each)
|
|
221
|
+
const usable = (list: RailItem[]) => groupTools(list.filter(it => it === '|' || it[0] !== 'image' || onAddImage), phone);
|
|
222
|
+
const items = usable(railItems());
|
|
223
|
+
// the room it has, as last laid out, and the window it was laid out in — read at that same moment. A tablet turned
|
|
224
|
+
// changes the window at once and the room a layout later: the two taken together (a portrait room in a landscape
|
|
225
|
+
// window) shrank the rail to nothing and grew it back as it turned.
|
|
226
|
+
const [room, setRoom] = useState({ height: 0, y: 0, winW: 0, winH: 0 });
|
|
227
|
+
const { height, y: railY } = room;
|
|
228
|
+
// the tool a shared place draws with: the one in use, or the last picked
|
|
229
|
+
const pickedIn = (g: string) => (TOOL_GROUPS[g].tools.includes(ed.tool) ? ed.tool : ui.picked[g]);
|
|
230
|
+
const tablet = Platform.OS !== 'web' && !phone;
|
|
231
|
+
// a tablet's rail keeps its proportions as it sizes; elsewhere the 40 pt rail closes up or shrinks to fit
|
|
232
|
+
// the rail's room in landscape: the same as now when held that way; in portrait, less by the difference between the sides
|
|
233
|
+
const landscapeHeight = landscapeRoom(height, room.winW, room.winH);
|
|
234
|
+
const layoutOf = (list: RailItem[], less = 0) => {
|
|
235
|
+
const dividers = list.filter(it => it === '|').length, buttons = list.length - dividers;
|
|
236
|
+
return tablet ? tabletRailLayout(landscapeHeight - less, buttons, dividers) : { ...railLayout(height - less, buttons, dividers, phone), div: DIV, pad: PAD, icon: 24 };
|
|
237
|
+
};
|
|
238
|
+
type Layout = ReturnType<typeof layoutOf>;
|
|
239
|
+
const tightest = (ls: Layout[]) => ls.reduce((a, b) => (b.size < a.size || (b.size === a.size && b.gap < a.gap) ? b : a));
|
|
240
|
+
const lengthOf = (list: RailItem[], l: Layout) => list.reduce((h, it) => h + (it === '|' ? l.div : l.size) + l.gap, 0) - l.gap + 2 * INSET * (l.size / BTN);
|
|
241
|
+
const pillWidth = (l: Layout) => l.size + 2 * INSET * (l.size / BTN);
|
|
242
|
+
// one spacing for every tool set — the one the longest rail (Draft's) can fit — so a shorter rail is just shorter
|
|
243
|
+
let L = tightest([items, usable(RAIL.draft), usable(RAIL.annotate)].map(l => layoutOf(l)));
|
|
244
|
+
// with a host that links, Annotate's rail leaves room under it for the + — or, where even a smaller rail wouldn't
|
|
245
|
+
// (a short window), the + floats in the corner instead
|
|
246
|
+
let plusUnder = false;
|
|
247
|
+
if (!phone && canLink() && height > 0) {
|
|
248
|
+
const annotate = usable(RAIL.annotate), withPlus = tightest([L, layoutOf(annotate, PLUS_GAP + pillWidth(L))]);
|
|
249
|
+
if (lengthOf(annotate, withPlus) + PLUS_GAP + pillWidth(withPlus) <= height + 0.5) { L = withPlus; plusUnder = true; }
|
|
250
|
+
}
|
|
251
|
+
const fab = !phone && canLink() && height > 0 && !plusUnder;
|
|
252
|
+
useEffect(() => { if (ui.linkFab !== fab) { ui.linkFab = fab; notify(); } }, [fab]);
|
|
253
|
+
const { size, gap, icon } = L, unit = size / BTN;
|
|
254
|
+
// where each item sits: the rail's top padding, then each button or divider above it and the gap after it
|
|
255
|
+
// (the space above the first button is the space beside it: the pill's ends are concentric with the end buttons)
|
|
256
|
+
const inset = INSET * unit;
|
|
257
|
+
let y = inset, dn = 0;
|
|
258
|
+
const placed: Placed[] = items.map(it => { const p = { key: slotKey(it, dn), y, it }; if (it === '|') dn++; y += (it === '|' ? L.div : size) + gap; return p; });
|
|
259
|
+
const total = y - gap + inset, width = size + 2 * inset, radius = size / 2 + inset;
|
|
260
|
+
// what was on the rail and isn't now fades out where it was — worked out in this same render, so it never drops
|
|
261
|
+
// out for a frame before fading (that read as a flicker)
|
|
262
|
+
const prev = useRef<Placed[]>(placed), leaving = useRef<{ items: Placed[]; seq: number }>({ items: [], seq: 0 }), mounted = useRef(false);
|
|
263
|
+
const keys = placed.map(p => p.key).join(), [, rerender] = useState(0);
|
|
264
|
+
if (keys !== prev.current.map(p => p.key).join()) {
|
|
265
|
+
const now = new Set(placed.map(p => p.key));
|
|
266
|
+
const gone = [...prev.current.filter(p => !now.has(p.key)), ...leaving.current.items.filter(p => !now.has(p.key))];
|
|
267
|
+
leaving.current = { items: gone.filter((p, i) => gone.findIndex(q => q.key === p.key) === i), seq: leaving.current.seq + 1 };
|
|
268
|
+
prev.current = placed;
|
|
269
|
+
}
|
|
270
|
+
const seq = leaving.current.seq;
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (!leaving.current.items.length) return;
|
|
273
|
+
const t = setTimeout(() => { if (leaving.current.seq === seq) { leaving.current = { items: [], seq }; rerender(n => n + 1); } }, MORPH + 20);
|
|
274
|
+
return () => clearTimeout(t);
|
|
275
|
+
}, [seq]);
|
|
276
|
+
// (what's on the rail when it's first shown is just there; what comes later fades in)
|
|
277
|
+
useEffect(() => { if (height) mounted.current = true; }, [height]);
|
|
278
|
+
const isOn = (id: string) => id === ed.tool || !!TOOL_GROUPS[id]?.tools.includes(ed.tool);
|
|
279
|
+
const sel = placed.find(p => p.it !== '|' && (!p.it[3] || p.it[3] === 'group') && isOn(p.it[0]));
|
|
280
|
+
const glide = useGlide(sel ? sel.y : null);
|
|
281
|
+
// (one function for every render, so the places' props stay equal: the host's own may be new each time)
|
|
282
|
+
const addImage = useRef(onAddImage);
|
|
283
|
+
addImage.current = onAddImage;
|
|
284
|
+
const onAdd = useCallback(() => addImage.current?.(), []);
|
|
285
|
+
/** A place's props: plain values only (see SlotProps). */
|
|
286
|
+
const slot = (p: Placed, isLeaving: boolean): SlotProps => {
|
|
287
|
+
const common = { y: p.y, appear: !isLeaving && mounted.current, leaving: isLeaving, size, icon, unit, div: L.div, onAddImage: onAdd };
|
|
288
|
+
if (p.it === '|') return { ...common, id: null, label: '', tool: '', on: false, dot: null, menuX: 0, menuY: 0 };
|
|
289
|
+
const [id, rawLabel, , kind] = p.it, group = kind === 'group';
|
|
290
|
+
// a shared place stands for the tool it'll draw with
|
|
291
|
+
const tool = group ? pickedIn(id) : id;
|
|
292
|
+
return {
|
|
293
|
+
...common, id, kind, tool, label: group ? toolLabel(tool) : rawLabel,
|
|
294
|
+
on: !isLeaving && (kind === 'toggle' ? !!TOGGLES[id]?.on() : isOn(id)),
|
|
295
|
+
dot: id === 'color' ? markupTarget().color : null,
|
|
296
|
+
// (the rail's own place, plus where this sits on it)
|
|
297
|
+
menuX: group ? 12 + width + 10 : 0, menuY: group ? railY + p.y : 0,
|
|
298
|
+
};
|
|
299
|
+
};
|
|
14
300
|
return (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
);
|
|
33
|
-
})}
|
|
34
|
-
</ScrollView>
|
|
301
|
+
// (on a phone the bar runs under it, so it stops above; elsewhere the bar is centred and the rail runs to the bottom)
|
|
302
|
+
<View style={[s.rail, phone && { top: 162, bottom: 84 }]} onLayout={e => {
|
|
303
|
+
const { height: h, y: top } = e.nativeEvent.layout, w = Dimensions.get('window');
|
|
304
|
+
setRoom({ height: h, y: top, winW: w.width, winH: w.height });
|
|
305
|
+
}}>
|
|
306
|
+
{/* the pill eases to its new length as the tool set changes (on the UI thread on a device: see TweenView). It's
|
|
307
|
+
shown once its room is known, at the size that room gives it: no settling as it opens. */}
|
|
308
|
+
{height > 0 && <TweenView testID="railPill" to={{ height: total }} duration={MORPH} style={[s.inner, { width, maxHeight: '100%', borderRadius: radius }]}>
|
|
309
|
+
{/* it scrolls only when its tools can't all fit (a phone): otherwise a drag down the rail would move it */}
|
|
310
|
+
<ScrollView style={{ flex: 1, borderRadius: radius }} contentContainerStyle={{ height: total }} showsVerticalScrollIndicator={false}
|
|
311
|
+
scrollEnabled={!!height && total > height + 1} bounces={false}>
|
|
312
|
+
{sel && <Animated.View pointerEvents="none" style={[s.disc, { left: inset, width: size, height: size, borderRadius: size / 2, opacity: glide.o, transform: [{ translateY: glide.y }] }]} />}
|
|
313
|
+
{leaving.current.items.map(p => <RailSlot key={'gone-' + p.key} {...slot(p, true)} />)}
|
|
314
|
+
{placed.map(p => <RailSlot key={p.key} {...slot(p, false)} />)}
|
|
315
|
+
</ScrollView>
|
|
316
|
+
</TweenView>}
|
|
317
|
+
{height > 0 && plusUnder && <RailPlus top={Math.min(total, height) + PLUS_GAP} size={width} shown={linkPlusVisible()} />}
|
|
35
318
|
</View>
|
|
36
319
|
);
|
|
37
320
|
}
|
|
38
321
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Annotate's +, under the rail: picks the job's files and measurements to put on the plan. It follows the rail's end as
|
|
324
|
+
* the rail changes length, and fades in and out with Annotate's tools (all on the native driver).
|
|
325
|
+
*/
|
|
326
|
+
const RailPlus = memo(function RailPlus({ top, size, shown }: { top: number; size: number; shown: boolean }) {
|
|
327
|
+
const y = useMorphed(top), vis = useRef(new Animated.Value(shown ? 1 : 0)).current;
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
Animated.timing(vis, shown ? { toValue: 1, duration: MORPH - FADE_IN_AT, delay: FADE_IN_AT, easing: TOGGLE_EASE, useNativeDriver: NATIVE }
|
|
330
|
+
: { toValue: 0, duration: FADE_OUT, easing: TOGGLE_EASE, useNativeDriver: NATIVE }).start();
|
|
331
|
+
}, [shown, vis]);
|
|
332
|
+
return (
|
|
333
|
+
<Animated.View pointerEvents={shown ? 'box-none' : 'none'} aria-hidden={!shown || undefined} style={[s.plusAt, { opacity: vis, transform: [{ translateY: y }] }]}>
|
|
334
|
+
<Tap accessibilityRole="button" accessibilityLabel={LINK_PLUS_LABEL} testID={shown ? 'linkPlus' : undefined} onPress={() => { void addLinks(); }}
|
|
335
|
+
style={[s.plus, { width: size, height: size, borderRadius: size / 2 }]}>
|
|
336
|
+
<Icon name="plus" size={Math.round(size * 0.5)} color={C.dark} />
|
|
337
|
+
</Tap>
|
|
338
|
+
</Animated.View>
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* A rail button: a tool sits on the gliding disc when selected; a toggle fades its own blue in and out. One that opens
|
|
344
|
+
* a menu on a long press has a small nub on its right edge, pointing where the menu opens (as in BOLDR's annotation rail).
|
|
345
|
+
*/
|
|
346
|
+
function RailButton({ id, label, on, toggle, size, onPress, onLongPress, hint, children, testID }: {
|
|
347
|
+
id: string; label: string; on: boolean; toggle: boolean; size: number; onPress: () => void; onLongPress?: () => void; hint?: string;
|
|
348
|
+
children: React.ReactNode; testID?: string;
|
|
349
|
+
}) {
|
|
350
|
+
const t = useEased(on && toggle);
|
|
351
|
+
const fill = useMemo(() => t.interpolate({ inputRange: [0, 1], outputRange: ['rgba(80,166,255,0)', C.lblue] }), [t]);
|
|
352
|
+
return (
|
|
353
|
+
<Tap accessibilityRole="button" accessibilityLabel={label} accessibilityHint={hint} accessibilityState={{ selected: on }} aria-pressed={toggle ? on : undefined} testID={testID}
|
|
354
|
+
onPress={onPress} onLongPress={onLongPress}
|
|
355
|
+
style={st => [s.btn, { width: size, height: size, borderRadius: size / 2 },
|
|
356
|
+
toggle && { backgroundColor: fill as unknown as string },
|
|
357
|
+
!on && hovered(st) && { backgroundColor: 'rgba(255,255,255,0.08)' }]}>
|
|
358
|
+
{children}
|
|
359
|
+
{onLongPress && <View pointerEvents="none" style={[s.nub, { top: size / 2 - 4 }, on && { borderLeftColor: C.dark }]} />}
|
|
360
|
+
</Tap>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const s = createSheet({
|
|
365
|
+
// (both moved by a transform from the top: see useGlide, RailSlot)
|
|
366
|
+
disc: { position: 'absolute', top: 0, backgroundColor: '#fff' },
|
|
367
|
+
rail: { position: 'absolute', left: 12, top: RAIL_TOP, bottom: RAIL_BOTTOM, zIndex: 22, alignItems: 'flex-start' },
|
|
368
|
+
inner: { backgroundColor: C.g2, ...shadow },
|
|
369
|
+
slot: { position: 'absolute', top: 0, left: 0, right: 0, alignItems: 'center' },
|
|
42
370
|
btn: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
|
|
43
371
|
div: { width: 28, height: 1, backgroundColor: C.g3, marginVertical: 5 },
|
|
372
|
+
// a right-pointing triangle, 8 tall, hugging the right edge
|
|
373
|
+
nub: { position: 'absolute', right: 3, width: 0, height: 0, borderTopWidth: 4, borderBottomWidth: 4, borderLeftWidth: 5, borderTopColor: 'transparent', borderBottomColor: 'transparent', borderLeftColor: 'rgba(255,255,255,0.75)' },
|
|
44
374
|
dot: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: 'rgba(255,255,255,0.85)' },
|
|
375
|
+
// (moved by a transform from the rail's top: see RailPlus)
|
|
376
|
+
plusAt: { position: 'absolute', top: 0, left: 0 },
|
|
377
|
+
plus: { backgroundColor: C.yellow, alignItems: 'center', justifyContent: 'center', ...shadow },
|
|
45
378
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { Animated, Text, useWindowDimensions, View } from 'react-native';
|
|
3
|
+
import { ed } from '../app/state';
|
|
4
|
+
import { railIcon, TOOL_GROUPS, toolLabel } from '../editor/mode';
|
|
5
|
+
import { toolTip } from '../editor/tooltips';
|
|
6
|
+
import { setTool } from '../tools/registry';
|
|
7
|
+
import { Icon } from './Icon';
|
|
8
|
+
import { notify, ui, useEditorKey } from './store';
|
|
9
|
+
import { C, FONT, shadow } from './theme';
|
|
10
|
+
import { Tap, usePopup } from './motion';
|
|
11
|
+
import { createSheet } from './corners';
|
|
12
|
+
|
|
13
|
+
const ROW = 44, GAP = 6, PAD = 8;
|
|
14
|
+
/** A menu's height for `n` rows: the rows, the gaps between them, its padding and border. */
|
|
15
|
+
const heightFor = (n: number) => n * ROW + (n - 1) * GAP + 2 * PAD + 2;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The menu of the tools sharing one place on the rail (a long press on it; a click in a browser) — Draft's shapes on a
|
|
19
|
+
* phone, Annotate's markup shapes: each by icon and name, the one in use on a blue disc, as BOLDR's annotation rail lays out its sub-menus. Picking one draws with it from then on, and the rail's
|
|
20
|
+
* place shows it. It sits beside the rail, level with the place; there's no scrim, and a tap on the drawing, the rail
|
|
21
|
+
* or the top bar closes it.
|
|
22
|
+
*/
|
|
23
|
+
export function ShapeMenu() {
|
|
24
|
+
useEditorKey(() => (ui.shapeMenu ? [ui.shapeMenu.x, ui.shapeMenu.y, ui.shapeMenu.group, ed.mode, ed.tool].join('|') : ''));
|
|
25
|
+
const { height } = useWindowDimensions(), now = ui.shapeMenu, nowGroup = now ? TOOL_GROUPS[now.group] : null;
|
|
26
|
+
const open = !!now && !!nowGroup && ed.mode === nowGroup.mode;
|
|
27
|
+
// (it grows out of the rail's place, sideways; as it goes it keeps what it showed)
|
|
28
|
+
const last = useRef(now), pop = usePopup(open, { x: -8 }, 'left center');
|
|
29
|
+
if (open) last.current = now;
|
|
30
|
+
const at = last.current, group = at ? TOOL_GROUPS[at.group] : null;
|
|
31
|
+
if (!pop.present || !at || !group) return null;
|
|
32
|
+
// (lifted to stay on screen when the place sits low)
|
|
33
|
+
const top = Math.max(12, Math.min(at.y - PAD, height - heightFor(group.tools.length) - 96));
|
|
34
|
+
const pick = (id: string) => {
|
|
35
|
+
ui.picked[at.group] = id;
|
|
36
|
+
ui.shapeMenu = null;
|
|
37
|
+
setTool(id);
|
|
38
|
+
const t = toolTip(id, toolLabel(id));
|
|
39
|
+
if (t) ui.toolTip = { text: t, seq: (ui.toolTip?.seq ?? 0) + 1 };
|
|
40
|
+
notify();
|
|
41
|
+
};
|
|
42
|
+
return (
|
|
43
|
+
<Animated.View style={[s.menu, { left: at.x, top }, pop.style]} pointerEvents={open ? 'auto' : 'none'} accessibilityRole="menu" accessibilityLabel={group.label} testID={open ? 'shapeMenu' : undefined}>
|
|
44
|
+
{group.tools.map(id => {
|
|
45
|
+
const on = ed.tool === id;
|
|
46
|
+
return (
|
|
47
|
+
<Tap key={id} feel="dim" accessibilityRole="menuitem" accessibilityLabel={toolLabel(id)} accessibilityState={{ selected: on }}
|
|
48
|
+
onPress={() => pick(id)} style={s.row} testID={'shape-' + id}>
|
|
49
|
+
<View style={[s.disc, on && { backgroundColor: C.blue }]}><Icon name={railIcon(id)} color="#fff" /></View>
|
|
50
|
+
<Text style={s.label}>{toolLabel(id)}</Text>
|
|
51
|
+
</Tap>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</Animated.View>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const s = createSheet({
|
|
59
|
+
menu: {
|
|
60
|
+
position: 'absolute', zIndex: 46, backgroundColor: C.g2, borderColor: C.g3, borderWidth: 1, borderRadius: 30,
|
|
61
|
+
padding: PAD, gap: GAP, ...shadow,
|
|
62
|
+
},
|
|
63
|
+
row: { flexDirection: 'row', alignItems: 'center', gap: 12, height: ROW, paddingRight: 24, borderRadius: ROW / 2 },
|
|
64
|
+
disc: { width: ROW, height: ROW, borderRadius: ROW / 2, alignItems: 'center', justifyContent: 'center' },
|
|
65
|
+
label: { fontFamily: FONT, fontSize: 16, fontWeight: '500', color: '#fff' },
|
|
66
|
+
});
|
package/src/react/Sheet.tsx
CHANGED
|
@@ -1,12 +1,51 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { Pressable, StyleSheet, View } from 'react-native';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
2
|
+
import { Animated, Easing, Pressable, StyleSheet, View, type ViewStyle } from 'react-native';
|
|
3
|
+
import { EASE, NATIVE } from './easing';
|
|
4
|
+
import { POP_EASE_OUT, POP_IN, POP_OUT, reducedMotion } from './motion';
|
|
3
5
|
import { ui } from './store';
|
|
4
6
|
import { C } from './theme';
|
|
7
|
+
import { createSheet } from './corners';
|
|
5
8
|
|
|
6
9
|
/** Taps on the backdrop right after a sheet opens are the opening tap's ghost click (touch screens): ignore them. */
|
|
7
10
|
const GHOST_MS = 450;
|
|
8
11
|
|
|
9
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* How a sheet (the keypad, a conflict) comes and goes. On a tablet or a computer it's a card in the corner, and comes
|
|
14
|
+
* and goes as the popup menus do (motion.tsx usePopup): fading in and growing from just under its size out of its
|
|
15
|
+
* corner, rising a few points, in about 140 ms; fading and shrinking back, gathering pace, in about 100. On a phone it
|
|
16
|
+
* rises from the bottom as it fades in. A transform and opacity, on the native driver; with Reduce Motion on, it only
|
|
17
|
+
* fades. `present`: on screen — shown, or still going (it keeps what it showed till then).
|
|
18
|
+
*/
|
|
19
|
+
const SHEET_IN = 200, SHEET_OUT = 160, SHEET_EASE_OUT = Easing.bezier(0.4, 0, 1, 1);
|
|
20
|
+
/** how far it rises on a phone, and a card on a wide screen (as a popup does) */
|
|
21
|
+
const RISE_BY = 80, CARD_RISE = 8;
|
|
22
|
+
export function useSheetMotion(shown: boolean, wide: boolean): { present: boolean; motion: Animated.WithAnimatedObject<Pick<ViewStyle, 'transform' | 'opacity'>> } {
|
|
23
|
+
const x = useRef(new Animated.Value(1)).current, [present, setPresent] = useState(shown);
|
|
24
|
+
const motion = useMemo(() => {
|
|
25
|
+
const opacity = x.interpolate({ inputRange: [0, 1], outputRange: [1, 0] });
|
|
26
|
+
if (reducedMotion()) return { opacity };
|
|
27
|
+
return (wide
|
|
28
|
+
? {
|
|
29
|
+
opacity, transformOrigin: 'bottom right',
|
|
30
|
+
transform: [{ translateY: x.interpolate({ inputRange: [0, 1], outputRange: [0, CARD_RISE] }) }, { scale: x.interpolate({ inputRange: [0, 1], outputRange: [1, 0.96] }) }],
|
|
31
|
+
}
|
|
32
|
+
: { opacity, transform: [{ translateY: x.interpolate({ inputRange: [0, 1], outputRange: [0, RISE_BY] }) }] }) as Animated.WithAnimatedObject<Pick<ViewStyle, 'transform' | 'opacity'>>;
|
|
33
|
+
}, [wide, x]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (shown) setPresent(true);
|
|
36
|
+
// (on the next frame: see Drawer — strict mode's second mount would otherwise stop it)
|
|
37
|
+
const frame = requestAnimationFrame(() => {
|
|
38
|
+
Animated.timing(x, shown
|
|
39
|
+
? { toValue: 0, duration: wide ? POP_IN : SHEET_IN, easing: EASE, useNativeDriver: NATIVE }
|
|
40
|
+
: { toValue: 1, duration: wide ? POP_OUT : SHEET_OUT, easing: wide ? POP_EASE_OUT : SHEET_EASE_OUT, useNativeDriver: NATIVE })
|
|
41
|
+
.start(({ finished }) => { if (finished && !shown) setPresent(false); });
|
|
42
|
+
});
|
|
43
|
+
return () => cancelAnimationFrame(frame);
|
|
44
|
+
}, [shown, wide, x]);
|
|
45
|
+
return { present: shown || present, motion };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Dims the drawing behind a sheet on a phone; a tap outside closes the sheet. (Elsewhere the drawing stays in reach.) */
|
|
10
49
|
export function Backdrop({ wide, onClose }: { wide: boolean; onClose: () => void }) {
|
|
11
50
|
return (
|
|
12
51
|
<Pressable accessibilityLabel="Close" testID="backdrop" style={[StyleSheet.absoluteFill, s.backdrop, wide && { backgroundColor: 'transparent' }]}
|
|
@@ -14,20 +53,26 @@ export function Backdrop({ wide, onClose }: { wide: boolean; onClose: () => void
|
|
|
14
53
|
);
|
|
15
54
|
}
|
|
16
55
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
56
|
+
/**
|
|
57
|
+
* A sheet: a card in the bottom-right corner on wide screens, a bottom sheet on phones. `motion`: a transform (and
|
|
58
|
+
* opacity) it's moved by as it comes and goes; `live` false while it's going (nothing in it can be pressed).
|
|
59
|
+
*/
|
|
60
|
+
export function Sheet({ wide, children, label, testID, motion, live = true }: {
|
|
61
|
+
wide: boolean; children: ReactNode; label: string; testID?: string;
|
|
62
|
+
motion?: Animated.WithAnimatedObject<Pick<ViewStyle, 'transform' | 'opacity'>>; live?: boolean;
|
|
63
|
+
}) {
|
|
19
64
|
return (
|
|
20
|
-
<View accessibilityLabel={label} testID={testID} style={[s.sheet, wide ? s.card : s.bottom]}>
|
|
65
|
+
<Animated.View accessibilityLabel={label} testID={testID} pointerEvents={live ? 'auto' : 'none'} style={[s.sheet, wide ? s.card : s.bottom, motion]}>
|
|
21
66
|
{!wide && <View style={s.handle} />}
|
|
22
67
|
{children}
|
|
23
|
-
</View>
|
|
68
|
+
</Animated.View>
|
|
24
69
|
);
|
|
25
70
|
}
|
|
26
71
|
|
|
27
|
-
const s =
|
|
72
|
+
const s = createSheet({
|
|
28
73
|
backdrop: { backgroundColor: 'rgba(0,0,0,0.35)', zIndex: 55 },
|
|
29
|
-
sheet: { position: 'absolute', backgroundColor: C.g1, paddingTop:
|
|
30
|
-
card: { right: 16, bottom: 16, width:
|
|
31
|
-
bottom: { left: 0, right: 0, bottom: 0, borderTopLeftRadius: 20, borderTopRightRadius: 20 },
|
|
74
|
+
sheet: { position: 'absolute', backgroundColor: C.g1, borderColor: C.g3, borderWidth: 1, paddingTop: 14, paddingHorizontal: 16, paddingBottom: 16, zIndex: 60, maxHeight: '88%', boxShadow: '0px 5px 15px rgba(0, 0, 0, 0.75)' },
|
|
75
|
+
card: { right: 16, bottom: 16, width: 360, borderRadius: 20, paddingTop: 16 },
|
|
76
|
+
bottom: { left: 0, right: 0, bottom: 0, borderTopLeftRadius: 20, borderTopRightRadius: 20, borderBottomWidth: 0 },
|
|
32
77
|
handle: { width: 50, height: 5, borderRadius: 3, backgroundColor: C.g4, alignSelf: 'center', marginBottom: 14 },
|
|
33
78
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { View, type GestureResponderEvent, type LayoutChangeEvent } from 'react-native';
|
|
3
|
+
import { C } from './theme';
|
|
4
|
+
import { createSheet } from './corners';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A horizontal slider: drag (or tap) along the track; the screen reader's adjust gestures step it. `onStart` comes once
|
|
8
|
+
* as a drag begins (an undo step), `onChange` as the value moves, `onEnd` as it's let go. Values snap to `step`
|
|
9
|
+
* between `min` and `max`.
|
|
10
|
+
*/
|
|
11
|
+
export function Slider({ value, min, max, step, onChange, onStart, onEnd, label, testID, format = v => v + '%' }: {
|
|
12
|
+
value: number; min: number; max: number; step: number; onChange: (v: number) => void; onStart?: () => void; onEnd?: () => void; label: string; testID?: string;
|
|
13
|
+
/** the value as a screen reader says it (percent unless given) */
|
|
14
|
+
format?: (v: number) => string;
|
|
15
|
+
}) {
|
|
16
|
+
const width = useRef(0), [, setW] = useState(0);
|
|
17
|
+
const snap = (v: number) => Math.min(max, Math.max(min, Math.round(v / step) * step));
|
|
18
|
+
// the thumb and fill take no touches, so a touch's x is always measured from the track's left edge
|
|
19
|
+
const at = (e: GestureResponderEvent) => snap(min + (e.nativeEvent.locationX / (width.current || 1)) * (max - min));
|
|
20
|
+
const t = (value - min) / (max - min);
|
|
21
|
+
return (
|
|
22
|
+
<View
|
|
23
|
+
accessibilityRole="adjustable" accessibilityLabel={label} testID={testID}
|
|
24
|
+
accessibilityValue={{ min, max, now: value, text: format(value) }}
|
|
25
|
+
onAccessibilityAction={e => { onChange(snap(value + (e.nativeEvent.actionName === 'increment' ? step : -step))); onEnd?.(); }}
|
|
26
|
+
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
|
|
27
|
+
onLayout={(e: LayoutChangeEvent) => { width.current = e.nativeEvent.layout.width; setW(width.current); }}
|
|
28
|
+
onStartShouldSetResponder={() => true} onMoveShouldSetResponder={() => true} onResponderTerminationRequest={() => false}
|
|
29
|
+
onResponderGrant={e => { onStart?.(); onChange(at(e)); }}
|
|
30
|
+
onResponderMove={e => onChange(at(e))}
|
|
31
|
+
onResponderRelease={() => onEnd?.()} onResponderTerminate={() => onEnd?.()}
|
|
32
|
+
style={s.hit}>
|
|
33
|
+
<View style={s.track} pointerEvents="none">
|
|
34
|
+
<View style={[s.fill, { width: `${t * 100}%` }]} />
|
|
35
|
+
</View>
|
|
36
|
+
<View pointerEvents="none" style={[s.thumb, { left: `${t * 100}%` }]} />
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const THUMB = 22;
|
|
42
|
+
const s = createSheet({
|
|
43
|
+
// a tall hit area around a thin track
|
|
44
|
+
hit: { height: 36, justifyContent: 'center', marginHorizontal: THUMB / 2 },
|
|
45
|
+
track: { height: 4, borderRadius: 2, backgroundColor: C.g3, overflow: 'hidden' },
|
|
46
|
+
fill: { height: 4, backgroundColor: '#fff' },
|
|
47
|
+
thumb: { position: 'absolute', top: (36 - THUMB) / 2, width: THUMB, height: THUMB, marginLeft: -THUMB / 2, borderRadius: THUMB / 2, backgroundColor: '#fff', boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.3)' },
|
|
48
|
+
});
|