@reekon-tools/boldr-cad 1.0.0 → 1.1.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 +18 -7
- 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 +1 -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 +2 -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/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/openings.js +3 -3
- package/dist/tools/openings.js.map +1 -1
- 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 +1 -1
- 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 +2 -1
- package/src/model/planGeom.ts +2 -2
- package/src/react/CadEditor.tsx +10 -5
- package/src/react/Popovers.tsx +3 -3
- 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/openings.ts +3 -3
- package/src/units/format.ts +6 -3
- package/src/units/parse.ts +4 -3
- package/src/units/prefs.ts +13 -1
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,7 +6,8 @@
|
|
|
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
13
|
export { laserReading, setLaserConnected } from './react/host';
|
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/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/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;
|
package/src/units/format.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getUnitPrefs, type UnitPrefs } from './prefs';
|
|
1
|
+
import { getUnitPrefs, isMetric, type UnitPrefs } from './prefs';
|
|
2
2
|
|
|
3
3
|
let custom: ((inches: number) => string) | null = null;
|
|
4
4
|
let hostPrefs: UnitPrefs | null = null;
|
|
@@ -19,11 +19,14 @@ export const hostUnits = (): UnitPrefs | null => (custom ? hostPrefs ?? { unit:
|
|
|
19
19
|
export const followingHostUnits = (): boolean => !!custom && !own;
|
|
20
20
|
export function setOwnUnits(v: boolean): void { own = v; }
|
|
21
21
|
|
|
22
|
-
/** Format a length in inches: 150.5 → 12'-6 1/2" (ft-in), 150 1/2" (in), 3,823 mm (mm). */
|
|
22
|
+
/** Format a length in inches: 150.5 → 12'-6 1/2" (ft-in), 150 1/2" (in), 12.54' (ft), 3,823 mm (mm), 382.27 cm (cm), 3.823 m (m). */
|
|
23
23
|
export function fmtLen(v: number, u: UnitPrefs = getUnitPrefs()): string {
|
|
24
24
|
v = Math.abs(v);
|
|
25
25
|
if (custom && !own && u === getUnitPrefs()) return custom(v);
|
|
26
26
|
if (u.unit === 'mm') return Math.round(v * 25.4).toLocaleString() + ' mm';
|
|
27
|
+
if (u.unit === 'cm') return (v * 2.54).toFixed(2) + ' cm';
|
|
28
|
+
if (u.unit === 'm') return (v * 0.0254).toFixed(3) + ' m';
|
|
29
|
+
if (u.unit === 'ft') return (v / 12).toFixed(2) + "'";
|
|
27
30
|
const d = u.denom;
|
|
28
31
|
const t = Math.round(v * d) / d;
|
|
29
32
|
let ft = u.unit === 'ftin' ? Math.floor(t / 12 + 1e-9) : 0;
|
|
@@ -41,7 +44,7 @@ export function fmtLen(v: number, u: UnitPrefs = getUnitPrefs()): string {
|
|
|
41
44
|
|
|
42
45
|
/** Format an area given in square inches. */
|
|
43
46
|
export function fmtArea(sqin: number, u: UnitPrefs = getUnitPrefs()): string {
|
|
44
|
-
if (u.unit
|
|
47
|
+
if (isMetric(u.unit)) return (sqin * 0.00064516).toFixed(2) + ' m²';
|
|
45
48
|
return (sqin / 144).toFixed(1) + ' sq ft';
|
|
46
49
|
}
|
|
47
50
|
|
package/src/units/parse.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getUnitPrefs, type UnitPrefs } from './prefs';
|
|
1
|
+
import { bareScale, getUnitPrefs, type UnitPrefs } from './prefs';
|
|
2
2
|
|
|
3
3
|
/** Tiny recursive-descent evaluator for + - * / and parentheses. Throws on bad input. */
|
|
4
4
|
export function evalExpr(src: string): number {
|
|
@@ -41,7 +41,8 @@ export function evalExpr(src: string): number {
|
|
|
41
41
|
/**
|
|
42
42
|
* Parse a length and return inches, or null if it can't be read.
|
|
43
43
|
* Accepts 12' 6 1/2", 12ft 6in, 150.5, 3.2m, 25mm, and arithmetic (10'+6).
|
|
44
|
-
* Bare numbers are inches in ft-in
|
|
44
|
+
* Bare numbers are read in the current unit: inches in ft-in and in modes, feet in ft, millimetres, centimetres or
|
|
45
|
+
* metres in those.
|
|
45
46
|
*/
|
|
46
47
|
export function parseLen(str: string | null | undefined, u: UnitPrefs = getUnitPrefs()): number | null {
|
|
47
48
|
if (str == null) return null;
|
|
@@ -58,7 +59,7 @@ export function parseLen(str: string | null | undefined, u: UnitPrefs = getUnitP
|
|
|
58
59
|
let v: number;
|
|
59
60
|
try { v = evalExpr(s); } catch { return null; }
|
|
60
61
|
if (!isFinite(v)) return null;
|
|
61
|
-
if (
|
|
62
|
+
if (!hasUnit) v *= bareScale(u.unit);
|
|
62
63
|
return v;
|
|
63
64
|
}
|
|
64
65
|
|
package/src/units/prefs.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export type Unit = 'ftin' | 'in' | 'mm';
|
|
1
|
+
export type Unit = 'ftin' | 'in' | 'ft' | 'mm' | 'cm' | 'm';
|
|
2
2
|
|
|
3
3
|
/** Display preferences. Geometry is always stored in inches; these only change how values read. */
|
|
4
4
|
export interface UnitPrefs {
|
|
5
5
|
unit: Unit;
|
|
6
6
|
/** Fraction precision for imperial display: 16 → 1/16". */
|
|
7
7
|
denom: number;
|
|
8
|
+
/** Whether lengths are typed and read with fractions (the fraction keys, Precision). Unset: feet-inches and inches are. */
|
|
9
|
+
fractions?: boolean;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
let current: UnitPrefs = { unit: 'ftin', denom: 16 };
|
|
@@ -17,3 +19,13 @@ export function useUnitPrefs(p: UnitPrefs): void {
|
|
|
17
19
|
export function getUnitPrefs(): UnitPrefs {
|
|
18
20
|
return current;
|
|
19
21
|
}
|
|
22
|
+
|
|
23
|
+
/** Metric units: bare numbers, the grid and areas are metric. */
|
|
24
|
+
export const isMetric = (unit: Unit): boolean => unit === 'mm' || unit === 'cm' || unit === 'm';
|
|
25
|
+
|
|
26
|
+
/** Inches per unit typed as a bare number: what a number with no unit means in `unit`. */
|
|
27
|
+
export const bareScale = (unit: Unit): number =>
|
|
28
|
+
unit === 'ft' ? 12 : unit === 'mm' ? 1 / 25.4 : unit === 'cm' ? 1 / 2.54 : unit === 'm' ? 1 / 0.0254 : 1;
|
|
29
|
+
|
|
30
|
+
/** Whether lengths read with fractions (the fraction keys, Precision): as the prefs say, else for feet-inches and inches. */
|
|
31
|
+
export const showFractions = (u: UnitPrefs = getUnitPrefs()): boolean => u.fractions ?? (u.unit === 'ftin' || u.unit === 'in');
|