@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
package/src/react/BottomBar.tsx
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { memo, useRef, useState } from 'react';
|
|
1
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import { Animated, ScrollView, Text, View } from 'react-native';
|
|
3
3
|
import { ed } from '../app/state';
|
|
4
|
-
import { applyCtxAction, barItems, deleteSelection, plusAction, plusLabel, plusVisible } from '../editor/actions';
|
|
5
|
-
import { addLinks, LINK_PLUS_LABEL, linkPlusVisible, linkSizeVisible, pageLinkSize,
|
|
4
|
+
import { applyCtxAction, barItems, deleteSelection, iconAlone, plusAction, plusLabel, plusVisible } from '../editor/actions';
|
|
5
|
+
import { addLinks, LINK_PLUS_LABEL, linkPlusVisible, linkSizeVisible, openLinkTarget, opensWithChoice, pageLinkSize, pageTilePct, pctToStep, selectedOpenTarget, setTilePct, SIZE_STEPS, stepToPct } from '../editor/linkActions';
|
|
6
6
|
import { afterEdit } from '../app/edit';
|
|
7
|
-
import { LINK_SIDE_IN } from '../model/links';
|
|
8
|
-
import { fmtLen } from '../units';
|
|
9
7
|
import { bar3d, run3d } from '../editor/view3d';
|
|
10
8
|
import { is3d } from '../editor/view3dState';
|
|
11
9
|
import { OPACITY_MIN, OPACITY_STEP, ROTATE_MAX, ROTATE_STEP, setUnderlayOpacity, setUnderlayRotation } from '../editor/project';
|
|
@@ -15,48 +13,65 @@ import { Slider } from './Slider';
|
|
|
15
13
|
import { notify, ui, useEditorKey } from './store';
|
|
16
14
|
import { currentHint } from '../editor/status';
|
|
17
15
|
import { hovered } from './Buttons';
|
|
18
|
-
import { Icon } from './Icon';
|
|
16
|
+
import { Icon, RawSvg } from './Icon';
|
|
19
17
|
import { RichText } from './RichText';
|
|
20
18
|
import { C, FONT, shadow } from './theme';
|
|
21
|
-
import { Tap, usePopup } from './motion';
|
|
19
|
+
import { reducedMotion, Tap, usePopup } from './motion';
|
|
22
20
|
import { createSheet } from './corners';
|
|
21
|
+
import { NATIVE } from './easing';
|
|
22
|
+
import { barCut, barSpans, FADE, FADE_RAMP, fadeSvg, NUDGE, NUDGE_AFTER, NUDGE_HOLD } from './barScroll';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* The contextual bar: what fits the tool and selection, the yellow + to measure, and the trash on its own. It's as
|
|
26
26
|
* long as what's in it, centred on the screen — on a phone, unless that would put it under the trash, when it moves
|
|
27
27
|
* just clear of it (and scrolls only if even that leaves too little room). In Annotate on a phone (or a window too
|
|
28
28
|
* short for it under the rail) the + that adds the job's files and measurements has the trash's corner while nothing's
|
|
29
|
-
* selected: selecting anything puts the trash there instead, and letting go brings the + back.
|
|
29
|
+
* selected: selecting anything puts the trash there instead, and letting go brings the + back. A bar that scrolls
|
|
30
|
+
* says so (react/barScroll.ts).
|
|
30
31
|
*/
|
|
31
32
|
export function BottomBar({ phone }: { phone: boolean }) {
|
|
32
|
-
// what's in the bar, and the selected image's opacity and turn (its sliders)
|
|
33
|
+
// what's in the bar and what it's for, and the selected image's opacity and turn (its sliders)
|
|
33
34
|
useEditorKey(() => {
|
|
34
35
|
const u = ed.sel.length === 1 && ed.sel[0].type === 'underlay' ? imageById(ed.sel[0].id) : undefined;
|
|
35
|
-
return [buttonsKey(), plusVisible(), ed.sel.length, u ? u.opacity ?? 0.6 : '', u ? (u.rot ?? 0) + (u.locked ? 'L' : '') : '', linkPlusVisible(), ui.linkFab, linkSizeVisible(), pageLinkSize() ?? ''].join('|');
|
|
36
|
+
return [buttonsKey(), barFor(), plusVisible(), ed.sel.length, u ? u.opacity ?? 0.6 : '', u ? (u.rot ?? 0) + (u.locked ? 'L' : '') : '', linkPlusVisible(), ui.linkFab, linkSizeVisible(), pageLinkSize() ?? ''].join('|');
|
|
36
37
|
});
|
|
37
38
|
// (on a phone only buttons show: a bar that would hold just a label isn't shown at all. The 3D tab measures and
|
|
38
39
|
// deletes nothing: what's tapped there shows its sizes, and what can be done with it from there)
|
|
39
40
|
const items = shown(phone), plus = !is3d() && plusVisible(), trash = !is3d() && ed.sel.length > 0;
|
|
40
41
|
const fab = !trash && (phone || ui.linkFab) && linkPlusVisible(), corner = trash || fab;
|
|
41
|
-
//
|
|
42
|
-
|
|
42
|
+
// It scrolls only when its buttons can't all fit (a narrow phone): otherwise a drag on the bar would move it. Its
|
|
43
|
+
// room comes from its row's width, not the scroller's, which barCut makes shorter than that; where its buttons are
|
|
44
|
+
// comes from their widths, which a browser reports as they change (not as they move along).
|
|
45
|
+
const [row, setRow] = useState(0), [content, setContent] = useState(0), [widths, setWidths] = useState<readonly (number | undefined)[]>([]);
|
|
46
|
+
const room = row - (phone && corner ? TRASH_ROOM : 0) - 2 * PAD - (plus ? ADD + ADD_GAP : 0), over = row > 0 && content > room + 1;
|
|
47
|
+
const spans = over ? barSpans(items.map((it, i) => (it === 'sep' ? { w: SEP_W + 2 * SEP_M, button: false } : { w: widths[i], button: !('lbl' in it) })), GAP) : null;
|
|
48
|
+
const cut = !over ? undefined : spans ? barCut(spans, room) : room;
|
|
49
|
+
const onWidth = useCallback((i: number, w: number) => setWidths(p => (p[i] === w ? p : Object.assign(p.slice(), { [i]: w }))), []);
|
|
50
|
+
const scroll = useBarScroll(over, cut == null ? 0 : content - cut, barFor());
|
|
43
51
|
return (
|
|
44
52
|
<>
|
|
45
53
|
{(items.length > 0 || plus) && (
|
|
46
|
-
<View style={[s.bottom, phone ? s.phoneRow : s.centered]}>
|
|
54
|
+
<View style={[s.bottom, phone ? s.phoneRow : s.centered]} onLayout={e => setRow(e.nativeEvent.layout.width)}>
|
|
47
55
|
<ImagePopover />
|
|
48
56
|
<LinkSizePopover />
|
|
57
|
+
<OpenPopover />
|
|
49
58
|
{/* a phone's bar sits between two springs that start as wide as the trash takes and share what's left
|
|
50
59
|
equally, so it's centred; only the left one gives way (first, before the bar), so a long bar slides left
|
|
51
60
|
to clear the trash. (A minimum width on the right one alone did that in a browser, but a device's layout
|
|
52
61
|
shares the room out differently and left a short bar off-centre.) The springs let touches through. */}
|
|
53
62
|
{phone && <View pointerEvents="none" style={[s.springL, { flexBasis: corner ? TRASH_ROOM : 0 }]} />}
|
|
54
63
|
<View testID="ctxBar" style={s.ctx}>
|
|
55
|
-
{/*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
{/* the buttons and their fades, clipped to the round ends the buttons have: scrolled, they'd stick out past
|
|
65
|
+
the bar's own. (A native ScrollView grows to fill its row by default, which stretched the bar across a
|
|
66
|
+
tablet.) */}
|
|
67
|
+
<View style={s.clip}>
|
|
68
|
+
<Animated.ScrollView ref={scroll.ref} testID="ctxScroll" horizontal showsHorizontalScrollIndicator={false} scrollEnabled={over} bounces={over}
|
|
69
|
+
contentContainerStyle={s.items} style={[s.scroller, cut != null && { width: cut }]} onContentSizeChange={w => setContent(w)}
|
|
70
|
+
onScroll={scroll.onScroll} scrollEventThrottle={16} onScrollBeginDrag={scroll.stop}>
|
|
71
|
+
<BarButtons k={buttonsKey()} phone={phone} onWidth={onWidth} />
|
|
72
|
+
</Animated.ScrollView>
|
|
73
|
+
{cut != null && <Fades x={scroll.x} end={cut} max={content - cut} />}
|
|
74
|
+
</View>
|
|
60
75
|
{plus && (
|
|
61
76
|
<Tap accessibilityRole="button" accessibilityLabel={plusLabel()} testID="addbtn" onPress={plusAction} style={s.add}>
|
|
62
77
|
<Icon name="plus" color={C.dark} />
|
|
@@ -96,6 +111,61 @@ function buttonsKey(): string {
|
|
|
96
111
|
return JSON.stringify(its) + '|' + ui.imgPop;
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
/** What the bar is for: the selection, or what's tapped in 3D. A new one's bar starts at its start. */
|
|
115
|
+
const barFor = (): string => (is3d() ? JSON.stringify(bar3d()) : ed.sel.map(x => x.type + ':' + x.id).join(','));
|
|
116
|
+
|
|
117
|
+
/** The first bar that scrolls in a session (until the app is next started) is nudged; no other is. */
|
|
118
|
+
let nudged = false;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A bar's scrolling: how far it's scrolled (for its fades), back to its start for each new selection, and the nudge —
|
|
122
|
+
* the first bar of a session that doesn't fit slides NUDGE along and back, unless Reduce Motion is on. `max`: how far
|
|
123
|
+
* it scrolls.
|
|
124
|
+
*/
|
|
125
|
+
function useBarScroll(over: boolean, max: number, forKey: string) {
|
|
126
|
+
const ref = useRef<ComponentRef<typeof ScrollView>>(null), x = useRef(new Animated.Value(0)).current;
|
|
127
|
+
const back = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
128
|
+
const onScroll = useMemo(() => Animated.event([{ nativeEvent: { contentOffset: { x } } }], { useNativeDriver: NATIVE }), [x]);
|
|
129
|
+
// (and one that's come to fit has only its start)
|
|
130
|
+
useEffect(() => { ref.current?.scrollTo({ x: 0, animated: false }); x.setValue(0); }, [forKey, over, x]);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!over || nudged || max <= 0 || reducedMotion()) return;
|
|
133
|
+
// (once what's in it has stopped changing: its buttons' widths come in one by one)
|
|
134
|
+
const t = setTimeout(() => {
|
|
135
|
+
nudged = true;
|
|
136
|
+
ref.current?.scrollTo({ x: Math.min(NUDGE, max), animated: true });
|
|
137
|
+
back.current = setTimeout(() => ref.current?.scrollTo({ x: 0, animated: true }), NUDGE_HOLD);
|
|
138
|
+
}, NUDGE_AFTER);
|
|
139
|
+
return () => clearTimeout(t);
|
|
140
|
+
}, [over, max]);
|
|
141
|
+
// a finger dragging it mid-nudge keeps it where it's taken
|
|
142
|
+
const stop = useCallback(() => clearTimeout(back.current), []);
|
|
143
|
+
useEffect(() => stop, [stop]);
|
|
144
|
+
return { ref, x, onScroll, stop };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const FADE_START = fadeSvg('left'), FADE_END = fadeSvg('right');
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A scrolling bar's fades, over its scroller: at its start once it's scrolled from there, and at its end (`end` along
|
|
151
|
+
* it) while there's more past it. They come and go as it's scrolled, on the native driver; touches go through them.
|
|
152
|
+
*/
|
|
153
|
+
function Fades({ x, end, max }: { x: Animated.Value; end: number; max: number }) {
|
|
154
|
+
const [start, more] = useMemo(() => {
|
|
155
|
+
const ramp = Math.max(1, Math.min(FADE_RAMP, max));
|
|
156
|
+
return [
|
|
157
|
+
x.interpolate({ inputRange: [0, ramp], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
158
|
+
x.interpolate({ inputRange: [max - ramp, max], outputRange: [1, 0], extrapolate: 'clamp' }),
|
|
159
|
+
];
|
|
160
|
+
}, [x, max]);
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
<Animated.View testID="barFadeStart" style={[s.fade, { left: 0, opacity: start }]}><RawSvg xml={FADE_START} width={FADE} height={BTN_H} /></Animated.View>
|
|
164
|
+
<Animated.View testID="barFadeEnd" style={[s.fade, { left: end - FADE, opacity: more }]}><RawSvg xml={FADE_END} width={FADE} height={BTN_H} /></Animated.View>
|
|
165
|
+
</>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
99
169
|
/**
|
|
100
170
|
* A phone's bar: the buttons alone — the line saying what's selected and its sizes ("Cabinet 24" L × 34 1/2" H…") takes
|
|
101
171
|
* room a phone hasn't got, and the drawing shows the sizes anyway — with no divider left at an end or doubled up.
|
|
@@ -113,23 +183,30 @@ function buttonsOnly(items: ReturnType<typeof barItems>): ReturnType<typeof barI
|
|
|
113
183
|
|
|
114
184
|
/**
|
|
115
185
|
* The bar's buttons, drawn again only when what they show changes (`k`, see buttonsKey) — not when the bar itself
|
|
116
|
-
* is, as it measures whether they fit.
|
|
186
|
+
* is, as it measures whether they fit. Each says how wide it is (`onWidth`, by its place; a divider's is fixed).
|
|
117
187
|
*/
|
|
118
|
-
const BarButtons = memo(function BarButtons({ phone }: { k: string; phone: boolean }) {
|
|
188
|
+
const BarButtons = memo(function BarButtons({ phone, onWidth }: { k: string; phone: boolean; onWidth: (i: number, w: number) => void }) {
|
|
119
189
|
return (
|
|
120
190
|
<>
|
|
121
191
|
{shown(phone).map((it, i) => {
|
|
122
192
|
if (it === 'sep') return <View key={'s' + i} style={s.sep} />;
|
|
123
|
-
if ('lbl' in it) return <Text key={'l' + i} style={s.lbl}>{it.lbl}</Text>;
|
|
193
|
+
if ('lbl' in it) return <Text key={'l' + i} style={s.lbl} onLayout={e => onWidth(i, e.nativeEvent.layout.width)}>{it.lbl}</Text>;
|
|
124
194
|
const fg = it.on ? C.dark : it.locked ? C.red : '#fff';
|
|
125
195
|
return (
|
|
126
|
-
<Tap key={it.act + i} accessibilityRole="button" accessibilityLabel={it.label}
|
|
196
|
+
<Tap key={it.act + i} onLayout={e => onWidth(i, e.nativeEvent.layout.width)} accessibilityRole="button" accessibilityLabel={it.label}
|
|
127
197
|
accessibilityState={it.on != null || it.locked != null ? { selected: !!(it.on || it.locked) } : undefined}
|
|
128
|
-
onPress={e => {
|
|
129
|
-
|
|
198
|
+
onPress={e => {
|
|
199
|
+
if (is3d() && run3d(it.act)) return;
|
|
200
|
+
// (Open asks how — here, or beside the drawing — where the host can show it there and no pane's open yet)
|
|
201
|
+
const t = isOpen(it.act) ? selectedOpenTarget() : null;
|
|
202
|
+
if (t && opensWithChoice(t)) { ui.imgPop = ui.imgPop === 'open' ? null : 'open'; notify(); return; }
|
|
203
|
+
applyCtxAction(it.act, e.currentTarget);
|
|
204
|
+
}}
|
|
205
|
+
style={st => [s.cb, iconAlone(it) && s.lead, it.on || (it.act === 'imgOpacity' && ui.imgPop === 'opacity') || (it.act === 'imgRotate' && ui.imgPop === 'rotate') || (it.act === 'linkSize' && ui.imgPop === 'tilesize') || (isOpen(it.act) && ui.imgPop === 'open') ? { backgroundColor: 'rgba(255,255,255,0.14)' } : null,
|
|
130
206
|
it.on ? { backgroundColor: '#fff' } : hovered(st) && { backgroundColor: 'rgba(255,255,255,0.08)' }]}>
|
|
131
207
|
<Icon name={it.ic} color={fg} />
|
|
132
|
-
|
|
208
|
+
{/* (the bar's lead — Lock, or Open — and Lock wherever it is: its icon alone, its name said, not shown) */}
|
|
209
|
+
{!iconAlone(it) && <Text style={[s.cbText, { color: fg }]}>{it.label}</Text>}
|
|
133
210
|
</Tap>
|
|
134
211
|
);
|
|
135
212
|
})}
|
|
@@ -137,6 +214,41 @@ const BarButtons = memo(function BarButtons({ phone }: { k: string; phone: boole
|
|
|
137
214
|
);
|
|
138
215
|
});
|
|
139
216
|
|
|
217
|
+
/** The bar's Open: a tile's or expanded photo's, or a linked dimension's. */
|
|
218
|
+
const isOpen = (act: string): boolean => act === 'linkOpen' || act === 'dimLinkOpen';
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Open's question, just above the bar (the user's call, 2026-10-30): Open, the host's own way as before, or Open side
|
|
222
|
+
* by side, in a pane beside the drawing. Asked only where the host can show it there and no pane is open yet
|
|
223
|
+
* (opensWithChoice); a press on the drawing closes it, as it does the bar's sliders.
|
|
224
|
+
*/
|
|
225
|
+
function OpenPopover() {
|
|
226
|
+
const t = ui.imgPop === 'open' ? selectedOpenTarget() : null, open = !!t && opensWithChoice(t);
|
|
227
|
+
const pop = usePopup(open, { y: 8 }, 'bottom center');
|
|
228
|
+
if (!pop.present) return null;
|
|
229
|
+
const go = (how: 'here' | 'beside') => {
|
|
230
|
+
const target = selectedOpenTarget();
|
|
231
|
+
ui.imgPop = null;
|
|
232
|
+
notify();
|
|
233
|
+
if (target) openLinkTarget(target, how);
|
|
234
|
+
};
|
|
235
|
+
return (
|
|
236
|
+
<View style={s.popWrap} pointerEvents="box-none">
|
|
237
|
+
<Animated.View style={[s.pop, s.choices, pop.style]} pointerEvents={open ? 'auto' : 'none'} testID={open ? 'openPop' : undefined}>
|
|
238
|
+
<Tap feel="dim" accessibilityRole="button" accessibilityLabel="Open" testID="openHere" onPress={() => go('here')}
|
|
239
|
+
style={st => [s.choice, hovered(st) && s.choiceHover]}>
|
|
240
|
+
<Text style={s.choiceText}>Open</Text>
|
|
241
|
+
</Tap>
|
|
242
|
+
<View style={s.sep} />
|
|
243
|
+
<Tap feel="dim" accessibilityRole="button" accessibilityLabel="Open side by side" testID="openBeside" onPress={() => go('beside')}
|
|
244
|
+
style={st => [s.choice, hovered(st) && s.choiceHover]}>
|
|
245
|
+
<Text style={s.choiceText}>Open side by side</Text>
|
|
246
|
+
</Tap>
|
|
247
|
+
</Animated.View>
|
|
248
|
+
</View>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
140
252
|
/** Degrees, as the rotation slider shows them: whole, or to the half degree. */
|
|
141
253
|
const fmtDeg = (v: number): string => (Math.round(v * 2) / 2).toString().replace('-', '−') + '°';
|
|
142
254
|
|
|
@@ -198,36 +310,32 @@ function ImagePopover() {
|
|
|
198
310
|
);
|
|
199
311
|
}
|
|
200
312
|
|
|
201
|
-
/** A tile size from the slider, as it's kept: to the quarter inch. */
|
|
202
|
-
const tileSize = (step: number): number => Math.round(stepToSize(step) * 4) / 4;
|
|
203
|
-
|
|
204
313
|
/**
|
|
205
314
|
* The tiles' size, just above the bar (Size in it opens and closes it): one size for every tile on the page, on the drawing
|
|
206
|
-
* (they zoom with the plan and print at it)
|
|
207
|
-
*
|
|
315
|
+
* (they zoom with the plan and print at it), said in percent of the size they came in at — never feet or metres (the
|
|
316
|
+
* user's call, 2026-11-03: see editor/linkActions.ts). Kept quick on a device, as ImagePopover is: the thumb and the
|
|
317
|
+
* number follow the finger from here, the tiles are sized at most once a frame, and the rest of the editor hears of it
|
|
318
|
+
* as it's let go.
|
|
208
319
|
*/
|
|
209
320
|
function LinkSizePopover() {
|
|
210
321
|
const [live, setLive] = useState<number | null>(null);
|
|
211
|
-
// the number's width as it was laid out, held while a drag goes on (the track ends at it: as long as the number is,
|
|
212
|
-
// no longer — but never narrower mid-drag, which would move the track under the finger)
|
|
213
|
-
const [numW, setNumW] = useState(0);
|
|
214
322
|
const pending = useRef<{ v: number; frame: number } | null>(null);
|
|
215
323
|
// (opened from the bar's Size, as an image's sliders are; a press on the drawing closes it)
|
|
216
|
-
const
|
|
324
|
+
const pct = ui.imgPop === 'tilesize' && linkSizeVisible() ? pageTilePct() : null, open = pct != null;
|
|
217
325
|
// (as it goes it keeps showing the size it had)
|
|
218
|
-
const last = useRef(
|
|
219
|
-
if (
|
|
326
|
+
const last = useRef(pct ?? 100), pop = usePopup(open, { y: 8 }, 'bottom center');
|
|
327
|
+
if (pct != null) last.current = pct;
|
|
220
328
|
if (!pop.present) return null;
|
|
221
|
-
const value = live ??
|
|
329
|
+
const value = live ?? pctToStep(last.current);
|
|
222
330
|
const change = (v: number) => {
|
|
223
331
|
setLive(v);
|
|
224
332
|
const p = pending.current;
|
|
225
333
|
if (p) { p.v = v; return; }
|
|
226
|
-
pending.current = { v, frame: requestAnimationFrame(() => { const q = pending.current; pending.current = null; if (q)
|
|
334
|
+
pending.current = { v, frame: requestAnimationFrame(() => { const q = pending.current; pending.current = null; if (q) setTilePct(stepToPct(q.v)); }) };
|
|
227
335
|
};
|
|
228
336
|
const end = () => {
|
|
229
337
|
const p = pending.current;
|
|
230
|
-
if (p) { cancelAnimationFrame(p.frame); pending.current = null;
|
|
338
|
+
if (p) { cancelAnimationFrame(p.frame); pending.current = null; setTilePct(stepToPct(p.v)); }
|
|
231
339
|
setLive(null);
|
|
232
340
|
afterEdit();
|
|
233
341
|
notify();
|
|
@@ -237,11 +345,11 @@ function LinkSizePopover() {
|
|
|
237
345
|
<Animated.View style={[s.pop, pop.style]} pointerEvents={open ? 'auto' : 'none'} testID={open ? 'tileSizePop' : undefined}>
|
|
238
346
|
<Text style={s.popLabel}>Tile size</Text>
|
|
239
347
|
<View style={{ flex: 1 }}>
|
|
240
|
-
<Slider label="Tile size" testID="tileSizeSlider" value={value} min={0} max={SIZE_STEPS} step={1} format={v =>
|
|
348
|
+
<Slider label="Tile size" testID="tileSizeSlider" value={value} min={0} max={SIZE_STEPS} step={1} format={v => stepToPct(v) + '%'}
|
|
241
349
|
onStart={pushHistory} onChange={change} onEnd={end} />
|
|
242
350
|
</View>
|
|
243
|
-
|
|
244
|
-
|
|
351
|
+
{/* (a fixed width, as the image sliders' numbers: the track stays one length whatever the number — "70%" or "1000%" — the user's call, 2026-11-03) */}
|
|
352
|
+
<Text style={s.popValue}>{(live != null ? stepToPct(live) : last.current) + '%'}</Text>
|
|
245
353
|
</Animated.View>
|
|
246
354
|
</View>
|
|
247
355
|
);
|
|
@@ -259,6 +367,12 @@ export function SpokenHint() {
|
|
|
259
367
|
|
|
260
368
|
/** What the trash (or Annotate's +, in its place) takes at the right of a phone's bar row: its 60 pt, and the 12 between them. */
|
|
261
369
|
const TRASH_ROOM = 72;
|
|
370
|
+
/**
|
|
371
|
+
* The bar's measures, which its room is worked out from as well as drawn with: its height and its buttons', its padding
|
|
372
|
+
* at each end, the yellow + and the gap before it, a divider and the margin at each side of it, and the gap between
|
|
373
|
+
* its buttons.
|
|
374
|
+
*/
|
|
375
|
+
const BAR_H = 60, BTN_H = 54, PAD = 6, ADD = 48, ADD_GAP = 4, SEP_W = 1, SEP_M = 4, GAP = 2;
|
|
262
376
|
|
|
263
377
|
const s = createSheet({
|
|
264
378
|
bottom: { pointerEvents: 'box-none', position: 'absolute', bottom: 14, zIndex: 24, flexDirection: 'row', alignItems: 'center' },
|
|
@@ -266,28 +380,39 @@ const s = createSheet({
|
|
|
266
380
|
phoneRow: { left: 12, right: 12 },
|
|
267
381
|
springL: { flexGrow: 1, flexShrink: 1000 },
|
|
268
382
|
springR: { flexGrow: 1, flexShrink: 0 },
|
|
269
|
-
//
|
|
383
|
+
// 60 tall (Figma's bars, 2528:101128 — the user's call, 2026-10-27: "60 px in height - not 66"): its 54 buttons 3 in
|
|
384
|
+
// from its top and bottom, 6 from its ends, its ends round
|
|
270
385
|
// (it gives way when its buttons don't fit — React Native doesn't shrink a view unless told — and then scrolls)
|
|
271
|
-
ctx: { flexDirection: 'row', alignItems: 'center', backgroundColor: C.g2, borderRadius:
|
|
272
|
-
|
|
273
|
-
|
|
386
|
+
ctx: { flexDirection: 'row', alignItems: 'center', backgroundColor: C.g2, borderRadius: BAR_H / 2, minHeight: BAR_H, paddingVertical: (BAR_H - BTN_H) / 2, paddingHorizontal: PAD, maxWidth: '100%', flexShrink: 1, ...shadow },
|
|
387
|
+
clip: { flexGrow: 0, flexShrink: 1, borderRadius: BTN_H / 2, overflow: 'hidden' },
|
|
388
|
+
scroller: { flexGrow: 0, flexShrink: 1 },
|
|
389
|
+
items: { flexDirection: 'row', alignItems: 'center', gap: GAP },
|
|
390
|
+
// a scrolling bar's fade, over an end of its scroller
|
|
391
|
+
fade: { position: 'absolute', top: 0, bottom: 0, width: FADE, pointerEvents: 'none' },
|
|
392
|
+
cb: { minWidth: 58, height: BTN_H, borderRadius: BTN_H / 2, alignItems: 'center', justifyContent: 'center', gap: 3, paddingHorizontal: 8 },
|
|
393
|
+
// the lead: a 48 round button, its icon alone
|
|
394
|
+
lead: { minWidth: 48, width: 48, height: 48, borderRadius: 24, paddingHorizontal: 0 },
|
|
274
395
|
cbText: { fontFamily: FONT, fontSize: 10.5, fontWeight: '500' },
|
|
275
|
-
sep: { width:
|
|
396
|
+
sep: { width: SEP_W, height: 32, backgroundColor: C.g3, marginHorizontal: SEP_M },
|
|
276
397
|
lbl: { fontFamily: FONT, fontSize: 12, color: C.lg1, paddingHorizontal: 10 },
|
|
277
398
|
// the slider sits just above the bar, centred on it
|
|
278
|
-
popWrap: { position: 'absolute', left: 0, right: 0, bottom:
|
|
399
|
+
popWrap: { position: 'absolute', left: 0, right: 0, bottom: 70, alignItems: 'center' },
|
|
279
400
|
pop: { width: 400, maxWidth: '100%', flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: C.g2, borderRadius: 999, paddingHorizontal: 16, paddingVertical: 4, ...shadow },
|
|
280
401
|
popLabel: { fontFamily: FONT, fontSize: 12.5, fontWeight: '600', color: C.lg2 },
|
|
281
402
|
popValue: { fontFamily: FONT, fontSize: 12.5, fontWeight: '600', color: '#fff', width: 48, textAlign: 'right' },
|
|
282
|
-
// (a length: as long as it is, the track running up to it)
|
|
283
|
-
sizeValue: { width: 'auto', minWidth: 0 },
|
|
284
403
|
// back to upright: a pill at the end of the rotation slider, concentric with it — the same 4 above, below and beside
|
|
285
404
|
// it, and the slider's radius (22, half its 44) less that 4 for its own
|
|
286
405
|
popReset: { paddingRight: 4 },
|
|
287
406
|
reset: { height: 36, paddingHorizontal: 16, borderRadius: 18, backgroundColor: C.g3, justifyContent: 'center' },
|
|
288
407
|
resetText: { fontFamily: FONT, fontSize: 12.5, fontWeight: '600', color: '#fff' },
|
|
289
|
-
//
|
|
290
|
-
|
|
408
|
+
// Open's question: its two answers as the bar's own buttons are (white on the bar's grey, a divider between),
|
|
409
|
+
// concentric with it (4 in all round)
|
|
410
|
+
choices: { width: 'auto', paddingHorizontal: 4, gap: 2 },
|
|
411
|
+
choice: { height: 40, paddingHorizontal: 16, borderRadius: 20, justifyContent: 'center' },
|
|
412
|
+
choiceHover: { backgroundColor: 'rgba(255,255,255,0.08)' },
|
|
413
|
+
choiceText: { fontFamily: FONT, fontSize: 14, fontWeight: '600', color: '#fff' },
|
|
414
|
+
// (6 in from the bar's end, top and bottom: concentric with it)
|
|
415
|
+
add: { width: ADD, height: ADD, borderRadius: ADD / 2, backgroundColor: C.yellow, alignItems: 'center', justifyContent: 'center', marginLeft: ADD_GAP },
|
|
291
416
|
// BOLDR's delete FAB (the annotation editors' DeleteFab): a 60 pt solid red circle 12 from the right and 14 from the
|
|
292
417
|
// bottom, level with the bar beside it, on every screen size
|
|
293
418
|
trash: {
|
package/src/react/CadEditor.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import { useColorScheme, useWindowDimensions, View } from 'react-native';
|
|
3
3
|
import { analyze } from '../app/edit';
|
|
4
4
|
import { ed } from '../app/state';
|
|
@@ -19,7 +19,6 @@ import { confirmCancel, confirmOpen } from '../editor/confirm';
|
|
|
19
19
|
import { Drawer } from './Drawer';
|
|
20
20
|
import { Duck } from './Duck';
|
|
21
21
|
import { elevOpen } from '../editor/elevation';
|
|
22
|
-
import { LaserTray } from './LaserTray';
|
|
23
22
|
import { MarkupPanel } from './MarkupPanel';
|
|
24
23
|
import { Menu, NoteInput } from './Popovers';
|
|
25
24
|
import { KeypadSheet } from './KeypadSheet';
|
|
@@ -29,7 +28,7 @@ import { Empty3D } from './Empty3D';
|
|
|
29
28
|
import { is3d } from '../editor/view3dState';
|
|
30
29
|
import { reactPorts, setFileSaver } from './ports';
|
|
31
30
|
import { editorHost } from './host';
|
|
32
|
-
import { checkChanges, startManualSave, stopManualSave, type CadSaveState } from './saving';
|
|
31
|
+
import { announceSaveState, checkChanges, startManualSave, stopManualSave, type CadSaveState } from './saving';
|
|
33
32
|
import { PAGE_LOCK_ROOT, usePageLock } from './pageLock';
|
|
34
33
|
import { setSkiaFonts } from '../skia/fonts';
|
|
35
34
|
import type { SkiaFonts } from '../skia/SkiaDrawCtx';
|
|
@@ -47,6 +46,7 @@ import { setLinksHost, type CadLinks } from '../editor/links';
|
|
|
47
46
|
import { ShapeMenu } from './ShapeMenu';
|
|
48
47
|
import { PHONE_MAX } from './theme';
|
|
49
48
|
import { createSheet } from './corners';
|
|
49
|
+
import { EditorBoxContext, type EditorBox } from './editorBox';
|
|
50
50
|
|
|
51
51
|
export interface CadEditorProps {
|
|
52
52
|
/** Back to the host (otherwise the back button opens the project's pages) */
|
|
@@ -154,6 +154,8 @@ export function CadEditor({ onBack, headerRight, pickImage = pickImageInBrowser,
|
|
|
154
154
|
clearTimeout(closing.current);
|
|
155
155
|
setPorts(reactPorts);
|
|
156
156
|
editorHost.open = true;
|
|
157
|
+
// saving manually: the state it opens in, now it's mounted (not as it rendered: see startManualSave)
|
|
158
|
+
announceSaveState();
|
|
157
159
|
return () => {
|
|
158
160
|
// saving as they draw: the last edits and the preview go now. Saving manually: unsaved edits are dropped.
|
|
159
161
|
if (!manualSave) {
|
|
@@ -172,17 +174,21 @@ export function CadEditor({ onBack, headerRight, pickImage = pickImageInBrowser,
|
|
|
172
174
|
// the canvases read their colors when they draw: set them before this render's children draw
|
|
173
175
|
setCanvasTheme(theme);
|
|
174
176
|
useEffect(() => { requestDraw(); }, [theme]);
|
|
175
|
-
|
|
177
|
+
// the chrome is laid out in the editor's own area, not the window: the host can put it in a pane (editorBox.ts)
|
|
178
|
+
const win = useWindowDimensions(), [laid, setLaid] = useState<EditorBox | null>(null);
|
|
179
|
+
const box = useMemo(() => laid ?? { width: win.width, height: win.height }, [laid, win.width, win.height]);
|
|
180
|
+
const phone = box.width < PHONE_MAX, wide = box.width >= 900;
|
|
176
181
|
// (asked about once the drawing's loaded: what it links to)
|
|
177
182
|
useEffect(() => {
|
|
178
183
|
setLinksHost(links ?? null);
|
|
179
184
|
return () => setLinksHost(null);
|
|
180
185
|
}, [links]);
|
|
181
|
-
// (a phone asks with the host's keypad, when it has one)
|
|
186
|
+
// (a phone asks with the host's keypad, when it has one: the device is a phone, whatever the editor's area)
|
|
187
|
+
const phoneDevice = win.width < PHONE_MAX;
|
|
182
188
|
useEffect(() => {
|
|
183
|
-
setHostKeypad(
|
|
189
|
+
setHostKeypad(phoneDevice && keypad ? keypad : null);
|
|
184
190
|
return () => setHostKeypad(null);
|
|
185
|
-
}, [
|
|
191
|
+
}, [phoneDevice, keypad]);
|
|
186
192
|
// (a modeless keypad leaves the drawing live: no backdrop. On a tablet or a computer the drawing stays in reach
|
|
187
193
|
// around a sheet too — it pans and zooms, and a tap on it (or the chrome over it) closes the sheet: see
|
|
188
194
|
// editor/gestures.ts sheetUp. A phone's sheet covers most of it: there a backdrop closes it.)
|
|
@@ -198,7 +204,11 @@ export function CadEditor({ onBack, headerRight, pickImage = pickImageInBrowser,
|
|
|
198
204
|
return () => clearTimeout(t);
|
|
199
205
|
}, [threeD, kept3d]);
|
|
200
206
|
return (
|
|
201
|
-
<
|
|
207
|
+
<EditorBoxContext.Provider value={box}>
|
|
208
|
+
<View nativeID={PAGE_LOCK_ROOT} style={[s.root, { backgroundColor: CANVAS_THEMES[theme].bg }]} onLayout={e => {
|
|
209
|
+
const { width, height } = e.nativeEvent.layout;
|
|
210
|
+
setLaid(b => (b && Math.abs(b.width - width) < 0.5 && Math.abs(b.height - height) < 0.5 ? b : { width, height }));
|
|
211
|
+
}}>
|
|
202
212
|
<PlanCanvas />
|
|
203
213
|
{/* the 3D tab: over the plan, which stays as it was underneath */}
|
|
204
214
|
{(threeD || kept3d) && <View3D theme={theme} active={threeD} />}
|
|
@@ -212,7 +222,6 @@ export function CadEditor({ onBack, headerRight, pickImage = pickImageInBrowser,
|
|
|
212
222
|
{/* (none in 3D: it's a view, its camera moved with the fingers) */}
|
|
213
223
|
{!readOnly && !threeD && <Rail phone={phone} onAddImage={() => { pickImage().then(img => { if (img) placeUnderlay(img.dataUrl, img.w, img.h); }); }} />}
|
|
214
224
|
{!readOnly && <BottomBar phone={phone} />}
|
|
215
|
-
{!threeD && <LaserTray phone={phone} />}
|
|
216
225
|
{!threeD && <MarkupPanel phone={phone} />}
|
|
217
226
|
</Duck>
|
|
218
227
|
{!readOnly && !elevOpen() && !threeD && <ShapeMenu />}
|
|
@@ -226,6 +235,7 @@ export function CadEditor({ onBack, headerRight, pickImage = pickImageInBrowser,
|
|
|
226
235
|
<ConflictSheet wide={wide} open={!!ui.conflict && !ui.keypad} />
|
|
227
236
|
<ConfirmSheet wide={wide} />
|
|
228
237
|
</View>
|
|
238
|
+
</EditorBoxContext.Provider>
|
|
229
239
|
);
|
|
230
240
|
}
|
|
231
241
|
|
package/src/react/Drawer.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
Animated, Easing, Keyboard, PanResponder, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput,
|
|
3
|
+
Animated, Easing, Keyboard, PanResponder, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View,
|
|
4
4
|
} from 'react-native';
|
|
5
5
|
import {
|
|
6
6
|
addPage, deletePage, deletePending, duplicatePage, focusSpace, renamePage, renameSpace, spaceSchedule, switchPage,
|
|
7
7
|
} from '../editor/project';
|
|
8
8
|
import { ed } from '../app/state';
|
|
9
9
|
import type { DrawerTab } from '../editor/ports';
|
|
10
|
+
import { noScaleYet } from '../editor/scaleGate';
|
|
10
11
|
import { page, store } from '../model/store';
|
|
11
12
|
import { fmtArea, fmtLen } from '../units';
|
|
12
13
|
import { Icon, RawSvg } from './Icon';
|
|
@@ -17,6 +18,7 @@ import { C, FONT } from './theme';
|
|
|
17
18
|
import { EASE, Segmented, Tap } from './motion';
|
|
18
19
|
import { NATIVE } from './easing';
|
|
19
20
|
import { createSheet } from './corners';
|
|
21
|
+
import { useEditorBox } from './editorBox';
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* The Pages frame's sizes, scaled to the panel: Figma draws it 515 wide, the panel's content is about 370 (K). Its
|
|
@@ -43,7 +45,7 @@ const CLOSE_DRAG = 80, CLOSE_FLICK = 0.5;
|
|
|
43
45
|
* down or a tap outside). Either way it slides back out when closed.
|
|
44
46
|
*/
|
|
45
47
|
export function Drawer({ phone = false }: { phone?: boolean }) {
|
|
46
|
-
const version = useEditorVersion(), { width, height } =
|
|
48
|
+
const version = useEditorVersion(), { width, height } = useEditorBox(), w = Math.min(400, width);
|
|
47
49
|
const open = !!ui.drawer;
|
|
48
50
|
// what it showed last stays while it slides away
|
|
49
51
|
const last = useRef<DrawerTab>('pages');
|
|
@@ -60,7 +62,7 @@ export function Drawer({ phone = false }: { phone?: boolean }) {
|
|
|
60
62
|
const slide = useMemo(() => x.interpolate({ inputRange: [0, 1], outputRange: [0, (phone ? travel ?? height : w) + 40] }), [x, w, height, phone, travel]);
|
|
61
63
|
const fade = useMemo(() => x.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }), [x]);
|
|
62
64
|
// the phone's sheet: how far it's dragged down, when it opened, and how far the keyboard lifts it
|
|
63
|
-
const drag = useRef(new Animated.Value(0)).current, openedAt = useRef(0), sheet = useRef<View
|
|
65
|
+
const drag = useRef(new Animated.Value(0)).current, openedAt = useRef(0), sheet = useRef<ComponentRef<typeof View>>(null), [lift, setLift] = useState(0);
|
|
64
66
|
const dropped = useMemo(() => Animated.add(slide, drag), [slide, drag]);
|
|
65
67
|
useEffect(() => {
|
|
66
68
|
if (open) { setPresent(true); openedAt.current = Date.now(); }
|
|
@@ -151,7 +153,7 @@ export function Drawer({ phone = false }: { phone?: boolean }) {
|
|
|
151
153
|
function NameField({ value, onCommit, style, label, autoFocus, onFocus }: {
|
|
152
154
|
value: string; onCommit: (v: string) => void; style?: object; label: string; autoFocus?: boolean; onFocus?: () => void;
|
|
153
155
|
}) {
|
|
154
|
-
const [text, setText] = useState(value), editing = useRef(false), ref = useRef<TextInput
|
|
156
|
+
const [text, setText] = useState(value), editing = useRef(false), ref = useRef<ComponentRef<typeof TextInput>>(null);
|
|
155
157
|
useEffect(() => { if (!editing.current) setText(value); }, [value]);
|
|
156
158
|
useEffect(() => { if (autoFocus) setTimeout(() => ref.current?.focus(), 320); }, [autoFocus]);
|
|
157
159
|
return (
|
|
@@ -215,26 +217,29 @@ function Pages({ version, thumbs }: { version: number; thumbs: boolean }) {
|
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
function Spaces() {
|
|
218
|
-
const sched = spaceSchedule();
|
|
220
|
+
const sched = spaceSchedule(), sketchOnly = noScaleYet();
|
|
219
221
|
if (!sched.rows.length) return <Text style={s.empty}>No spaces on this page yet. Close a loop of walls or lines, or pick the Space tool and tap inside a closed shape.</Text>;
|
|
220
222
|
return (
|
|
221
223
|
<>
|
|
222
224
|
{sched.rows.map(r => (
|
|
223
225
|
<View key={r.id} style={s.roomRow}>
|
|
224
226
|
<NameField label="Space name" value={r.name} onCommit={v => renameSpace(r.id, v)} autoFocus={ui.renameRoom === r.id} onFocus={() => focusSpace(r.id)} style={{ fontSize: 14 }} />
|
|
225
|
-
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
{!sketchOnly && (
|
|
228
|
+
<View style={s.nums}>
|
|
229
|
+
<Text style={s.num}><Text style={s.numB}>{fmtArea(r.area)}</Text> floor</Text>
|
|
230
|
+
<Text style={s.num}><Text style={s.numB}>{fmtLen(r.per)}</Text> perimeter</Text>
|
|
231
|
+
</View>
|
|
232
|
+
)}
|
|
229
233
|
</View>
|
|
230
234
|
))}
|
|
231
|
-
|
|
235
|
+
{/* (a sketch's sizes are placeholders until its first length: no areas until then) */}
|
|
236
|
+
{sketchOnly ? <Text style={s.empty}>Areas show once the drawing has a length. Select a line and tap + to enter one.</Text> : <View style={s.row}>
|
|
232
237
|
<View style={{ flex: 1 }}>
|
|
233
238
|
<Text style={[s.rowText, { fontWeight: '700' }]}>Total</Text>
|
|
234
239
|
<Text style={s.rowSub}>Measured to inside wall faces</Text>
|
|
235
240
|
</View>
|
|
236
241
|
<Text style={[s.rowText, { fontWeight: '700' }]}>{fmtArea(sched.total)}</Text>
|
|
237
|
-
</View>
|
|
242
|
+
</View>}
|
|
238
243
|
</>
|
|
239
244
|
);
|
|
240
245
|
}
|
package/src/react/Duck.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useEffect, useRef, useSyncExternalStore, type ReactNode } from 'react';
|
|
2
|
-
import { Animated, Platform, StyleSheet,
|
|
1
|
+
import { useEffect, useRef, useSyncExternalStore, type ComponentRef, type ReactNode } from 'react';
|
|
2
|
+
import { Animated, Platform, StyleSheet, View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
On a phone the floating chrome — the top bar, the tabs, the rail, the bottom bar and trash, the trays — steps aside
|
|
@@ -31,7 +31,7 @@ const OUT_MS = 120, IN_MS = 200;
|
|
|
31
31
|
*/
|
|
32
32
|
export function Duck({ active, held = false, children }: { active: boolean; held?: boolean; children: ReactNode }) {
|
|
33
33
|
const away = useSyncExternalStore(subscribe, isMoving) && active, off = away || held;
|
|
34
|
-
const opacity = useRef(new Animated.Value(1)).current, ref = useRef<View
|
|
34
|
+
const opacity = useRef(new Animated.Value(1)).current, ref = useRef<ComponentRef<typeof View>>(null);
|
|
35
35
|
useEffect(() => {
|
|
36
36
|
Animated.timing(opacity, { toValue: away ? 0 : 1, duration: away ? OUT_MS : IN_MS, useNativeDriver: Platform.OS !== 'web' }).start();
|
|
37
37
|
}, [away, opacity]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState, type ReactNode } from 'react';
|
|
1
|
+
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState, type ComponentRef, type ReactNode } from 'react';
|
|
2
2
|
import { FlatList, ScrollView, Text, View } from 'react-native';
|
|
3
3
|
import {
|
|
4
4
|
hostPicksMeasurements, measurementFits, offersJobMeasurements, pickJobMeasurement, watchJobMeasurements, type MeasurementKind,
|
|
@@ -164,7 +164,7 @@ function Keys({ spec, wide, selectFromList }: { spec: NonNullable<ReturnType<typ
|
|
|
164
164
|
* It's the host's list when it draws one (measurementList), and the editor's grid when it doesn't.
|
|
165
165
|
*/
|
|
166
166
|
const Pager = forwardRef<PagerHandle, { kind: MeasurementKind; pick: (t: LinkTarget) => void; measurementList?: MeasurementList; children: ReactNode }>(function Pager({ kind, pick, measurementList, children }, ref) {
|
|
167
|
-
const scroll = useRef<ScrollView
|
|
167
|
+
const scroll = useRef<ComponentRef<typeof ScrollView>>(null);
|
|
168
168
|
const [w, setW] = useState(0), [h, setH] = useState(0), [page, setPage] = useState(0), [listed, setListed] = useState(false);
|
|
169
169
|
const show = (p: number) => { if (p) setListed(true); scroll.current?.scrollTo({ x: p * w, animated: true }); };
|
|
170
170
|
useImperativeHandle(ref, () => ({ show }));
|
|
@@ -124,7 +124,7 @@ export function MarkupPanel({ phone }: { phone: boolean }) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
const s = createSheet({
|
|
127
|
-
wrap: { position: 'absolute', left: 76, top: 112, bottom:
|
|
127
|
+
wrap: { position: 'absolute', left: 76, top: 112, bottom: 78, zIndex: 46, alignItems: 'flex-start' },
|
|
128
128
|
panel: { width: 264, maxHeight: '100%', backgroundColor: C.g1, borderRadius: 20, borderWidth: 1, borderColor: C.g2, ...shadow },
|
|
129
129
|
body: { padding: 12, gap: 8 },
|
|
130
130
|
section: { gap: 8 },
|