@reekon-tools/boldr-cad 1.0.0 → 1.2.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 +38 -8
- package/dist/data/memory.d.ts +4 -1
- package/dist/data/memory.js +11 -6
- package/dist/data/memory.js.map +1 -1
- package/dist/data/provider.d.ts +11 -3
- package/dist/data/sync.d.ts +7 -1
- package/dist/data/sync.js +18 -2
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.js +13 -1
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/elevation.js +1 -1
- package/dist/editor/elevation.js.map +1 -1
- package/dist/editor/face.js +2 -2
- package/dist/editor/face.js.map +1 -1
- package/dist/editor/keypad.d.ts +1 -1
- package/dist/editor/keypad.js +24 -7
- package/dist/editor/keypad.js.map +1 -1
- package/dist/editor/ports.d.ts +2 -0
- package/dist/editor/ports.js.map +1 -1
- package/dist/editor/project.d.ts +2 -2
- package/dist/editor/project.js +7 -4
- package/dist/editor/project.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model/planGeom.js +2 -2
- package/dist/model/planGeom.js.map +1 -1
- package/dist/react/CadEditor.d.ts +8 -1
- package/dist/react/CadEditor.js +3 -2
- package/dist/react/CadEditor.js.map +1 -1
- package/dist/react/Popovers.js +2 -2
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/host.d.ts +10 -0
- package/dist/react/host.js +19 -1
- package/dist/react/host.js.map +1 -1
- package/dist/tools/chamfer.js +3 -3
- package/dist/tools/chamfer.js.map +1 -1
- package/dist/tools/dim.js +4 -4
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/elevation.js +4 -4
- package/dist/tools/elevation.js.map +1 -1
- package/dist/tools/fillet.js +1 -1
- package/dist/tools/fillet.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/openings.js +3 -3
- package/dist/tools/openings.js.map +1 -1
- package/dist/tools/survey.d.ts +24 -0
- package/dist/tools/survey.js +233 -0
- package/dist/tools/survey.js.map +1 -0
- package/dist/units/format.d.ts +1 -1
- package/dist/units/format.js +9 -3
- package/dist/units/format.js.map +1 -1
- package/dist/units/parse.d.ts +2 -1
- package/dist/units/parse.js +5 -4
- package/dist/units/parse.js.map +1 -1
- package/dist/units/prefs.d.ts +9 -1
- package/dist/units/prefs.js +6 -0
- package/dist/units/prefs.js.map +1 -1
- package/package.json +1 -1
- package/src/data/memory.ts +13 -8
- package/src/data/provider.ts +10 -3
- package/src/data/sync.ts +20 -3
- package/src/editor/actions.ts +14 -3
- package/src/editor/elevation.ts +1 -1
- package/src/editor/face.ts +2 -2
- package/src/editor/keypad.ts +24 -7
- package/src/editor/ports.ts +2 -0
- package/src/editor/project.ts +7 -5
- package/src/index.ts +3 -2
- package/src/model/planGeom.ts +2 -2
- package/src/react/CadEditor.tsx +10 -5
- package/src/react/Popovers.tsx +3 -3
- package/src/react/host.ts +19 -1
- package/src/tools/chamfer.ts +3 -3
- package/src/tools/dim.ts +4 -4
- package/src/tools/elevation.ts +4 -4
- package/src/tools/fillet.ts +1 -1
- package/src/tools/index.ts +4 -0
- package/src/tools/openings.ts +3 -3
- package/src/tools/survey.ts +216 -0
- package/src/units/format.ts +6 -3
- package/src/units/parse.ts +4 -3
- package/src/units/prefs.ts +13 -1
package/src/editor/keypad.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sessionState } from '../model/session';
|
|
2
2
|
import { settings } from '../model/settings';
|
|
3
|
-
import { fmtAng, fmtLen, parseLen, parseNum } from '../units';
|
|
3
|
+
import { fmtAng, fmtLen, parseLen, parseNum, showFractions } from '../units';
|
|
4
4
|
import { closeKeypad, refreshKeypad, type KeypadSpec } from './ports';
|
|
5
5
|
|
|
6
6
|
/*
|
|
@@ -13,6 +13,8 @@ interface Kp {
|
|
|
13
13
|
spec: KeypadSpec; text: string; fresh: boolean; error?: string; host?: boolean;
|
|
14
14
|
/** the last laser reading's record (laserReading's capture), and what it put in the display: kept while that's unchanged */
|
|
15
15
|
capture?: { of: unknown; text: string };
|
|
16
|
+
/** the exact value behind the display (inches), and the text it was shown as: it's the value while that text stands */
|
|
17
|
+
exact?: { v: number; text: string };
|
|
16
18
|
}
|
|
17
19
|
const kps = sessionState(() => ({ cur: null as Kp | null }));
|
|
18
20
|
|
|
@@ -32,12 +34,24 @@ export const keypadOpen = (): boolean => !!kps.cur;
|
|
|
32
34
|
export const keypadSpec = (): KeypadSpec | null => kps.cur?.spec ?? null;
|
|
33
35
|
export const keypadText = (): string => kps.cur?.text ?? '';
|
|
34
36
|
export const keypadFresh = (): boolean => !!kps.cur?.fresh;
|
|
35
|
-
/** The fraction row shows for lengths
|
|
36
|
-
export const fractionRowShown = (): boolean => !(kps.cur?.spec.numeric ||
|
|
37
|
+
/** The fraction row shows for lengths read with fractions (feet and inches), not for degrees, metric or decimal units. */
|
|
38
|
+
export const fractionRowShown = (): boolean => !(kps.cur?.spec.numeric || !showFractions());
|
|
39
|
+
|
|
40
|
+
const isMetricUnit = (): boolean => settings.unit === 'mm' || settings.unit === 'cm' || settings.unit === 'm';
|
|
41
|
+
/** An example to type, in the person's units. */
|
|
42
|
+
const lengthExample = (): string => {
|
|
43
|
+
const u = settings.unit;
|
|
44
|
+
if (u === 'mm') return '2540 or 8\' 4"';
|
|
45
|
+
if (u === 'cm') return '254 or 2.5 m';
|
|
46
|
+
if (u === 'm') return '2.54 or 254 cm';
|
|
47
|
+
if (u === 'ft') return '8.5 or 10\' 6"';
|
|
48
|
+
return showFractions() ? '12\' 6" or 150.5' : '12\' 6.5" or 150.5';
|
|
49
|
+
};
|
|
37
50
|
|
|
38
51
|
/** Open the keypad for `spec`; `host`: the host shows its own keypad for it (see react/hostKeypad.ts). */
|
|
39
52
|
export function keypadStart(spec: KeypadSpec, host = false): void {
|
|
40
|
-
|
|
53
|
+
const text = spec.initial || '';
|
|
54
|
+
kps.cur = { spec, text, fresh: !!spec.selectAll, host, exact: spec.value !== undefined && text ? { v: spec.value, text } : undefined };
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/** Whether the open keypad is the host's own. */
|
|
@@ -99,6 +113,7 @@ export function keypadDeleteLast(): void {
|
|
|
99
113
|
export function keypadSetReading(v: number): void {
|
|
100
114
|
if (!kps.cur) return;
|
|
101
115
|
kps.cur.text = fmtLen(v);
|
|
116
|
+
kps.cur.exact = { v, text: kps.cur.text };
|
|
102
117
|
kps.cur.fresh = true;
|
|
103
118
|
kps.cur.error = undefined;
|
|
104
119
|
}
|
|
@@ -120,7 +135,9 @@ export function keypadTakeLaser(v: number, capture?: unknown): boolean {
|
|
|
120
135
|
/** The last reading's record, if the display still shows what it put there. */
|
|
121
136
|
const readingCapture = (k: Kp): unknown => (k.capture && k.capture.text === k.text ? k.capture.of : undefined);
|
|
122
137
|
|
|
123
|
-
|
|
138
|
+
// (a length shown rounded, left as it is, applies as the exact value it stands for)
|
|
139
|
+
const parse = (k: Kp): number | null =>
|
|
140
|
+
k.spec.numeric ? parseNum(k.text) : k.exact && k.exact.text === k.text ? k.exact.v : parseLen(k.text);
|
|
124
141
|
|
|
125
142
|
/** The value currently typed, or null if none/unreadable (for live previews on the canvas). */
|
|
126
143
|
export function keypadTyped(): number | null {
|
|
@@ -134,7 +151,7 @@ export function keypadPreview(): { text: string; bad: boolean } {
|
|
|
134
151
|
const k = kps.cur;
|
|
135
152
|
if (!k) return { text: '', bad: false };
|
|
136
153
|
if (k.error) return { text: k.error, bad: true };
|
|
137
|
-
if (!k.text.trim()) return { text: k.spec.numeric ? 'Type a number' : 'Try 12\' 6 1/2" or 150.5', bad: false };
|
|
154
|
+
if (!k.text.trim()) return { text: k.spec.numeric ? 'Type a number' : 'Try ' + (showFractions() && !isMetricUnit() ? '12\' 6 1/2" or 150.5' : lengthExample()), bad: false };
|
|
138
155
|
const v = parse(k);
|
|
139
156
|
if (v == null) return { text: 'Can’t read that yet', bad: true };
|
|
140
157
|
return { text: '= ' + (k.spec.numeric ? fmtAng(v) : fmtLen(v)), bad: false };
|
|
@@ -143,7 +160,7 @@ export function keypadPreview(): { text: string; bad: boolean } {
|
|
|
143
160
|
/** Apply: the value, or the message to show when it can't be read. The caller closes the keypad and runs onDone. */
|
|
144
161
|
export function keypadValue(): { v: number } | { error: string } {
|
|
145
162
|
const v = kps.cur ? parse(kps.cur) : null;
|
|
146
|
-
return v == null ? { error: 'Enter a length like
|
|
163
|
+
return v == null ? { error: 'Enter a length like ' + lengthExample() } : { v };
|
|
147
164
|
}
|
|
148
165
|
|
|
149
166
|
/** A value from the host's own keypad (or a laser reading into it): close the keypad and hand it on, as Apply does. */
|
package/src/editor/ports.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface KeypadSpec {
|
|
|
26
26
|
title: string;
|
|
27
27
|
sub?: string;
|
|
28
28
|
initial?: string;
|
|
29
|
+
/** the exact value `initial` shows (inches): applied as it is while the text isn't changed, so a value shown rounded doesn't move */
|
|
30
|
+
value?: number;
|
|
29
31
|
/** typing replaces the initial value */
|
|
30
32
|
selectAll?: boolean;
|
|
31
33
|
/** degrees keypad instead of lengths */
|
package/src/editor/project.ts
CHANGED
|
@@ -215,7 +215,7 @@ export function setDefaultWallHeight(v: number): boolean {
|
|
|
215
215
|
/** Settings → Wall height → Custom: any height, on the keypad. */
|
|
216
216
|
export function editDefaultWallHeight(): void {
|
|
217
217
|
openKeypad({
|
|
218
|
-
title: 'Wall height', sub: 'Walls without their own height', initial: fmtLen(settings.wallH), selectAll: true, laser: true,
|
|
218
|
+
title: 'Wall height', sub: 'Walls without their own height', initial: fmtLen(settings.wallH), value: settings.wallH, selectAll: true, laser: true,
|
|
219
219
|
onDone: v => { setDefaultWallHeight(v); },
|
|
220
220
|
});
|
|
221
221
|
}
|
|
@@ -224,16 +224,17 @@ export type UnitChoice = 'account' | Unit;
|
|
|
224
224
|
const UNITS_KEY = 'boldr-cad-units';
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
|
-
* Settings → Units: the editor's own units (ft-in, in, mm), or 'account' — the host's (BOLDR: the person's
|
|
228
|
-
* settings). Remembered in this browser, so it sticks from drawing to drawing.
|
|
227
|
+
* Settings → Units: the editor's own units (ft-in, in, ft, mm, cm, m), or 'account' — the host's (BOLDR: the person's
|
|
228
|
+
* account settings). Remembered in this browser, so it sticks from drawing to drawing.
|
|
229
229
|
*/
|
|
230
230
|
export function setUnits(choice: UnitChoice): void {
|
|
231
231
|
const host = hostUnits();
|
|
232
232
|
if (choice === 'account') {
|
|
233
|
-
if (host) { settings.unit = host.unit; settings.denom = host.denom; }
|
|
233
|
+
if (host) { settings.unit = host.unit; settings.denom = host.denom; settings.fractions = host.fractions; }
|
|
234
234
|
setOwnUnits(false);
|
|
235
235
|
} else {
|
|
236
236
|
settings.unit = choice;
|
|
237
|
+
settings.fractions = undefined; // (the editor's own units read with fractions where that's their way)
|
|
237
238
|
setOwnUnits(!!host);
|
|
238
239
|
}
|
|
239
240
|
rememberUnits();
|
|
@@ -259,8 +260,9 @@ export function restoreUnits(): void {
|
|
|
259
260
|
if (!hostUnits()) return;
|
|
260
261
|
try {
|
|
261
262
|
const v = JSON.parse(localStorage.getItem(UNITS_KEY) || 'null') as 'account' | { unit?: string; denom?: number } | null;
|
|
262
|
-
if (!v || v === 'account' || !['ftin', 'in', 'mm'].includes(v.unit ?? '')) return;
|
|
263
|
+
if (!v || v === 'account' || !['ftin', 'in', 'ft', 'mm', 'cm', 'm'].includes(v.unit ?? '')) return;
|
|
263
264
|
settings.unit = v.unit as Unit;
|
|
265
|
+
settings.fractions = undefined;
|
|
264
266
|
if (v.denom === 4 || v.denom === 8 || v.denom === 16) settings.denom = v.denom;
|
|
265
267
|
setOwnUnits(true);
|
|
266
268
|
} catch { /* nothing remembered */ }
|
package/src/index.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
export { CadEditor, type CadEditorProps } from './react/CadEditor';
|
|
8
8
|
export { openCadDrawing, measurementOf, NotACadDrawing, type CadSync, type CadSyncOptions } from './data/sync';
|
|
9
|
-
export type { CadDataProvider, CadFileMeta, CadMeasurements, CadMeasurementValue, CadPageDoc } from './data/provider';
|
|
9
|
+
export type { CadDataProvider, CadFileMeta, CadMeasurements, CadMeasurementValue, CadPageDoc, CadSnapshotMeta } from './data/provider';
|
|
10
|
+
export type { Unit } from './units';
|
|
10
11
|
export { memoryProvider } from './data/memory';
|
|
11
12
|
export { setFileSaver } from './react/ports';
|
|
12
|
-
export { laserReading, setLaserConnected } from './react/host';
|
|
13
|
+
export { laserReading, setLaserConnected, surveyPoint } from './react/host';
|
|
13
14
|
export type { HostKeypad, HostKeypadExtra, HostKeypadRequest, MeasurementList, MeasurementListPage } from './react/hostKeypad';
|
|
14
15
|
export type { CadLinkPick, CadLinks, CadSaveMeasurement, MeasurementKind } from './editor/links';
|
|
15
16
|
export type { LinkInfo, LinkTarget } from './model/types';
|
package/src/model/planGeom.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { roomGeom } from './rooms';
|
|
|
3
3
|
import { frameMemo, roomValid } from './sketch';
|
|
4
4
|
import type { Arc, Constraint, LineKind, Sketch, XY } from './types';
|
|
5
5
|
import { faceCorner } from './wallfaces';
|
|
6
|
-
import { getUnitPrefs, type Unit } from '../units';
|
|
6
|
+
import { getUnitPrefs, isMetric, type Unit } from '../units';
|
|
7
7
|
|
|
8
8
|
/*
|
|
9
9
|
Plan geometry shared by drawing, hit testing, snapping and the tools: grid steps, where a door/window sits on
|
|
@@ -23,7 +23,7 @@ const MM = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000,
|
|
|
23
23
|
const MM_MAJOR = [10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000].map(v => v / 25.4);
|
|
24
24
|
|
|
25
25
|
export function gridSteps(s: number, unit: Unit = getUnitPrefs().unit): { minor: number; major: number } {
|
|
26
|
-
const metric = unit
|
|
26
|
+
const metric = isMetric(unit), list = metric ? MM : IMPERIAL;
|
|
27
27
|
const minor = list.find(v => v * s >= GRID_MIN_PX) || list[list.length - 1];
|
|
28
28
|
const major = (metric ? MM_MAJOR : IMPERIAL_MAJOR).find(v => v >= minor * 4 - 1e-9) || minor * 5;
|
|
29
29
|
return { minor, major };
|
package/src/react/CadEditor.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { settings } from '../model/settings';
|
|
|
8
8
|
import { load, saveNow, setPersistence, store } from '../model/store';
|
|
9
9
|
import { renderThumbnail } from './thumbnail';
|
|
10
10
|
import type { CadSync } from '../data/sync';
|
|
11
|
-
import { setLengthFormatter } from '../units';
|
|
11
|
+
import { setLengthFormatter, type Unit } from '../units';
|
|
12
12
|
import { CANVAS_THEMES, setCanvasTheme } from '../render/theme';
|
|
13
13
|
import { requestDraw, setImageResolver } from '../render/view';
|
|
14
14
|
import { registerTools } from '../tools';
|
|
@@ -62,8 +62,12 @@ export interface CadEditorProps {
|
|
|
62
62
|
theme?: 'light' | 'dark';
|
|
63
63
|
/** The drawing, opened with openCadDrawing (saved there as it changes). Without it, the drawing lives in this browser. */
|
|
64
64
|
sync?: CadSync;
|
|
65
|
-
/**
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Lengths the host's way: how to write one (inches in), the unit a bare number is typed in (`unit`; without it,
|
|
67
|
+
* `metric` alone means millimetres), whether lengths are typed with fractions (the fraction keys; unset: feet-inches
|
|
68
|
+
* and inches are), and the fraction precision. Settings' Units then offers Account beside the editor's own units.
|
|
69
|
+
*/
|
|
70
|
+
units?: { format: (inches: number) => string; metric: boolean; unit?: Unit; fractions?: boolean; denom?: number };
|
|
67
71
|
/** Developer conveniences in Settings (reset to the demo, open and save project files). Default on. */
|
|
68
72
|
devTools?: boolean;
|
|
69
73
|
/** View only: pan and zoom, no tools, nothing saved (for people who can't edit the job). */
|
|
@@ -129,10 +133,11 @@ export function CadEditor({ onBack, headerRight, pickImage = pickImageInBrowser,
|
|
|
129
133
|
: sync.persistence)
|
|
130
134
|
: null);
|
|
131
135
|
if (units) {
|
|
132
|
-
settings.unit = units.metric ? 'mm' : 'ftin';
|
|
136
|
+
settings.unit = units.unit ?? (units.metric ? 'mm' : 'ftin');
|
|
137
|
+
settings.fractions = units.fractions;
|
|
133
138
|
if (units.denom) settings.denom = units.denom;
|
|
134
139
|
}
|
|
135
|
-
setLengthFormatter(units ? units.format : null, { unit: settings.unit, denom: settings.denom });
|
|
140
|
+
setLengthFormatter(units ? units.format : null, { unit: settings.unit, denom: settings.denom, fractions: settings.fractions });
|
|
136
141
|
setSkiaFonts(fonts ?? null);
|
|
137
142
|
setFileSaver(saveFile ?? null);
|
|
138
143
|
setImageResolver(sync?.imageUrl ?? null);
|
package/src/react/Popovers.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { page } from '../model/store';
|
|
9
9
|
import { fitView } from '../render/view';
|
|
10
10
|
import { command3d, is3d } from '../editor/view3dState';
|
|
11
|
-
import { fmtLen, followingHostUnits } from '../units';
|
|
11
|
+
import { fmtLen, followingHostUnits, showFractions } from '../units';
|
|
12
12
|
import { settings } from '../model/settings';
|
|
13
13
|
import { Toggle } from './Buttons';
|
|
14
14
|
import { FitScroll } from './FitScroll';
|
|
@@ -90,11 +90,11 @@ function MoreMenu({ open, style, accountUnits, devTools, themeChoice, pickText,
|
|
|
90
90
|
<MRow label="Units">
|
|
91
91
|
<Seg<UnitChoice>
|
|
92
92
|
value={accountUnits && followingHostUnits() ? 'account' : settings.unit}
|
|
93
|
-
opts={[...(accountUnits ? [['account', 'Account'] as [UnitChoice, string]] : []), ['ftin', 'ft-in'], ['in', 'in'], ['mm', 'mm']]}
|
|
93
|
+
opts={[...(accountUnits ? [['account', 'Account'] as [UnitChoice, string]] : []), ['ftin', 'ft-in'], ['in', 'in'], ['ft', 'ft'], ['mm', 'mm'], ['cm', 'cm'], ['m', 'm']]}
|
|
94
94
|
onPick={setUnits}
|
|
95
95
|
/>
|
|
96
96
|
</MRow>
|
|
97
|
-
{!(accountUnits && followingHostUnits()) &&
|
|
97
|
+
{!(accountUnits && followingHostUnits()) && showFractions() && (
|
|
98
98
|
<MRow label="Precision"><Seg value={settings.denom} opts={[[16, '1/16"'], [8, '1/8"'], [4, '1/4"']]} onPick={setPrecision} /></MRow>
|
|
99
99
|
)}
|
|
100
100
|
{/* 8', 9', 10' in a tap; Custom (showing the height when it's one) opens the keypad */}
|
package/src/react/host.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { applyReading, laser } from '../editor/laser';
|
|
2
2
|
import { store } from '../model/store';
|
|
3
|
+
import { takeSurveyPoint } from '../tools/survey';
|
|
3
4
|
import { notify } from './store';
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
7
|
For the host app's measuring devices (BOLDR's Bluetooth lasers and tapes): say when one is connected, and pass its
|
|
7
|
-
readings in. A reading goes into an open keypad, onto the one selected wall or line, or into the tray.
|
|
8
|
+
readings in. A reading goes into an open keypad, onto the one selected wall or line, or into the tray. A templating
|
|
9
|
+
device's points (surveyPoint) build the drawing itself.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
/** Whether an editor is on screen (CadEditor sets it): readings that arrive after it closed are left to the host. */
|
|
@@ -30,3 +32,19 @@ export function laserReading(inches: number, capture?: unknown): boolean {
|
|
|
30
32
|
notify();
|
|
31
33
|
return true;
|
|
32
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A point measured by a templating device (BOLDR's T1R), in inches, on the plan's axes: x to the right, y UP. Turning
|
|
38
|
+
* the device's own frame into that is the host's job, and it's where the handedness goes: a device whose angle turns
|
|
39
|
+
* clockwise (the T1R's does) has to negate y, or every template is drawn mirrored. Each point joins the last, with its length and the
|
|
40
|
+
* corner's angle held (src/tools/survey.ts). The first one on a page lands at the middle of the screen, and the rest
|
|
41
|
+
* keep their true place relative to it for the session. False when no drawing is open to take it, or it can't now
|
|
42
|
+
* (a wall view or keypad open: the editor says so). `capture`: the host's record of the point, kept with the
|
|
43
|
+
* dimensions it sets and handed back on Save to group, as laserReading's is.
|
|
44
|
+
*/
|
|
45
|
+
export function surveyPoint(x: number, y: number, capture?: unknown): boolean {
|
|
46
|
+
if (!editorHost.open || !store.project || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
|
47
|
+
const ok = takeSurveyPoint(x, y, capture);
|
|
48
|
+
notify();
|
|
49
|
+
return ok;
|
|
50
|
+
}
|
package/src/tools/chamfer.ts
CHANGED
|
@@ -35,7 +35,7 @@ export function chamferPrompt(pid: string): void {
|
|
|
35
35
|
if (!k) { toast('Select a corner where two lines meet to cut it off.', { warn: true }); return; }
|
|
36
36
|
const most = Math.min(k.len1, k.len2), start = Math.round(Math.min(6, most * 0.5) * 16) / 16;
|
|
37
37
|
openKeypad({
|
|
38
|
-
title: 'Chamfer', sub: 'Back from the corner along each side — up to ' + fmtLen(most), initial: fmtLen(start), selectAll: true, laser: true,
|
|
38
|
+
title: 'Chamfer', sub: 'Back from the corner along each side — up to ' + fmtLen(most), initial: fmtLen(start), value: start, selectAll: true, laser: true,
|
|
39
39
|
extras: () => [{ label: 'Unequal', ariaLabel: 'Unequal: a distance for each side', onPress: () => { closeKeypad(); unequal(pid); } }],
|
|
40
40
|
onDone: d => { chamferCorner(pid, d, d); },
|
|
41
41
|
});
|
|
@@ -48,12 +48,12 @@ function unequal(pid: string): void {
|
|
|
48
48
|
const back = ed.sel;
|
|
49
49
|
showSide(k.l1);
|
|
50
50
|
openKeypad({
|
|
51
|
-
title: 'Chamfer · first side', sub: 'Along the selected line — up to ' + fmtLen(k.len1), initial: fmtLen(Math.round(Math.min(6, k.len1 * 0.5) * 16) / 16), selectAll: true, laser: true,
|
|
51
|
+
title: 'Chamfer · first side', sub: 'Along the selected line — up to ' + fmtLen(k.len1), initial: fmtLen(Math.round(Math.min(6, k.len1 * 0.5) * 16) / 16), value: Math.round(Math.min(6, k.len1 * 0.5) * 16) / 16, selectAll: true, laser: true,
|
|
52
52
|
onClose: () => { if (ed.sel.length === 1 && ed.sel[0].id === k.l1) { ed.sel = back; updateCtx(); requestDraw(); } },
|
|
53
53
|
onDone: d1 => {
|
|
54
54
|
showSide(k.l2);
|
|
55
55
|
openKeypad({
|
|
56
|
-
title: 'Chamfer · second side', sub: 'Along the selected line — up to ' + fmtLen(k.len2), initial: fmtLen(Math.round(Math.min(6, k.len2 * 0.5) * 16) / 16), selectAll: true, laser: true,
|
|
56
|
+
title: 'Chamfer · second side', sub: 'Along the selected line — up to ' + fmtLen(k.len2), initial: fmtLen(Math.round(Math.min(6, k.len2 * 0.5) * 16) / 16), value: Math.round(Math.min(6, k.len2 * 0.5) * 16) / 16, selectAll: true, laser: true,
|
|
57
57
|
onClose: () => { if (ed.sel.length === 1 && ed.sel[0].id === k.l2) { ed.sel = back; updateCtx(); requestDraw(); } },
|
|
58
58
|
onDone: d2 => { chamferCorner(pid, d1, d2); },
|
|
59
59
|
});
|
package/src/tools/dim.ts
CHANGED
|
@@ -190,7 +190,7 @@ export function dimLine(lid: string, print: boolean, side?: 1 | -1): void {
|
|
|
190
190
|
const show = () => { ed.sel = face ? [{ type: 'wface', id: edgeId(lid, face) }] : [{ type: 'line', id: lid }]; requestDraw(); };
|
|
191
191
|
show();
|
|
192
192
|
const spec: KeypadSpec = {
|
|
193
|
-
title: 'Length', sub: wall ? lengthSub(sk, lid, face) : '', initial: fmtLen(len()), selectAll: true, laser: true,
|
|
193
|
+
title: 'Length', sub: wall ? lengthSub(sk, lid, face) : '', initial: fmtLen(len()), value: len(), selectAll: true, laser: true,
|
|
194
194
|
extras: wall ? () => lengthChoices(sk, lid).map(ch => ({
|
|
195
195
|
label: ch.label!, on: ch.face === face,
|
|
196
196
|
onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, lid, face); show(); if (keypadFresh()) keypadSetReading(len()); },
|
|
@@ -423,7 +423,7 @@ export function dimPoints(a: string, b: string, print: boolean): void {
|
|
|
423
423
|
requestDraw();
|
|
424
424
|
openKeypad({
|
|
425
425
|
onClose: () => { ed.preview = null; requestDraw(); },
|
|
426
|
-
title: 'Distance', initial: fmtLen(L), selectAll: true, laser: true,
|
|
426
|
+
title: 'Distance', initial: fmtLen(L), value: L, selectAll: true, laser: true,
|
|
427
427
|
onDone: (v, capture) => {
|
|
428
428
|
pushHistory();
|
|
429
429
|
const r = tryAddConstraint({ type: 'dist', a, b, kind: 'al', value: v, driving: true, off: 0, ...(capture !== undefined ? { capture } : {}) });
|
|
@@ -492,7 +492,7 @@ export function dimPointLine(p: string, lid: string, opts: { keepParallel?: stri
|
|
|
492
492
|
const spec: KeypadSpec = {
|
|
493
493
|
onClose: () => { ed.preview = null; requestDraw(); },
|
|
494
494
|
// (which faces it's measured between: the choices beside the keys)
|
|
495
|
-
title: opts.title || 'Distance to line', sub: opts.keepParallel ? 'Measured square to the lines' : '', initial: fmtLen(measure(sk, c)), selectAll: true,
|
|
495
|
+
title: opts.title || 'Distance to line', sub: opts.keepParallel ? 'Measured square to the lines' : '', initial: fmtLen(measure(sk, c)), value: measure(sk, c), selectAll: true,
|
|
496
496
|
extras: choices.length ? () => choices.map(ch => ({
|
|
497
497
|
label: ch.label!, on: onChoice(c, ch),
|
|
498
498
|
onPress: () => { applyFace(c, ch); requestOverlayDraw(); if (keypadFresh()) keypadSetReading(measure(sk, c)); },
|
|
@@ -525,7 +525,7 @@ export function dimCurve(id: string, side?: 1 | -1): void {
|
|
|
525
525
|
const show = () => { ed.sel = face ? [{ type: 'wface', id: edgeId(id, face) }] : [{ type, id }]; requestDraw(); };
|
|
526
526
|
show();
|
|
527
527
|
const spec: KeypadSpec = {
|
|
528
|
-
title: 'Diameter', sub: wall ? lengthSub(sk, id, face) : '', initial: fmtLen(now()), selectAll: true,
|
|
528
|
+
title: 'Diameter', sub: wall ? lengthSub(sk, id, face) : '', initial: fmtLen(now()), value: now(), selectAll: true,
|
|
529
529
|
extras: wall ? () => curveChoices(sk, id).map(ch => ({
|
|
530
530
|
label: ch.label!, on: ch.face === face,
|
|
531
531
|
onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, id, face); show(); if (keypadFresh()) keypadSetReading(now()); },
|
package/src/tools/elevation.ts
CHANGED
|
@@ -25,7 +25,7 @@ export function editSill(opId: string): void {
|
|
|
25
25
|
if (!l) return;
|
|
26
26
|
const { sill, head } = openingHeights(o), h = head - sill, H = wallHeight(l);
|
|
27
27
|
openKeypad({
|
|
28
|
-
title: Name(o) + ' sill height', sub: 'Floor to the bottom of the ' + nameOf(o) + ' — locks its height above the floor', initial: fmtLen(sill), selectAll: true, laser: true,
|
|
28
|
+
title: Name(o) + ' sill height', sub: 'Floor to the bottom of the ' + nameOf(o) + ' — locks its height above the floor', initial: fmtLen(sill), value: sill, selectAll: true, laser: true,
|
|
29
29
|
onDone: v => {
|
|
30
30
|
if (v < 0) return;
|
|
31
31
|
if (v + h > H + 1e-6) {
|
|
@@ -46,7 +46,7 @@ export function editOpeningHeight(opId: string): void {
|
|
|
46
46
|
if (!l) return;
|
|
47
47
|
const { sill, head } = openingHeights(o), H = wallHeight(l);
|
|
48
48
|
openKeypad({
|
|
49
|
-
title: Name(o) + ' height', sub: 'Bottom to top of the ' + nameOf(o), initial: fmtLen(head - sill), selectAll: true, laser: true,
|
|
49
|
+
title: Name(o) + ' height', sub: 'Bottom to top of the ' + nameOf(o), initial: fmtLen(head - sill), value: head - sill, selectAll: true, laser: true,
|
|
50
50
|
onDone: v => {
|
|
51
51
|
if (v <= 0) return;
|
|
52
52
|
if (sill + v > H + 1e-6) {
|
|
@@ -67,7 +67,7 @@ export function editWallHeight(lid: string): void {
|
|
|
67
67
|
if (!l) return;
|
|
68
68
|
const tallest = Math.max(0, ...Object.values(sk.openings).filter(o => o.line === lid).map(o => openingHeights(o).head));
|
|
69
69
|
openKeypad({
|
|
70
|
-
title: 'Wall height', sub: 'Floor to ceiling, this wall only', initial: fmtLen(wallHeight(l)), selectAll: true, laser: true,
|
|
70
|
+
title: 'Wall height', sub: 'Floor to ceiling, this wall only', initial: fmtLen(wallHeight(l)), value: wallHeight(l), selectAll: true, laser: true,
|
|
71
71
|
onDone: v => {
|
|
72
72
|
if (v <= 0) return;
|
|
73
73
|
if (v < tallest - 1e-6) { toast('A door or window here reaches ' + fmtLen(tallest) + '. Enter ' + fmtLen(tallest) + ' or more, or lower it first.', { warn: true }); return; }
|
|
@@ -96,7 +96,7 @@ export function editElevationPosition(opId: string, edge: OpeningEdge, ref: Open
|
|
|
96
96
|
const d: OpeningDim = { id: uid('od'), edge, ref, value: cur, sign: e >= r ? 1 : -1, driving: true };
|
|
97
97
|
openKeypad({
|
|
98
98
|
title, sub: 'Locks the ' + nameOf(o) + '’s position',
|
|
99
|
-
initial: fmtLen(cur), selectAll: true, laser: true,
|
|
99
|
+
initial: fmtLen(cur), value: cur, selectAll: true, laser: true,
|
|
100
100
|
onDone: v => {
|
|
101
101
|
if (v < 0) return;
|
|
102
102
|
if (!fits(o, d, v)) return;
|
package/src/tools/fillet.ts
CHANGED
|
@@ -35,7 +35,7 @@ export function filletPrompt(pid: string): void {
|
|
|
35
35
|
if (!cornerAt(sk, pid)) { toast('Select a corner where two lines meet to round it off.', { warn: true }); return; }
|
|
36
36
|
const most = maxFillet(sk, pid), start = Math.min(6, most * 0.5);
|
|
37
37
|
openKeypad({
|
|
38
|
-
title: 'Fillet radius', sub: 'Up to ' + fmtLen(most) + ' fits this corner', initial: fmtLen(Math.round(start * 16) / 16), selectAll: true, laser: true,
|
|
38
|
+
title: 'Fillet radius', sub: 'Up to ' + fmtLen(most) + ' fits this corner', initial: fmtLen(Math.round(start * 16) / 16), value: Math.round(start * 16) / 16, selectAll: true, laser: true,
|
|
39
39
|
onDone: r => { filletCorner(pid, r); },
|
|
40
40
|
});
|
|
41
41
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -10,10 +10,12 @@ import { ed } from '../app/state';
|
|
|
10
10
|
import { tools } from './registry';
|
|
11
11
|
import { selectTool } from './select';
|
|
12
12
|
import { arcTool, circleTool, rectTool } from './shapes';
|
|
13
|
+
import { surveyTool } from './survey';
|
|
13
14
|
import { cabinetTool, heightTool, wallDoorTool, wallWindowTool } from '../editor/elevation';
|
|
14
15
|
|
|
15
16
|
export { tools, setTool, cancelTool } from './registry';
|
|
16
17
|
export type { LineTool } from './line';
|
|
18
|
+
export type { SurveyTool } from './survey';
|
|
17
19
|
|
|
18
20
|
/** Register every tool. Called once from main (not at import time, so import cycles stay harmless). */
|
|
19
21
|
export function registerTools(): void {
|
|
@@ -31,6 +33,8 @@ export function registerTools(): void {
|
|
|
31
33
|
door: Object.assign({}, openingTool, { enter() { ed.openingType = 'door'; } }),
|
|
32
34
|
window: Object.assign({}, openingTool, { enter() { ed.openingType = 'window'; } }),
|
|
33
35
|
room: roomTool,
|
|
36
|
+
// the templator: not on the rail — a point from the host's device picks it up (see react/host.ts surveyPoint)
|
|
37
|
+
survey: surveyTool,
|
|
34
38
|
aselect: makeAselectTool(),
|
|
35
39
|
// markup tools: a press on a handle of the just-drawn markup reshapes it instead of drawing new
|
|
36
40
|
pen: withHandles(penTool),
|
package/src/tools/openings.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function addOpeningDim(opId: string, edge: OpeningEdge, ref: OpeningRef,
|
|
|
90
90
|
onClose: () => { ed.preview = null; requestDraw(); },
|
|
91
91
|
title: title || 'Distance to ' + name.toLowerCase(),
|
|
92
92
|
sub: d.driving ? 'Locks the ' + name.toLowerCase() + '’s position' : 'Already set by other dimensions — this one can only be a reference',
|
|
93
|
-
initial: fmtLen(cur), selectAll: true, laser: true,
|
|
93
|
+
initial: fmtLen(cur), value: cur, selectAll: true, laser: true,
|
|
94
94
|
onDone: v => {
|
|
95
95
|
if (v < 0) return;
|
|
96
96
|
if (!d.driving) {
|
|
@@ -125,7 +125,7 @@ export function editOpeningDim(id: string, part: OpeningDimPart): void {
|
|
|
125
125
|
focusOpening(id);
|
|
126
126
|
const name = nameOf(o);
|
|
127
127
|
openKeypad({
|
|
128
|
-
title: name + ' width', sub: 'Edge to edge, along the wall', initial: fmtLen(g.w), selectAll: true, laser: true,
|
|
128
|
+
title: name + ' width', sub: 'Edge to edge, along the wall', initial: fmtLen(g.w), value: g.w, selectAll: true, laser: true,
|
|
129
129
|
onDone: v => {
|
|
130
130
|
if (v <= 0) return;
|
|
131
131
|
if (v > g.L + 1e-9) { toast('That’s wider than the wall (' + fmtLen(g.L) + '). Enter ' + fmtLen(g.L) + ' or less.', { warn: true }); return; }
|
|
@@ -148,7 +148,7 @@ export function editPlacedOpeningDim(opId: string, dimId: string): void {
|
|
|
148
148
|
openKeypad({
|
|
149
149
|
title: 'Distance to ' + name,
|
|
150
150
|
sub: d.driving ? 'Driving — locks the ' + name + '’s position' : 'Reference — a value makes it lock the ' + name,
|
|
151
|
-
initial: fmtLen(cur), selectAll: true, laser: true,
|
|
151
|
+
initial: fmtLen(cur), value: cur, selectAll: true, laser: true,
|
|
152
152
|
// (no Reference or Delete here: they're the bar's, for the dimension selected)
|
|
153
153
|
onDone: v => {
|
|
154
154
|
if (v < 0) return;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { afterEdit, analyze, tryAddConstraint } from '../app/edit';
|
|
2
|
+
import { ed } from '../app/state';
|
|
3
|
+
import { measure } from '../model/constraints';
|
|
4
|
+
import { hyp } from '../model/geom';
|
|
5
|
+
import { pushHistory } from '../model/history';
|
|
6
|
+
import { maybeRoom, roomGeom } from '../model/rooms';
|
|
7
|
+
import { onSessionReset, sessionState } from '../model/session';
|
|
8
|
+
import { addLine, addPoint, gc, lineBetween, pinnedPoints } from '../model/sketch';
|
|
9
|
+
import { page, SK, store } from '../model/store';
|
|
10
|
+
import type { Constraint } from '../model/types';
|
|
11
|
+
import { requestDraw, requestOverlayDraw, S, V } from '../render/view';
|
|
12
|
+
import { fmtArea, fmtLen } from '../units';
|
|
13
|
+
import { elevOpen } from '../editor/elevation';
|
|
14
|
+
import { keypadOpen } from '../editor/keypad';
|
|
15
|
+
import type { KeyIn } from '../editor/keys';
|
|
16
|
+
import { drawsWalls, setMode } from '../editor/mode';
|
|
17
|
+
import { toast, updateCtx, updateHint } from '../editor/ports';
|
|
18
|
+
import { close3d } from '../editor/view3d';
|
|
19
|
+
import { is3d } from '../editor/view3dState';
|
|
20
|
+
import { anchorMark, setTool, type Tool } from './registry';
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
The templator: points measured by a device in the host app (BOLDR's T1R) build the drawing directly. Each point joins
|
|
24
|
+
the last with a line (or a wall, with the Walls toggle on), and the drawing holds what was measured: a driving
|
|
25
|
+
length on every edge, and a driving angle at every corner. That fixes the shape's FORM — a later drag moves it
|
|
26
|
+
whole, it doesn't flex it — while leaving it free to be moved and turned into place. On a closed shape the last
|
|
27
|
+
edge and the corners either side of it are already set by the rest, so they come out as reference dimensions
|
|
28
|
+
(grey, in parentheses) rather than being refused: rule 3, and the closing edge's length still shows.
|
|
29
|
+
|
|
30
|
+
Where it lands: the device measures from itself, not from the page. The first point taken on a page puts its frame
|
|
31
|
+
there — that point at the middle of the screen — and every later point keeps its true place relative to it, for
|
|
32
|
+
the rest of the session. Kept per page and not per run on purpose: a cutout taken after its outline, from the same
|
|
33
|
+
setup, has to land where it really is relative to the outline, or templating it was pointless.
|
|
34
|
+
|
|
35
|
+
Points come in on the plan's own axes — x to the right, y up (render/view.ts's S) — and are placed as given. Which
|
|
36
|
+
way a device's angle turns is the host's business, not the editor's: the host turns its device's frame into this
|
|
37
|
+
one, and that's where a mirror gets in or doesn't. BOLDR's T1R turns clockwise, so the app negates y
|
|
38
|
+
(templatorPlanInches); passing its x = r·cos θ, y = r·sin θ straight in drew every template mirrored (seen on a device,
|
|
39
|
+
2026-10-28).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** How close a point has to land to the first one to close the shape: about 50 mm, inside the device's own error. */
|
|
43
|
+
export const SURVEY_CLOSE_IN = 2;
|
|
44
|
+
/** A point this close to the last one is the same point taken twice (a double press), not a new corner. */
|
|
45
|
+
export const SURVEY_SAME_IN = 1 / 16;
|
|
46
|
+
|
|
47
|
+
interface SurveyState {
|
|
48
|
+
/** the run's points, in order */
|
|
49
|
+
chain: string[];
|
|
50
|
+
/** each point's host record (surveyPoint's `capture`), kept with the dimensions it sets */
|
|
51
|
+
captures: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SurveyTool extends Tool {
|
|
55
|
+
st: SurveyState;
|
|
56
|
+
closeShape(): void;
|
|
57
|
+
finish(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Each page's frame for the session: what's added to a device point to place it on the plan. */
|
|
61
|
+
const frames = sessionState(() => ({ byPage: {} as Record<string, { dx: number; dy: number }> }));
|
|
62
|
+
|
|
63
|
+
const fresh = (): SurveyState => ({ chain: [], captures: {} });
|
|
64
|
+
const withCapture = (capture: unknown) => (capture !== undefined ? { capture } : {});
|
|
65
|
+
|
|
66
|
+
/** A driving length on line `lid`, as the two points it joins are now (so nothing moves). */
|
|
67
|
+
function holdLength(lid: string, a: string, b: string, capture: unknown): void {
|
|
68
|
+
const sk = SK(), pa = sk.points[a], pb = sk.points[b];
|
|
69
|
+
if (!pa || !pb) return;
|
|
70
|
+
tryAddConstraint({ type: 'dist', a, b, kind: 'al', value: hyp(pb.x - pa.x, pb.y - pa.y), line: lid, driving: true, off: 0, ...withCapture(capture) }, { silent: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A corner this close to dead straight (or folded right back) is held as Collinear, not as an angle: the angle's
|
|
75
|
+
* equation turns on |cross|, whose slope vanishes there, so the solver reads a 180° angle as adding nothing (a
|
|
76
|
+
* reference dimension) and the corner is left as a hinge. Collinear says the same thing and holds it. 1e-4° is far
|
|
77
|
+
* outside where that happens (about 2e-6°) and far inside any real capture's spread, so a measured corner is never
|
|
78
|
+
* squared off by it — over a 30" edge it's a micrometre.
|
|
79
|
+
*/
|
|
80
|
+
const STRAIGHT_DEG = 1e-4;
|
|
81
|
+
|
|
82
|
+
/** A driving angle at the corner where `l1` and `l2` meet, as it is now (Collinear, if it's straight). */
|
|
83
|
+
function holdAngle(l1: string | undefined, l2: string | undefined, capture: unknown): void {
|
|
84
|
+
if (!l1 || !l2 || l1 === l2) return;
|
|
85
|
+
const c: Constraint = { type: 'angle', l1, l2, value: 0, driving: true };
|
|
86
|
+
c.value = measure(SK(), c);
|
|
87
|
+
if (c.value < STRAIGHT_DEG || c.value > 180 - STRAIGHT_DEG) { tryAddConstraint({ type: 'col', l1, l2 }, { silent: true }); return; }
|
|
88
|
+
tryAddConstraint({ ...c, ...withCapture(capture) }, { silent: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const surveyTool: SurveyTool = {
|
|
92
|
+
st: fresh(),
|
|
93
|
+
/** Picked up (by a point arriving with another tool in hand): a new run. */
|
|
94
|
+
enter() { this.st = fresh(); },
|
|
95
|
+
/**
|
|
96
|
+
* Put down by cancelTool. Deliberately NOT the end of the run: undo cancels the tool in hand before it steps back,
|
|
97
|
+
* and ending the run there would leave a gap after every bad point undone. The run ends on Finish, Close shape,
|
|
98
|
+
* Esc or Enter (key, below) — which also put the templator down — or when another tool is picked up; either way the
|
|
99
|
+
* next point starts a new run (enter).
|
|
100
|
+
*/
|
|
101
|
+
reset() { ed.preview = null; },
|
|
102
|
+
busy() { return false; },
|
|
103
|
+
/** Esc and Enter finish the run here, before the generic Esc (which can't tell itself from undo's cancel). */
|
|
104
|
+
key(e: KeyIn) {
|
|
105
|
+
if ((e.key === 'Escape' || e.key === 'Enter') && this.st.chain.length) { this.finish(); return true; }
|
|
106
|
+
return false;
|
|
107
|
+
},
|
|
108
|
+
/**
|
|
109
|
+
* End the run: the shape stays as drawn, and the templator is put down (back to Select). Left in hand it would
|
|
110
|
+
* swallow every tap — it has no pointer handling, its points come from the device — so the shape just made couldn't
|
|
111
|
+
* be selected, and a phone's bar, which drops the "Templator" label, wouldn't even say why. The next point picks it
|
|
112
|
+
* up again, in the same frame. A run of one point leaves nothing (a point on its own isn't a shape).
|
|
113
|
+
*/
|
|
114
|
+
finish() {
|
|
115
|
+
const first = this.st.chain[0];
|
|
116
|
+
if (first) pinnedPoints.delete(first);
|
|
117
|
+
this.st = fresh();
|
|
118
|
+
ed.preview = null;
|
|
119
|
+
if (store.project) { gc(SK()); analyze(); }
|
|
120
|
+
if (ed.tool === 'survey') setTool('select');
|
|
121
|
+
updateCtx(); updateHint(); requestDraw();
|
|
122
|
+
},
|
|
123
|
+
/** Close the run back to its first point (the bar's Close shape, or a point taken there). */
|
|
124
|
+
closeShape() {
|
|
125
|
+
if (liveChain().length < 3) return;
|
|
126
|
+
pushHistory();
|
|
127
|
+
closeRun(undefined);
|
|
128
|
+
afterEdit();
|
|
129
|
+
},
|
|
130
|
+
hint() {
|
|
131
|
+
return this.st.chain.length
|
|
132
|
+
? 'Take the next point with your templator. Take one back at the first point to close the shape.'
|
|
133
|
+
: 'Take points with your templator: each one joins the last.';
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** The run's points that are still in the drawing (an undo since can have taken the last few back). */
|
|
138
|
+
function liveChain(): string[] {
|
|
139
|
+
const sk = SK(), st = surveyTool.st;
|
|
140
|
+
const keep = st.chain.filter(id => sk.points[id]);
|
|
141
|
+
if (keep.length !== st.chain.length) st.chain = keep;
|
|
142
|
+
return keep;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The run's first and last points, marked: where the next point joins, and where one would close the shape. */
|
|
146
|
+
function showRun(): void {
|
|
147
|
+
const sk = SK(), ch = surveyTool.st.chain;
|
|
148
|
+
const ends = [ch[0], ch[ch.length - 1]].filter((id, i, a) => id && sk.points[id] && a.indexOf(id) === i);
|
|
149
|
+
ed.preview = ends.length ? (c, T) => { for (const id of ends) { const p = sk.points[id]; if (p) { const [x, y] = S(T, p.x, p.y); anchorMark(c, x, y); } } } : null;
|
|
150
|
+
requestOverlayDraw();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Close the run: the last edge back to the first point, its length, the two corners it makes, and a space. */
|
|
154
|
+
function closeRun(capture: unknown): void {
|
|
155
|
+
const sk = SK(), st = surveyTool.st, ch = st.chain;
|
|
156
|
+
const first = ch[0], last = ch[ch.length - 1];
|
|
157
|
+
const lid = addLine(sk, last, first, { kind: drawsWalls() ? 'wall' : 'line', constr: !drawsWalls() && ed.drawConstr });
|
|
158
|
+
// (already set by the rest of the shape: these come out as reference dimensions)
|
|
159
|
+
holdLength(lid, last, first, capture ?? st.captures[first]);
|
|
160
|
+
holdAngle(lineBetween(sk, ch[ch.length - 2], last), lid, st.captures[last]);
|
|
161
|
+
holdAngle(lid, lineBetween(sk, first, ch[1]), st.captures[first]);
|
|
162
|
+
const n = ch.length, rid = drawsWalls() || !ed.drawConstr ? maybeRoom(sk, ch.slice()) : null;
|
|
163
|
+
surveyTool.finish();
|
|
164
|
+
toast(rid ? sk.rooms[rid].name + ' added — ' + fmtArea(roomGeom(sk, sk.rooms[rid]).area) : 'Closed shape, ' + n + ' sides');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A point from the host's device, in inches, on the plan's axes — y up (see the note at the top). Starts the
|
|
169
|
+
* templator if it isn't in hand, and the run's frame on a page with none yet. False when the drawing can't take it
|
|
170
|
+
* now (the toast says why).
|
|
171
|
+
*/
|
|
172
|
+
export function takeSurveyPoint(x: number, y: number, capture?: unknown): boolean {
|
|
173
|
+
if (ed.readOnly) return false;
|
|
174
|
+
if (elevOpen()) { toast('Back out of the wall view to take templator points on the plan.', { warn: true }); return false; }
|
|
175
|
+
if (keypadOpen()) { toast('Close the keypad, then take the point again.', { warn: true }); return false; }
|
|
176
|
+
// (the templator works on the plan: from the 3D tab or Annotate, back to Draft)
|
|
177
|
+
if (is3d()) close3d();
|
|
178
|
+
if (ed.mode !== 'draft') setMode('draft');
|
|
179
|
+
if (ed.tool !== 'survey') setTool('survey');
|
|
180
|
+
|
|
181
|
+
const sk = SK(), st = surveyTool.st, ch = liveChain();
|
|
182
|
+
const pg = page().id;
|
|
183
|
+
if (!frames.byPage[pg]) { const v = V(); frames.byPage[pg] = { dx: v.cx - x, dy: v.cy - y }; }
|
|
184
|
+
const f = frames.byPage[pg], wx = x + f.dx, wy = y + f.dy;
|
|
185
|
+
|
|
186
|
+
const last = ch.length ? sk.points[ch[ch.length - 1]] : null;
|
|
187
|
+
if (last && hyp(wx - last.x, wy - last.y) < SURVEY_SAME_IN) { toast('Same point as the last one — move the templator, then take the next.', { warn: true }); return false; }
|
|
188
|
+
if (ch.length >= 3) {
|
|
189
|
+
const p0 = sk.points[ch[0]];
|
|
190
|
+
if (hyp(wx - p0.x, wy - p0.y) < SURVEY_CLOSE_IN) { pushHistory(); closeRun(capture); afterEdit(); return true; }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
pushHistory();
|
|
194
|
+
const id = addPoint(sk, wx, wy);
|
|
195
|
+
if (capture !== undefined) st.captures[id] = capture;
|
|
196
|
+
if (!ch.length) {
|
|
197
|
+
// (on its own until the next point, garbage collection would take it: kept until the run ends)
|
|
198
|
+
pinnedPoints.add(id);
|
|
199
|
+
} else {
|
|
200
|
+
const prev = ch[ch.length - 1];
|
|
201
|
+
const lid = addLine(sk, prev, id, { kind: drawsWalls() ? 'wall' : 'line', constr: !drawsWalls() && ed.drawConstr });
|
|
202
|
+
holdLength(lid, prev, id, capture);
|
|
203
|
+
if (ch.length >= 2) holdAngle(lineBetween(sk, ch[ch.length - 2], prev), lid, st.captures[prev]);
|
|
204
|
+
}
|
|
205
|
+
st.chain.push(id);
|
|
206
|
+
ed.sel = [];
|
|
207
|
+
afterEdit();
|
|
208
|
+
showRun();
|
|
209
|
+
updateHint();
|
|
210
|
+
const n = st.chain.length, a = sk.points[st.chain[n - 2]];
|
|
211
|
+
toast(n === 1 ? 'Templator: point 1, at the middle of the screen' : 'Point ' + n + ' · ' + fmtLen(hyp(wx - a.x, wy - a.y)));
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// a new session (another drawing): every page's frame and the run start again
|
|
216
|
+
onSessionReset(() => { surveyTool.st = fresh(); });
|