@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.
Files changed (72) hide show
  1. package/README.md +18 -7
  2. package/dist/data/memory.d.ts +4 -1
  3. package/dist/data/memory.js +11 -6
  4. package/dist/data/memory.js.map +1 -1
  5. package/dist/data/provider.d.ts +11 -3
  6. package/dist/data/sync.d.ts +7 -1
  7. package/dist/data/sync.js +18 -2
  8. package/dist/data/sync.js.map +1 -1
  9. package/dist/editor/actions.js +1 -1
  10. package/dist/editor/actions.js.map +1 -1
  11. package/dist/editor/elevation.js +1 -1
  12. package/dist/editor/elevation.js.map +1 -1
  13. package/dist/editor/face.js +2 -2
  14. package/dist/editor/face.js.map +1 -1
  15. package/dist/editor/keypad.d.ts +1 -1
  16. package/dist/editor/keypad.js +24 -7
  17. package/dist/editor/keypad.js.map +1 -1
  18. package/dist/editor/ports.d.ts +2 -0
  19. package/dist/editor/ports.js.map +1 -1
  20. package/dist/editor/project.d.ts +2 -2
  21. package/dist/editor/project.js +7 -4
  22. package/dist/editor/project.js.map +1 -1
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/model/planGeom.js +2 -2
  26. package/dist/model/planGeom.js.map +1 -1
  27. package/dist/react/CadEditor.d.ts +8 -1
  28. package/dist/react/CadEditor.js +3 -2
  29. package/dist/react/CadEditor.js.map +1 -1
  30. package/dist/react/Popovers.js +2 -2
  31. package/dist/react/Popovers.js.map +1 -1
  32. package/dist/tools/chamfer.js +3 -3
  33. package/dist/tools/chamfer.js.map +1 -1
  34. package/dist/tools/dim.js +4 -4
  35. package/dist/tools/dim.js.map +1 -1
  36. package/dist/tools/elevation.js +4 -4
  37. package/dist/tools/elevation.js.map +1 -1
  38. package/dist/tools/fillet.js +1 -1
  39. package/dist/tools/fillet.js.map +1 -1
  40. package/dist/tools/openings.js +3 -3
  41. package/dist/tools/openings.js.map +1 -1
  42. package/dist/units/format.d.ts +1 -1
  43. package/dist/units/format.js +9 -3
  44. package/dist/units/format.js.map +1 -1
  45. package/dist/units/parse.d.ts +2 -1
  46. package/dist/units/parse.js +5 -4
  47. package/dist/units/parse.js.map +1 -1
  48. package/dist/units/prefs.d.ts +9 -1
  49. package/dist/units/prefs.js +6 -0
  50. package/dist/units/prefs.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/data/memory.ts +13 -8
  53. package/src/data/provider.ts +10 -3
  54. package/src/data/sync.ts +20 -3
  55. package/src/editor/actions.ts +1 -1
  56. package/src/editor/elevation.ts +1 -1
  57. package/src/editor/face.ts +2 -2
  58. package/src/editor/keypad.ts +24 -7
  59. package/src/editor/ports.ts +2 -0
  60. package/src/editor/project.ts +7 -5
  61. package/src/index.ts +2 -1
  62. package/src/model/planGeom.ts +2 -2
  63. package/src/react/CadEditor.tsx +10 -5
  64. package/src/react/Popovers.tsx +3 -3
  65. package/src/tools/chamfer.ts +3 -3
  66. package/src/tools/dim.ts +4 -4
  67. package/src/tools/elevation.ts +4 -4
  68. package/src/tools/fillet.ts +1 -1
  69. package/src/tools/openings.ts +3 -3
  70. package/src/units/format.ts +6 -3
  71. package/src/units/parse.ts +4 -3
  72. package/src/units/prefs.ts +13 -1
@@ -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 account
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';
@@ -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 === 'mm', list = metric ? MM : IMPERIAL;
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 };
@@ -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
- /** Lengths the host's way: how to write one (inches in), and whether bare numbers mean millimetres. Hides Units in Settings. */
66
- units?: { format: (inches: number) => string; metric: boolean; denom?: number };
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);
@@ -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()) && settings.unit !== 'mm' && (
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 */}
@@ -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()); },
@@ -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;
@@ -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
  }
@@ -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;
@@ -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 === 'mm') return (sqin * 0.00064516).toFixed(2) + ' m²';
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
 
@@ -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 / in mode and millimetres in mm mode.
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 (u.unit === 'mm' && !hasUnit) v /= 25.4;
62
+ if (!hasUnit) v *= bareScale(u.unit);
62
63
  return v;
63
64
  }
64
65
 
@@ -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');