@reekon-tools/boldr-cad 1.3.0 → 1.5.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 +21 -4
- 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 +3 -5
- 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 +149 -169
- 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 +2 -2
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/linkActions.d.ts +22 -3
- package/dist/editor/linkActions.js +95 -13
- package/dist/editor/linkActions.js.map +1 -1
- package/dist/editor/mode.js +4 -3
- 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 +9 -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 -1
- package/dist/index.js +1 -1
- package/dist/index.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/history.js +4 -1
- package/dist/model/history.js.map +1 -1
- package/dist/model/links.d.ts +4 -1
- package/dist/model/links.js +9 -2
- 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 +120 -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 +21 -1
- package/dist/model/sketch.js +58 -4
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/stroke.js +2 -2
- package/dist/model/types.d.ts +38 -7
- package/dist/react/BottomBar.d.ts +2 -1
- package/dist/react/BottomBar.js +94 -25
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/Drawer.js +3 -2
- 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/Popovers.js +14 -3
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/Rail.js +1 -1
- package/dist/react/Sheet.d.ts +2 -2
- package/dist/react/Sheet.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/host.d.ts +7 -0
- package/dist/react/host.js +14 -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/render/draw.d.ts +21 -4
- package/dist/render/draw.js +184 -149
- 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 +5 -1
- package/dist/render/snap.js +3 -1
- 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 +31 -4
- 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 +23 -6
- 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.js +6 -1
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +5 -0
- package/dist/tools/registry.js +6 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +19 -0
- package/dist/tools/select.js +116 -17
- 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/tools/survey.d.ts +6 -0
- package/dist/tools/survey.js +28 -0
- package/dist/tools/survey.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 +3 -5
- package/src/data/sync.ts +7 -2
- package/src/editor/actions.ts +126 -112
- package/src/editor/elevation.ts +13 -16
- package/src/editor/gestures.ts +83 -13
- package/src/editor/keys.ts +2 -2
- package/src/editor/linkActions.ts +90 -13
- package/src/editor/mode.ts +4 -3
- 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 +8 -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 -1
- package/src/model/demo.ts +2 -1
- package/src/model/elevation.ts +4 -4
- package/src/model/history.ts +4 -1
- package/src/model/links.ts +8 -2
- 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 +105 -0
- package/src/model/settings.ts +1 -2
- package/src/model/sketch.ts +48 -4
- package/src/model/stroke.ts +2 -2
- package/src/model/types.ts +38 -7
- package/src/react/BottomBar.tsx +114 -29
- package/src/react/Drawer.tsx +14 -10
- 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/Popovers.tsx +24 -9
- package/src/react/Rail.tsx +1 -1
- package/src/react/Sheet.tsx +3 -3
- package/src/react/barScroll.ts +57 -0
- package/src/react/corners.ts +6 -3
- package/src/react/host.ts +14 -1
- package/src/react/motion.tsx +6 -6
- package/src/render/draw.ts +156 -116
- 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 +7 -2
- package/src/render/svgPath.ts +95 -0
- package/src/render/tileIcons.ts +75 -0
- package/src/render/view.ts +37 -5
- package/src/skia/SkiaDrawCtx.ts +26 -4
- package/src/solver/equations.ts +22 -6
- 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 +5 -1
- package/src/tools/registry.ts +10 -1
- package/src/tools/select.ts +99 -18
- package/src/tools/shapes.ts +9 -4
- package/src/tools/survey.ts +21 -0
- package/src/ui/ctx.ts +4 -3
- package/src/ui/drawer.ts +8 -2
- package/src/view3d/build.ts +11 -10
package/src/react/BottomBar.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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';
|
|
4
|
+
import { applyCtxAction, barItems, deleteSelection, iconAlone, plusAction, plusLabel, plusVisible } from '../editor/actions';
|
|
5
5
|
import { addLinks, LINK_PLUS_LABEL, linkPlusVisible, linkSizeVisible, pageLinkSize, setLinkSize, SIZE_STEPS, sizeToStep, stepToSize } from '../editor/linkActions';
|
|
6
6
|
import { afterEdit } from '../app/edit';
|
|
7
7
|
import { LINK_SIDE_IN } from '../model/links';
|
|
@@ -15,35 +15,45 @@ import { Slider } from './Slider';
|
|
|
15
15
|
import { notify, ui, useEditorKey } from './store';
|
|
16
16
|
import { currentHint } from '../editor/status';
|
|
17
17
|
import { hovered } from './Buttons';
|
|
18
|
-
import { Icon } from './Icon';
|
|
18
|
+
import { Icon, RawSvg } from './Icon';
|
|
19
19
|
import { RichText } from './RichText';
|
|
20
20
|
import { C, FONT, shadow } from './theme';
|
|
21
|
-
import { Tap, usePopup } from './motion';
|
|
21
|
+
import { reducedMotion, Tap, usePopup } from './motion';
|
|
22
22
|
import { createSheet } from './corners';
|
|
23
|
+
import { NATIVE } from './easing';
|
|
24
|
+
import { barCut, barSpans, FADE, FADE_RAMP, fadeSvg, NUDGE, NUDGE_AFTER, NUDGE_HOLD } from './barScroll';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* The contextual bar: what fits the tool and selection, the yellow + to measure, and the trash on its own. It's as
|
|
26
28
|
* 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
29
|
* just clear of it (and scrolls only if even that leaves too little room). In Annotate on a phone (or a window too
|
|
28
30
|
* 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.
|
|
31
|
+
* selected: selecting anything puts the trash there instead, and letting go brings the + back. A bar that scrolls
|
|
32
|
+
* says so (react/barScroll.ts).
|
|
30
33
|
*/
|
|
31
34
|
export function BottomBar({ phone }: { phone: boolean }) {
|
|
32
|
-
// what's in the bar, and the selected image's opacity and turn (its sliders)
|
|
35
|
+
// what's in the bar and what it's for, and the selected image's opacity and turn (its sliders)
|
|
33
36
|
useEditorKey(() => {
|
|
34
37
|
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('|');
|
|
38
|
+
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
39
|
});
|
|
37
40
|
// (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
41
|
// deletes nothing: what's tapped there shows its sizes, and what can be done with it from there)
|
|
39
42
|
const items = shown(phone), plus = !is3d() && plusVisible(), trash = !is3d() && ed.sel.length > 0;
|
|
40
43
|
const fab = !trash && (phone || ui.linkFab) && linkPlusVisible(), corner = trash || fab;
|
|
41
|
-
//
|
|
42
|
-
|
|
44
|
+
// It scrolls only when its buttons can't all fit (a narrow phone): otherwise a drag on the bar would move it. Its
|
|
45
|
+
// room comes from its row's width, not the scroller's, which barCut makes shorter than that; where its buttons are
|
|
46
|
+
// comes from their widths, which a browser reports as they change (not as they move along).
|
|
47
|
+
const [row, setRow] = useState(0), [content, setContent] = useState(0), [widths, setWidths] = useState<readonly (number | undefined)[]>([]);
|
|
48
|
+
const room = row - (phone && corner ? TRASH_ROOM : 0) - 2 * PAD - (plus ? ADD + ADD_GAP : 0), over = row > 0 && content > room + 1;
|
|
49
|
+
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;
|
|
50
|
+
const cut = !over ? undefined : spans ? barCut(spans, room) : room;
|
|
51
|
+
const onWidth = useCallback((i: number, w: number) => setWidths(p => (p[i] === w ? p : Object.assign(p.slice(), { [i]: w }))), []);
|
|
52
|
+
const scroll = useBarScroll(over, cut == null ? 0 : content - cut, barFor());
|
|
43
53
|
return (
|
|
44
54
|
<>
|
|
45
55
|
{(items.length > 0 || plus) && (
|
|
46
|
-
<View style={[s.bottom, phone ? s.phoneRow : s.centered]}>
|
|
56
|
+
<View style={[s.bottom, phone ? s.phoneRow : s.centered]} onLayout={e => setRow(e.nativeEvent.layout.width)}>
|
|
47
57
|
<ImagePopover />
|
|
48
58
|
<LinkSizePopover />
|
|
49
59
|
{/* a phone's bar sits between two springs that start as wide as the trash takes and share what's left
|
|
@@ -52,11 +62,17 @@ export function BottomBar({ phone }: { phone: boolean }) {
|
|
|
52
62
|
shares the room out differently and left a short bar off-centre.) The springs let touches through. */}
|
|
53
63
|
{phone && <View pointerEvents="none" style={[s.springL, { flexBasis: corner ? TRASH_ROOM : 0 }]} />}
|
|
54
64
|
<View testID="ctxBar" style={s.ctx}>
|
|
55
|
-
{/*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
{/* the buttons and their fades, clipped to the round ends the buttons have: scrolled, they'd stick out past
|
|
66
|
+
the bar's own. (A native ScrollView grows to fill its row by default, which stretched the bar across a
|
|
67
|
+
tablet.) */}
|
|
68
|
+
<View style={s.clip}>
|
|
69
|
+
<Animated.ScrollView ref={scroll.ref} testID="ctxScroll" horizontal showsHorizontalScrollIndicator={false} scrollEnabled={over} bounces={over}
|
|
70
|
+
contentContainerStyle={s.items} style={[s.scroller, cut != null && { width: cut }]} onContentSizeChange={w => setContent(w)}
|
|
71
|
+
onScroll={scroll.onScroll} scrollEventThrottle={16} onScrollBeginDrag={scroll.stop}>
|
|
72
|
+
<BarButtons k={buttonsKey()} phone={phone} onWidth={onWidth} />
|
|
73
|
+
</Animated.ScrollView>
|
|
74
|
+
{cut != null && <Fades x={scroll.x} end={cut} max={content - cut} />}
|
|
75
|
+
</View>
|
|
60
76
|
{plus && (
|
|
61
77
|
<Tap accessibilityRole="button" accessibilityLabel={plusLabel()} testID="addbtn" onPress={plusAction} style={s.add}>
|
|
62
78
|
<Icon name="plus" color={C.dark} />
|
|
@@ -96,6 +112,61 @@ function buttonsKey(): string {
|
|
|
96
112
|
return JSON.stringify(its) + '|' + ui.imgPop;
|
|
97
113
|
}
|
|
98
114
|
|
|
115
|
+
/** What the bar is for: the selection, or what's tapped in 3D. A new one's bar starts at its start. */
|
|
116
|
+
const barFor = (): string => (is3d() ? JSON.stringify(bar3d()) : ed.sel.map(x => x.type + ':' + x.id).join(','));
|
|
117
|
+
|
|
118
|
+
/** The first bar that scrolls in a session (until the app is next started) is nudged; no other is. */
|
|
119
|
+
let nudged = false;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A bar's scrolling: how far it's scrolled (for its fades), back to its start for each new selection, and the nudge —
|
|
123
|
+
* the first bar of a session that doesn't fit slides NUDGE along and back, unless Reduce Motion is on. `max`: how far
|
|
124
|
+
* it scrolls.
|
|
125
|
+
*/
|
|
126
|
+
function useBarScroll(over: boolean, max: number, forKey: string) {
|
|
127
|
+
const ref = useRef<ComponentRef<typeof ScrollView>>(null), x = useRef(new Animated.Value(0)).current;
|
|
128
|
+
const back = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
129
|
+
const onScroll = useMemo(() => Animated.event([{ nativeEvent: { contentOffset: { x } } }], { useNativeDriver: NATIVE }), [x]);
|
|
130
|
+
// (and one that's come to fit has only its start)
|
|
131
|
+
useEffect(() => { ref.current?.scrollTo({ x: 0, animated: false }); x.setValue(0); }, [forKey, over, x]);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!over || nudged || max <= 0 || reducedMotion()) return;
|
|
134
|
+
// (once what's in it has stopped changing: its buttons' widths come in one by one)
|
|
135
|
+
const t = setTimeout(() => {
|
|
136
|
+
nudged = true;
|
|
137
|
+
ref.current?.scrollTo({ x: Math.min(NUDGE, max), animated: true });
|
|
138
|
+
back.current = setTimeout(() => ref.current?.scrollTo({ x: 0, animated: true }), NUDGE_HOLD);
|
|
139
|
+
}, NUDGE_AFTER);
|
|
140
|
+
return () => clearTimeout(t);
|
|
141
|
+
}, [over, max]);
|
|
142
|
+
// a finger dragging it mid-nudge keeps it where it's taken
|
|
143
|
+
const stop = useCallback(() => clearTimeout(back.current), []);
|
|
144
|
+
useEffect(() => stop, [stop]);
|
|
145
|
+
return { ref, x, onScroll, stop };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const FADE_START = fadeSvg('left'), FADE_END = fadeSvg('right');
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A scrolling bar's fades, over its scroller: at its start once it's scrolled from there, and at its end (`end` along
|
|
152
|
+
* it) while there's more past it. They come and go as it's scrolled, on the native driver; touches go through them.
|
|
153
|
+
*/
|
|
154
|
+
function Fades({ x, end, max }: { x: Animated.Value; end: number; max: number }) {
|
|
155
|
+
const [start, more] = useMemo(() => {
|
|
156
|
+
const ramp = Math.max(1, Math.min(FADE_RAMP, max));
|
|
157
|
+
return [
|
|
158
|
+
x.interpolate({ inputRange: [0, ramp], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
159
|
+
x.interpolate({ inputRange: [max - ramp, max], outputRange: [1, 0], extrapolate: 'clamp' }),
|
|
160
|
+
];
|
|
161
|
+
}, [x, max]);
|
|
162
|
+
return (
|
|
163
|
+
<>
|
|
164
|
+
<Animated.View testID="barFadeStart" style={[s.fade, { left: 0, opacity: start }]}><RawSvg xml={FADE_START} width={FADE} height={BTN_H} /></Animated.View>
|
|
165
|
+
<Animated.View testID="barFadeEnd" style={[s.fade, { left: end - FADE, opacity: more }]}><RawSvg xml={FADE_END} width={FADE} height={BTN_H} /></Animated.View>
|
|
166
|
+
</>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
99
170
|
/**
|
|
100
171
|
* 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
172
|
* 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 +184,24 @@ function buttonsOnly(items: ReturnType<typeof barItems>): ReturnType<typeof barI
|
|
|
113
184
|
|
|
114
185
|
/**
|
|
115
186
|
* 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.
|
|
187
|
+
* is, as it measures whether they fit. Each says how wide it is (`onWidth`, by its place; a divider's is fixed).
|
|
117
188
|
*/
|
|
118
|
-
const BarButtons = memo(function BarButtons({ phone }: { k: string; phone: boolean }) {
|
|
189
|
+
const BarButtons = memo(function BarButtons({ phone, onWidth }: { k: string; phone: boolean; onWidth: (i: number, w: number) => void }) {
|
|
119
190
|
return (
|
|
120
191
|
<>
|
|
121
192
|
{shown(phone).map((it, i) => {
|
|
122
193
|
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>;
|
|
194
|
+
if ('lbl' in it) return <Text key={'l' + i} style={s.lbl} onLayout={e => onWidth(i, e.nativeEvent.layout.width)}>{it.lbl}</Text>;
|
|
124
195
|
const fg = it.on ? C.dark : it.locked ? C.red : '#fff';
|
|
125
196
|
return (
|
|
126
|
-
<Tap key={it.act + i} accessibilityRole="button" accessibilityLabel={it.label}
|
|
197
|
+
<Tap key={it.act + i} onLayout={e => onWidth(i, e.nativeEvent.layout.width)} accessibilityRole="button" accessibilityLabel={it.label}
|
|
127
198
|
accessibilityState={it.on != null || it.locked != null ? { selected: !!(it.on || it.locked) } : undefined}
|
|
128
199
|
onPress={e => { if (!(is3d() && run3d(it.act))) applyCtxAction(it.act, e.currentTarget); }}
|
|
129
|
-
style={st => [s.cb, it.on || (it.act === 'imgOpacity' && ui.imgPop === 'opacity') || (it.act === 'imgRotate' && ui.imgPop === 'rotate') || (it.act === 'linkSize' && ui.imgPop === 'tilesize') ? { backgroundColor: 'rgba(255,255,255,0.14)' } : null,
|
|
200
|
+
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') ? { backgroundColor: 'rgba(255,255,255,0.14)' } : null,
|
|
130
201
|
it.on ? { backgroundColor: '#fff' } : hovered(st) && { backgroundColor: 'rgba(255,255,255,0.08)' }]}>
|
|
131
202
|
<Icon name={it.ic} color={fg} />
|
|
132
|
-
|
|
203
|
+
{/* (the bar's lead — Lock, or Open — and Lock wherever it is: its icon alone, its name said, not shown) */}
|
|
204
|
+
{!iconAlone(it) && <Text style={[s.cbText, { color: fg }]}>{it.label}</Text>}
|
|
133
205
|
</Tap>
|
|
134
206
|
);
|
|
135
207
|
})}
|
|
@@ -259,6 +331,12 @@ export function SpokenHint() {
|
|
|
259
331
|
|
|
260
332
|
/** 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
333
|
const TRASH_ROOM = 72;
|
|
334
|
+
/**
|
|
335
|
+
* The bar's measures, which its room is worked out from as well as drawn with: its height and its buttons', its padding
|
|
336
|
+
* at each end, the yellow + and the gap before it, a divider and the margin at each side of it, and the gap between
|
|
337
|
+
* its buttons.
|
|
338
|
+
*/
|
|
339
|
+
const BAR_H = 60, BTN_H = 54, PAD = 6, ADD = 48, ADD_GAP = 4, SEP_W = 1, SEP_M = 4, GAP = 2;
|
|
262
340
|
|
|
263
341
|
const s = createSheet({
|
|
264
342
|
bottom: { pointerEvents: 'box-none', position: 'absolute', bottom: 14, zIndex: 24, flexDirection: 'row', alignItems: 'center' },
|
|
@@ -266,16 +344,23 @@ const s = createSheet({
|
|
|
266
344
|
phoneRow: { left: 12, right: 12 },
|
|
267
345
|
springL: { flexGrow: 1, flexShrink: 1000 },
|
|
268
346
|
springR: { flexGrow: 1, flexShrink: 0 },
|
|
269
|
-
//
|
|
347
|
+
// 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
|
|
348
|
+
// from its top and bottom, 6 from its ends, its ends round
|
|
270
349
|
// (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
|
-
|
|
350
|
+
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 },
|
|
351
|
+
clip: { flexGrow: 0, flexShrink: 1, borderRadius: BTN_H / 2, overflow: 'hidden' },
|
|
352
|
+
scroller: { flexGrow: 0, flexShrink: 1 },
|
|
353
|
+
items: { flexDirection: 'row', alignItems: 'center', gap: GAP },
|
|
354
|
+
// a scrolling bar's fade, over an end of its scroller
|
|
355
|
+
fade: { position: 'absolute', top: 0, bottom: 0, width: FADE, pointerEvents: 'none' },
|
|
356
|
+
cb: { minWidth: 58, height: BTN_H, borderRadius: BTN_H / 2, alignItems: 'center', justifyContent: 'center', gap: 3, paddingHorizontal: 8 },
|
|
357
|
+
// the lead: a 48 round button, its icon alone
|
|
358
|
+
lead: { minWidth: 48, width: 48, height: 48, borderRadius: 24, paddingHorizontal: 0 },
|
|
274
359
|
cbText: { fontFamily: FONT, fontSize: 10.5, fontWeight: '500' },
|
|
275
|
-
sep: { width:
|
|
360
|
+
sep: { width: SEP_W, height: 32, backgroundColor: C.g3, marginHorizontal: SEP_M },
|
|
276
361
|
lbl: { fontFamily: FONT, fontSize: 12, color: C.lg1, paddingHorizontal: 10 },
|
|
277
362
|
// the slider sits just above the bar, centred on it
|
|
278
|
-
popWrap: { position: 'absolute', left: 0, right: 0, bottom:
|
|
363
|
+
popWrap: { position: 'absolute', left: 0, right: 0, bottom: 70, alignItems: 'center' },
|
|
279
364
|
pop: { width: 400, maxWidth: '100%', flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: C.g2, borderRadius: 999, paddingHorizontal: 16, paddingVertical: 4, ...shadow },
|
|
280
365
|
popLabel: { fontFamily: FONT, fontSize: 12.5, fontWeight: '600', color: C.lg2 },
|
|
281
366
|
popValue: { fontFamily: FONT, fontSize: 12.5, fontWeight: '600', color: '#fff', width: 48, textAlign: 'right' },
|
|
@@ -286,8 +371,8 @@ const s = createSheet({
|
|
|
286
371
|
popReset: { paddingRight: 4 },
|
|
287
372
|
reset: { height: 36, paddingHorizontal: 16, borderRadius: 18, backgroundColor: C.g3, justifyContent: 'center' },
|
|
288
373
|
resetText: { fontFamily: FONT, fontSize: 12.5, fontWeight: '600', color: '#fff' },
|
|
289
|
-
// (
|
|
290
|
-
add: { width:
|
|
374
|
+
// (6 in from the bar's end, top and bottom: concentric with it)
|
|
375
|
+
add: { width: ADD, height: ADD, borderRadius: ADD / 2, backgroundColor: C.yellow, alignItems: 'center', justifyContent: 'center', marginLeft: ADD_GAP },
|
|
291
376
|
// BOLDR's delete FAB (the annotation editors' DeleteFab): a 60 pt solid red circle 12 from the right and 14 from the
|
|
292
377
|
// bottom, level with the bar beside it, on every screen size
|
|
293
378
|
trash: {
|
package/src/react/Drawer.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Animated, Easing, Keyboard, PanResponder, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useWindowDimensions, View,
|
|
4
4
|
} from 'react-native';
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
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';
|
|
@@ -60,7 +61,7 @@ export function Drawer({ phone = false }: { phone?: boolean }) {
|
|
|
60
61
|
const slide = useMemo(() => x.interpolate({ inputRange: [0, 1], outputRange: [0, (phone ? travel ?? height : w) + 40] }), [x, w, height, phone, travel]);
|
|
61
62
|
const fade = useMemo(() => x.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }), [x]);
|
|
62
63
|
// 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
|
|
64
|
+
const drag = useRef(new Animated.Value(0)).current, openedAt = useRef(0), sheet = useRef<ComponentRef<typeof View>>(null), [lift, setLift] = useState(0);
|
|
64
65
|
const dropped = useMemo(() => Animated.add(slide, drag), [slide, drag]);
|
|
65
66
|
useEffect(() => {
|
|
66
67
|
if (open) { setPresent(true); openedAt.current = Date.now(); }
|
|
@@ -151,7 +152,7 @@ export function Drawer({ phone = false }: { phone?: boolean }) {
|
|
|
151
152
|
function NameField({ value, onCommit, style, label, autoFocus, onFocus }: {
|
|
152
153
|
value: string; onCommit: (v: string) => void; style?: object; label: string; autoFocus?: boolean; onFocus?: () => void;
|
|
153
154
|
}) {
|
|
154
|
-
const [text, setText] = useState(value), editing = useRef(false), ref = useRef<TextInput
|
|
155
|
+
const [text, setText] = useState(value), editing = useRef(false), ref = useRef<ComponentRef<typeof TextInput>>(null);
|
|
155
156
|
useEffect(() => { if (!editing.current) setText(value); }, [value]);
|
|
156
157
|
useEffect(() => { if (autoFocus) setTimeout(() => ref.current?.focus(), 320); }, [autoFocus]);
|
|
157
158
|
return (
|
|
@@ -215,26 +216,29 @@ function Pages({ version, thumbs }: { version: number; thumbs: boolean }) {
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
function Spaces() {
|
|
218
|
-
const sched = spaceSchedule();
|
|
219
|
+
const sched = spaceSchedule(), sketchOnly = noScaleYet();
|
|
219
220
|
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
221
|
return (
|
|
221
222
|
<>
|
|
222
223
|
{sched.rows.map(r => (
|
|
223
224
|
<View key={r.id} style={s.roomRow}>
|
|
224
225
|
<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
|
-
|
|
226
|
+
{!sketchOnly && (
|
|
227
|
+
<View style={s.nums}>
|
|
228
|
+
<Text style={s.num}><Text style={s.numB}>{fmtArea(r.area)}</Text> floor</Text>
|
|
229
|
+
<Text style={s.num}><Text style={s.numB}>{fmtLen(r.per)}</Text> perimeter</Text>
|
|
230
|
+
</View>
|
|
231
|
+
)}
|
|
229
232
|
</View>
|
|
230
233
|
))}
|
|
231
|
-
|
|
234
|
+
{/* (a sketch's sizes are placeholders until its first length: no areas until then) */}
|
|
235
|
+
{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
236
|
<View style={{ flex: 1 }}>
|
|
233
237
|
<Text style={[s.rowText, { fontWeight: '700' }]}>Total</Text>
|
|
234
238
|
<Text style={s.rowSub}>Measured to inside wall faces</Text>
|
|
235
239
|
</View>
|
|
236
240
|
<Text style={[s.rowText, { fontWeight: '700' }]}>{fmtArea(sched.total)}</Text>
|
|
237
|
-
</View>
|
|
241
|
+
</View>}
|
|
238
242
|
</>
|
|
239
243
|
);
|
|
240
244
|
}
|
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 },
|
|
@@ -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>
|
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/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]}>
|