@reekon-tools/boldr-cad 1.4.0 → 1.6.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 +13 -3
- package/dist/app/edit.d.ts +3 -3
- package/dist/app/edit.js +26 -1
- package/dist/app/edit.js.map +1 -1
- package/dist/app/state.d.ts +6 -8
- package/dist/app/state.js +0 -1
- package/dist/app/state.js.map +1 -1
- package/dist/data/sync.d.ts +1 -1
- package/dist/data/sync.js +7 -2
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.d.ts +8 -0
- package/dist/editor/actions.js +255 -181
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/elevation.d.ts +4 -1
- package/dist/editor/elevation.js +16 -17
- package/dist/editor/elevation.js.map +1 -1
- package/dist/editor/gestures.d.ts +21 -0
- package/dist/editor/gestures.js +92 -14
- package/dist/editor/gestures.js.map +1 -1
- package/dist/editor/keys.js +4 -3
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/laser.d.ts +4 -2
- package/dist/editor/laser.js +4 -2
- package/dist/editor/laser.js.map +1 -1
- package/dist/editor/linkActions.d.ts +49 -12
- package/dist/editor/linkActions.js +164 -35
- package/dist/editor/linkActions.js.map +1 -1
- package/dist/editor/links.d.ts +13 -0
- package/dist/editor/links.js +7 -1
- package/dist/editor/links.js.map +1 -1
- package/dist/editor/mode.d.ts +2 -1
- package/dist/editor/mode.js +8 -5
- package/dist/editor/mode.js.map +1 -1
- package/dist/editor/project.js +1 -2
- package/dist/editor/project.js.map +1 -1
- package/dist/editor/scaleGate.d.ts +13 -0
- package/dist/editor/scaleGate.js +27 -0
- package/dist/editor/scaleGate.js.map +1 -0
- package/dist/editor/selection.js +13 -9
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/tooltips.js +10 -0
- package/dist/editor/tooltips.js.map +1 -1
- package/dist/editor/view3d.d.ts +4 -1
- package/dist/editor/view3d.js +6 -2
- package/dist/editor/view3d.js.map +1 -1
- package/dist/export/pdf.d.ts +5 -2
- package/dist/export/pdf.js +12 -8
- package/dist/export/pdf.js.map +1 -1
- package/dist/export/plan.d.ts +1 -1
- package/dist/export/plan.js +14 -9
- package/dist/export/plan.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model/constraints.js +2 -0
- package/dist/model/constraints.js.map +1 -1
- package/dist/model/demo.d.ts +1 -0
- package/dist/model/demo.js +2 -1
- package/dist/model/demo.js.map +1 -1
- package/dist/model/elevation.d.ts +1 -1
- package/dist/model/elevation.js +3 -3
- package/dist/model/elevation.js.map +1 -1
- package/dist/model/geom.d.ts +2 -0
- package/dist/model/geom.js +11 -0
- package/dist/model/geom.js.map +1 -1
- package/dist/model/history.js +4 -1
- package/dist/model/history.js.map +1 -1
- package/dist/model/links.d.ts +10 -2
- package/dist/model/links.js +15 -3
- package/dist/model/links.js.map +1 -1
- package/dist/model/measurement.d.ts +2 -2
- package/dist/model/measurement.js +3 -3
- package/dist/model/measurement.js.map +1 -1
- package/dist/model/openings.d.ts +10 -0
- package/dist/model/openings.js +18 -0
- package/dist/model/openings.js.map +1 -1
- package/dist/model/planGeom.d.ts +37 -2
- package/dist/model/planGeom.js +72 -9
- package/dist/model/planGeom.js.map +1 -1
- package/dist/model/project.js +5 -2
- package/dist/model/project.js.map +1 -1
- package/dist/model/rooms.d.ts +18 -5
- package/dist/model/rooms.js +178 -73
- package/dist/model/rooms.js.map +1 -1
- package/dist/model/scale.d.ts +30 -0
- package/dist/model/scale.js +123 -0
- package/dist/model/scale.js.map +1 -0
- package/dist/model/settings.d.ts +0 -1
- package/dist/model/settings.js +1 -1
- package/dist/model/settings.js.map +1 -1
- package/dist/model/sketch.d.ts +26 -2
- package/dist/model/sketch.js +77 -22
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/stroke.js +2 -2
- package/dist/model/trim.d.ts +54 -0
- package/dist/model/trim.js +219 -0
- package/dist/model/trim.js.map +1 -0
- package/dist/model/types.d.ts +62 -9
- package/dist/react/BottomBar.d.ts +2 -1
- package/dist/react/BottomBar.js +144 -46
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/CadEditor.js +18 -9
- package/dist/react/CadEditor.js.map +1 -1
- package/dist/react/Drawer.js +6 -4
- package/dist/react/Drawer.js.map +1 -1
- package/dist/react/Duck.js.map +1 -1
- package/dist/react/KeypadSheet.js.map +1 -1
- package/dist/react/MarkupPanel.js +1 -1
- package/dist/react/PlanCanvas.native.js +54 -7
- package/dist/react/PlanCanvas.native.js.map +1 -1
- package/dist/react/PlanCanvas.web.js +17 -5
- package/dist/react/PlanCanvas.web.js.map +1 -1
- package/dist/react/Popovers.js +14 -3
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/Rail.js +1 -1
- package/dist/react/ShapeMenu.js +3 -2
- package/dist/react/ShapeMenu.js.map +1 -1
- package/dist/react/Sheet.d.ts +2 -2
- package/dist/react/Sheet.js.map +1 -1
- package/dist/react/ToolTip.js +3 -2
- package/dist/react/ToolTip.js.map +1 -1
- package/dist/react/TopBar.js +4 -3
- package/dist/react/TopBar.js.map +1 -1
- package/dist/react/barScroll.d.ts +23 -0
- package/dist/react/barScroll.js +47 -0
- package/dist/react/barScroll.js.map +1 -0
- package/dist/react/corners.d.ts +6 -2
- package/dist/react/corners.js.map +1 -1
- package/dist/react/editorBox.d.ts +8 -0
- package/dist/react/editorBox.js +10 -0
- package/dist/react/editorBox.js.map +1 -0
- package/dist/react/host.d.ts +1 -1
- package/dist/react/host.js +1 -1
- package/dist/react/host.js.map +1 -1
- package/dist/react/motion.d.ts +1 -1
- package/dist/react/motion.js +1 -1
- package/dist/react/motion.js.map +1 -1
- package/dist/react/pageLock.native.d.ts +2 -0
- package/dist/react/pageLock.native.js +2 -0
- package/dist/react/pageLock.native.js.map +1 -1
- package/dist/react/pageLock.web.d.ts +6 -0
- package/dist/react/pageLock.web.js +12 -3
- package/dist/react/pageLock.web.js.map +1 -1
- package/dist/react/ports.js +8 -3
- package/dist/react/ports.js.map +1 -1
- package/dist/react/saving.d.ts +7 -1
- package/dist/react/saving.js +10 -2
- package/dist/react/saving.js.map +1 -1
- package/dist/react/store.d.ts +5 -2
- package/dist/react/store.js +4 -1
- package/dist/react/store.js.map +1 -1
- package/dist/render/draw.d.ts +21 -4
- package/dist/render/draw.js +203 -151
- package/dist/render/draw.js.map +1 -1
- package/dist/render/elevation.js +3 -3
- package/dist/render/elevation.js.map +1 -1
- package/dist/render/figmaIcons.d.ts +6 -0
- package/dist/render/figmaIcons.js +16 -1
- package/dist/render/figmaIcons.js.map +1 -1
- package/dist/render/hit.d.ts +8 -1
- package/dist/render/hit.js +23 -4
- package/dist/render/hit.js.map +1 -1
- package/dist/render/icons.js +27 -20
- package/dist/render/icons.js.map +1 -1
- package/dist/render/lens.d.ts +27 -0
- package/dist/render/lens.js +41 -0
- package/dist/render/lens.js.map +1 -0
- package/dist/render/linkTile.d.ts +20 -2
- package/dist/render/linkTile.js +476 -246
- package/dist/render/linkTile.js.map +1 -1
- package/dist/render/snap.d.ts +10 -1
- package/dist/render/snap.js +87 -15
- package/dist/render/snap.js.map +1 -1
- package/dist/render/svgPath.d.ts +29 -0
- package/dist/render/svgPath.js +114 -0
- package/dist/render/svgPath.js.map +1 -0
- package/dist/render/tileIcons.d.ts +18 -0
- package/dist/render/tileIcons.js +67 -0
- package/dist/render/tileIcons.js.map +1 -0
- package/dist/render/view.d.ts +18 -1
- package/dist/render/view.js +38 -6
- package/dist/render/view.js.map +1 -1
- package/dist/skia/SkiaDrawCtx.d.ts +8 -0
- package/dist/skia/SkiaDrawCtx.js +27 -5
- package/dist/skia/SkiaDrawCtx.js.map +1 -1
- package/dist/solver/equations.d.ts +6 -0
- package/dist/solver/equations.js +53 -22
- package/dist/solver/equations.js.map +1 -1
- package/dist/solver/gauss-newton.js +6 -1
- package/dist/solver/gauss-newton.js.map +1 -1
- package/dist/tools/annotate.d.ts +0 -2
- package/dist/tools/annotate.js +1 -22
- package/dist/tools/annotate.js.map +1 -1
- package/dist/tools/chamfer.js +4 -0
- package/dist/tools/chamfer.js.map +1 -1
- package/dist/tools/dim.d.ts +3 -3
- package/dist/tools/dim.js +26 -65
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/fillet.js +4 -0
- package/dist/tools/fillet.js.map +1 -1
- package/dist/tools/freehand.js +1 -4
- package/dist/tools/freehand.js.map +1 -1
- package/dist/tools/line.js +15 -11
- package/dist/tools/line.js.map +1 -1
- package/dist/tools/modify.d.ts +32 -4
- package/dist/tools/modify.js +253 -98
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +7 -0
- package/dist/tools/registry.js +14 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +30 -8
- package/dist/tools/select.js +165 -34
- package/dist/tools/select.js.map +1 -1
- package/dist/tools/shapes.d.ts +1 -1
- package/dist/tools/shapes.js +12 -5
- package/dist/tools/shapes.js.map +1 -1
- package/dist/view3d/build.js +9 -8
- package/dist/view3d/build.js.map +1 -1
- package/package.json +9 -9
- package/src/app/edit.ts +25 -1
- package/src/app/state.ts +6 -8
- package/src/data/sync.ts +7 -2
- package/src/editor/actions.ts +213 -122
- package/src/editor/elevation.ts +13 -16
- package/src/editor/gestures.ts +83 -13
- package/src/editor/keys.ts +3 -2
- package/src/editor/laser.ts +7 -4
- package/src/editor/linkActions.ts +169 -31
- package/src/editor/links.ts +17 -1
- package/src/editor/mode.ts +8 -5
- package/src/editor/project.ts +1 -2
- package/src/editor/scaleGate.ts +32 -0
- package/src/editor/selection.ts +13 -7
- package/src/editor/tooltips.ts +9 -0
- package/src/editor/view3d.ts +6 -2
- package/src/export/pdf.ts +7 -4
- package/src/export/plan.ts +14 -10
- package/src/index.ts +1 -0
- package/src/model/constraints.ts +1 -0
- package/src/model/demo.ts +2 -1
- package/src/model/elevation.ts +4 -4
- package/src/model/geom.ts +10 -0
- package/src/model/history.ts +4 -1
- package/src/model/links.ts +14 -3
- package/src/model/measurement.ts +3 -3
- package/src/model/openings.ts +18 -0
- package/src/model/planGeom.ts +70 -10
- package/src/model/project.ts +6 -2
- package/src/model/rooms.ts +151 -63
- package/src/model/scale.ts +107 -0
- package/src/model/settings.ts +1 -2
- package/src/model/sketch.ts +62 -11
- package/src/model/stroke.ts +2 -2
- package/src/model/trim.ts +188 -0
- package/src/model/types.ts +57 -9
- package/src/react/BottomBar.tsx +177 -52
- package/src/react/CadEditor.tsx +19 -9
- package/src/react/Drawer.tsx +17 -12
- package/src/react/Duck.tsx +3 -3
- package/src/react/KeypadSheet.tsx +2 -2
- package/src/react/MarkupPanel.tsx +1 -1
- package/src/react/PlanCanvas.native.tsx +60 -6
- package/src/react/PlanCanvas.web.tsx +17 -5
- package/src/react/Popovers.tsx +24 -9
- package/src/react/Rail.tsx +1 -1
- package/src/react/ShapeMenu.tsx +3 -2
- package/src/react/Sheet.tsx +3 -3
- package/src/react/ToolTip.tsx +3 -2
- package/src/react/TopBar.tsx +4 -3
- package/src/react/barScroll.ts +57 -0
- package/src/react/corners.ts +6 -3
- package/src/react/editorBox.ts +20 -0
- package/src/react/host.ts +1 -1
- package/src/react/motion.tsx +6 -6
- package/src/react/pageLock.native.ts +2 -0
- package/src/react/pageLock.web.ts +12 -3
- package/src/react/ports.ts +7 -2
- package/src/react/saving.ts +10 -2
- package/src/react/store.ts +5 -2
- package/src/render/draw.ts +169 -118
- package/src/render/elevation.ts +5 -4
- package/src/render/figmaIcons.ts +17 -1
- package/src/render/hit.ts +21 -4
- package/src/render/icons.ts +27 -20
- package/src/render/lens.ts +52 -0
- package/src/render/linkTile.ts +436 -190
- package/src/render/snap.ts +87 -17
- package/src/render/svgPath.ts +95 -0
- package/src/render/tileIcons.ts +75 -0
- package/src/render/view.ts +43 -7
- package/src/skia/SkiaDrawCtx.ts +26 -4
- package/src/solver/equations.ts +52 -22
- package/src/solver/gauss-newton.ts +4 -1
- package/src/styles.css +3 -2
- package/src/tools/annotate.ts +1 -18
- package/src/tools/chamfer.ts +3 -0
- package/src/tools/dim.ts +26 -52
- package/src/tools/fillet.ts +3 -0
- package/src/tools/freehand.ts +1 -3
- package/src/tools/line.ts +12 -11
- package/src/tools/modify.ts +210 -70
- package/src/tools/registry.ts +18 -1
- package/src/tools/select.ts +144 -36
- package/src/tools/shapes.ts +9 -4
- package/src/ui/ctx.ts +4 -3
- package/src/ui/drawer.ts +8 -2
- package/src/view3d/build.ts +11 -10
- package/dist/react/LaserTray.d.ts +0 -4
- package/dist/react/LaserTray.js +0 -26
- package/dist/react/LaserTray.js.map +0 -1
- package/src/react/LaserTray.tsx +0 -42
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Canvas, Group, Picture, Skia, type SkPicture, type Transforms3d } from '@shopify/react-native-skia';
|
|
2
|
-
import { memo, useEffect, useMemo, useRef } from 'react';
|
|
3
|
-
import { View, type LayoutChangeEvent } from 'react-native';
|
|
1
|
+
import { Canvas, Group, Path, Picture, RoundedRect, Skia, type SkPicture, type Transforms3d } from '@shopify/react-native-skia';
|
|
2
|
+
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useWindowDimensions, View, type LayoutChangeEvent } from 'react-native';
|
|
4
4
|
import { Gesture, GestureDetector, PointerType, type GestureTouchEvent, type TouchData } from 'react-native-gesture-handler';
|
|
5
5
|
import { runOnJS, useDerivedValue, useSharedValue } from 'react-native-reanimated';
|
|
6
6
|
import { ed } from '../app/state';
|
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
} from '../render/view';
|
|
15
15
|
import { page } from '../model/store';
|
|
16
16
|
import { recordLayers, skiaImageLoader, skiaTextMeasure } from '../skia/host';
|
|
17
|
+
import { LENS_ARM, LENS_R, LENS_ZOOM, lensNow, type Lens } from '../render/lens';
|
|
18
|
+
import { PHONE_MAX } from './theme';
|
|
19
|
+
import { TABS_TOP, topLayout } from './topRow';
|
|
17
20
|
|
|
18
21
|
/*
|
|
19
22
|
The plan on a device. Pan and zoom never wait for the JavaScript thread: the fingers are followed on the UI thread
|
|
@@ -29,10 +32,15 @@ import { recordLayers, skiaImageLoader, skiaTextMeasure } from '../skia/host';
|
|
|
29
32
|
As a tool follows the pointer, only the moving parts (preview, snap marker, selection box) are drawn again; the
|
|
30
33
|
drawing is kept until it changes. Touches still reach editor/gestures.ts for the tools, Apple Pencil, and two- and
|
|
31
34
|
three-finger taps (undo, redo); it leaves the view to this canvas (setHostNavigation).
|
|
35
|
+
|
|
36
|
+
While a finger or Pencil places or drags something, the lens (render/lens.ts) shows the drawing around it twice as
|
|
37
|
+
close in a top corner: the same pictures again, through its zoom, clipped to it — nothing more is drawn for it.
|
|
32
38
|
*/
|
|
33
39
|
|
|
34
40
|
/** How far past each edge of the screen a picture of the drawing reaches, as a share of the screen's longer side. */
|
|
35
41
|
const OVERSCAN = 0.5;
|
|
42
|
+
/** A tablet's rail's right edge (Rail.tsx: 12 in, its buttons 44 at most and 6 in from its sides); its top row's buttons' height. */
|
|
43
|
+
const RAIL_RIGHT = 12 + 44 + 2 * 6, TOP_BUTTON = 46;
|
|
36
44
|
const EMPTY = Skia.Picture.MakePicture(null)!;
|
|
37
45
|
|
|
38
46
|
/** What the JavaScript thread drew last, and the view it drew it at. */
|
|
@@ -50,6 +58,9 @@ type Packed = [id: number, x: number, y: number, ax: number, ay: number];
|
|
|
50
58
|
/** The latest of the UI thread's views this thread has taken; each frame says which it includes. */
|
|
51
59
|
let hostSeq = 0;
|
|
52
60
|
|
|
61
|
+
/** Where the lens is now (render/lens.ts), told to the UI thread: after each touch, and each frame. */
|
|
62
|
+
let placeLens: (() => void) | null = null;
|
|
63
|
+
|
|
53
64
|
/**
|
|
54
65
|
* Touches from the UI thread: the view first, if the fingers moved it, then each touch to the editor's gestures (as a
|
|
55
66
|
* pointer lands, `down` is every one still down: any other's lift was lost).
|
|
@@ -63,6 +74,7 @@ function touchesOnJS(type: number, kind: string, ts: Packed[], view: UiView | nu
|
|
|
63
74
|
// it reached the screen a frame behind the finger. Only when nothing but those moving parts changed — the drawing
|
|
64
75
|
// itself, and the view, are drawn on the frame as before.
|
|
65
76
|
if (!view && !drawingChanged() && !imagesChanged()) paintPending();
|
|
77
|
+
placeLens?.();
|
|
66
78
|
}
|
|
67
79
|
|
|
68
80
|
const kindOf = (t: PointerType): string => {
|
|
@@ -81,6 +93,14 @@ export const PlanCanvas = memo(function PlanCanvas() {
|
|
|
81
93
|
// the UI thread's own: the fingers down, whether they're navigating and where they were last, a pen seen
|
|
82
94
|
const nav = useSharedValue<NavState>({ fingers: {}, navigating: false, last: null, pen: false });
|
|
83
95
|
const readOnly = useSharedValue(false);
|
|
96
|
+
// the lens, and whether it's up (mounted only then: nothing drawn twice for it otherwise)
|
|
97
|
+
const lens = useSharedValue<Lens | null>(null);
|
|
98
|
+
const [lensUp, setLensUp] = useState(false);
|
|
99
|
+
// what the chrome leaves it: a phone's all (its chrome steps aside as a finger moves: Duck); a tablet's, right of
|
|
100
|
+
// the rail and under the top row, and under the tabs where they've a row of their own
|
|
101
|
+
const { width, height } = useWindowDimensions(), phone = width < PHONE_MAX;
|
|
102
|
+
const room = useRef({ left: 0, top: 0 });
|
|
103
|
+
room.current = phone ? { left: 0, top: 0 } : { left: RAIL_RIGHT, top: (topLayout(width, height, false).below ? TABS_TOP.below : TABS_TOP.row) + TOP_BUTTON };
|
|
84
104
|
|
|
85
105
|
// the last pictures, carried from the view they were drawn at to the view the fingers have reached since
|
|
86
106
|
const shift = useDerivedValue<Transforms3d>(() => {
|
|
@@ -92,6 +112,19 @@ export const PlanCanvas = memo(function PlanCanvas() {
|
|
|
92
112
|
const overlay = useDerivedValue(() => frame.value?.overlay ?? EMPTY);
|
|
93
113
|
const baseFixed = useDerivedValue(() => frame.value?.baseFixed ?? EMPTY);
|
|
94
114
|
const overlayFixed = useDerivedValue(() => frame.value?.overlayFixed ?? EMPTY);
|
|
115
|
+
// the lens: its square, the finger at its middle LENS_ZOOM times closer, and its cross
|
|
116
|
+
const lensClip = useDerivedValue(() => { const l = lens.value; return Skia.RRectXY(Skia.XYWHRect(l?.x ?? 0, l?.y ?? 0, l?.size ?? 0, l?.size ?? 0), LENS_R, LENS_R); });
|
|
117
|
+
const lensZoom = useDerivedValue<Transforms3d>(() => {
|
|
118
|
+
const l = lens.value;
|
|
119
|
+
return l ? [{ translateX: l.x + l.size / 2 }, { translateY: l.y + l.size / 2 }, { scale: LENS_ZOOM }, { translateX: -l.fx }, { translateY: -l.fy }] : [];
|
|
120
|
+
});
|
|
121
|
+
const lensCross = useDerivedValue(() => {
|
|
122
|
+
const l = lens.value, b = Skia.PathBuilder.Make();
|
|
123
|
+
if (l) { b.moveTo(l.cx - LENS_ARM, l.cy); b.lineTo(l.cx + LENS_ARM, l.cy); b.moveTo(l.cx, l.cy - LENS_ARM); b.lineTo(l.cx, l.cy + LENS_ARM); }
|
|
124
|
+
return b.build();
|
|
125
|
+
});
|
|
126
|
+
const lensX = useDerivedValue(() => lens.value?.x ?? 0), lensY = useDerivedValue(() => lens.value?.y ?? 0);
|
|
127
|
+
const lensSize = useDerivedValue(() => lens.value?.size ?? 0), lensInk = useDerivedValue(() => lens.value?.ink ?? '#000');
|
|
95
128
|
|
|
96
129
|
useEffect(() => {
|
|
97
130
|
setImageLoader(skiaImageLoader);
|
|
@@ -104,7 +137,12 @@ export const PlanCanvas = memo(function PlanCanvas() {
|
|
|
104
137
|
* not every wall and dimension over them (a frame's worth of the JavaScript thread on a big plan).
|
|
105
138
|
*/
|
|
106
139
|
let kept: { under: SkPicture; base: SkPicture; fixed: SkPicture; T: Xform; W: number; H: number } | null = null;
|
|
107
|
-
let wasReadOnly: boolean | null = null;
|
|
140
|
+
let wasReadOnly: boolean | null = null, lensShown = false;
|
|
141
|
+
placeLens = () => {
|
|
142
|
+
const l = lensNow(cvs.W, cvs.H, room.current);
|
|
143
|
+
if (l || lensShown) lens.value = l;
|
|
144
|
+
if (!!l !== lensShown) { lensShown = !!l; setLensUp(lensShown); }
|
|
145
|
+
};
|
|
108
146
|
setFrameRenderer(() => {
|
|
109
147
|
const { W, H } = cvs;
|
|
110
148
|
if (!W || !H) return;
|
|
@@ -133,9 +171,10 @@ export const PlanCanvas = memo(function PlanCanvas() {
|
|
|
133
171
|
under: kept.under, base: kept.base, overlay: over.moving, baseFixed: kept.fixed, overlayFixed: over.fixed,
|
|
134
172
|
s: T.s, ox: T.ox, oy: T.oy, W, H, view: { s: v.s, cx: v.cx, cy: v.cy }, seq: hostSeq,
|
|
135
173
|
};
|
|
174
|
+
placeLens?.();
|
|
136
175
|
});
|
|
137
|
-
return () => { setFrameRenderer(null); setImageLoader(null); setTextMeasure(null); setHostNavigation(false); };
|
|
138
|
-
}, [frame, readOnly]);
|
|
176
|
+
return () => { setFrameRenderer(null); setImageLoader(null); setTextMeasure(null); setHostNavigation(false); placeLens = null; };
|
|
177
|
+
}, [frame, readOnly, lens]);
|
|
139
178
|
|
|
140
179
|
const onLayout = (e: LayoutChangeEvent) => {
|
|
141
180
|
const { width, height } = e.nativeEvent.layout;
|
|
@@ -193,6 +232,21 @@ export const PlanCanvas = memo(function PlanCanvas() {
|
|
|
193
232
|
</Group>
|
|
194
233
|
<Picture picture={baseFixed} />
|
|
195
234
|
<Picture picture={overlayFixed} />
|
|
235
|
+
{lensUp && (
|
|
236
|
+
<Group>
|
|
237
|
+
<Group clip={lensClip}>
|
|
238
|
+
<Group transform={lensZoom}>
|
|
239
|
+
<Group transform={shift}>
|
|
240
|
+
<Picture picture={under} />
|
|
241
|
+
<Picture picture={base} />
|
|
242
|
+
<Picture picture={overlay} />
|
|
243
|
+
</Group>
|
|
244
|
+
</Group>
|
|
245
|
+
<Path path={lensCross} color={lensInk} style="stroke" strokeWidth={1.5} />
|
|
246
|
+
</Group>
|
|
247
|
+
<RoundedRect x={lensX} y={lensY} width={lensSize} height={lensSize} r={LENS_R} color={lensInk} style="stroke" strokeWidth={2} />
|
|
248
|
+
</Group>
|
|
249
|
+
)}
|
|
196
250
|
</Canvas>
|
|
197
251
|
</View>
|
|
198
252
|
</GestureDetector>
|
|
@@ -2,8 +2,8 @@ import { useEffect, useRef } from 'react';
|
|
|
2
2
|
import { pointerCancel, pointerDown, pointerLeave, pointerMove, pointerUp, wheelZoom, type PointerIn } from '../editor/gestures';
|
|
3
3
|
import { keyDown, keyUp, type KeyIn } from '../editor/keys';
|
|
4
4
|
import { page } from '../model/store';
|
|
5
|
-
import { fitView, initCanvas, resize, setFrameRenderer } from '../render/view';
|
|
6
|
-
import { PAGE_LOCK_ROOT } from './pageLock';
|
|
5
|
+
import { cvs, fitView, initCanvas, resize, setFrameRenderer } from '../render/view';
|
|
6
|
+
import { BESIDE, PAGE_LOCK_ROOT } from './pageLock';
|
|
7
7
|
import { setCursorTarget } from './ports';
|
|
8
8
|
|
|
9
9
|
/*
|
|
@@ -14,14 +14,19 @@ import { setCursorTarget } from './ports';
|
|
|
14
14
|
|
|
15
15
|
const keyIn = (e: KeyboardEvent): KeyIn => ({ key: e.key, shift: e.shiftKey, mod: e.metaKey || e.ctrlKey, alt: e.altKey });
|
|
16
16
|
|
|
17
|
+
/** The host's pane beside the editor that the last press on the page was in (BESIDE), if it was in one. */
|
|
18
|
+
let pressedBeside: Element | null = null;
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* Whether a key is the editor's: pressed in it, or with nothing focused — not typed into a field, nor pressed in the
|
|
19
22
|
* host's own UI over the drawing (a picker, a measurement's detail), which is outside the editor's root. Delete there
|
|
20
|
-
* would otherwise delete what's selected on the drawing.
|
|
23
|
+
* would otherwise delete what's selected on the drawing. With nothing focused it's the editor's unless the last press
|
|
24
|
+
* was in a pane beside it (a split view) that's still open: a click on its page, where nothing takes focus, leaves the
|
|
25
|
+
* focus on the page itself.
|
|
21
26
|
*/
|
|
22
27
|
function forEditor(e: KeyboardEvent): boolean {
|
|
23
28
|
const t = e.target instanceof HTMLElement ? e.target : null;
|
|
24
|
-
if (!t || t === document.body || t === document.documentElement) return
|
|
29
|
+
if (!t || t === document.body || t === document.documentElement) return !pressedBeside?.isConnected;
|
|
25
30
|
if (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable) return false;
|
|
26
31
|
return !!t.closest('#' + PAGE_LOCK_ROOT);
|
|
27
32
|
}
|
|
@@ -36,7 +41,9 @@ export function PlanCanvas() {
|
|
|
36
41
|
let first = true;
|
|
37
42
|
const ro = new ResizeObserver(() => {
|
|
38
43
|
resize();
|
|
39
|
-
|
|
44
|
+
// fitted once, at its first real size (not while a pane opening round it is still a sliver)
|
|
45
|
+
if (!first || cvs.W < 40 || cvs.H < 40) return;
|
|
46
|
+
if (!page().view || page()._fit !== false) fitView();
|
|
40
47
|
first = false;
|
|
41
48
|
});
|
|
42
49
|
ro.observe(cv);
|
|
@@ -60,6 +67,11 @@ export function PlanCanvas() {
|
|
|
60
67
|
// shift+wheel arrives as deltaX on some systems
|
|
61
68
|
wheelZoom(e.clientX - r.left, e.clientY - r.top, e.deltaY || e.deltaX, e.ctrlKey || e.metaKey, e.deltaMode !== 0);
|
|
62
69
|
}, { passive: false, signal: ac.signal });
|
|
70
|
+
// (whether the last press was in a pane beside the editor, for keys pressed with nothing focused: see forEditor)
|
|
71
|
+
pressedBeside = null;
|
|
72
|
+
document.addEventListener('pointerdown', e => {
|
|
73
|
+
pressedBeside = e.target instanceof Element ? e.target.closest(`[${BESIDE}]`) : null;
|
|
74
|
+
}, { capture: true, signal: ac.signal });
|
|
63
75
|
// capture phase: a key the editor uses (Enter on the keypad, Space to pan, …) must not also press whatever button
|
|
64
76
|
// last took focus
|
|
65
77
|
document.addEventListener('keydown', e => {
|
package/src/react/Popovers.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
1
|
+
import { useEffect, useRef, useState, type ComponentRef, type ReactNode } from 'react';
|
|
2
2
|
import { Animated, Text, TextInput, View, type ViewStyle } from 'react-native';
|
|
3
3
|
import { disarmReset, exportDxf, exportPdf, importPagesText, openProjectText, resetArmed, resetToDemo, saveProjectFile } from '../editor/files';
|
|
4
4
|
import {
|
|
@@ -28,14 +28,24 @@ function MenuLink({ label, onPress, danger }: { label: string; onPress: () => vo
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/** A setting's choices; `fill`: across the whole width it's given (MStack), each choice growing into the room. */
|
|
32
|
+
function Seg<T extends string | number>({ value, opts, onPick, fill = false }: { value: T; opts: [T, string][]; onPick: (v: T) => void; fill?: boolean }) {
|
|
33
|
+
return <Segmented value={value} options={opts} onPick={onPick} style={fill ? [s.seg, s.segFill] : s.seg} optionStyle={fill ? [s.segBtn, s.segBtnFill] : s.segBtn}
|
|
34
|
+
textStyle={s.segText} onText={C.dark} offText={C.lg1} />;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const MRow = ({ label, children }: { label: string; children: ReactNode }) => (
|
|
36
38
|
<View style={s.mrow}><Text style={s.rowText}>{label}</Text>{children}</View>
|
|
37
39
|
);
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A setting whose choices need the menu's whole width, its label above them. Units has up to seven (six, and the
|
|
43
|
+
* host's Account): beside its label they ran off the menu's edge, and the last of them (m) couldn't be picked at all.
|
|
44
|
+
*/
|
|
45
|
+
const MStack = ({ label, children }: { label: string; children: ReactNode }) => (
|
|
46
|
+
<View style={s.mstack}><Text style={s.rowText}>{label}</Text>{children}</View>
|
|
47
|
+
);
|
|
48
|
+
|
|
39
49
|
/**
|
|
40
50
|
* The top bar's menus: Layers, or ••• (on a phone Fit to screen, then exports, opening a file, settings). `accountUnits`: the host formats
|
|
41
51
|
* lengths (BOLDR: the person's account settings), offered as Units → Account beside the editor's own units.
|
|
@@ -63,7 +73,7 @@ export function Menu({ accountUnits = false, devTools = true, themeChoice = true
|
|
|
63
73
|
|
|
64
74
|
/** ••• : the exports, opening a file, and the settings. */
|
|
65
75
|
function MoreMenu({ open, style, accountUnits, devTools, themeChoice, pickText, readOnly, phone }: {
|
|
66
|
-
open: boolean; style: Animated.
|
|
76
|
+
open: boolean; style: Animated.WithAnimatedValue<ViewStyle>;
|
|
67
77
|
accountUnits: boolean; devTools: boolean; themeChoice: boolean; pickText: () => Promise<string | null>; readOnly: boolean; phone: boolean;
|
|
68
78
|
}) {
|
|
69
79
|
const run = (fn: () => void) => () => { closeMenu(); fn(); };
|
|
@@ -87,13 +97,13 @@ function MoreMenu({ open, style, accountUnits, devTools, themeChoice, pickText,
|
|
|
87
97
|
<View style={s.hr} />
|
|
88
98
|
</>
|
|
89
99
|
)}
|
|
90
|
-
<
|
|
91
|
-
<Seg<UnitChoice>
|
|
100
|
+
<MStack label="Units">
|
|
101
|
+
<Seg<UnitChoice> fill
|
|
92
102
|
value={accountUnits && followingHostUnits() ? 'account' : settings.unit}
|
|
93
103
|
opts={[...(accountUnits ? [['account', 'Account'] as [UnitChoice, string]] : []), ['ftin', 'ft-in'], ['in', 'in'], ['ft', 'ft'], ['mm', 'mm'], ['cm', 'cm'], ['m', 'm']]}
|
|
94
104
|
onPick={setUnits}
|
|
95
105
|
/>
|
|
96
|
-
</
|
|
106
|
+
</MStack>
|
|
97
107
|
{!(accountUnits && followingHostUnits()) && showFractions() && (
|
|
98
108
|
<MRow label="Precision"><Seg value={settings.denom} opts={[[16, '1/16"'], [8, '1/8"'], [4, '1/4"']]} onPick={setPrecision} /></MRow>
|
|
99
109
|
)}
|
|
@@ -139,7 +149,7 @@ function LayerRow({ label, sub, value, onChange }: { label: string; sub?: string
|
|
|
139
149
|
|
|
140
150
|
/** The Layers menu: what's shown — the photos and plans under the drawing, then each layer. (An image's own lock, scale and
|
|
141
151
|
* opacity are in its bar when it's selected.) */
|
|
142
|
-
function LayersMenu({ open, style }: { open: boolean; style: Animated.
|
|
152
|
+
function LayersMenu({ open, style }: { open: boolean; style: Animated.WithAnimatedValue<ViewStyle> }) {
|
|
143
153
|
const ims = page().underlays, n = ims.length;
|
|
144
154
|
return (
|
|
145
155
|
<Animated.View style={[s.menu, s.layers, style]} pointerEvents={open ? 'auto' : 'none'} accessibilityLabel="Layers" testID={open ? 'layersMenu' : undefined}>
|
|
@@ -157,7 +167,7 @@ function LayersMenu({ open, style }: { open: boolean; style: Animated.WithAnimat
|
|
|
157
167
|
/** Typing a note where the text tool was tapped: Enter places it, Esc cancels, leaving the field places it too. */
|
|
158
168
|
export function NoteInput() {
|
|
159
169
|
useEditorKey(() => (ui.note ? String(editorVersion()) : ''));
|
|
160
|
-
const n = ui.note, [text, setText] = useState(''), ref = useRef<TextInput
|
|
170
|
+
const n = ui.note, [text, setText] = useState(''), ref = useRef<ComponentRef<typeof TextInput>>(null), done = useRef(false);
|
|
161
171
|
useEffect(() => { setText(n?.initial ?? ''); done.current = false; if (n) setTimeout(() => ref.current?.focus(), 30); }, [n]);
|
|
162
172
|
if (!n) return null;
|
|
163
173
|
const finish = (commit: boolean) => {
|
|
@@ -181,11 +191,16 @@ const s = createSheet({
|
|
|
181
191
|
linkText: { fontFamily: FONT, fontSize: 14, color: '#fff' },
|
|
182
192
|
hr: { height: 1, backgroundColor: C.g2, marginVertical: 6 },
|
|
183
193
|
mrow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 8, paddingHorizontal: 16, gap: 10 },
|
|
194
|
+
mstack: { paddingVertical: 8, paddingHorizontal: 16, gap: 8 },
|
|
184
195
|
layers: { width: 340 },
|
|
185
196
|
rowSub: { fontFamily: FONT, fontSize: 12, color: C.g4, marginTop: 2 },
|
|
186
197
|
rowText: { fontFamily: FONT, fontSize: 14, color: '#fff', flexShrink: 1 },
|
|
187
198
|
seg: { flexDirection: 'row', backgroundColor: C.dark, borderRadius: 999, padding: 3 },
|
|
188
199
|
segBtn: { paddingVertical: 5, paddingHorizontal: 10, borderRadius: 999 },
|
|
200
|
+
// (the track a column, so its row of choices runs the track's whole width; each choice grows into the room there, and
|
|
201
|
+
// shrinks rather than running off the menu when the text is set large — its label then cut short, never wrapped)
|
|
202
|
+
segFill: { flexDirection: 'column' },
|
|
203
|
+
segBtnFill: { flexGrow: 1, flexShrink: 1, minWidth: 0, paddingHorizontal: 6 },
|
|
189
204
|
segText: { fontFamily: FONT, fontSize: 12.5, fontWeight: '600', color: C.lg1 },
|
|
190
205
|
note: { position: 'absolute', zIndex: 40, minWidth: 220, height: 36, paddingHorizontal: 10, borderRadius: 8, backgroundColor: '#fff', color: C.dark, fontFamily: FONT, fontSize: 14, borderWidth: 2, borderColor: C.purple },
|
|
191
206
|
});
|
package/src/react/Rail.tsx
CHANGED
|
@@ -299,7 +299,7 @@ export function Rail({ phone, onAddImage }: { phone: boolean; onAddImage?: () =>
|
|
|
299
299
|
};
|
|
300
300
|
return (
|
|
301
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:
|
|
302
|
+
<View style={[s.rail, phone && { top: 162, bottom: 78 }]} onLayout={e => {
|
|
303
303
|
const { height: h, y: top } = e.nativeEvent.layout, w = Dimensions.get('window');
|
|
304
304
|
setRoom({ height: h, y: top, winW: w.width, winH: w.height });
|
|
305
305
|
}}>
|
package/src/react/ShapeMenu.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useRef } from 'react';
|
|
2
|
-
import { Animated, Text,
|
|
2
|
+
import { Animated, Text, View } from 'react-native';
|
|
3
3
|
import { ed } from '../app/state';
|
|
4
4
|
import { railIcon, TOOL_GROUPS, toolLabel } from '../editor/mode';
|
|
5
5
|
import { toolTip } from '../editor/tooltips';
|
|
@@ -9,6 +9,7 @@ import { notify, ui, useEditorKey } from './store';
|
|
|
9
9
|
import { C, FONT, shadow } from './theme';
|
|
10
10
|
import { Tap, usePopup } from './motion';
|
|
11
11
|
import { createSheet } from './corners';
|
|
12
|
+
import { useEditorBox } from './editorBox';
|
|
12
13
|
|
|
13
14
|
const ROW = 44, GAP = 6, PAD = 8;
|
|
14
15
|
/** A menu's height for `n` rows: the rows, the gaps between them, its padding and border. */
|
|
@@ -22,7 +23,7 @@ const heightFor = (n: number) => n * ROW + (n - 1) * GAP + 2 * PAD + 2;
|
|
|
22
23
|
*/
|
|
23
24
|
export function ShapeMenu() {
|
|
24
25
|
useEditorKey(() => (ui.shapeMenu ? [ui.shapeMenu.x, ui.shapeMenu.y, ui.shapeMenu.group, ed.mode, ed.tool].join('|') : ''));
|
|
25
|
-
const { height } =
|
|
26
|
+
const { height } = useEditorBox(), now = ui.shapeMenu, nowGroup = now ? TOOL_GROUPS[now.group] : null;
|
|
26
27
|
const open = !!now && !!nowGroup && ed.mode === nowGroup.mode;
|
|
27
28
|
// (it grows out of the rail's place, sideways; as it goes it keeps what it showed)
|
|
28
29
|
const last = useRef(now), pop = usePopup(open, { x: -8 }, 'left center');
|
package/src/react/Sheet.tsx
CHANGED
|
@@ -19,7 +19,7 @@ const GHOST_MS = 450;
|
|
|
19
19
|
const SHEET_IN = 200, SHEET_OUT = 160, SHEET_EASE_OUT = Easing.bezier(0.4, 0, 1, 1);
|
|
20
20
|
/** how far it rises on a phone, and a card on a wide screen (as a popup does) */
|
|
21
21
|
const RISE_BY = 80, CARD_RISE = 8;
|
|
22
|
-
export function useSheetMotion(shown: boolean, wide: boolean): { present: boolean; motion: Animated.
|
|
22
|
+
export function useSheetMotion(shown: boolean, wide: boolean): { present: boolean; motion: Animated.WithAnimatedValue<Pick<ViewStyle, 'transform' | 'opacity'>> } {
|
|
23
23
|
const x = useRef(new Animated.Value(1)).current, [present, setPresent] = useState(shown);
|
|
24
24
|
const motion = useMemo(() => {
|
|
25
25
|
const opacity = x.interpolate({ inputRange: [0, 1], outputRange: [1, 0] });
|
|
@@ -29,7 +29,7 @@ export function useSheetMotion(shown: boolean, wide: boolean): { present: boolea
|
|
|
29
29
|
opacity, transformOrigin: 'bottom right',
|
|
30
30
|
transform: [{ translateY: x.interpolate({ inputRange: [0, 1], outputRange: [0, CARD_RISE] }) }, { scale: x.interpolate({ inputRange: [0, 1], outputRange: [1, 0.96] }) }],
|
|
31
31
|
}
|
|
32
|
-
: { opacity, transform: [{ translateY: x.interpolate({ inputRange: [0, 1], outputRange: [0, RISE_BY] }) }] }) as Animated.
|
|
32
|
+
: { opacity, transform: [{ translateY: x.interpolate({ inputRange: [0, 1], outputRange: [0, RISE_BY] }) }] }) as Animated.WithAnimatedValue<Pick<ViewStyle, 'transform' | 'opacity'>>;
|
|
33
33
|
}, [wide, x]);
|
|
34
34
|
useEffect(() => {
|
|
35
35
|
if (shown) setPresent(true);
|
|
@@ -59,7 +59,7 @@ export function Backdrop({ wide, onClose }: { wide: boolean; onClose: () => void
|
|
|
59
59
|
*/
|
|
60
60
|
export function Sheet({ wide, children, label, testID, motion, live = true }: {
|
|
61
61
|
wide: boolean; children: ReactNode; label: string; testID?: string;
|
|
62
|
-
motion?: Animated.
|
|
62
|
+
motion?: Animated.WithAnimatedValue<Pick<ViewStyle, 'transform' | 'opacity'>>; live?: boolean;
|
|
63
63
|
}) {
|
|
64
64
|
return (
|
|
65
65
|
<Animated.View accessibilityLabel={label} testID={testID} pointerEvents={live ? 'auto' : 'none'} style={[s.sheet, wide ? s.card : s.bottom, motion]}>
|
package/src/react/ToolTip.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Animated,
|
|
2
|
+
import { Animated, View } from 'react-native';
|
|
3
3
|
import { TIP_TOP, topLayout } from './topRow';
|
|
4
4
|
import { RichText } from './RichText';
|
|
5
5
|
import { ui, useEditorKey } from './store';
|
|
@@ -7,6 +7,7 @@ import { C, FONT, shadow } from './theme';
|
|
|
7
7
|
import { TOGGLE_EASE } from './motion';
|
|
8
8
|
import { NATIVE } from './easing';
|
|
9
9
|
import { createSheet } from './corners';
|
|
10
|
+
import { useEditorBox } from './editorBox';
|
|
10
11
|
|
|
11
12
|
/** How long the note stays, and how it comes and goes. */
|
|
12
13
|
const SHOW_MS = 2600, IN_MS = 160, OUT_MS = 240;
|
|
@@ -16,7 +17,7 @@ const SHOW_MS = 2600, IN_MS = 160, OUT_MS = 240;
|
|
|
16
17
|
* moment and goes by itself; picking another tool replaces it at once.
|
|
17
18
|
*/
|
|
18
19
|
export function ToolTip({ phone }: { phone: boolean }) {
|
|
19
|
-
const { width, height } =
|
|
20
|
+
const { width, height } = useEditorBox();
|
|
20
21
|
useEditorKey(() => String(ui.toolTip?.seq ?? '') + '|' + topLayout(width, height, phone).below);
|
|
21
22
|
const tip = ui.toolTip, v = useRef(new Animated.Value(0)).current, [shown, setShown] = useState<{ text: string; seq: number } | null>(null);
|
|
22
23
|
const slide = useRef(v.interpolate({ inputRange: [0, 1], outputRange: [-4, 0] })).current;
|
package/src/react/TopBar.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { Text,
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
3
|
import { closeElevation, elev, elevOpen, elevTitle, flipElevation } from '../editor/elevation';
|
|
4
4
|
import { setMode } from '../editor/mode';
|
|
5
5
|
import { openDrawer } from '../editor/ports';
|
|
@@ -17,6 +17,7 @@ import { C, FONT, shadow } from './theme';
|
|
|
17
17
|
import { Segmented, Tap } from './motion';
|
|
18
18
|
import { createSheet } from './corners';
|
|
19
19
|
import { setTopRoom, TABS_TOP, topKey, topLayout } from './topRow';
|
|
20
|
+
import { useEditorBox } from './editorBox';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* The top bar: back, the project and page, view controls, the host's own buttons, settings, and Save. In the wall
|
|
@@ -25,7 +26,7 @@ import { setTopRoom, TABS_TOP, topKey, topLayout } from './topRow';
|
|
|
25
26
|
export function TopBar({ onBack, headerRight, phone, readOnly }: {
|
|
26
27
|
onBack?: () => void; headerRight?: ReactNode | ((buttonSize: number) => ReactNode); phone: boolean; readOnly?: boolean;
|
|
27
28
|
}) {
|
|
28
|
-
const { width, height } =
|
|
29
|
+
const { width, height } = useEditorBox(), size = phone ? 42 : 46;
|
|
29
30
|
useEditorKey(() => {
|
|
30
31
|
const w = elevOpen() && !!elev.frame, title = w ? elevTitle() : null;
|
|
31
32
|
return [w, title?.title, title?.sub, store.project.name, page().name, ui.menu, saveState(), manualSave(), is3d(), topKey(topLayout(width, height, phone, size))].join('|');
|
|
@@ -95,7 +96,7 @@ const TABS: [Mode | '3d', string][] = [['annotate', 'Annotate'], ['draft', 'Draf
|
|
|
95
96
|
|
|
96
97
|
/** Annotate / Draft / 3D: centred in the top row, or on a row of their own under it (see topRow.ts). */
|
|
97
98
|
export function ModeTabs({ phone }: { phone: boolean }) {
|
|
98
|
-
const { width, height } =
|
|
99
|
+
const { width, height } = useEditorBox();
|
|
99
100
|
useEditorKey(() => (is3d() ? '3d' : ed.mode) + '|' + topKey(topLayout(width, height, phone)));
|
|
100
101
|
const at = topLayout(width, height, phone);
|
|
101
102
|
// (straight over: no line under the tabs saying how to get about in 3D)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { C } from './theme';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A bottom bar too long for the screen scrolls, and has to look like it does (the user's call, 2026-10-27: "on mobile
|
|
5
|
+
it is hard to tell that the bottom bar is scrollable"). Three things say so, in react/BottomBar.tsx:
|
|
6
|
+
- a fade at each end that has more of the bar past it, there as it's scrolled;
|
|
7
|
+
- its end always cuts through a button, never at a gap: there, what shows looks like all there is (barCut);
|
|
8
|
+
- the first bar of a session that doesn't fit slides a little way along and back, unless Reduce Motion is on.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** A button's place along the bar's buttons: from their start, and its width. */
|
|
12
|
+
export type Span = readonly [x: number, w: number];
|
|
13
|
+
|
|
14
|
+
/** How much of a button the end leaves showing: what it's cut to, and the shares that already read as cut. */
|
|
15
|
+
export const CUT = 0.6, CUT_MIN = 0.3, CUT_MAX = 0.8;
|
|
16
|
+
/** A fade's width; how far the bar scrolls as one comes or goes. */
|
|
17
|
+
export const FADE = 20, FADE_RAMP = 24;
|
|
18
|
+
/** The nudge: how far along, how long after the bar shows, and how long before it goes back (ms). */
|
|
19
|
+
export const NUDGE = 40, NUDGE_AFTER = 400, NUDGE_HOLD = 450;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The buttons' places along the bar, from the widths of everything in it in order (a divider's too) and the gap
|
|
23
|
+
* between each: null while any is still unmeasured.
|
|
24
|
+
*/
|
|
25
|
+
export function barSpans(items: readonly { w: number | undefined; button: boolean }[], gap: number): Span[] | null {
|
|
26
|
+
const out: Span[] = [];
|
|
27
|
+
let x = 0;
|
|
28
|
+
for (const { w, button } of items) {
|
|
29
|
+
if (w == null) return null;
|
|
30
|
+
if (button) out.push([x, w]);
|
|
31
|
+
x += w + gap;
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* How much of a scrolling bar shows, given `room` for it: all of that, if it already ends partway through a button
|
|
38
|
+
* (between CUT_MIN and CUT_MAX of it showing); otherwise less, ending at CUT of the last button that leaves showing.
|
|
39
|
+
*/
|
|
40
|
+
export function barCut(spans: readonly Span[], room: number): number {
|
|
41
|
+
let best = 0;
|
|
42
|
+
for (const [x, w] of spans) {
|
|
43
|
+
const f = (room - x) / w;
|
|
44
|
+
if (f >= CUT_MIN && f <= CUT_MAX) return room;
|
|
45
|
+
const c = x + CUT * w;
|
|
46
|
+
if (c <= room && c > best) best = c;
|
|
47
|
+
}
|
|
48
|
+
return best || room;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A fade, as an SVG: the bar's grey, clear at one side, whole at the other (`to`: the side that's whole). */
|
|
52
|
+
export const fadeSvg = (to: 'left' | 'right'): string => {
|
|
53
|
+
const id = 'barFade' + (to === 'left' ? 'L' : 'R'), [a, b] = to === 'right' ? [0, 1] : [1, 0];
|
|
54
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${FADE}" height="54" viewBox="0 0 1 1" preserveAspectRatio="none">` +
|
|
55
|
+
`<defs><linearGradient id="${id}" x1="${a}" y1="0" x2="${b}" y2="0"><stop offset="0" stop-color="${C.g2}" stop-opacity="0"/>` +
|
|
56
|
+
`<stop offset="1" stop-color="${C.g2}" stop-opacity="1"/></linearGradient></defs><rect width="1" height="1" fill="url(#${id})"/></svg>`;
|
|
57
|
+
};
|
package/src/react/corners.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Platform, StyleSheet } from 'react-native';
|
|
1
|
+
import { Platform, StyleSheet, type ImageStyle, type TextStyle, type ViewStyle } from 'react-native';
|
|
2
2
|
import { CORNER_SMOOTHING } from '../render/squircle';
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -44,9 +44,12 @@ export function smoothCorners<T extends Style>(st: T): T {
|
|
|
44
44
|
return out as T;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** A sheet's styles by name. (React Native 0.88's generated types have no `StyleSheet.NamedStyles`.) */
|
|
48
|
+
type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle };
|
|
49
|
+
|
|
47
50
|
/** StyleSheet.create, every style's corners smoothed (smoothCorners). */
|
|
48
|
-
export function createSheet<T extends
|
|
51
|
+
export function createSheet<T extends NamedStyles<T>>(styles: T): T {
|
|
49
52
|
const out = {} as Record<string, unknown>;
|
|
50
53
|
for (const k of Object.keys(styles)) out[k] = smoothCorners((styles as Record<string, Style>)[k]);
|
|
51
|
-
return StyleSheet.create(out as T
|
|
54
|
+
return StyleSheet.create(out as Record<string, ViewStyle | TextStyle | ImageStyle>) as unknown as T;
|
|
52
55
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { useWindowDimensions } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The editor's own area: what its chrome is laid out in. It's the window when the editor fills it, and less when the
|
|
6
|
+
host puts it in a pane beside something else (a split view, beside the app's sidebar). The phone or tablet layout,
|
|
7
|
+
the top row, the drawer and the menus all go by this, never by the window; the drawing already does, its canvas
|
|
8
|
+
sized by its own layout.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface EditorBox { width: number; height: number }
|
|
12
|
+
|
|
13
|
+
/** Set by CadEditor from its root's layout; until that first layout, the window stands in. */
|
|
14
|
+
export const EditorBoxContext = createContext<EditorBox | null>(null);
|
|
15
|
+
|
|
16
|
+
/** The editor's area (the window until it's been laid out). */
|
|
17
|
+
export function useEditorBox(): EditorBox {
|
|
18
|
+
const box = useContext(EditorBoxContext), win = useWindowDimensions();
|
|
19
|
+
return box ?? win;
|
|
20
|
+
}
|
package/src/react/host.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { notify } from './store';
|
|
|
12
12
|
/** Whether an editor is on screen (CadEditor sets it): readings that arrive after it closed are left to the host. */
|
|
13
13
|
export const editorHost = { open: false };
|
|
14
14
|
|
|
15
|
-
/** A measure connected or disconnected:
|
|
15
|
+
/** A measure connected or disconnected: enables the keypad's Laser key (no tray of readings shows: see applyReading). */
|
|
16
16
|
export function setLaserConnected(on: boolean): void {
|
|
17
17
|
laser.on = on;
|
|
18
18
|
laser.external = on;
|
package/src/react/motion.tsx
CHANGED
|
@@ -51,7 +51,7 @@ export const reducedMotion = (): boolean => reduceMotion;
|
|
|
51
51
|
* device, the wait before it moved). `mounted` says whether to draw it at all; the style hides it when it's away.
|
|
52
52
|
*/
|
|
53
53
|
export function usePopup(open: boolean, from: { x?: number; y?: number }, origin: string, keep = false): {
|
|
54
|
-
present: boolean; mounted: boolean; style: Animated.
|
|
54
|
+
present: boolean; mounted: boolean; style: Animated.WithAnimatedValue<ViewStyle>;
|
|
55
55
|
} {
|
|
56
56
|
const v = useRef(new Animated.Value(open ? 1 : 0)).current, [present, setPresent] = useState(open), been = useRef(open);
|
|
57
57
|
if (open) been.current = true;
|
|
@@ -77,9 +77,9 @@ export function usePopup(open: boolean, from: { x?: number; y?: number }, origin
|
|
|
77
77
|
{ translateY: v.interpolate({ inputRange: [0, 1], outputRange: [fy, 0] }) },
|
|
78
78
|
{ scale: v.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) },
|
|
79
79
|
],
|
|
80
|
-
}) as Animated.
|
|
80
|
+
}) as Animated.WithAnimatedValue<ViewStyle>, [v, fx, fy, origin]);
|
|
81
81
|
const shown = open || present;
|
|
82
|
-
const hidden = useMemo(() => [style, { display: 'none' as const }] as unknown as Animated.
|
|
82
|
+
const hidden = useMemo(() => [style, { display: 'none' as const }] as unknown as Animated.WithAnimatedValue<ViewStyle>, [style]);
|
|
83
83
|
return { present: shown, mounted: shown || (keep && been.current), style: shown ? style : hidden };
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -185,13 +185,13 @@ function SegOption({ label, on, onPress, onLayout, style, textStyle, onText, off
|
|
|
185
185
|
onLayout={e => onLayout(e.nativeEvent.layout)} style={[segCenter, style, { backgroundColor: 'transparent' }]}>
|
|
186
186
|
{NATIVE ? (
|
|
187
187
|
<>
|
|
188
|
-
<Animated.Text style={[textStyle, { color: offText, opacity: ink }]}>{label}</Animated.Text>
|
|
188
|
+
<Animated.Text numberOfLines={1} style={[textStyle, { color: offText, opacity: ink }]}>{label}</Animated.Text>
|
|
189
189
|
{/* (the chosen colour's label, exactly over the other) */}
|
|
190
190
|
<View pointerEvents="none" style={segOver}>
|
|
191
|
-
<Animated.Text accessible={false} style={[textStyle, { color: onText, opacity: t }]}>{label}</Animated.Text>
|
|
191
|
+
<Animated.Text numberOfLines={1} accessible={false} style={[textStyle, { color: onText, opacity: t }]}>{label}</Animated.Text>
|
|
192
192
|
</View>
|
|
193
193
|
</>
|
|
194
|
-
) : <Animated.Text style={[textStyle, { color: ink }]}>{label}</Animated.Text>}
|
|
194
|
+
) : <Animated.Text numberOfLines={1} style={[textStyle, { color: ink }]}>{label}</Animated.Text>}
|
|
195
195
|
</Tap>
|
|
196
196
|
);
|
|
197
197
|
}
|
|
@@ -17,6 +17,13 @@ const LOCKED_VIEWPORT = 'width=device-width, initial-scale=1, maximum-scale=1, u
|
|
|
17
17
|
|
|
18
18
|
export const PAGE_LOCK_ROOT = ROOT;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Put on a host's pane beside the editor (a split view). What starts in it is its own: the lock leaves its pinches and
|
|
22
|
+
* two-finger moves alone, and a key pressed with nothing focused after a press in it isn't the editor's (see
|
|
23
|
+
* PlanCanvas.web.tsx).
|
|
24
|
+
*/
|
|
25
|
+
export const BESIDE = 'data-cad-beside';
|
|
26
|
+
|
|
20
27
|
export function usePageLock(): void {
|
|
21
28
|
useEffect(() => {
|
|
22
29
|
if (typeof document === 'undefined') return;
|
|
@@ -31,9 +38,11 @@ export function usePageLock(): void {
|
|
|
31
38
|
if (!meta) { meta = document.createElement('meta'); meta.name = 'viewport'; document.head.appendChild(meta); }
|
|
32
39
|
const before = meta.content;
|
|
33
40
|
meta.content = LOCKED_VIEWPORT;
|
|
34
|
-
// Safari's pinch zoom (it ignores user-scalable=no): its gesture events, and two-finger touch moves
|
|
35
|
-
|
|
36
|
-
const
|
|
41
|
+
// Safari's pinch zoom (it ignores user-scalable=no): its gesture events, and two-finger touch moves — except in a
|
|
42
|
+
// host's pane beside the editor, which handles its own (BESIDE)
|
|
43
|
+
const own = (e: Event) => e.target instanceof Element && !!e.target.closest(`[${BESIDE}]`);
|
|
44
|
+
const stop = (e: Event) => { if (!own(e)) e.preventDefault(); };
|
|
45
|
+
const pinch = (e: TouchEvent) => { if (e.touches.length > 1 && !own(e)) e.preventDefault(); };
|
|
37
46
|
const opts: AddEventListenerOptions = { passive: false };
|
|
38
47
|
const gestures = ['gesturestart', 'gesturechange', 'gestureend'];
|
|
39
48
|
gestures.forEach(g => document.addEventListener(g, stop, opts));
|
package/src/react/ports.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { manualSave, noteEdits, saveDrawing } from './saving';
|
|
|
12
12
|
import { requestDraw, requestOverlayDraw } from '../render/view';
|
|
13
13
|
import { notify, ui } from './store';
|
|
14
14
|
import { setInteracting } from './Duck';
|
|
15
|
+
import { PAGE_LOCK_ROOT } from './pageLock';
|
|
15
16
|
|
|
16
17
|
/* The editor's ports for the React Native interface: each request updates the interface's state and re-renders. */
|
|
17
18
|
|
|
@@ -110,8 +111,12 @@ export const reactPorts: EditorPorts = {
|
|
|
110
111
|
requestDraw();
|
|
111
112
|
return;
|
|
112
113
|
}
|
|
113
|
-
// the keypad has no text field: let typed keys reach the editor, not the button that opened it (Enter would press it
|
|
114
|
-
|
|
114
|
+
// the keypad has no text field: let typed keys reach the editor, not the button that opened it (Enter would press it
|
|
115
|
+
// again). Only a button of the editor's: focus elsewhere on the page (a pane beside it) stays where it is.
|
|
116
|
+
if (typeof document !== 'undefined') {
|
|
117
|
+
const a = document.activeElement as HTMLElement | null;
|
|
118
|
+
if (a?.closest?.('#' + PAGE_LOCK_ROOT)) a.blur?.();
|
|
119
|
+
}
|
|
115
120
|
keypadStart(spec);
|
|
116
121
|
ui.keypad = true;
|
|
117
122
|
sheetOpened();
|