@reekon-tools/boldr-cad 1.4.0 → 1.6.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 +13 -3
- package/dist/app/edit.d.ts +3 -3
- package/dist/app/edit.js +26 -1
- package/dist/app/edit.js.map +1 -1
- package/dist/app/state.d.ts +6 -8
- package/dist/app/state.js +0 -1
- package/dist/app/state.js.map +1 -1
- package/dist/data/sync.d.ts +1 -1
- package/dist/data/sync.js +7 -2
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.d.ts +8 -0
- package/dist/editor/actions.js +255 -181
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/elevation.d.ts +4 -1
- package/dist/editor/elevation.js +16 -17
- package/dist/editor/elevation.js.map +1 -1
- package/dist/editor/gestures.d.ts +21 -0
- package/dist/editor/gestures.js +92 -14
- package/dist/editor/gestures.js.map +1 -1
- package/dist/editor/keys.js +4 -3
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/laser.d.ts +4 -2
- package/dist/editor/laser.js +4 -2
- package/dist/editor/laser.js.map +1 -1
- package/dist/editor/linkActions.d.ts +49 -12
- package/dist/editor/linkActions.js +164 -35
- package/dist/editor/linkActions.js.map +1 -1
- package/dist/editor/links.d.ts +13 -0
- package/dist/editor/links.js +7 -1
- package/dist/editor/links.js.map +1 -1
- package/dist/editor/mode.d.ts +2 -1
- package/dist/editor/mode.js +8 -5
- package/dist/editor/mode.js.map +1 -1
- package/dist/editor/project.js +1 -2
- package/dist/editor/project.js.map +1 -1
- package/dist/editor/scaleGate.d.ts +13 -0
- package/dist/editor/scaleGate.js +27 -0
- package/dist/editor/scaleGate.js.map +1 -0
- package/dist/editor/selection.js +13 -9
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/tooltips.js +10 -0
- package/dist/editor/tooltips.js.map +1 -1
- package/dist/editor/view3d.d.ts +4 -1
- package/dist/editor/view3d.js +6 -2
- package/dist/editor/view3d.js.map +1 -1
- package/dist/export/pdf.d.ts +5 -2
- package/dist/export/pdf.js +12 -8
- package/dist/export/pdf.js.map +1 -1
- package/dist/export/plan.d.ts +1 -1
- package/dist/export/plan.js +14 -9
- package/dist/export/plan.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model/constraints.js +2 -0
- package/dist/model/constraints.js.map +1 -1
- package/dist/model/demo.d.ts +1 -0
- package/dist/model/demo.js +2 -1
- package/dist/model/demo.js.map +1 -1
- package/dist/model/elevation.d.ts +1 -1
- package/dist/model/elevation.js +3 -3
- package/dist/model/elevation.js.map +1 -1
- package/dist/model/geom.d.ts +2 -0
- package/dist/model/geom.js +11 -0
- package/dist/model/geom.js.map +1 -1
- package/dist/model/history.js +4 -1
- package/dist/model/history.js.map +1 -1
- package/dist/model/links.d.ts +10 -2
- package/dist/model/links.js +15 -3
- package/dist/model/links.js.map +1 -1
- package/dist/model/measurement.d.ts +2 -2
- package/dist/model/measurement.js +3 -3
- package/dist/model/measurement.js.map +1 -1
- package/dist/model/openings.d.ts +10 -0
- package/dist/model/openings.js +18 -0
- package/dist/model/openings.js.map +1 -1
- package/dist/model/planGeom.d.ts +37 -2
- package/dist/model/planGeom.js +72 -9
- package/dist/model/planGeom.js.map +1 -1
- package/dist/model/project.js +5 -2
- package/dist/model/project.js.map +1 -1
- package/dist/model/rooms.d.ts +18 -5
- package/dist/model/rooms.js +178 -73
- package/dist/model/rooms.js.map +1 -1
- package/dist/model/scale.d.ts +30 -0
- package/dist/model/scale.js +123 -0
- package/dist/model/scale.js.map +1 -0
- package/dist/model/settings.d.ts +0 -1
- package/dist/model/settings.js +1 -1
- package/dist/model/settings.js.map +1 -1
- package/dist/model/sketch.d.ts +26 -2
- package/dist/model/sketch.js +77 -22
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/stroke.js +2 -2
- package/dist/model/trim.d.ts +54 -0
- package/dist/model/trim.js +219 -0
- package/dist/model/trim.js.map +1 -0
- package/dist/model/types.d.ts +62 -9
- package/dist/react/BottomBar.d.ts +2 -1
- package/dist/react/BottomBar.js +144 -46
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/CadEditor.js +18 -9
- package/dist/react/CadEditor.js.map +1 -1
- package/dist/react/Drawer.js +6 -4
- package/dist/react/Drawer.js.map +1 -1
- package/dist/react/Duck.js.map +1 -1
- package/dist/react/KeypadSheet.js.map +1 -1
- package/dist/react/MarkupPanel.js +1 -1
- package/dist/react/PlanCanvas.native.js +54 -7
- package/dist/react/PlanCanvas.native.js.map +1 -1
- package/dist/react/PlanCanvas.web.js +17 -5
- package/dist/react/PlanCanvas.web.js.map +1 -1
- package/dist/react/Popovers.js +14 -3
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/Rail.js +1 -1
- package/dist/react/ShapeMenu.js +3 -2
- package/dist/react/ShapeMenu.js.map +1 -1
- package/dist/react/Sheet.d.ts +2 -2
- package/dist/react/Sheet.js.map +1 -1
- package/dist/react/ToolTip.js +3 -2
- package/dist/react/ToolTip.js.map +1 -1
- package/dist/react/TopBar.js +4 -3
- package/dist/react/TopBar.js.map +1 -1
- package/dist/react/barScroll.d.ts +23 -0
- package/dist/react/barScroll.js +47 -0
- package/dist/react/barScroll.js.map +1 -0
- package/dist/react/corners.d.ts +6 -2
- package/dist/react/corners.js.map +1 -1
- package/dist/react/editorBox.d.ts +8 -0
- package/dist/react/editorBox.js +10 -0
- package/dist/react/editorBox.js.map +1 -0
- package/dist/react/host.d.ts +1 -1
- package/dist/react/host.js +1 -1
- package/dist/react/host.js.map +1 -1
- package/dist/react/motion.d.ts +1 -1
- package/dist/react/motion.js +1 -1
- package/dist/react/motion.js.map +1 -1
- package/dist/react/pageLock.native.d.ts +2 -0
- package/dist/react/pageLock.native.js +2 -0
- package/dist/react/pageLock.native.js.map +1 -1
- package/dist/react/pageLock.web.d.ts +6 -0
- package/dist/react/pageLock.web.js +12 -3
- package/dist/react/pageLock.web.js.map +1 -1
- package/dist/react/ports.js +8 -3
- package/dist/react/ports.js.map +1 -1
- package/dist/react/saving.d.ts +7 -1
- package/dist/react/saving.js +10 -2
- package/dist/react/saving.js.map +1 -1
- package/dist/react/store.d.ts +5 -2
- package/dist/react/store.js +4 -1
- package/dist/react/store.js.map +1 -1
- package/dist/render/draw.d.ts +21 -4
- package/dist/render/draw.js +203 -151
- package/dist/render/draw.js.map +1 -1
- package/dist/render/elevation.js +3 -3
- package/dist/render/elevation.js.map +1 -1
- package/dist/render/figmaIcons.d.ts +6 -0
- package/dist/render/figmaIcons.js +16 -1
- package/dist/render/figmaIcons.js.map +1 -1
- package/dist/render/hit.d.ts +8 -1
- package/dist/render/hit.js +23 -4
- package/dist/render/hit.js.map +1 -1
- package/dist/render/icons.js +27 -20
- package/dist/render/icons.js.map +1 -1
- package/dist/render/lens.d.ts +27 -0
- package/dist/render/lens.js +41 -0
- package/dist/render/lens.js.map +1 -0
- package/dist/render/linkTile.d.ts +20 -2
- package/dist/render/linkTile.js +476 -246
- package/dist/render/linkTile.js.map +1 -1
- package/dist/render/snap.d.ts +10 -1
- package/dist/render/snap.js +87 -15
- package/dist/render/snap.js.map +1 -1
- package/dist/render/svgPath.d.ts +29 -0
- package/dist/render/svgPath.js +114 -0
- package/dist/render/svgPath.js.map +1 -0
- package/dist/render/tileIcons.d.ts +18 -0
- package/dist/render/tileIcons.js +67 -0
- package/dist/render/tileIcons.js.map +1 -0
- package/dist/render/view.d.ts +18 -1
- package/dist/render/view.js +38 -6
- package/dist/render/view.js.map +1 -1
- package/dist/skia/SkiaDrawCtx.d.ts +8 -0
- package/dist/skia/SkiaDrawCtx.js +27 -5
- package/dist/skia/SkiaDrawCtx.js.map +1 -1
- package/dist/solver/equations.d.ts +6 -0
- package/dist/solver/equations.js +53 -22
- package/dist/solver/equations.js.map +1 -1
- package/dist/solver/gauss-newton.js +6 -1
- package/dist/solver/gauss-newton.js.map +1 -1
- package/dist/tools/annotate.d.ts +0 -2
- package/dist/tools/annotate.js +1 -22
- package/dist/tools/annotate.js.map +1 -1
- package/dist/tools/chamfer.js +4 -0
- package/dist/tools/chamfer.js.map +1 -1
- package/dist/tools/dim.d.ts +3 -3
- package/dist/tools/dim.js +26 -65
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/fillet.js +4 -0
- package/dist/tools/fillet.js.map +1 -1
- package/dist/tools/freehand.js +1 -4
- package/dist/tools/freehand.js.map +1 -1
- package/dist/tools/line.js +15 -11
- package/dist/tools/line.js.map +1 -1
- package/dist/tools/modify.d.ts +32 -4
- package/dist/tools/modify.js +253 -98
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +7 -0
- package/dist/tools/registry.js +14 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +30 -8
- package/dist/tools/select.js +165 -34
- package/dist/tools/select.js.map +1 -1
- package/dist/tools/shapes.d.ts +1 -1
- package/dist/tools/shapes.js +12 -5
- package/dist/tools/shapes.js.map +1 -1
- package/dist/view3d/build.js +9 -8
- package/dist/view3d/build.js.map +1 -1
- package/package.json +9 -9
- package/src/app/edit.ts +25 -1
- package/src/app/state.ts +6 -8
- package/src/data/sync.ts +7 -2
- package/src/editor/actions.ts +213 -122
- package/src/editor/elevation.ts +13 -16
- package/src/editor/gestures.ts +83 -13
- package/src/editor/keys.ts +3 -2
- package/src/editor/laser.ts +7 -4
- package/src/editor/linkActions.ts +169 -31
- package/src/editor/links.ts +17 -1
- package/src/editor/mode.ts +8 -5
- package/src/editor/project.ts +1 -2
- package/src/editor/scaleGate.ts +32 -0
- package/src/editor/selection.ts +13 -7
- package/src/editor/tooltips.ts +9 -0
- package/src/editor/view3d.ts +6 -2
- package/src/export/pdf.ts +7 -4
- package/src/export/plan.ts +14 -10
- package/src/index.ts +1 -0
- package/src/model/constraints.ts +1 -0
- package/src/model/demo.ts +2 -1
- package/src/model/elevation.ts +4 -4
- package/src/model/geom.ts +10 -0
- package/src/model/history.ts +4 -1
- package/src/model/links.ts +14 -3
- package/src/model/measurement.ts +3 -3
- package/src/model/openings.ts +18 -0
- package/src/model/planGeom.ts +70 -10
- package/src/model/project.ts +6 -2
- package/src/model/rooms.ts +151 -63
- package/src/model/scale.ts +107 -0
- package/src/model/settings.ts +1 -2
- package/src/model/sketch.ts +62 -11
- package/src/model/stroke.ts +2 -2
- package/src/model/trim.ts +188 -0
- package/src/model/types.ts +57 -9
- package/src/react/BottomBar.tsx +177 -52
- package/src/react/CadEditor.tsx +19 -9
- package/src/react/Drawer.tsx +17 -12
- package/src/react/Duck.tsx +3 -3
- package/src/react/KeypadSheet.tsx +2 -2
- package/src/react/MarkupPanel.tsx +1 -1
- package/src/react/PlanCanvas.native.tsx +60 -6
- package/src/react/PlanCanvas.web.tsx +17 -5
- package/src/react/Popovers.tsx +24 -9
- package/src/react/Rail.tsx +1 -1
- package/src/react/ShapeMenu.tsx +3 -2
- package/src/react/Sheet.tsx +3 -3
- package/src/react/ToolTip.tsx +3 -2
- package/src/react/TopBar.tsx +4 -3
- package/src/react/barScroll.ts +57 -0
- package/src/react/corners.ts +6 -3
- package/src/react/editorBox.ts +20 -0
- package/src/react/host.ts +1 -1
- package/src/react/motion.tsx +6 -6
- package/src/react/pageLock.native.ts +2 -0
- package/src/react/pageLock.web.ts +12 -3
- package/src/react/ports.ts +7 -2
- package/src/react/saving.ts +10 -2
- package/src/react/store.ts +5 -2
- package/src/render/draw.ts +169 -118
- package/src/render/elevation.ts +5 -4
- package/src/render/figmaIcons.ts +17 -1
- package/src/render/hit.ts +21 -4
- package/src/render/icons.ts +27 -20
- package/src/render/lens.ts +52 -0
- package/src/render/linkTile.ts +436 -190
- package/src/render/snap.ts +87 -17
- package/src/render/svgPath.ts +95 -0
- package/src/render/tileIcons.ts +75 -0
- package/src/render/view.ts +43 -7
- package/src/skia/SkiaDrawCtx.ts +26 -4
- package/src/solver/equations.ts +52 -22
- package/src/solver/gauss-newton.ts +4 -1
- package/src/styles.css +3 -2
- package/src/tools/annotate.ts +1 -18
- package/src/tools/chamfer.ts +3 -0
- package/src/tools/dim.ts +26 -52
- package/src/tools/fillet.ts +3 -0
- package/src/tools/freehand.ts +1 -3
- package/src/tools/line.ts +12 -11
- package/src/tools/modify.ts +210 -70
- package/src/tools/registry.ts +18 -1
- package/src/tools/select.ts +144 -36
- package/src/tools/shapes.ts +9 -4
- package/src/ui/ctx.ts +4 -3
- package/src/ui/drawer.ts +8 -2
- package/src/view3d/build.ts +11 -10
- package/dist/react/LaserTray.d.ts +0 -4
- package/dist/react/LaserTray.js +0 -26
- package/dist/react/LaserTray.js.map +0 -1
- package/src/react/LaserTray.tsx +0 -42
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isUnscaled } from '../model/scale';
|
|
2
|
+
import { planSK } from '../model/store';
|
|
3
|
+
import { fmtLen } from '../units';
|
|
4
|
+
import { toast } from './ports';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Before a new page's first length (model/scale.ts), its sizes are placeholders. So nothing shows one as if it were
|
|
8
|
+
real, and what needs real sizes waits: the 3D view, the wall view, doors, windows and cabinets (the user's call,
|
|
9
|
+
2026-10-29), and the tools whose typed size has nothing drawn to be compared with (offset, fillet, chamfer).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The page being drawn has no scale yet: its sizes are placeholders. */
|
|
13
|
+
export const noScaleYet = (): boolean => isUnscaled(planSK());
|
|
14
|
+
|
|
15
|
+
export const NEEDS_SCALE = 'Enter a length first: select a line and tap +. It sets the size of the whole drawing.';
|
|
16
|
+
|
|
17
|
+
/** What needs real sizes, asked for before the page has its scale: says what to do first, and true (wait). */
|
|
18
|
+
export function waitForScale(): boolean {
|
|
19
|
+
if (!noScaleYet()) return false;
|
|
20
|
+
toast(NEEDS_SCALE, { warn: true });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Tools that place something with a real size, or take one typed: doors, windows and cabinets (the plan's and the wall view's), offsets. */
|
|
25
|
+
const SIZED_TOOLS = new Set(['door', 'window', 'wdoor', 'wwindow', 'cabinet', 'height', 'offset']);
|
|
26
|
+
export const toolWaitsForScale = (t: string): boolean => SIZED_TOOLS.has(t);
|
|
27
|
+
|
|
28
|
+
/** A length being entered, as its label shows it: what's typed, else what it measures now — nothing while that's a placeholder. */
|
|
29
|
+
export const pendingLabel = (typed: number | null, now: number): string => (typed != null ? fmtLen(typed) : noScaleYet() ? '' : fmtLen(now));
|
|
30
|
+
|
|
31
|
+
/** A keypad's starting value: what it measures now, or nothing while that's only a placeholder. */
|
|
32
|
+
export const startingValue = (v: number): { initial: string; value?: number } => (noScaleYet() ? { initial: '' } : { initial: fmtLen(v), value: v });
|
package/src/editor/selection.ts
CHANGED
|
@@ -11,9 +11,13 @@ import { faceLength } from '../model/wallfaces';
|
|
|
11
11
|
import { planSK, SK } from '../model/store';
|
|
12
12
|
import type { Sketch } from '../model/types';
|
|
13
13
|
import { fmtArea, fmtLen } from '../units';
|
|
14
|
+
import { noScaleYet } from './scaleGate';
|
|
14
15
|
|
|
15
16
|
/* What is selected, grouped by kind, and the one-line hint describing it (bold parts in <b>). */
|
|
16
17
|
|
|
18
|
+
/** A size in a hint, after a space — none on a drawing without its scale yet (a placeholder isn't a size). */
|
|
19
|
+
const sized = (text: string): string => (noScaleYet() ? '' : ' ' + text);
|
|
20
|
+
|
|
17
21
|
export function selInfo() {
|
|
18
22
|
const g = (t: string) => ed.sel.filter(s => s.type === t).map(s => s.id);
|
|
19
23
|
const circles = g('circle'), arcs = g('arc');
|
|
@@ -54,13 +58,13 @@ export function selectionHint(): string {
|
|
|
54
58
|
if (s.n === 1 && s.lines.length && sk.lines[s.lines[0]]?.anchor) return '<b>Set by the plan</b> · pick a point or line too, to constrain or dimension to it';
|
|
55
59
|
if (s.n === 1 && s.lines.length) {
|
|
56
60
|
const l = sk.lines[s.lines[0]];
|
|
57
|
-
return '<b>' + (l.kind === 'wall' ? 'Wall' : l.constr ? 'Construction line' : 'Line') +
|
|
61
|
+
return '<b>' + (l.kind === 'wall' ? 'Wall' : l.constr ? 'Construction line' : 'Line') + sized(fmtLen(lineLen(sk, l))) + '</b> · ' + (lineDefined(ed.ana, l) ? 'fully defined' : lengthDimOf(sk, s.lines[0]) ? 'pick a constraint below or drag to adjust' : 'tap <b>+</b> below ' + forLength() + ', or pick a constraint');
|
|
58
62
|
}
|
|
59
63
|
if (s.n === 1 && s.wfaces.length) {
|
|
60
64
|
const [id, side] = edgeOf(s.wfaces[0]);
|
|
61
65
|
// (a curved wall's: its diameter across it)
|
|
62
|
-
if (!sk.lines[id]) return '<b>' + edgeLabel(sk, id, side) + '
|
|
63
|
-
return '<b>' + edgeLabel(sk, id, side) +
|
|
66
|
+
if (!sk.lines[id]) return '<b>' + edgeLabel(sk, id, side) + sized('Ø ' + fmtLen(measure(sk, { type: 'diam', c: id, face: side }))) + '</b> · tap <b>+</b> for its diameter';
|
|
67
|
+
return '<b>' + edgeLabel(sk, id, side) + sized(fmtLen(faceLength(sk, id, side))) + '</b> · tap <b>+</b> ' + forLength() + ', or pick another edge or a point to measure from it';
|
|
64
68
|
}
|
|
65
69
|
if (edgePair(s)) {
|
|
66
70
|
const one = s.wfaces.length === 2 && edgeOf(s.wfaces[0])[0] === edgeOf(s.wfaces[1])[0];
|
|
@@ -75,6 +79,7 @@ export function selectionHint(): string {
|
|
|
75
79
|
if (s.n === 1 && s.cons.length) return '<b>' + consLabel(sk.cons[s.cons[0]]) + '</b> constraint · delete it to free the shape';
|
|
76
80
|
if (s.n === 1 && s.rooms.length) {
|
|
77
81
|
const r = sk.rooms[s.rooms[0]], g = roomGeom(sk, r);
|
|
82
|
+
if (noScaleYet()) return '<b>' + r.name + '</b> · its area shows once the drawing has a length';
|
|
78
83
|
return '<b>' + r.name + '</b> · ' + fmtArea(g.area) + ' · ' + fmtLen(g.per) + ' perimeter';
|
|
79
84
|
}
|
|
80
85
|
if (s.n === 1 && s.openings.length) {
|
|
@@ -86,7 +91,6 @@ export function selectionHint(): string {
|
|
|
86
91
|
}
|
|
87
92
|
if (s.n === 1 && s.dims.length) {
|
|
88
93
|
const c = sk.cons[s.dims[0]];
|
|
89
|
-
if (c.layer === 'print') return '<b>Print dimension</b> · drag to move the label';
|
|
90
94
|
if (c.measurementRef) return '<b>' + consLabel(c) + '</b> · linked to <b>' + linkName(c.measurementRef) + '</b> · tap <b>+</b> to change it';
|
|
91
95
|
return '<b>' + consLabel(c) + '</b> · tap <b>+</b> to change it';
|
|
92
96
|
}
|
|
@@ -96,9 +100,11 @@ export function selectionHint(): string {
|
|
|
96
100
|
if (part === 'w') return '<b>' + (planSK().openings[s.odims[0].split('|')[0]]?.type === 'door' ? 'Door' : 'Window') + ' width</b> · tap <b>+</b> to change it';
|
|
97
101
|
return part.startsWith('d:') ? '<b>Door/window dimension</b> · tap <b>+</b> to change it' : '<b>Distance to the wall end</b> · tap <b>+</b> to lock it';
|
|
98
102
|
}
|
|
99
|
-
if (s.n === 1 && s.image)
|
|
100
|
-
|
|
101
|
-
: '<b>Image</b>
|
|
103
|
+
if (s.n === 1 && s.image) {
|
|
104
|
+
// (a photo of the job's, expanded from its tile: its link opens it, and Expand folds it back)
|
|
105
|
+
const u = imageById(ed.sel[0].id), what = u?.link ? '<b>Photo</b>' : '<b>Image</b>', link = u?.link ? ' · its link opens it' : '';
|
|
106
|
+
return u?.locked ? what + ' · locked — tap Unlock to move or resize it' + link : what + ' · drag to move · corners scale, side handles crop' + link;
|
|
107
|
+
}
|
|
102
108
|
if (s.n === 1 && s.notes.length && SK().notes.find(n => n.id === s.notes[0])?.type === 'link') return '<b>Link</b> · drag to move · open it, or point it at the drawing with a leader, below · Size sizes every tile on the page';
|
|
103
109
|
if (s.notes.length) return 'Drag to move. Turn a pen stroke into lines below.';
|
|
104
110
|
return '<b>' + s.n + ' selected</b> · choose a constraint below';
|
package/src/editor/tooltips.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ed } from '../app/state';
|
|
2
2
|
import { settings } from '../model/settings';
|
|
3
|
+
import { noScaleYet, toolWaitsForScale } from './scaleGate';
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
What a tool does, said in a line when it's picked from the rail (a quick note under the Annotate / Draft tabs that
|
|
@@ -14,6 +15,7 @@ const TIPS: Record<string, string> = {
|
|
|
14
15
|
rect: 'Tap or drag corner to corner.',
|
|
15
16
|
circle: 'Tap the center, then the edge.',
|
|
16
17
|
arc: 'Tap both ends, then drag to bend.',
|
|
18
|
+
trim: 'Tap or drag across the pieces to cut away.',
|
|
17
19
|
door: 'Tap a wall to add a 3\'-0" door.',
|
|
18
20
|
window: 'Tap a wall to add a 3\'-0" window.',
|
|
19
21
|
room: 'Tap inside a closed shape.',
|
|
@@ -34,14 +36,21 @@ const TIPS: Record<string, string> = {
|
|
|
34
36
|
text: 'Tap where it goes, then type.',
|
|
35
37
|
};
|
|
36
38
|
|
|
39
|
+
/** The tools that draw shapes: on a page without its scale, what they draw is a sketch. */
|
|
40
|
+
const SKETCHING = new Set(['line', 'freehand', 'rect', 'circle', 'arc']);
|
|
41
|
+
|
|
37
42
|
/**
|
|
38
43
|
* The line for a rail item just picked (its name in bold), or null if it has none (Undo, a colour, an action) or the
|
|
39
44
|
* notes are turned off (••• → Tool tips).
|
|
40
45
|
*/
|
|
41
46
|
export function toolTip(id: string, label: string): string | null {
|
|
42
47
|
if (settings.toolTips === false) return null;
|
|
48
|
+
// (on a page without its scale a door, a window or an offset isn't picked: a toast says to enter a length first)
|
|
49
|
+
if (toolWaitsForScale(id) && noScaleYet()) return null;
|
|
43
50
|
if (id === 'constr') return ed.drawConstr ? '<b>Construction lines on</b> · new lines are dashed guides.' : '<b>Construction lines off</b> · new lines are solid.';
|
|
44
51
|
if (id === 'wall') return ed.drawWalls ? '<b>Walls on</b> · what you draw is walls.' : '<b>Walls off</b> · what you draw is lines.';
|
|
45
52
|
const t = TIPS[id];
|
|
53
|
+
// drawing on a page without its scale: sketched in proportion, sized by its first length (editor/scaleGate.ts)
|
|
54
|
+
if (t && SKETCHING.has(id) && noScaleYet()) return '<b>' + label + '</b> · ' + t + ' Your first length sets the size.';
|
|
46
55
|
return t ? '<b>' + label + '</b> · ' + t : null;
|
|
47
56
|
}
|
package/src/editor/view3d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { KeyIn } from './keys';
|
|
|
16
16
|
import { setMode } from './mode';
|
|
17
17
|
import { dismissConflict, dismissPopovers, refreshModes, updateCtx, updateHint, updateRail } from './ports';
|
|
18
18
|
import { command3d, pick3d, v3 } from './view3dState';
|
|
19
|
+
import { waitForScale } from './scaleGate';
|
|
19
20
|
|
|
20
21
|
/*
|
|
21
22
|
The 3D tab: opening and leaving it, its rail's views, what the bar shows for something tapped in it (its sizes, the
|
|
@@ -30,9 +31,12 @@ import { command3d, pick3d, v3 } from './view3dState';
|
|
|
30
31
|
export const view3dKey = (): string =>
|
|
31
32
|
[store.curPageId, ed.editSeq, settings.wallH, v3.cutaway, v3.cmd.seq, v3.pick ? v3.pick.kind + ':' + v3.pick.id : ''].join('|');
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Show the 3D tab: whatever tool was in hand is put down, and nothing is selected. Not before the page's first length:
|
|
36
|
+
* its walls' heights are real and their lengths only placeholders until then (editor/scaleGate.ts).
|
|
37
|
+
*/
|
|
34
38
|
export function open3d(): void {
|
|
35
|
-
if (v3.open) return;
|
|
39
|
+
if (v3.open || waitForScale()) return;
|
|
36
40
|
cancelTool();
|
|
37
41
|
dismissPopovers();
|
|
38
42
|
if (elevOpen()) closeElevation();
|
package/src/export/pdf.ts
CHANGED
|
@@ -20,10 +20,13 @@ const SCALES: [k: number, label: string][] = [
|
|
|
20
20
|
[3 / 16, '3/16"'], [1 / 8, '1/8"'], [3 / 32, '3/32"'], [1 / 16, '1/16"'], [1 / 32, '1/32"'],
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Pick a scale for a drawing w × h inches: points per world inch, and its label. A sketch without a scale yet (its
|
|
25
|
+
* sizes only placeholders: model/scale.ts) is fitted to the sheet, not to scale.
|
|
26
|
+
*/
|
|
27
|
+
export function pickScale(w: number, h: number, sketchOnly = false): { s: number; label: string } {
|
|
25
28
|
const aw = AREA.x1 - AREA.x0, ah = AREA.y1 - AREA.y0;
|
|
26
|
-
for (const [k, label] of SCALES) {
|
|
29
|
+
if (!sketchOnly) for (const [k, label] of SCALES) {
|
|
27
30
|
const s = (72 * k) / 12;
|
|
28
31
|
if (w * s <= aw && h * s <= ah) return { s, label: label + " = 1'-0\"" };
|
|
29
32
|
}
|
|
@@ -166,7 +169,7 @@ export function sketchToPdf(sk: Sketch, opts: PdfOptions = {}): string {
|
|
|
166
169
|
const rec = new Recorder();
|
|
167
170
|
emitDrawing(sk, rec, opts);
|
|
168
171
|
const bb = rec.bbox();
|
|
169
|
-
const { s, label } = bb ? pickScale(bb.x1 - bb.x0, bb.y1 - bb.y0) : { s: 1, label: 'Not to scale' };
|
|
172
|
+
const { s, label } = bb ? pickScale(bb.x1 - bb.x0, bb.y1 - bb.y0, !!sk.unscaled) : { s: 1, label: 'Not to scale' };
|
|
170
173
|
const cx = bb ? (bb.x0 + bb.x1) / 2 : 0, cy = bb ? (bb.y0 + bb.y1) / 2 : 0;
|
|
171
174
|
const ox = (AREA.x0 + AREA.x1) / 2, oy = (AREA.y0 + AREA.y1) / 2;
|
|
172
175
|
const T = (p: XY): XY => ({ x: ox + (p.x - cx) * s, y: oy + (p.y - cy) * s });
|
package/src/export/plan.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { isDim, measure } from '../model/constraints';
|
|
2
2
|
import { hyp, norm2, pointInPoly } from '../model/geom';
|
|
3
|
-
import { edgeS, measureOpeningDim, openingSpan, refPoint, refS } from '../model/openings';
|
|
3
|
+
import { edgeS, hingeEdge, measureOpeningDim, openingSpan, refPoint, refS } from '../model/openings';
|
|
4
4
|
import { roomGeom } from '../model/rooms';
|
|
5
5
|
import { smoothPolyline } from '../model/smooth';
|
|
6
6
|
import { roomValid } from '../model/sketch';
|
|
7
|
-
import { dimEnds, dimRadius } from '../model/planGeom';
|
|
7
|
+
import { angleLabel, dimEnds, dimRadius } from '../model/planGeom';
|
|
8
8
|
import { wallPieces } from '../model/wallshape';
|
|
9
9
|
import { cabinetFootprints } from '../model/facesolve';
|
|
10
10
|
import { isSegment, noteLook, noteTextIn, shapeOutline } from '../model/markup';
|
|
@@ -41,7 +41,7 @@ export interface Plan {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export interface DrawOptions {
|
|
44
|
-
/** include dimensions (driving, reference,
|
|
44
|
+
/** include dimensions (driving, reference, door/window) — default true */
|
|
45
45
|
dims?: boolean;
|
|
46
46
|
/** include markup (pen, lines, arrows, shapes, clouds, notes) — default true */
|
|
47
47
|
markup?: boolean;
|
|
@@ -59,7 +59,7 @@ const defaultLinkLabel = (t: LinkTarget): { name: string; value?: string } => {
|
|
|
59
59
|
const info = linkInfoOf(t);
|
|
60
60
|
return { name: (info?.exists === false ? 'Removed: ' : '') + linkName(t), value: t.kind === 'measurement' ? info?.text ?? t.text : undefined };
|
|
61
61
|
};
|
|
62
|
-
const KIND_WORD: Record<string, string> = { photo: 'Photo', pdf: 'PDF', note: 'Note', calculator: 'Calculator', label: 'Label', drawing: '
|
|
62
|
+
const KIND_WORD: Record<string, string> = { photo: 'Photo', pdf: 'PDF', note: 'Note', calculator: 'Calculator', conversion: 'Conversion', label: 'Label', drawing: 'Sketch', cad: 'CAD' };
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* A name on paper, where nothing can be measured (a DXF has no fonts): about `perLine` characters a line, broken at a
|
|
@@ -201,7 +201,8 @@ export function emitDrawing(sk: Sketch, d: Plan, opts: DrawOptions = {}): void {
|
|
|
201
201
|
if (o.type === 'window') {
|
|
202
202
|
for (const k of [t / 6, -t / 6]) d.line(layer, Q(s0, k), Q(s1, k));
|
|
203
203
|
} else {
|
|
204
|
-
const f = o.flip ? -1 : 1,
|
|
204
|
+
const f = o.flip ? -1 : 1, [hs, ls] = hingeEdge(o) === 's1' ? [s1, s0] : [s0, s1];
|
|
205
|
+
const hinge = Q(hs, (f * t) / 2), leaf = Q(hs, f * (t / 2 + g.w)), shut = Q(ls, (f * t) / 2);
|
|
205
206
|
d.line(layer, hinge, leaf);
|
|
206
207
|
// swing: from the closed position to the open leaf, the short way round
|
|
207
208
|
const a0 = Math.atan2(shut.y - hinge.y, shut.x - hinge.x), a1 = Math.atan2(leaf.y - hinge.y, leaf.x - hinge.x);
|
|
@@ -219,14 +220,15 @@ export function emitDrawing(sk: Sketch, d: Plan, opts: DrawOptions = {}): void {
|
|
|
219
220
|
if (!roomValid(sk, r)) continue;
|
|
220
221
|
const g = roomGeom(sk, r);
|
|
221
222
|
d.text('SPACES', { x: g.c.x, y: g.c.y + 4 }, 6, r.name);
|
|
222
|
-
|
|
223
|
+
// (a sketch without its scale yet: its area is only a placeholder)
|
|
224
|
+
if (!sk.unscaled) d.text('SPACES', { x: g.c.x, y: g.c.y - 4 }, TEXT_H, fmtArea(g.area));
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
if (opts.dims !== false) {
|
|
226
228
|
for (const id in sk.cons) {
|
|
227
229
|
const c = sk.cons[id];
|
|
228
230
|
if (!isDim(c)) continue;
|
|
229
|
-
const ref = c.driving === false
|
|
231
|
+
const ref = c.driving === false;
|
|
230
232
|
const v = c.driving === false ? measure(sk, c) : c.value!;
|
|
231
233
|
const wrap = (s: string) => (ref ? '(' + s + ')' : s);
|
|
232
234
|
if (c.type === 'dist') {
|
|
@@ -241,11 +243,11 @@ export function emitDrawing(sk: Sketch, d: Plan, opts: DrawOptions = {}): void {
|
|
|
241
243
|
const off = c.off || (c.line ? WALL_DIM_OFF * outsideSide(sk, c.line) : 0);
|
|
242
244
|
linearDim(d, a, b, n, off, wrap(fmtLen(v)));
|
|
243
245
|
} else if (c.type === 'pldist') {
|
|
244
|
-
// (between walls' faces: face to face)
|
|
246
|
+
// (between walls' faces: face to face, where it was slid to along them)
|
|
245
247
|
const e = dimEnds(sk, c);
|
|
246
248
|
if (!e) continue;
|
|
247
249
|
const n = norm2(-(e.b.y - e.a.y), e.b.x - e.a.x);
|
|
248
|
-
linearDim(d, e.a, e.b, n, 0, wrap(fmtLen(v)));
|
|
250
|
+
linearDim(d, e.a, e.b, n, c.off || 0, wrap(fmtLen(v)));
|
|
249
251
|
} else if (c.type === 'diam') {
|
|
250
252
|
// (across a curved wall's face: on that face)
|
|
251
253
|
const g = dimRadius(sk, c);
|
|
@@ -261,7 +263,9 @@ export function emitDrawing(sk: Sketch, d: Plan, opts: DrawOptions = {}): void {
|
|
|
261
263
|
if (!sh) continue;
|
|
262
264
|
const s = P(sh), o1 = P(l1.a === sh ? l1.b : l1.a), o2 = P(l2.a === sh ? l2.b : l2.a);
|
|
263
265
|
const u1 = norm2(o1.x - s.x, o1.y - s.y), u2 = norm2(o2.x - s.x, o2.y - s.y), bis = norm2(u1[0] + u2[0], u1[1] + u2[1]);
|
|
264
|
-
|
|
266
|
+
// (pulled off its corner: where it was left)
|
|
267
|
+
const moved = angleLabel(sk, c);
|
|
268
|
+
d.text('DIMENSIONS', moved ? { x: moved.vtx.x + Math.cos(moved.ang) * moved.r, y: moved.vtx.y + Math.sin(moved.ang) * moved.r } : { x: s.x + bis[0] * 18, y: s.y + bis[1] * 18 }, TEXT_H, wrap(fmtAng(v)));
|
|
265
269
|
}
|
|
266
270
|
}
|
|
267
271
|
// door/window dimensions: placed ones, and the width
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type { CadDataProvider, CadFileMeta, CadMeasurements, CadMeasurementValue
|
|
|
10
10
|
export type { Unit } from './units';
|
|
11
11
|
export { memoryProvider } from './data/memory';
|
|
12
12
|
export { setFileSaver } from './react/ports';
|
|
13
|
+
export { BESIDE as CAD_BESIDE } from './react/pageLock';
|
|
13
14
|
export { laserReading, setLaserConnected, surveyClose, surveyCursor, surveyPoint } from './react/host';
|
|
14
15
|
export type { HostKeypad, HostKeypadExtra, HostKeypadRequest, MeasurementList, MeasurementListPage } from './react/hostKeypad';
|
|
15
16
|
export type { CadLinkPick, CadLinks, CadSaveMeasurement, MeasurementKind } from './editor/links';
|
package/src/model/constraints.ts
CHANGED
|
@@ -16,6 +16,7 @@ export function consLabel(c: Constraint): string {
|
|
|
16
16
|
if (c.type === 'dist' || c.type === 'pldist') return (c.type === 'dist' ? (c.line ? 'Length ' : 'Distance ') : 'Offset ') + fmtLen(c.value!);
|
|
17
17
|
if (c.type === 'diam') return c.rad ? 'Radius ' + fmtLen(c.value! / 2) : 'Diameter ' + fmtLen(c.value!);
|
|
18
18
|
if (c.type === 'angle') return 'Angle ' + fmtAng(c.value!);
|
|
19
|
+
if (c.type === 'online' && c.face) return 'Point on face';
|
|
19
20
|
return CONS_LABEL[c.type] || c.type;
|
|
20
21
|
}
|
|
21
22
|
|
package/src/model/demo.ts
CHANGED
|
@@ -24,8 +24,9 @@ export function demoSketch(): Sketch {
|
|
|
24
24
|
return sk;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** A page: a copy's drawing (`sk`), or a new one's — without a scale until its first length (model/scale.ts). */
|
|
27
28
|
export function newPage(name: string, sk?: Sketch): Page {
|
|
28
|
-
return { id: uid('pg'), name, sk: sk || emptySketch(), underlays: [], view: null };
|
|
29
|
+
return { id: uid('pg'), name, sk: sk || { ...emptySketch(), unscaled: true }, underlays: [], view: null };
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export function seedProject(): Project {
|
package/src/model/elevation.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hyp, pointInPoly } from './geom';
|
|
2
|
-
import { edgeS, openingSpan } from './openings';
|
|
2
|
+
import { edgeS, hingeEdge, openingSpan } from './openings';
|
|
3
3
|
import { roomGeom } from './rooms';
|
|
4
4
|
import { settings } from './settings';
|
|
5
5
|
import { roomValid } from './sketch';
|
|
@@ -87,7 +87,7 @@ export function elevFrame(sk: Sketch, lid: string, flip = false): ElevFrame | nu
|
|
|
87
87
|
|
|
88
88
|
export interface ElevOpening {
|
|
89
89
|
id: string; type: Opening['type']; u0: number; u1: number; sill: number; head: number;
|
|
90
|
-
/** the door's hinge edge as seen (
|
|
90
|
+
/** the door's hinge edge as seen (Opening.hinge) */
|
|
91
91
|
hinge: 'l' | 'r';
|
|
92
92
|
/** the opening's edge (anchor) at u0 and at u1 */
|
|
93
93
|
e0: OpeningEdge; e1: OpeningEdge;
|
|
@@ -101,8 +101,8 @@ export function elevOpenings(sk: Sketch, f: ElevFrame): ElevOpening[] {
|
|
|
101
101
|
if (o.line !== f.lid) continue;
|
|
102
102
|
const g = openingSpan(sk, o);
|
|
103
103
|
if (!g) continue;
|
|
104
|
-
const a = toU(f, edgeS(o, g.w, 's0')), b = toU(f, edgeS(o, g.w, 's1'));
|
|
105
|
-
out.push({ id, type: o.type, u0: Math.min(a, b), u1: Math.max(a, b), ...openingHeights(o), hinge:
|
|
104
|
+
const a = toU(f, edgeS(o, g.w, 's0')), b = toU(f, edgeS(o, g.w, 's1')), left = (hingeEdge(o) === 's1' ? b <= a : a <= b);
|
|
105
|
+
out.push({ id, type: o.type, u0: Math.min(a, b), u1: Math.max(a, b), ...openingHeights(o), hinge: left ? 'l' : 'r', e0: a <= b ? 's0' : 's1', e1: a <= b ? 's1' : 's0' });
|
|
106
106
|
}
|
|
107
107
|
return out.sort((p, q) => p.u0 - q.u0);
|
|
108
108
|
}
|
package/src/model/geom.ts
CHANGED
|
@@ -34,6 +34,16 @@ export function lineCircleT(A: XY, B: XY, c: XY, r: number): number[] {
|
|
|
34
34
|
return [(-b - s) / (2 * a), (-b + s) / (2 * a)].filter(t => t > 1e-6 && t < 1 - 1e-6);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Where circle (c1, r1) crosses circle (c2, r2): none, one where they touch, or two. */
|
|
38
|
+
export function circleCircle(c1: XY, r1: number, c2: XY, r2: number): XY[] {
|
|
39
|
+
const dx = c2.x - c1.x, dy = c2.y - c1.y, d = hyp(dx, dy);
|
|
40
|
+
if (d < 1e-12 || d > r1 + r2 + 1e-9 || d < Math.abs(r1 - r2) - 1e-9) return [];
|
|
41
|
+
const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d), h2 = r1 * r1 - a * a, h = h2 > 0 ? Math.sqrt(h2) : 0;
|
|
42
|
+
const mx = c1.x + (dx * a) / d, my = c1.y + (dy * a) / d;
|
|
43
|
+
if (h < 1e-12) return [{ x: mx, y: my }];
|
|
44
|
+
return [{ x: mx - (dy * h) / d, y: my + (dx * h) / d }, { x: mx + (dy * h) / d, y: my - (dx * h) / d }];
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
/** Circumcenter of three points, or null if they are collinear. */
|
|
38
48
|
export function circum(a: XY, b: XY, c: XY): XY | null {
|
|
39
49
|
const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
|
package/src/model/history.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { page, store } from './store';
|
|
2
|
+
import { rezoomAcrossScale } from './scale';
|
|
2
3
|
import type { Underlay } from './types';
|
|
3
4
|
import { onSessionReset } from './session';
|
|
4
5
|
|
|
@@ -20,8 +21,10 @@ function take(): Snap {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function restore(s: Snap): void {
|
|
23
|
-
const pg = page();
|
|
24
|
+
const pg = page(), was = pg.sk;
|
|
24
25
|
pg.sk = JSON.parse(s.sk);
|
|
26
|
+
// the step that gave the drawing its scale zoomed the view to match (views aren't kept here): zoom back, or again
|
|
27
|
+
rezoomAcrossScale(pg, was, pg.sk);
|
|
25
28
|
// an image uploaded since keeps its stored address (not the local copy it had when this was taken)
|
|
26
29
|
const now = new Map(pg.underlays.map(u => [u.id, u.src]));
|
|
27
30
|
pg.underlays = s.imgs.map(u => ({ ...u, src: stored(u.src, now.get(u.id)) }));
|
package/src/model/links.ts
CHANGED
|
@@ -22,8 +22,13 @@ export const isLink = (n: Note): n is Note & { link: LinkTarget } => n.type ===
|
|
|
22
22
|
|
|
23
23
|
/** How big a tile is on the screen as it's placed (points): its side on the drawing is what that is at the zoom then. */
|
|
24
24
|
export const LINK_TILE_PX = 96;
|
|
25
|
-
/** How big a tile can be on the drawing, inches; unset (never, once placed), this. */
|
|
25
|
+
/** How big a tile can be on the drawing as it's placed, inches; unset (never, once placed), this. */
|
|
26
26
|
export const LINK_MIN_IN = 6, LINK_MAX_IN = 480, LINK_SIDE_IN = 24;
|
|
27
|
+
/**
|
|
28
|
+
* How big the Tile size slider can make one, inches: wide enough that its 10%–1000% of the size tiles came in at is
|
|
29
|
+
* there to reach, and inside what the tile's drawing clips to (render/linkTile.ts: clipToBar).
|
|
30
|
+
*/
|
|
31
|
+
export const LINK_HARD_MIN_IN = 1, LINK_HARD_MAX_IN = 2400;
|
|
27
32
|
/**
|
|
28
33
|
* On the screen, points: from LINK_FULL_PX across a tile shows its picture or icon and its name; under LINK_PIN_PX it's
|
|
29
34
|
* a pin of that size, however far out the drawing is.
|
|
@@ -64,10 +69,16 @@ export function sketchLinks(sk: Sketch): LinkTarget[] {
|
|
|
64
69
|
return out;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Everything a project links to, each once: its pages' tiles, the photos expanded from tiles among their images, and
|
|
74
|
+
* the measurements its dimensions show.
|
|
75
|
+
*/
|
|
68
76
|
export function linkTargets(p: Project): LinkTarget[] {
|
|
69
77
|
const out = new Map<string, LinkTarget>();
|
|
70
|
-
for (const pg of p.pages)
|
|
78
|
+
for (const pg of p.pages) {
|
|
79
|
+
for (const t of sketchLinks(pg.sk)) if (!out.has(linkKey(t))) out.set(linkKey(t), t);
|
|
80
|
+
for (const u of pg.underlays ?? []) if (u.link && !out.has(linkKey(u.link))) out.set(linkKey(u.link), u.link);
|
|
81
|
+
}
|
|
71
82
|
return [...out.values()];
|
|
72
83
|
}
|
|
73
84
|
|
package/src/model/measurement.ts
CHANGED
|
@@ -9,11 +9,11 @@ import type { Constraint } from './types';
|
|
|
9
9
|
const LENGTHS = new Set<Constraint['type']>(['dist', 'pldist', 'diam']);
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Dimension `c`'s value (or `value`, one it's about to have) as a measurement. Null for what isn't one: reference
|
|
13
|
-
*
|
|
12
|
+
* Dimension `c`'s value (or `value`, one it's about to have) as a measurement. Null for what isn't one: reference
|
|
13
|
+
* dimensions, measured-only ones, and constraints without a value.
|
|
14
14
|
*/
|
|
15
15
|
export function dimMeasurement(c: Constraint, value = c.value): CadMeasurementValue | null {
|
|
16
|
-
if (c.driving === false ||
|
|
16
|
+
if (c.driving === false || value == null || c.auto) return null;
|
|
17
17
|
if (c.type === 'angle') return { kind: 'angle', value: Math.round(value * 100) / 100 };
|
|
18
18
|
// a fillet's radius is kept as its diameter; BOLDR keeps the radius
|
|
19
19
|
if (LENGTHS.has(c.type)) return { kind: 'length', value: Math.round((c.rad ? value / 2 : value) * 25400) };
|
package/src/model/openings.ts
CHANGED
|
@@ -28,6 +28,24 @@ export function openingSpan(sk: Sketch, o: Opening): Span | null {
|
|
|
28
28
|
/** Position of an edge, inches along the opening's line. */
|
|
29
29
|
export const edgeS = (o: Opening, w: number, edge: OpeningEdge): number => (edge === 's0' ? o.at - w / 2 : o.at + w / 2);
|
|
30
30
|
|
|
31
|
+
/** The jamb a door hangs on (Opening.hinge): the plan, the wall view, 3D and exports all draw its leaf from there. */
|
|
32
|
+
export const hingeEdge = (o: Opening): OpeningEdge => o.hinge ?? 's0';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A door turned to the next of its four ways round, by Swing in the bar (the user's call, 2026-10-27: "4 options of flippings instead of 2
|
|
36
|
+
* (inner, outer, inner left, outer left) pressing the one button should toggle between the different options"):
|
|
37
|
+
* swinging into the room, it swings out on the same jamb; swinging out, it swings back in, hung on its other jamb. So
|
|
38
|
+
* inner, outer, inner left, outer left, and inner again, from whichever it was. `inside`: the side of its wall the
|
|
39
|
+
* room is on (+1: the left of its line's start → end).
|
|
40
|
+
*/
|
|
41
|
+
export function flipDoor(o: Opening, inside: 1 | -1): void {
|
|
42
|
+
if ((o.flip ? -1 : 1) !== inside) {
|
|
43
|
+
if (hingeEdge(o) === 's0') o.hinge = 's1';
|
|
44
|
+
else delete o.hinge;
|
|
45
|
+
}
|
|
46
|
+
o.flip = !o.flip;
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
/** World position of an opening edge anchor. */
|
|
32
50
|
export function edgePoint(sk: Sketch, opId: string, edge: OpeningEdge): XY | null {
|
|
33
51
|
const o = sk.openings[opId], g = o && openingSpan(sk, o);
|
package/src/model/planGeom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clamp, hyp, norm2, normAng, pointInPoly } from './geom';
|
|
2
|
-
import { roomGeom } from './rooms';
|
|
3
|
-
import { frameMemo, roomValid } from './sketch';
|
|
2
|
+
import { roomGeom, roomOutline } from './rooms';
|
|
3
|
+
import { circleEdge, frameMemo, roomValid } from './sketch';
|
|
4
4
|
import type { Arc, Constraint, LineKind, Sketch, XY } from './types';
|
|
5
5
|
import { faceCorner } from './wallfaces';
|
|
6
6
|
import { getUnitPrefs, isMetric, type Unit } from '../units';
|
|
@@ -52,9 +52,16 @@ export function openingFrame(sk: Sketch, id: string): OpeningFrame | null {
|
|
|
52
52
|
return { A, ux, uy, nx: -uy, ny: ux, s0: at - w / 2, s1: at + w / 2, at, w, t: l.kind === 'wall' ? l.thick : 2 };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* An arc's centre, radius and angles: it runs counter-clockwise from t0 (its start, a) to t1 (its end, b). `full`: its
|
|
57
|
+
* ends have met — dragged all the way round onto its start — so it goes all the way round (t1 is t0 + 2π), as the
|
|
58
|
+
* circle it becomes when they join (select.ts; gc in model/sketch.ts), not nothing.
|
|
59
|
+
*/
|
|
60
|
+
export function arcAngles(sk: Sketch, a: Arc): { o: XY; r: number; t0: number; t1: number; full: boolean } {
|
|
56
61
|
const o = sk.points[a.c], s = sk.points[a.a], e = sk.points[a.b];
|
|
57
|
-
|
|
62
|
+
const r = hyp(s.x - o.x, s.y - o.y), t0 = Math.atan2(s.y - o.y, s.x - o.x);
|
|
63
|
+
const full = a.a === a.b || hyp(e.x - s.x, e.y - s.y) <= r * 1e-7;
|
|
64
|
+
return { o, r, t0, t1: full ? t0 + 2 * Math.PI : Math.atan2(e.y - o.y, e.x - o.x), full };
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/** +1 or -1: the side of a line facing away from any room (where opening dimensions go). */
|
|
@@ -65,7 +72,7 @@ export function outsideSide(sk: Sketch, lid: string): number {
|
|
|
65
72
|
// (asked for every door and window on every frame: the rooms' outlines, and the boxes around them, are worked out
|
|
66
73
|
// once a frame — a room the box rules out isn't looked at more closely)
|
|
67
74
|
const rooms = frameMemo(sk, 'roomOutlines', () => Object.values(sk.rooms).filter(r => roomValid(sk, r)).map(r => {
|
|
68
|
-
const poly =
|
|
75
|
+
const poly = roomOutline(sk, r), xs = poly.map(p => p.x), ys = poly.map(p => p.y);
|
|
69
76
|
return { poly, x0: Math.min(...xs), x1: Math.max(...xs), y0: Math.min(...ys), y1: Math.max(...ys) };
|
|
70
77
|
}));
|
|
71
78
|
for (const r of rooms) if (m.x >= r.x0 && m.x <= r.x1 && m.y >= r.y0 && m.y <= r.y1 && pointInPoly(m, r.poly)) return -1;
|
|
@@ -88,13 +95,14 @@ export function curveFrame(sk: Sketch, id: string): { o: XY; r: number; arc?: {
|
|
|
88
95
|
const a = sk.arcs[id];
|
|
89
96
|
if (!a || !sk.points[a.c] || !sk.points[a.a] || !sk.points[a.b]) return null;
|
|
90
97
|
const g = arcAngles(sk, a);
|
|
91
|
-
|
|
98
|
+
// (its ends met: all the way round, framed as the circle it's becoming)
|
|
99
|
+
return g.full ? { o: g.o, r: g.r } : { o: g.o, r: g.r, arc: { t0: g.t0, t1: g.t1 } };
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
/**
|
|
95
103
|
* The space against one side of a wall, if any (a point just past its face there is inside it): its id. A straight
|
|
96
|
-
* wall's sides are the left (+1) and right (−1) of its A → B; a curved one's, its outer (+1) and inner (−1) faces —
|
|
97
|
-
*
|
|
104
|
+
* wall's sides are the left (+1) and right (−1) of its A → B; a curved one's, its outer (+1) and inner (−1) faces — a
|
|
105
|
+
* circle's inside too, a round space (model/rooms.ts).
|
|
98
106
|
*/
|
|
99
107
|
export function spaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null {
|
|
100
108
|
const l = sk.lines[id];
|
|
@@ -105,21 +113,25 @@ export function spaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null
|
|
|
105
113
|
const m = { x: (a.x + b.x) / 2 + n[0] * k, y: (a.y + b.y) / 2 + n[1] * k };
|
|
106
114
|
for (const rid in sk.rooms) {
|
|
107
115
|
const r = sk.rooms[rid];
|
|
108
|
-
if (roomValid(sk, r) && pointInPoly(m,
|
|
116
|
+
if (roomValid(sk, r) && pointInPoly(m, roomOutline(sk, r))) return rid;
|
|
109
117
|
}
|
|
110
118
|
return null;
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
function curveSpaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null {
|
|
114
122
|
const f = curveFrame(sk, id), w = sk.circles[id] ?? sk.arcs[id];
|
|
115
|
-
if (!f || !w
|
|
123
|
+
if (!f || !w) return null;
|
|
116
124
|
// (just past the face at the middle of the arc, or anywhere around a circle)
|
|
117
125
|
const at = f.arc ? f.arc.t0 + normAng(f.arc.t1 - f.arc.t0) / 2 : 0, rr = f.r + side * (wallThick(w) / 2 + 1);
|
|
118
126
|
if (rr <= 0) return null;
|
|
119
127
|
const m = { x: f.o.x + Math.cos(at) * rr, y: f.o.y + Math.sin(at) * rr };
|
|
128
|
+
// inside a circle: only a space it bounds — a round column's inside isn't the room it stands in (a space has no
|
|
129
|
+
// holes: the room's outline goes over it)
|
|
130
|
+
const own = !f.arc && side === -1;
|
|
120
131
|
// (a space's outline as it's drawn: its arcs as arcs, not chords)
|
|
121
132
|
for (const rid in sk.rooms) {
|
|
122
133
|
const r = sk.rooms[rid];
|
|
134
|
+
if (own && !(r.edges ?? []).some(e => circleEdge(e)?.cid === id)) continue;
|
|
123
135
|
if (roomValid(sk, r) && pointInPoly(m, roomGeom(sk, r).poly)) return rid;
|
|
124
136
|
}
|
|
125
137
|
return null;
|
|
@@ -188,6 +200,54 @@ export function dimEnds(sk: Sketch, c: Constraint): { a: XY; b: XY } | null {
|
|
|
188
200
|
return null;
|
|
189
201
|
}
|
|
190
202
|
|
|
203
|
+
/**
|
|
204
|
+
* An angle dimension's corner: where its two lines meet, with `u1`/`u2` along each away from it (`len1`/`len2`: as far
|
|
205
|
+
* as each goes), and the bisector. For two lines that don't meet: the middle of the first, its label straight up.
|
|
206
|
+
*/
|
|
207
|
+
export function angleCorner(sk: Sketch, c: Constraint): { vtx: XY; bis: [number, number]; u1?: [number, number]; u2?: [number, number]; len1?: number; len2?: number } | null {
|
|
208
|
+
const l1 = c.l1 ? sk.lines[c.l1] : undefined, l2 = c.l2 ? sk.lines[c.l2] : undefined;
|
|
209
|
+
if (!l1 || !l2) return null;
|
|
210
|
+
const P = (id: string) => sk.points[id];
|
|
211
|
+
const sh = [l1.a, l1.b].find(p => p === l2.a || p === l2.b);
|
|
212
|
+
if (sh) {
|
|
213
|
+
const s = P(sh), o1 = P(l1.a === sh ? l1.b : l1.a), o2 = P(l2.a === sh ? l2.b : l2.a);
|
|
214
|
+
const u1 = norm2(o1.x - s.x, o1.y - s.y), u2 = norm2(o2.x - s.x, o2.y - s.y);
|
|
215
|
+
return { vtx: s, bis: norm2(u1[0] + u2[0], u1[1] + u2[1]), u1, u2, len1: hyp(o1.x - s.x, o1.y - s.y), len2: hyp(o2.x - s.x, o2.y - s.y) };
|
|
216
|
+
}
|
|
217
|
+
const a = P(l1.a), b = P(l1.b);
|
|
218
|
+
return { vtx: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, bis: [0, 1] };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const wrapPi = (x: number) => { while (x > Math.PI) x -= 2 * Math.PI; while (x < -Math.PI) x += 2 * Math.PI; return x; };
|
|
222
|
+
|
|
223
|
+
/** The short way round from `u1` to `u2`: where it starts and how far it turns, world radians (negative: clockwise). */
|
|
224
|
+
export function angleSweep(u1: [number, number], u2: [number, number]): { t1: number; sweep: number } {
|
|
225
|
+
const t1 = Math.atan2(u1[1], u1[0]);
|
|
226
|
+
return { t1, sweep: wrapPi(Math.atan2(u2[1], u2[0]) - t1) };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Direction `a` (world radians) kept within a sweep: past either end of it, the nearer end. In (-π, π]. */
|
|
230
|
+
export function inSweep(a: number, t1: number, sweep: number): number {
|
|
231
|
+
const rel = wrapPi(a - t1), lo = Math.min(0, sweep), hi = Math.max(0, sweep);
|
|
232
|
+
if (rel >= lo && rel <= hi) return wrapPi(t1 + rel);
|
|
233
|
+
return wrapPi(t1 + (Math.abs(wrapPi(rel - lo)) <= Math.abs(wrapPi(rel - hi)) ? lo : hi));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* An angle dimension pulled off its corner (the user's call, 2026-10-24: angles pull like lengths): its arc's radius
|
|
238
|
+
* (`off`), and its label's direction (`ang`), kept between its two lines, in the angle it measures. Null for one that
|
|
239
|
+
* hasn't been moved: it's drawn as it always was, a little arc at the corner and the label out along the bisector.
|
|
240
|
+
*/
|
|
241
|
+
export function angleLabel(sk: Sketch, c: Constraint): { vtx: XY; r: number; ang: number } | null {
|
|
242
|
+
if (c.type !== 'angle' || c.off == null) return null;
|
|
243
|
+
const g = angleCorner(sk, c);
|
|
244
|
+
if (!g) return null;
|
|
245
|
+
const want = c.ang ?? Math.atan2(g.bis[1], g.bis[0]);
|
|
246
|
+
if (!g.u1 || !g.u2) return { vtx: g.vtx, r: c.off, ang: want };
|
|
247
|
+
const { t1, sweep } = angleSweep(g.u1, g.u2);
|
|
248
|
+
return { vtx: g.vtx, r: c.off, ang: inSweep(want, t1, sweep) };
|
|
249
|
+
}
|
|
250
|
+
|
|
191
251
|
/**
|
|
192
252
|
* Where a diameter (or radius) dimension's arrows go: its curve's centre, and the radius it measures at — the
|
|
193
253
|
* centreline's, or on a curved wall's face, that face's.
|
package/src/model/project.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { normalizeImages } from './images';
|
|
2
2
|
import { newPage } from './demo';
|
|
3
|
+
import { settleScale } from './scale';
|
|
3
4
|
import type { Page, Project } from './types';
|
|
4
5
|
|
|
6
|
+
/** A page as it's loaded: its images in today's form, and its scale settled (model/scale.ts). */
|
|
7
|
+
const tidy = (pg: Page): Page => { const n = normalizeImages(pg); settleScale(n.sk); return n; };
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Older saves group pages into spaces/levels (Project → Space → Page). Flatten them, in order; when the same page
|
|
7
11
|
* name appears in more than one level, it's prefixed with the level's name ("Level 2 · Floor plan").
|
|
8
12
|
*/
|
|
9
13
|
export function migrateProject(p: { name: string; pages?: Page[]; spaces?: { name: string; pages: Page[] }[] }): Project {
|
|
10
|
-
if (p.pages) return { name: p.name, pages: p.pages.map(
|
|
14
|
+
if (p.pages) return { name: p.name, pages: p.pages.map(tidy) };
|
|
11
15
|
const spaces = p.spaces || [], count: Record<string, number> = {};
|
|
12
16
|
for (const s of spaces) for (const pg of s.pages) count[pg.name] = (count[pg.name] || 0) + 1;
|
|
13
|
-
const pages = spaces.flatMap(s => s.pages.map(pg =>
|
|
17
|
+
const pages = spaces.flatMap(s => s.pages.map(pg => tidy(count[pg.name] > 1 ? { ...pg, name: s.name + ' · ' + pg.name } : pg)));
|
|
14
18
|
return { name: p.name, pages: pages.length ? pages : [newPage('Floor plan')] };
|
|
15
19
|
}
|