@reekon-tools/boldr-cad 1.6.0 → 1.7.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 +12 -1
- package/dist/data/provider.d.ts +2 -1
- package/dist/data/sync.js +4 -1
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.js +10 -1
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/advance.d.ts +2 -0
- package/dist/editor/advance.js +23 -0
- package/dist/editor/advance.js.map +1 -0
- package/dist/editor/dimSize.d.ts +13 -0
- package/dist/editor/dimSize.js +50 -0
- package/dist/editor/dimSize.js.map +1 -0
- package/dist/editor/gestures.js +30 -7
- package/dist/editor/gestures.js.map +1 -1
- package/dist/editor/keypad.d.ts +8 -0
- package/dist/editor/keypad.js +15 -1
- package/dist/editor/keypad.js.map +1 -1
- package/dist/editor/laser.d.ts +9 -0
- package/dist/editor/laser.js +38 -12
- package/dist/editor/laser.js.map +1 -1
- package/dist/editor/ports.d.ts +9 -0
- package/dist/editor/ports.js +2 -0
- package/dist/editor/ports.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/clockwise.d.ts +11 -0
- package/dist/model/clockwise.js +123 -0
- package/dist/model/clockwise.js.map +1 -0
- package/dist/model/settings.d.ts +2 -0
- package/dist/model/settings.js +1 -1
- package/dist/model/settings.js.map +1 -1
- package/dist/react/BottomBar.js +43 -3
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/PlanCanvas.native.js +15 -9
- package/dist/react/PlanCanvas.native.js.map +1 -1
- package/dist/react/host.d.ts +6 -0
- package/dist/react/host.js +11 -1
- package/dist/react/host.js.map +1 -1
- package/dist/react/ports.js +2 -1
- package/dist/react/ports.js.map +1 -1
- package/dist/react/store.d.ts +1 -1
- package/dist/react/store.js.map +1 -1
- package/dist/render/draw.d.ts +7 -0
- package/dist/render/draw.js +17 -8
- package/dist/render/draw.js.map +1 -1
- package/dist/render/navigate.d.ts +11 -6
- package/dist/render/navigate.js +34 -20
- package/dist/render/navigate.js.map +1 -1
- package/dist/tools/dim.d.ts +6 -0
- package/dist/tools/dim.js +28 -4
- package/dist/tools/dim.js.map +1 -1
- package/package.json +1 -1
- package/src/data/provider.ts +2 -2
- package/src/data/sync.ts +4 -1
- package/src/editor/actions.ts +6 -1
- package/src/editor/advance.ts +23 -0
- package/src/editor/dimSize.ts +53 -0
- package/src/editor/gestures.ts +31 -9
- package/src/editor/keypad.ts +17 -1
- package/src/editor/laser.ts +38 -10
- package/src/editor/ports.ts +10 -0
- package/src/index.ts +1 -2
- package/src/model/clockwise.ts +121 -0
- package/src/model/settings.ts +3 -1
- package/src/react/BottomBar.tsx +46 -2
- package/src/react/PlanCanvas.native.tsx +25 -12
- package/src/react/host.ts +11 -1
- package/src/react/ports.ts +3 -2
- package/src/react/store.ts +1 -1
- package/src/render/draw.ts +20 -9
- package/src/render/navigate.ts +31 -20
- package/src/tools/dim.ts +28 -4
- package/src/ui/domPorts.ts +2 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ed } from '../app/state';
|
|
2
|
+
import { clamp } from '../model/geom';
|
|
3
|
+
import { settings } from '../model/settings';
|
|
4
|
+
import { saveSoon } from '../model/store';
|
|
5
|
+
import { requestDraw } from '../render/view';
|
|
6
|
+
import { elevOpen } from './elevation';
|
|
7
|
+
import { selInfo } from './selection';
|
|
8
|
+
import { is3d } from './view3dState';
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
The size of the drawing's measurement tiles — its dimensions' chips: lengths, angles, diameters, a door's or window's
|
|
12
|
+
— one size for the whole file, every page, everyone's (the user's call, 2026-11-05: "rescaling is global so the scale
|
|
13
|
+
will apply to all measurement tiles in the cad file"). The bar's Size with a dimension picked opens its slider, as a
|
|
14
|
+
tile's Size does for the tiles (editor/linkActions.ts). It's kept with the file's settings (CadFileMeta.settings
|
|
15
|
+
.chipPct) and saved with the drawing; it isn't an undo step, as the wall height and the layers aren't.
|
|
16
|
+
|
|
17
|
+
Unlike a tile, a chip is drawn a set size on the screen, whatever the zoom, so its range is a chip's: from half to
|
|
18
|
+
three times its size, on the tiles' log scale, catching at 100%.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The slider's range, percent. */
|
|
22
|
+
export const DIM_PCT_MIN = 50, DIM_PCT_MAX = 300;
|
|
23
|
+
|
|
24
|
+
/** Whether the bar offers Size: the plan in Draft, for someone who can edit, with a dimension picked. */
|
|
25
|
+
export function dimSizeVisible(): boolean {
|
|
26
|
+
if (ed.readOnly || ed.mode !== 'draft' || elevOpen() || is3d() || !ed.sel.length) return false;
|
|
27
|
+
const s = selInfo();
|
|
28
|
+
return s.dims.length + s.odims.length > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Size every measurement tile to `pct` percent (DIM_PCT_MIN to DIM_PCT_MAX), drawn at once — for the slider as it moves. */
|
|
32
|
+
export function setDimPct(pct: number): void {
|
|
33
|
+
settings.chipPct = Math.round(clamp(pct, DIM_PCT_MIN, DIM_PCT_MAX));
|
|
34
|
+
requestDraw();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The slider let go: a change to save (the file's settings: Save, or the next save where saving is automatic). */
|
|
38
|
+
export function dimSizeDone(): void {
|
|
39
|
+
ed.editSeq++;
|
|
40
|
+
saveSoon();
|
|
41
|
+
requestDraw();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The slider's steps (as the tiles' slider has: SIZE_STEPS of them, 100% in the middle of a log scale). */
|
|
45
|
+
export const DIM_STEPS = 1000;
|
|
46
|
+
const LOG_MIN = Math.log(DIM_PCT_MIN), LOG_SPAN = Math.log(DIM_PCT_MAX) - LOG_MIN, LOG_100 = (Math.log(100) - LOG_MIN) / LOG_SPAN;
|
|
47
|
+
export const dimPctToStep = (pct: number): number => Math.round(((Math.log(clamp(pct, DIM_PCT_MIN, DIM_PCT_MAX)) - LOG_MIN) / LOG_SPAN) * DIM_STEPS);
|
|
48
|
+
/** A step as a percent: whole percents, catching at 100% so the size the drawing had is easy to find again. */
|
|
49
|
+
export function stepToDimPct(step: number): number {
|
|
50
|
+
const t = clamp(step, 0, DIM_STEPS) / DIM_STEPS;
|
|
51
|
+
if (Math.abs(t - LOG_100) < 0.02) return 100;
|
|
52
|
+
return Math.round(Math.exp(LOG_MIN + t * LOG_SPAN));
|
|
53
|
+
}
|
package/src/editor/gestures.ts
CHANGED
|
@@ -16,9 +16,12 @@ import type { SelItem } from '../model/types';
|
|
|
16
16
|
|
|
17
17
|
/*
|
|
18
18
|
Pointer routing for the plan canvas, whatever delivers the pointers (DOM pointer events, React Native touches).
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
(
|
|
19
|
+
A finger or the Pencil drives the tool, one at a time, and a second finger turns a finger's action into a pan/pinch —
|
|
20
|
+
a finger's press waits a moment for one first (pressTiming). Two-finger tap undoes, three-finger tap redoes.
|
|
21
|
+
Pencil and fingers at once (the user's call, 2026-11-05; fingers used to only pan and zoom once a Pencil had been
|
|
22
|
+
seen): fingers landing while the Pencil is down pan and pinch around it, and the Pencil draws while fingers move the
|
|
23
|
+
view. A finger already down when the Pencil lands and not moving the view — a hand resting on the screen — never
|
|
24
|
+
pressed anything: it's set aside until it lifts (see parked).
|
|
22
25
|
Mouse: middle or right button, or Space held, drags to pan; the wheel zooms.
|
|
23
26
|
With a sheet up over the drawing (the keypad, a conflict, a question), the drawing only pans and zooms, by any
|
|
24
27
|
pointer, and a tap on it closes the sheet. View only, a tap on a link's tile asks to open what it's for.
|
|
@@ -67,7 +70,9 @@ export const pressTiming = {
|
|
|
67
70
|
};
|
|
68
71
|
|
|
69
72
|
const ptrs = new Map<number, PointerIn>();
|
|
70
|
-
let
|
|
73
|
+
let gesture: Gesture | null = null, primary: PointerIn | null = null;
|
|
74
|
+
/** Fingers the Pencil landed on (setAsideFingers): they do nothing, and count for nothing, until they lift. */
|
|
75
|
+
const parked = new Set<number>();
|
|
71
76
|
/** close: a press under a sheet (see sheetUp) — a tap closes it; open: a view-only press — a tap opens a link's tile */
|
|
72
77
|
let panDrag: { x: number; y: number; id: number; moved: number; close?: boolean; open?: boolean } | null = null;
|
|
73
78
|
/** where the primary pointer went down, and whether a gesture on the drawing is in motion */
|
|
@@ -85,7 +90,7 @@ let held: { p: PointerIn; at: PointerIn; moves: PointerIn[]; timer: ReturnType<t
|
|
|
85
90
|
let wheelT: ReturnType<typeof setTimeout> | undefined;
|
|
86
91
|
export const keys = { spaceDown: false };
|
|
87
92
|
onSessionReset(() => {
|
|
88
|
-
ptrs.clear();
|
|
93
|
+
ptrs.clear(); parked.clear(); gesture = null; primary = null; panDrag = null; keys.spaceDown = false; primaryAt = null; beforePress = null; primaryMoved = false;
|
|
89
94
|
dropHeld();
|
|
90
95
|
setMotion(false);
|
|
91
96
|
clearTimeout(wheelT);
|
|
@@ -122,7 +127,8 @@ function evOf(p: PointerIn): PEv {
|
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
const curTool = () => tools[ed.tool];
|
|
125
|
-
const touchList = () => [...ptrs.values()].filter(q => q.kind === 'touch');
|
|
130
|
+
const touchList = () => [...ptrs.values()].filter(q => q.kind === 'touch' && !parked.has(q.id));
|
|
131
|
+
const penDown = () => [...ptrs.values()].some(q => q.kind === 'pen');
|
|
126
132
|
|
|
127
133
|
function gState(ts: PointerIn[]): GState | null {
|
|
128
134
|
if (ts.length >= 2) { const a = ts[0], b = ts[1]; return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, d: hyp(a.x - b.x, a.y - b.y), n: ts.length }; }
|
|
@@ -160,7 +166,8 @@ function endGesture(t: number): void {
|
|
|
160
166
|
if (g.moved < MOTION_PX && sheetUp()) closeSheet();
|
|
161
167
|
// (view only: a finger's tap on a link's tile opens it, once the person says so)
|
|
162
168
|
else if (g.moved < MOTION_PX && ed.readOnly && g.max === 1 && g.last && t - g.t0 < 500) readOnlyTap(g.last.x, g.last.y, true);
|
|
163
|
-
|
|
169
|
+
// (not with the Pencil mid-stroke: it would undo under it)
|
|
170
|
+
else if (g.moved < MOTION_PX && t - g.t0 < 380 && !ed.readOnly && !primary) { if (g.max === 2) undo(); else if (g.max >= 3) redo(); }
|
|
164
171
|
saveViewSoon();
|
|
165
172
|
}
|
|
166
173
|
|
|
@@ -216,9 +223,22 @@ function releaseHeld(): void {
|
|
|
216
223
|
for (const m of h.moves) movePrimary(m);
|
|
217
224
|
}
|
|
218
225
|
|
|
226
|
+
/**
|
|
227
|
+
* The Pencil landed: a finger already down that isn't moving the view — a hand resting on the screen, or a finger
|
|
228
|
+
* beaten to it — never pressed anything (a press it made is undone, as for a second finger) and is set aside until it
|
|
229
|
+
* lifts. Fingers already panning or pinching carry on.
|
|
230
|
+
*/
|
|
231
|
+
function setAsideFingers(): void {
|
|
232
|
+
if (gesture) return;
|
|
233
|
+
dropHeld();
|
|
234
|
+
if (primary && primary.kind === 'touch') cancelPrimary();
|
|
235
|
+
for (const q of touchList()) parked.add(q.id);
|
|
236
|
+
}
|
|
237
|
+
|
|
219
238
|
function endPtr(e: PointerIn, cancel: boolean): void {
|
|
220
239
|
const p = ptrs.get(e.id);
|
|
221
240
|
ptrs.delete(e.id);
|
|
241
|
+
if (parked.delete(e.id)) return;
|
|
222
242
|
if (panDrag && panDrag.id === e.id) {
|
|
223
243
|
const still = panDrag.moved < MOTION_PX && !cancel, close = !!panDrag.close && still, open = !!panDrag.open && still;
|
|
224
244
|
panDrag = null; setCursor(''); saveViewSoon();
|
|
@@ -239,7 +259,6 @@ function endPtr(e: PointerIn, cancel: boolean): void {
|
|
|
239
259
|
|
|
240
260
|
export function pointerDown(e: PointerIn): void {
|
|
241
261
|
dismissPopovers();
|
|
242
|
-
if (e.kind === 'pen') penSeen = true;
|
|
243
262
|
ed.lastPointerTouch = e.kind !== 'mouse';
|
|
244
263
|
const p = { ...e };
|
|
245
264
|
ptrs.set(e.id, p);
|
|
@@ -250,9 +269,11 @@ export function pointerDown(e: PointerIn): void {
|
|
|
250
269
|
setCursor('grabbing');
|
|
251
270
|
return;
|
|
252
271
|
}
|
|
272
|
+
if (e.kind === 'pen') setAsideFingers();
|
|
253
273
|
if (e.kind === 'touch') {
|
|
254
274
|
const touches = touchList();
|
|
255
|
-
|
|
275
|
+
// (with the Pencil down, a finger moves the view around it)
|
|
276
|
+
if (touches.length >= 2 || navOnly || penDown()) {
|
|
256
277
|
// (a second finger while the first one's press waited: that never pressed anything)
|
|
257
278
|
dropHeld();
|
|
258
279
|
if (primary && primary.kind === 'touch') cancelPrimary();
|
|
@@ -280,6 +301,7 @@ export function pointerMove(e: PointerIn): void {
|
|
|
280
301
|
return;
|
|
281
302
|
}
|
|
282
303
|
if (p) { p.x = e.x; p.y = e.y; }
|
|
304
|
+
if (parked.has(e.id)) return;
|
|
283
305
|
if (gesture && p && p.kind === 'touch') { updateGesture(); return; }
|
|
284
306
|
// (a finger whose press is waiting: its moves wait with it, and one past a tap sends them all now — a drag)
|
|
285
307
|
if (held && e.id === held.at.id) {
|
package/src/editor/keypad.ts
CHANGED
|
@@ -118,6 +118,22 @@ export function keypadSetReading(v: number): void {
|
|
|
118
118
|
kps.cur.error = undefined;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** Whether an open keypad would take a laser reading now (a length one, with laser on): keypadTakeLaser's test alone. */
|
|
122
|
+
export function keypadTakesLaser(): boolean {
|
|
123
|
+
const k = kps.cur;
|
|
124
|
+
return !!k && k.spec.laser !== false && !k.spec.numeric;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether a reading of `v` taken into the open keypad now would replace a measured value: only the host's own keypad
|
|
129
|
+
* applies a reading at once (this one puts it in the display, still to be applied), and only one opened on such a
|
|
130
|
+
* value says so (KeypadSpec.overwrites: a dimension's).
|
|
131
|
+
*/
|
|
132
|
+
export function keypadReadingOverwrites(v: number): boolean {
|
|
133
|
+
const k = kps.cur;
|
|
134
|
+
return !!k && !!k.host && keypadTakesLaser() && !!k.spec.overwrites?.(v);
|
|
135
|
+
}
|
|
136
|
+
|
|
121
137
|
/**
|
|
122
138
|
* A laser reading into an open length keypad. False if this keypad can't take one (degrees, or laser turned off).
|
|
123
139
|
* The host's own keypad can't be typed into from here, so a reading there is the value: it's applied at once.
|
|
@@ -125,7 +141,7 @@ export function keypadSetReading(v: number): void {
|
|
|
125
141
|
*/
|
|
126
142
|
export function keypadTakeLaser(v: number, capture?: unknown): boolean {
|
|
127
143
|
const k = kps.cur;
|
|
128
|
-
if (!k ||
|
|
144
|
+
if (!k || !keypadTakesLaser()) return false;
|
|
129
145
|
if (k.host) { keypadSubmit(v, capture); return true; }
|
|
130
146
|
keypadSetReading(v);
|
|
131
147
|
k.capture = capture !== undefined ? { of: capture, text: k.text } : undefined;
|
package/src/editor/laser.ts
CHANGED
|
@@ -9,9 +9,10 @@ import { lengthDimOf, lineLen } from '../model/sketch';
|
|
|
9
9
|
import { SK } from '../model/store';
|
|
10
10
|
import { faceLength } from '../model/wallfaces';
|
|
11
11
|
import { V } from '../render/view';
|
|
12
|
-
import { LENGTH_SET_ALREADY, setDimValue, type DimValueOpts } from '../tools/dim';
|
|
12
|
+
import { LENGTH_SET_ALREADY, replacesMeasured, setDimValue, type DimValueOpts } from '../tools/dim';
|
|
13
13
|
import { fmtLen } from '../units';
|
|
14
|
-
import {
|
|
14
|
+
import { advanceAfterReading } from './advance';
|
|
15
|
+
import { keypadReadingOverwrites, keypadTakeLaser, keypadTakesLaser } from './keypad';
|
|
15
16
|
import { refreshKeypad, refreshTray, toast } from './ports';
|
|
16
17
|
import { selInfo } from './selection';
|
|
17
18
|
|
|
@@ -82,9 +83,9 @@ function readingTarget(id: string): [string, (1 | -1)?] {
|
|
|
82
83
|
return sk.lines[id] || sk.circles[id] || sk.arcs[id] ? [id] : edgeOf(id);
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
/** A reading put on a line's (or straight wall's) length, or a circle's or arc's diameter. */
|
|
86
|
-
function setReading(id: string, v: number, side?: 1 | -1, capture?: unknown):
|
|
87
|
-
|
|
86
|
+
/** A reading put on a line's (or straight wall's) length, or a circle's or arc's diameter. Whether it took. */
|
|
87
|
+
function setReading(id: string, v: number, side?: 1 | -1, capture?: unknown): boolean {
|
|
88
|
+
return SK().lines[id] ? setLineLength(id, v, side, { capture }) : setCurveDiameter(id, v, side, { capture });
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
@@ -109,12 +110,13 @@ function appliedTo(id: string): string {
|
|
|
109
110
|
*/
|
|
110
111
|
export function applyReading(v: number, capture?: unknown): void {
|
|
111
112
|
if (keypadTakeLaser(v, capture)) { refreshKeypad(); return; }
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
setReading(id, v, side, capture);
|
|
113
|
+
const target = selectedReadingTarget();
|
|
114
|
+
if (target) {
|
|
115
|
+
const [id, side] = target;
|
|
116
|
+
const took = setReading(id, v, side, capture);
|
|
117
117
|
toast('Laser ' + fmtLen(v) + ' applied to ' + appliedTo(id));
|
|
118
|
+
// on round to the next line clockwise, for the next reading (not past one the drawing refused, or asked about)
|
|
119
|
+
if (took) advanceAfterReading(id, side);
|
|
118
120
|
return;
|
|
119
121
|
}
|
|
120
122
|
laser.readings.unshift({ id: uid('m'), v, ...(capture !== undefined ? { capture } : {}) });
|
|
@@ -123,6 +125,32 @@ export function applyReading(v: number, capture?: unknown): void {
|
|
|
123
125
|
toast('Select a line, then take the reading.', { warn: true });
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/** Where a reading arriving now goes when no keypad takes it: the one selected line, wall edge or curve, in Draft. */
|
|
129
|
+
function selectedReadingTarget(): [string, (1 | -1)?] | null {
|
|
130
|
+
const s = selInfo();
|
|
131
|
+
const one = s.n === 1 ? s.lines[0] ?? s.wfaces[0] ?? s.curves[0] : undefined;
|
|
132
|
+
return ed.mode === 'draft' && one ? readingTarget(one) : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Whether a reading of `v` arriving now would replace a measured value, for a host that warns before one does (BOLDR's
|
|
137
|
+
* mobile app, with the warning a filled annotation tile or form cell gets; the user's call, 2026-11-05): a value
|
|
138
|
+
* already entered (a driving dimension, not a default nobody measured), changed by it (replacesMeasured). That's a
|
|
139
|
+
* reading going onto the selected line, wall edge or curve; or into the host's own keypad opened on such a dimension (a
|
|
140
|
+
* phone's: it applies a reading at once). Not one into the editor's own keypad: it waits in the display to be applied.
|
|
141
|
+
* Not one linked to a measurement of the job: the editor asks about that itself (askLinkedChange).
|
|
142
|
+
*/
|
|
143
|
+
export function readingOverwrites(v: number): boolean {
|
|
144
|
+
if (keypadTakesLaser()) return keypadReadingOverwrites(v);
|
|
145
|
+
const target = selectedReadingTarget();
|
|
146
|
+
if (!target) return false;
|
|
147
|
+
const sk = SK(), [id] = target;
|
|
148
|
+
const ex = sk.lines[id] ? lengthDimOf(sk, id) : diamDimOf(id);
|
|
149
|
+
const c = ex ? sk.cons[ex] : undefined;
|
|
150
|
+
// (as setCurveDiameter gives it: a fillet's radius dimension holds the diameter)
|
|
151
|
+
return replacesMeasured(c, c?.type === 'diam' && c.rad ? v * 2 : v);
|
|
152
|
+
}
|
|
153
|
+
|
|
126
154
|
/**
|
|
127
155
|
* Use a reading from the tray: `dropOn` is the line it was dragged onto (null: dropped off any line); without it
|
|
128
156
|
* (a tap), it goes to the one selected line.
|
package/src/editor/ports.ts
CHANGED
|
@@ -38,6 +38,12 @@ export interface KeypadSpec {
|
|
|
38
38
|
extras?: () => KeypadExtra[];
|
|
39
39
|
/** the value; `capture`, a laser reading's record (laserReading's), when it's that reading applied as it came in */
|
|
40
40
|
onDone?: (v: number, capture?: unknown) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Whether `v` (as onDone takes it) would replace a value measured there: set by a keypad opened on a dimension, so a
|
|
43
|
+
* host that warns before a reading overwrites can ask, where the host's keypad applies a reading at once
|
|
44
|
+
* (laserReadingOverwrites).
|
|
45
|
+
*/
|
|
46
|
+
overwrites?: (v: number) => boolean;
|
|
41
47
|
/**
|
|
42
48
|
* A line's length, or a dimension's angle: the job's measurements are offered with the keys (a page swiped to on a
|
|
43
49
|
* touch screen, Select From List on a desktop), those of `kind` pickable — the one picked handed to `pick` (the
|
|
@@ -91,6 +97,8 @@ export interface EditorPorts {
|
|
|
91
97
|
toggleImageRotation(anchor?: unknown): void;
|
|
92
98
|
/** the link tiles' size slider, above the bar (its button again closes it) */
|
|
93
99
|
toggleTileSize(anchor?: unknown): void;
|
|
100
|
+
/** the measurement tiles' size slider, above the bar, as the tiles' (editor/dimSize.ts) */
|
|
101
|
+
toggleDimSize(anchor?: unknown): void;
|
|
94
102
|
/** let the user type a new name for a space */
|
|
95
103
|
editRoomName(roomId: string): void;
|
|
96
104
|
/** the mouse cursor over the plan (or the wall elevation), as a CSS cursor name ('' = the default); ignored without a mouse */
|
|
@@ -125,6 +133,7 @@ export const basePorts = (): EditorPorts => ({
|
|
|
125
133
|
toggleImageOpacity() {},
|
|
126
134
|
toggleImageRotation() {},
|
|
127
135
|
toggleTileSize() {},
|
|
136
|
+
toggleDimSize() {},
|
|
128
137
|
editRoomName() {},
|
|
129
138
|
setCursor() {},
|
|
130
139
|
dismissPopovers: () => false,
|
|
@@ -154,6 +163,7 @@ export const openPalette = (anchor?: unknown): void => ports.openPalette(anchor)
|
|
|
154
163
|
export const toggleImageOpacity = (anchor?: unknown): void => ports.toggleImageOpacity(anchor);
|
|
155
164
|
export const toggleImageRotation = (anchor?: unknown): void => ports.toggleImageRotation(anchor);
|
|
156
165
|
export const toggleTileSize = (anchor?: unknown): void => ports.toggleTileSize(anchor);
|
|
166
|
+
export const toggleDimSize = (anchor?: unknown): void => ports.toggleDimSize(anchor);
|
|
157
167
|
export const editRoomName = (roomId: string): void => ports.editRoomName(roomId);
|
|
158
168
|
export const setCursor = (cursor: string, where: 'plan' | 'elevation' = 'plan'): void => ports.setCursor(cursor, where);
|
|
159
169
|
export const dismissPopovers = (): boolean => ports.dismissPopovers();
|
package/src/index.ts
CHANGED
|
@@ -11,8 +11,7 @@ export type { Unit } from './units';
|
|
|
11
11
|
export { memoryProvider } from './data/memory';
|
|
12
12
|
export { setFileSaver } from './react/ports';
|
|
13
13
|
export { BESIDE as CAD_BESIDE } from './react/pageLock';
|
|
14
|
-
export { laserReading, setLaserConnected, surveyClose, surveyCursor, surveyPoint } from './react/host';
|
|
15
|
-
export type { HostKeypad, HostKeypadExtra, HostKeypadRequest, MeasurementList, MeasurementListPage } from './react/hostKeypad';
|
|
14
|
+
export { laserReading, laserReadingOverwrites, setLaserConnected, surveyClose, surveyCursor, surveyPoint } from './react/host';export type { HostKeypad, HostKeypadExtra, HostKeypadRequest, MeasurementList, MeasurementListPage } from './react/hostKeypad';
|
|
16
15
|
export type { CadLinkPick, CadLinks, CadSaveMeasurement, MeasurementKind } from './editor/links';
|
|
17
16
|
export type { LinkInfo, LinkTarget } from './model/types';
|
|
18
17
|
export { linkKey } from './model/links';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Sketch } from './types';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The next line clockwise round the shape a line is part of: where a laser reading moves on to once it has set a
|
|
5
|
+
line's length (editor/advance.ts; the user's call, 2026-11-05). Clockwise as seen: the plan's y runs up, so as it is
|
|
6
|
+
in the model too, and turning the view doesn't change it.
|
|
7
|
+
|
|
8
|
+
The shape is the face the line bounds, walked with it on the right: at each corner the walk takes the edge that turns
|
|
9
|
+
most to the right, so it keeps tracing the same face past a T (an inner wall) and comes round to where it started. A
|
|
10
|
+
line can bound two faces (a wall between two rooms) or only the outside: the walk that closes clockwise is the room
|
|
11
|
+
(its signed area negative), and between two rooms, the smaller one, or for a wall edge, the room that edge faces. A
|
|
12
|
+
run that never closes goes clockwise round its own middle, and stops at its end. Arcs are walked through (a fillet in
|
|
13
|
+
a room's corner) but never moved onto: the next straight line is. Guides aren't part of any shape.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** An edge walked one way: `id` (a line or an arc) from point `from` to point `to`. */
|
|
17
|
+
interface Step { id: string; from: string; to: string }
|
|
18
|
+
|
|
19
|
+
/** Every line and arc, by the points they end at (guides left out). */
|
|
20
|
+
function ends(sk: Sketch): Map<string, { id: string; other: string }[]> {
|
|
21
|
+
const m = new Map<string, { id: string; other: string }[]>();
|
|
22
|
+
const add = (id: string, a: string, b: string) => {
|
|
23
|
+
if (a === b || !sk.points[a] || !sk.points[b]) return;
|
|
24
|
+
(m.get(a) ?? m.set(a, []).get(a)!).push({ id, other: b });
|
|
25
|
+
(m.get(b) ?? m.set(b, []).get(b)!).push({ id, other: a });
|
|
26
|
+
};
|
|
27
|
+
for (const [id, l] of Object.entries(sk.lines)) if (!l.constr) add(id, l.a, l.b);
|
|
28
|
+
for (const [id, c] of Object.entries(sk.arcs)) if (!c.constr) add(id, c.a, c.b);
|
|
29
|
+
return m;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TURN = 2 * Math.PI;
|
|
33
|
+
|
|
34
|
+
/** The step after `s`, keeping the face on the right: the edge at its end turning most to the right. Null at a dead end. */
|
|
35
|
+
function after(sk: Sketch, at: Map<string, { id: string; other: string }[]>, s: Step): Step | null {
|
|
36
|
+
const v = sk.points[s.to], u = sk.points[s.from];
|
|
37
|
+
const back = Math.atan2(u.y - v.y, u.x - v.x);
|
|
38
|
+
let best: Step | null = null, least = Infinity;
|
|
39
|
+
for (const e of at.get(s.to) ?? []) {
|
|
40
|
+
if (e.id === s.id) continue;
|
|
41
|
+
const w = sk.points[e.other];
|
|
42
|
+
// (counter-clockwise from the way back: the least is the sharpest turn right)
|
|
43
|
+
let d = Math.atan2(w.y - v.y, w.x - v.x) - back;
|
|
44
|
+
while (d <= 1e-9) d += TURN;
|
|
45
|
+
while (d > TURN) d -= TURN;
|
|
46
|
+
if (d < least) { least = d; best = { id: e.id, from: s.to, to: e.other }; }
|
|
47
|
+
}
|
|
48
|
+
return best;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The walk from `s` on round: every step after it, and whether it came back to `s` (a closed shape). */
|
|
52
|
+
function walk(sk: Sketch, at: Map<string, { id: string; other: string }[]>, s: Step): { steps: Step[]; closed: boolean } {
|
|
53
|
+
const steps: Step[] = [];
|
|
54
|
+
let cur: Step | null = s;
|
|
55
|
+
const most = 2 * (Object.keys(sk.lines).length + Object.keys(sk.arcs).length) + 2;
|
|
56
|
+
while (steps.length < most) {
|
|
57
|
+
cur = after(sk, at, cur!);
|
|
58
|
+
if (!cur) return { steps, closed: false };
|
|
59
|
+
if (cur.id === s.id && cur.from === s.from) return { steps, closed: true };
|
|
60
|
+
steps.push(cur);
|
|
61
|
+
}
|
|
62
|
+
return { steps, closed: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Twice the signed area round `s` and the steps after it (negative: clockwise, y up). */
|
|
66
|
+
function area(sk: Sketch, s: Step, steps: Step[]): number {
|
|
67
|
+
let a = 0;
|
|
68
|
+
for (const t of [s, ...steps]) {
|
|
69
|
+
const p = sk.points[t.from], q = sk.points[t.to];
|
|
70
|
+
a += p.x * q.y - q.x * p.y;
|
|
71
|
+
}
|
|
72
|
+
return a;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The middle of everything joined to `pid` (the points it can reach along lines and arcs). */
|
|
76
|
+
function middle(sk: Sketch, at: Map<string, { id: string; other: string }[]>, pid: string): { x: number; y: number } {
|
|
77
|
+
const seen = new Set([pid]), todo = [pid];
|
|
78
|
+
while (todo.length) for (const e of at.get(todo.pop()!) ?? []) if (!seen.has(e.other)) { seen.add(e.other); todo.push(e.other); }
|
|
79
|
+
let x = 0, y = 0;
|
|
80
|
+
for (const id of seen) { x += sk.points[id].x; y += sk.points[id].y; }
|
|
81
|
+
return { x: x / seen.size, y: y / seen.size };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The side of a straight line on the right of walking it as `s` does (+1 the left of its A → B, −1 the right). */
|
|
85
|
+
const rightOf = (sk: Sketch, s: Step): 1 | -1 => (s.from === sk.lines[s.id]?.a ? -1 : 1);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The next line clockwise from line or arc `id`, round the shape it's part of; null when there's none (a lone line, a
|
|
89
|
+
* run's end, a circle). `side`: a wall's edge it was measured on (+1 the left of its A → B): the edge of the next wall on
|
|
90
|
+
* the same side of the shape (the inside stays the inside) comes back with it. A line between two rooms goes round the
|
|
91
|
+
* one that edge faces, or the smaller one.
|
|
92
|
+
*/
|
|
93
|
+
export function nextClockwise(sk: Sketch, id: string, side?: 1 | -1): { id: string; side?: 1 | -1 } | null {
|
|
94
|
+
const e = sk.lines[id] ?? sk.arcs[id];
|
|
95
|
+
if (!e || e.constr || e.a === e.b) return null;
|
|
96
|
+
const at = ends(sk);
|
|
97
|
+
const fwd: Step = { id, from: e.a, to: e.b }, rev: Step = { id, from: e.b, to: e.a };
|
|
98
|
+
const wf = walk(sk, at, fwd), wr = walk(sk, at, rev);
|
|
99
|
+
const room = (s: Step, w: { steps: Step[]; closed: boolean }) => w.closed && area(sk, s, w.steps) < 0;
|
|
100
|
+
const inF = room(fwd, wf), inR = room(rev, wr);
|
|
101
|
+
let start: Step, w: { steps: Step[]; closed: boolean };
|
|
102
|
+
if (inF && inR) {
|
|
103
|
+
// between two rooms: the one the edge faces (on the walk's right), or the smaller
|
|
104
|
+
const pickF = side && sk.lines[id] ? rightOf(sk, fwd) === side : Math.abs(area(sk, fwd, wf.steps)) <= Math.abs(area(sk, rev, wr.steps));
|
|
105
|
+
[start, w] = pickF ? [fwd, wf] : [rev, wr];
|
|
106
|
+
} else if (inF || inR) {
|
|
107
|
+
[start, w] = inF ? [fwd, wf] : [rev, wr];
|
|
108
|
+
} else {
|
|
109
|
+
// a run that doesn't close: clockwise round its middle (the middle on the right of the way it's walked)
|
|
110
|
+
const m = middle(sk, at, e.a), a = sk.points[e.a], b = sk.points[e.b];
|
|
111
|
+
const leftOfAB = (b.x - a.x) * (m.y - a.y) - (b.y - a.y) * (m.x - a.x) >= 0;
|
|
112
|
+
[start, w] = leftOfAB ? [rev, wr] : [fwd, wf];
|
|
113
|
+
}
|
|
114
|
+
const next = w.steps.find(s => s.id !== id && sk.lines[s.id]);
|
|
115
|
+
if (!next) return null;
|
|
116
|
+
if (!side || !sk.lines[id]) return { id: next.id };
|
|
117
|
+
// the measured edge's side of the shape (on the walk's right, or its left), kept on the next line
|
|
118
|
+
const sameRight = rightOf(sk, start) === side;
|
|
119
|
+
const r = rightOf(sk, next);
|
|
120
|
+
return { id: next.id, side: sameRight ? r : (-r as 1 | -1) };
|
|
121
|
+
}
|
package/src/model/settings.ts
CHANGED
|
@@ -24,13 +24,15 @@ export interface Settings extends UnitPrefs {
|
|
|
24
24
|
layers: Layers;
|
|
25
25
|
/** default wall height (floor to ceiling), inches */
|
|
26
26
|
wallH: number;
|
|
27
|
+
/** the size of the drawing's measurement tiles (its dimensions' chips), percent: the file's, everyone's (editor/dimSize.ts) */
|
|
28
|
+
chipPct: number;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export const defaultLayers = (): Layers => ({ underlay: true, construction: true, dims: true, rooms: true, annot: true, cabinets: true, objSnap: true });
|
|
30
32
|
|
|
31
33
|
export const settings = sessionState((): Settings => ({
|
|
32
34
|
unit: 'ftin', denom: 16, snapGrid: false, autoCons: true, toolTips: true, showCons: true, theme: 'system', welcomed: false,
|
|
33
|
-
layers: defaultLayers(), wallH: 96,
|
|
35
|
+
layers: defaultLayers(), wallH: 96, chipPct: 100,
|
|
34
36
|
}));
|
|
35
37
|
|
|
36
38
|
useUnitPrefs(settings);
|
package/src/react/BottomBar.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import { ed } from '../app/state';
|
|
|
4
4
|
import { applyCtxAction, barItems, deleteSelection, iconAlone, plusAction, plusLabel, plusVisible } from '../editor/actions';
|
|
5
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 { DIM_STEPS, dimPctToStep, dimSizeDone, dimSizeVisible, setDimPct, stepToDimPct } from '../editor/dimSize';
|
|
8
|
+
import { settings } from '../model/settings';
|
|
7
9
|
import { bar3d, run3d } from '../editor/view3d';
|
|
8
10
|
import { is3d } from '../editor/view3dState';
|
|
9
11
|
import { OPACITY_MIN, OPACITY_STEP, ROTATE_MAX, ROTATE_STEP, setUnderlayOpacity, setUnderlayRotation } from '../editor/project';
|
|
@@ -33,7 +35,7 @@ export function BottomBar({ phone }: { phone: boolean }) {
|
|
|
33
35
|
// what's in the bar and what it's for, and the selected image's opacity and turn (its sliders)
|
|
34
36
|
useEditorKey(() => {
|
|
35
37
|
const u = ed.sel.length === 1 && ed.sel[0].type === 'underlay' ? imageById(ed.sel[0].id) : undefined;
|
|
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('|');
|
|
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() ?? '', dimSizeVisible(), settings.chipPct].join('|');
|
|
37
39
|
});
|
|
38
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
|
|
39
41
|
// deletes nothing: what's tapped there shows its sizes, and what can be done with it from there)
|
|
@@ -54,6 +56,7 @@ export function BottomBar({ phone }: { phone: boolean }) {
|
|
|
54
56
|
<View style={[s.bottom, phone ? s.phoneRow : s.centered]} onLayout={e => setRow(e.nativeEvent.layout.width)}>
|
|
55
57
|
<ImagePopover />
|
|
56
58
|
<LinkSizePopover />
|
|
59
|
+
<DimSizePopover />
|
|
57
60
|
<OpenPopover />
|
|
58
61
|
{/* a phone's bar sits between two springs that start as wide as the trash takes and share what's left
|
|
59
62
|
equally, so it's centred; only the left one gives way (first, before the bar), so a long bar slides left
|
|
@@ -202,7 +205,7 @@ const BarButtons = memo(function BarButtons({ phone, onWidth }: { k: string; pho
|
|
|
202
205
|
if (t && opensWithChoice(t)) { ui.imgPop = ui.imgPop === 'open' ? null : 'open'; notify(); return; }
|
|
203
206
|
applyCtxAction(it.act, e.currentTarget);
|
|
204
207
|
}}
|
|
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,
|
|
208
|
+
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') || (it.act === 'dimSize' && ui.imgPop === 'dimsize') || (isOpen(it.act) && ui.imgPop === 'open') ? { backgroundColor: 'rgba(255,255,255,0.14)' } : null,
|
|
206
209
|
it.on ? { backgroundColor: '#fff' } : hovered(st) && { backgroundColor: 'rgba(255,255,255,0.08)' }]}>
|
|
207
210
|
<Icon name={it.ic} color={fg} />
|
|
208
211
|
{/* (the bar's lead — Lock, or Open — and Lock wherever it is: its icon alone, its name said, not shown) */}
|
|
@@ -355,6 +358,47 @@ function LinkSizePopover() {
|
|
|
355
358
|
);
|
|
356
359
|
}
|
|
357
360
|
|
|
361
|
+
/**
|
|
362
|
+
* The measurement tiles' size slider (editor/dimSize.ts): the bar's Size with a dimension picked, as LinkSizePopover is
|
|
363
|
+
* for the tiles — one size for every measurement tile in the file, in percent of the size they're drawn at (50% to
|
|
364
|
+
* 300%), catching at 100%. Kept quick the same way: the thumb and the number follow the finger, the drawing is redrawn
|
|
365
|
+
* at most once a frame, and the change is marked (to save) as it's let go. Not an undo step, as the file's other
|
|
366
|
+
* settings aren't.
|
|
367
|
+
*/
|
|
368
|
+
function DimSizePopover() {
|
|
369
|
+
const [live, setLive] = useState<number | null>(null);
|
|
370
|
+
const pending = useRef<{ v: number; frame: number } | null>(null);
|
|
371
|
+
const open = ui.imgPop === 'dimsize' && dimSizeVisible();
|
|
372
|
+
const pop = usePopup(open, { y: 8 }, 'bottom center');
|
|
373
|
+
if (!pop.present) return null;
|
|
374
|
+
const value = live ?? dimPctToStep(settings.chipPct);
|
|
375
|
+
const change = (v: number) => {
|
|
376
|
+
setLive(v);
|
|
377
|
+
const p = pending.current;
|
|
378
|
+
if (p) { p.v = v; return; }
|
|
379
|
+
pending.current = { v, frame: requestAnimationFrame(() => { const q = pending.current; pending.current = null; if (q) setDimPct(stepToDimPct(q.v)); }) };
|
|
380
|
+
};
|
|
381
|
+
const end = () => {
|
|
382
|
+
const p = pending.current;
|
|
383
|
+
if (p) { cancelAnimationFrame(p.frame); pending.current = null; setDimPct(stepToDimPct(p.v)); }
|
|
384
|
+
setLive(null);
|
|
385
|
+
dimSizeDone();
|
|
386
|
+
notify();
|
|
387
|
+
};
|
|
388
|
+
return (
|
|
389
|
+
<View style={s.popWrap} pointerEvents="box-none">
|
|
390
|
+
<Animated.View style={[s.pop, pop.style]} pointerEvents={open ? 'auto' : 'none'} testID={open ? 'dimSizePop' : undefined}>
|
|
391
|
+
<Text style={s.popLabel}>Measurement size</Text>
|
|
392
|
+
<View style={{ flex: 1 }}>
|
|
393
|
+
<Slider label="Measurement size" testID="dimSizeSlider" value={value} min={0} max={DIM_STEPS} step={1} format={v => stepToDimPct(v) + '%'}
|
|
394
|
+
onChange={change} onEnd={end} />
|
|
395
|
+
</View>
|
|
396
|
+
<Text style={s.popValue}>{(live != null ? stepToDimPct(live) : settings.chipPct) + '%'}</Text>
|
|
397
|
+
</Animated.View>
|
|
398
|
+
</View>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
358
402
|
/** The hint for screen readers: what to do next with the tool or what's picked, read out as it changes. */
|
|
359
403
|
export function SpokenHint() {
|
|
360
404
|
useEditorKey(currentHint);
|
|
@@ -31,7 +31,8 @@ import { TABS_TOP, topLayout } from './topRow';
|
|
|
31
31
|
|
|
32
32
|
As a tool follows the pointer, only the moving parts (preview, snap marker, selection box) are drawn again; the
|
|
33
33
|
drawing is kept until it changes. Touches still reach editor/gestures.ts for the tools, Apple Pencil, and two- and
|
|
34
|
-
three-finger taps (undo, redo); it leaves the view to this canvas (setHostNavigation).
|
|
34
|
+
three-finger taps (undo, redo); it leaves the view to this canvas (setHostNavigation). A Pencil and fingers can be
|
|
35
|
+
down at once (fingers pan and pinch while it draws), so each touch is handed over with what it is (OwnType).
|
|
35
36
|
|
|
36
37
|
While a finger or Pencil places or drags something, the lens (render/lens.ts) shows the drawing around it twice as
|
|
37
38
|
close in a top corner: the same pictures again, through its zoom, clipped to it — nothing more is drawn for it.
|
|
@@ -52,8 +53,15 @@ interface Frame {
|
|
|
52
53
|
/** the latest of the UI thread's views it was drawn at (see UiView) */
|
|
53
54
|
seq: number;
|
|
54
55
|
}
|
|
55
|
-
/** A touch as the UI thread hands it over: id, position on the canvas, position in the window. */
|
|
56
|
-
type Packed = [id: number, x: number, y: number, ax: number, ay: number];
|
|
56
|
+
/** A touch as the UI thread hands it over: id, position on the canvas, position in the window, what it is. */
|
|
57
|
+
type Packed = [id: number, x: number, y: number, ax: number, ay: number, kind: string];
|
|
58
|
+
/**
|
|
59
|
+
* A touch that says what it is. react-native-gesture-handler gives an event one pointer type, whichever touch iOS lists
|
|
60
|
+
* first, so with an Apple Pencil and a finger both down it's a coin toss which the other is taken for; BOLDR patches it
|
|
61
|
+
* to say for each touch too (BOLDR_App patches/react-native-gesture-handler+*.patch). Without that, the event's is used.
|
|
62
|
+
*/
|
|
63
|
+
type OwnType = TouchData & { pointerType?: PointerType };
|
|
64
|
+
interface Typed { id: number; x: number; y: number; ax: number; ay: number; kind: string }
|
|
57
65
|
|
|
58
66
|
/** The latest of the UI thread's views this thread has taken; each frame says which it includes. */
|
|
59
67
|
let hostSeq = 0;
|
|
@@ -65,11 +73,11 @@ let placeLens: (() => void) | null = null;
|
|
|
65
73
|
* Touches from the UI thread: the view first, if the fingers moved it, then each touch to the editor's gestures (as a
|
|
66
74
|
* pointer lands, `down` is every one still down: any other's lift was lost).
|
|
67
75
|
*/
|
|
68
|
-
function touchesOnJS(type: number,
|
|
76
|
+
function touchesOnJS(type: number, ts: Packed[], view: UiView | null, down: number[] | null): void {
|
|
69
77
|
if (view) { hostSeq = view.seq; setViewFromHost(view.s, view.cx, view.cy); }
|
|
70
78
|
if (down) pointersStill(down);
|
|
71
79
|
const fn = type === NAV_DOWN ? pointerDown : type === NAV_MOVE ? pointerMove : type === NAV_UP ? pointerUp : pointerCancel, t = Date.now();
|
|
72
|
-
for (const [id, x, y, ax, ay] of ts) fn({ id, kind, x, y, t, cx: ax, cy: ay });
|
|
80
|
+
for (const [id, x, y, ax, ay, kind] of ts) fn({ id, kind, x, y, t, cx: ax, cy: ay });
|
|
73
81
|
// what a tool shows following the finger (a shape's outline, its marks, the snap), drawn now: left for the next frame
|
|
74
82
|
// it reached the screen a frame behind the finger. Only when nothing but those moving parts changed — the drawing
|
|
75
83
|
// itself, and the view, are drawn on the frame as before.
|
|
@@ -81,17 +89,22 @@ const kindOf = (t: PointerType): string => {
|
|
|
81
89
|
'worklet';
|
|
82
90
|
return t === PointerType.STYLUS ? 'pen' : t === PointerType.MOUSE ? 'mouse' : 'touch';
|
|
83
91
|
};
|
|
84
|
-
|
|
92
|
+
/** Each touch, and what it is: its own pointer type where it says, else the event's (see OwnType). */
|
|
93
|
+
const typed = (ts: TouchData[], type: PointerType): Typed[] => {
|
|
94
|
+
'worklet';
|
|
95
|
+
return ts.map(t => ({ id: t.id, x: t.x, y: t.y, ax: t.absoluteX, ay: t.absoluteY, kind: kindOf((t as OwnType).pointerType ?? type) }));
|
|
96
|
+
};
|
|
97
|
+
const pack = (ts: Typed[]): Packed[] => {
|
|
85
98
|
'worklet';
|
|
86
|
-
return ts.map(t => [t.id, t.x, t.y, t.
|
|
99
|
+
return ts.map(t => [t.id, t.x, t.y, t.ax, t.ay, t.kind] as Packed);
|
|
87
100
|
};
|
|
88
101
|
|
|
89
102
|
export const PlanCanvas = memo(function PlanCanvas() {
|
|
90
103
|
const first = useRef(true);
|
|
91
104
|
const frame = useSharedValue<Frame | null>(null);
|
|
92
105
|
const view = useSharedValue<UiView>({ s: 1, cx: 0, cy: 0, seq: 0 });
|
|
93
|
-
// the UI thread's own: the fingers down, whether they're navigating and where they were last,
|
|
94
|
-
const nav = useSharedValue<NavState>({ fingers: {}, navigating: false, last: null,
|
|
106
|
+
// the UI thread's own: the fingers down, whether they're navigating and where they were last, any Pencil down
|
|
107
|
+
const nav = useSharedValue<NavState>({ fingers: {}, navigating: false, last: null, pens: [] });
|
|
95
108
|
const readOnly = useSharedValue(false);
|
|
96
109
|
// the lens, and whether it's up (mounted only then: nothing drawn twice for it otherwise)
|
|
97
110
|
const lens = useSharedValue<Lens | null>(null);
|
|
@@ -187,11 +200,11 @@ export const PlanCanvas = memo(function PlanCanvas() {
|
|
|
187
200
|
/** A touch event on the UI thread: follow the fingers (render/navigate.ts), then hand the touches to this thread. */
|
|
188
201
|
const touch = (type: number, e: GestureTouchEvent) => {
|
|
189
202
|
'worklet';
|
|
190
|
-
const
|
|
191
|
-
const r = navTouch(nav.value, type,
|
|
203
|
+
const ts = typed(e.changedTouches, e.pointerType), down = type === NAV_DOWN ? e.allTouches.map(t => t.id) : null;
|
|
204
|
+
const r = navTouch(nav.value, type, kindOf(e.pointerType), ts, down, readOnly.value, view.value, frame.value);
|
|
192
205
|
nav.value = r.st;
|
|
193
206
|
if (r.view !== view.value) view.value = r.view;
|
|
194
|
-
runOnJS(touchesOnJS)(type,
|
|
207
|
+
runOnJS(touchesOnJS)(type, pack(ts), r.moved ? r.view : null, down);
|
|
195
208
|
};
|
|
196
209
|
const touches = Gesture.Manual()
|
|
197
210
|
.onTouchesDown((e, manager) => {
|