@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
package/src/model/rooms.ts
CHANGED
|
@@ -1,25 +1,58 @@
|
|
|
1
1
|
import { hyp, normAng, pointInPoly, polyArea } from './geom';
|
|
2
2
|
import { uid } from './ids';
|
|
3
|
-
import { linesBetween } from './sketch';
|
|
3
|
+
import { circleEdge, linesBetween, onCircle } from './sketch';
|
|
4
4
|
import type { Room, Sketch, XY } from './types';
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
Rooms are faces of the drawing: closed loops of walls and lines, straight or curved
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Rooms are faces of the drawing: closed loops of walls and lines, straight or curved — and circles (the user's call,
|
|
8
|
+
2026-10-29: "a space can be created by all closed shapes").
|
|
9
|
+
A loop is a list of corner points plus (for loops that include curves) the edge between each corner and the next:
|
|
10
|
+
a line's or arc's id — or, along a circle, the circle's id and which way round the loop runs on it: '<id>+'
|
|
11
|
+
counter-clockwise, '<id>-' clockwise (circleEdge). A circle that is a room's whole outline has no corners: loop [],
|
|
12
|
+
edges ['<id>+']. Where a line or an arc ends on a circle, the circle is divided there, as lines are at a corner.
|
|
13
|
+
Curves are measured as densely sampled polylines; a round room's area and perimeter exactly.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
/** Arcs are sampled every 5° for room outlines (≥ 8 pieces). */
|
|
13
17
|
const ARC_STEP = Math.PI / 36;
|
|
14
18
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* An edge of the drawing that can bound a room: a non-construction line or arc, or a piece of a non-construction
|
|
21
|
+
* circle between two points on it (`circle`: counter-clockwise from a to b).
|
|
22
|
+
*/
|
|
23
|
+
interface Edge { id: string; a: string; b: string; circle?: string }
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The drawing as edges between points, for walking its faces. A circle is in pieces between the points on it where a
|
|
27
|
+
* line or an arc ends; one with fewer than two gets the walk's own points (opposite each other, or the one there), so
|
|
28
|
+
* it's a loop like any other. They're not the drawing's, and never left in a room (stored).
|
|
29
|
+
*/
|
|
30
|
+
function graph(sk: Sketch): { edges: Edge[]; pos: (p: string) => XY } {
|
|
31
|
+
const edges: Edge[] = [], ends = new Set<string>();
|
|
32
|
+
for (const id in sk.lines) { const l = sk.lines[id]; if (!l.constr) { edges.push({ id, a: l.a, b: l.b }); ends.add(l.a); ends.add(l.b); } }
|
|
33
|
+
for (const id in sk.arcs) { const a = sk.arcs[id]; if (!a.constr) { edges.push({ id, a: a.a, b: a.b }); ends.add(a.a); ends.add(a.b); } }
|
|
34
|
+
const own = new Map<string, XY>();
|
|
35
|
+
for (const cid in sk.circles) {
|
|
36
|
+
const ci = sk.circles[cid], o = sk.points[ci.c];
|
|
37
|
+
if (ci.constr || !o || !(ci.r > 0)) continue;
|
|
38
|
+
const on = [...ends].filter(p => onCircle(sk, cid, p)).map(p => ({ p, t: normAng(Math.atan2(sk.points[p].y - o.y, sk.points[p].x - o.x)) }));
|
|
39
|
+
const add = (t: number) => { const id = '~' + cid + '~' + on.length; own.set(id, { x: o.x + ci.r * Math.cos(t), y: o.y + ci.r * Math.sin(t) }); on.push({ p: id, t: normAng(t) }); };
|
|
40
|
+
if (!on.length) add(0);
|
|
41
|
+
if (on.length === 1) add(on[0].t + Math.PI);
|
|
42
|
+
on.sort((u, v) => u.t - v.t);
|
|
43
|
+
on.forEach((u, i) => edges.push({ id: cid + '#' + i, a: u.p, b: on[(i + 1) % on.length].p, circle: cid }));
|
|
44
|
+
}
|
|
45
|
+
return { edges, pos: p => sk.points[p] ?? own.get(p)! };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Round a circle (centre o, radius r) from A to B, counter-clockwise (dir 1) or clockwise (−1), sampled; all the way round when they're the same point. */
|
|
49
|
+
function roundCircle(o: XY, r: number, A: XY, B: XY, dir: 1 | -1): XY[] {
|
|
50
|
+
const t0 = Math.atan2(A.y - o.y, A.x - o.x), t1 = Math.atan2(B.y - o.y, B.x - o.x);
|
|
51
|
+
const sweep = normAng(dir * (t1 - t0)) || 2 * Math.PI, n = Math.max(8, Math.ceil(sweep / ARC_STEP));
|
|
52
|
+
const pts: XY[] = [];
|
|
53
|
+
for (let i = 0; i <= n; i++) { const t = t0 + (dir * sweep * i) / n; pts.push({ x: o.x + r * Math.cos(t), y: o.y + r * Math.sin(t) }); }
|
|
54
|
+
pts[0] = A; pts[n] = B;
|
|
55
|
+
return pts;
|
|
23
56
|
}
|
|
24
57
|
|
|
25
58
|
/** Ids of the lines and arcs joining two points. */
|
|
@@ -29,10 +62,18 @@ export function edgesBetween(sk: Sketch, a: string, b: string): string[] {
|
|
|
29
62
|
return out;
|
|
30
63
|
}
|
|
31
64
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
65
|
+
/**
|
|
66
|
+
* A room edge as a polyline starting at point `from` (lines: 2 points; arcs: sampled along the curve). Along a
|
|
67
|
+
* circle ('<id>±'), round it to point `to` — or, with no corners (a whole circle), from its rightmost point back to it.
|
|
68
|
+
*/
|
|
69
|
+
export function edgePolyline(sk: Sketch, id: string, from?: string, to?: string): XY[] {
|
|
70
|
+
const ce = circleEdge(id);
|
|
71
|
+
if (ce) {
|
|
72
|
+
const ci = sk.circles[ce.cid], o = sk.points[ci.c], A = from ? sk.points[from] : { x: o.x + ci.r, y: o.y };
|
|
73
|
+
return roundCircle(o, ci.r, A, to ? sk.points[to] : A, ce.dir);
|
|
74
|
+
}
|
|
34
75
|
const l = sk.lines[id];
|
|
35
|
-
if (l) { const other = l.a === from ? l.b : l.a; return [sk.points[from], sk.points[other]]; }
|
|
76
|
+
if (l) { const other = l.a === from ? l.b : l.a; return [sk.points[from!], sk.points[other]]; }
|
|
36
77
|
const ar = sk.arcs[id], o = sk.points[ar.c], s = sk.points[ar.a], e = sk.points[ar.b];
|
|
37
78
|
const r = hyp(s.x - o.x, s.y - o.y), t0 = Math.atan2(s.y - o.y, s.x - o.x);
|
|
38
79
|
const sweep = normAng(Math.atan2(e.y - o.y, e.x - o.x) - t0) || 2 * Math.PI; // arcs run counter-clockwise a → b
|
|
@@ -44,15 +85,14 @@ export function edgePolyline(sk: Sketch, id: string, from: string): XY[] {
|
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
const edgeThick = (sk: Sketch, id: string): number => {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return a && a.kind === 'wall' ? a.thick || 4.5 : 0;
|
|
88
|
+
const ce = circleEdge(id);
|
|
89
|
+
const o = ce ? sk.circles[ce.cid] : sk.lines[id] ?? sk.arcs[id];
|
|
90
|
+
return o && o.kind === 'wall' ? (o as { thick?: number }).thick || 4.5 : 0;
|
|
51
91
|
};
|
|
52
92
|
|
|
53
93
|
/** The edges of a room loop: stored ones, or (older rooms of straight lines) the line joining each pair. */
|
|
54
94
|
export function roomEdges(sk: Sketch, r: Room): string[] | null {
|
|
55
|
-
if (r.edges && r.edges.length === r.loop.length) return r.edges;
|
|
95
|
+
if (r.edges && (r.edges.length === r.loop.length || (!r.loop.length && r.edges.length === 1))) return r.edges;
|
|
56
96
|
const out: string[] = [];
|
|
57
97
|
for (let i = 0; i < r.loop.length; i++) {
|
|
58
98
|
const e = edgesBetween(sk, r.loop[i], r.loop[(i + 1) % r.loop.length])[0];
|
|
@@ -66,59 +106,90 @@ export function roomEdges(sk: Sketch, r: Room): string[] | null {
|
|
|
66
106
|
function outline(sk: Sketch, loop: string[], edges: string[]): { pts: XY[]; half: number[] } {
|
|
67
107
|
const pts: XY[] = [], half: number[] = [];
|
|
68
108
|
edges.forEach((id, i) => {
|
|
69
|
-
const pl = edgePolyline(sk, id, loop[i]), t = edgeThick(sk, id) / 2;
|
|
109
|
+
const pl = edgePolyline(sk, id, loop[i], loop[(i + 1) % loop.length]), t = edgeThick(sk, id) / 2;
|
|
70
110
|
for (let k = 0; k < pl.length - 1; k++) { pts.push(pl[k]); half.push(t); }
|
|
71
111
|
});
|
|
72
112
|
return { pts, half };
|
|
73
113
|
}
|
|
74
114
|
|
|
75
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* A face as it's walked, as it's kept: a circle's pieces as the circle the way round it's walked ('<id>±'), and no
|
|
117
|
+
* corner between two stretches of one circle the same way round — not the walk's own points, nor where something
|
|
118
|
+
* meets the circle from outside (the room would go with it). All the way round one circle: no corners at all.
|
|
119
|
+
*/
|
|
120
|
+
function kept(loop: string[], edges: Edge[]): { loop: string[]; edges: string[] } {
|
|
121
|
+
const L = loop.slice(), E = edges.map((e, i) => (e.circle ? e.circle + (L[i] === e.a ? '+' : '-') : e.id));
|
|
122
|
+
for (let again = true; again && L.length > 1;) {
|
|
123
|
+
again = false;
|
|
124
|
+
for (let i = 0; i < L.length; i++) {
|
|
125
|
+
if (E[(i - 1 + L.length) % L.length] === E[i] && circleEdge(E[i])) { L.splice(i, 1); E.splice(i, 1); again = true; break; }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return L.length === 1 && circleEdge(E[0]) ? { loop: [], edges: E } : { loop: L, edges: E };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find the smallest closed face of lines, arcs and circles around (wx, wy): its corner points and the edge after
|
|
133
|
+
* each. A ray to +x crosses the face's outline; the nearest crossing is on it unless it's an island inside the face
|
|
134
|
+
* (a column), whose own outline doesn't hold the point — then the next.
|
|
135
|
+
*/
|
|
76
136
|
export function findFace(sk: Sketch, wx: number, wy: number): { loop: string[]; edges: string[] } | null {
|
|
77
|
-
const
|
|
137
|
+
const g = graph(sk), byId = new Map(g.edges.map(e => [e.id, e]));
|
|
78
138
|
const adj = new Map<string, Edge[]>();
|
|
79
|
-
for (const e of edges) for (const p of [e.a, e.b]) { if (!adj.has(p)) adj.set(p, []); adj.get(p)!.push(e); }
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
139
|
+
for (const e of g.edges) for (const p of [e.a, e.b]) { if (!adj.has(p)) adj.set(p, []); adj.get(p)!.push(e); }
|
|
140
|
+
/** An edge as a polyline from one of its ends. */
|
|
141
|
+
const along = (e: Edge, from: string): XY[] => {
|
|
142
|
+
if (!e.circle) return edgePolyline(sk, e.id, from);
|
|
143
|
+
const ci = sk.circles[e.circle], pts = roundCircle(sk.points[ci.c], ci.r, g.pos(e.a), g.pos(e.b), 1);
|
|
144
|
+
return from === e.a ? pts : pts.reverse();
|
|
145
|
+
};
|
|
146
|
+
// every crossing of the ray, nearest first: walking the edge the way that has the point on its left
|
|
147
|
+
const hits: { x: number; e: Edge; forward: boolean }[] = [];
|
|
148
|
+
for (const e of g.edges) {
|
|
149
|
+
const pl = along(e, e.a);
|
|
84
150
|
for (let k = 0; k < pl.length - 1; k++) {
|
|
85
151
|
const A = pl[k], B = pl[k + 1];
|
|
86
152
|
if ((A.y > wy) === (B.y > wy)) continue;
|
|
87
153
|
const x = A.x + ((wy - A.y) * (B.x - A.x)) / (B.y - A.y);
|
|
88
|
-
if (x <= wx
|
|
89
|
-
|
|
90
|
-
const cr = (B.x - A.x) * (wy - A.y) - (B.y - A.y) * (wx - A.x);
|
|
91
|
-
best = { x, e, forward: cr >= 0 };
|
|
154
|
+
if (x <= wx) continue;
|
|
155
|
+
hits.push({ x, e, forward: (B.x - A.x) * (wy - A.y) - (B.y - A.y) * (wx - A.x) >= 0 });
|
|
92
156
|
}
|
|
93
157
|
}
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
158
|
+
hits.sort((p, q) => p.x - q.x);
|
|
159
|
+
const dirFrom = (e: Edge, p: string) => { const pl = along(e, p); return Math.atan2(pl[1].y - pl[0].y, pl[1].x - pl[0].x); };
|
|
160
|
+
const tried = new Set<string>();
|
|
161
|
+
for (const h of hits) {
|
|
162
|
+
const key = h.e.id + (h.forward ? '>' : '<');
|
|
163
|
+
if (tried.has(key)) continue;
|
|
164
|
+
tried.add(key);
|
|
165
|
+
const start = h.forward ? h.e.a : h.e.b;
|
|
166
|
+
const loop = [start], loopEdges = [h.e];
|
|
167
|
+
let inEdge = h.e, cur = h.forward ? h.e.b : h.e.a, guard = 0;
|
|
168
|
+
// walk the face, always taking the tightest turn (direction of travel measured along the curve at the corner)
|
|
169
|
+
while (cur !== start && guard++ < 400) {
|
|
170
|
+
loop.push(cur);
|
|
171
|
+
const nb = adj.get(cur) || [];
|
|
172
|
+
const a0 = dirFrom(inEdge, cur);
|
|
173
|
+
let next: Edge | null = null, bd = Infinity;
|
|
174
|
+
for (const e of nb) {
|
|
175
|
+
if (e.id === inEdge.id && nb.length > 1) continue;
|
|
176
|
+
let d = a0 - dirFrom(e, cur);
|
|
177
|
+
while (d <= 1e-9) d += 2 * Math.PI;
|
|
178
|
+
while (d > 2 * Math.PI + 1e-9) d -= 2 * Math.PI;
|
|
179
|
+
if (d < bd) { bd = d; next = e; }
|
|
180
|
+
}
|
|
181
|
+
if (!next) break;
|
|
182
|
+
loopEdges.push(next);
|
|
183
|
+
cur = next.a === cur ? next.b : next.a;
|
|
184
|
+
inEdge = byId.get(next.id)!;
|
|
111
185
|
}
|
|
112
|
-
if (
|
|
113
|
-
loopEdges
|
|
114
|
-
|
|
115
|
-
|
|
186
|
+
if (cur !== start) continue;
|
|
187
|
+
const face = kept(loop, loopEdges);
|
|
188
|
+
if (face.loop.some(p => !sk.points[p])) continue;
|
|
189
|
+
const poly = outline(sk, face.loop, face.edges).pts;
|
|
190
|
+
if (polyArea(poly) > 0 && pointInPoly({ x: wx, y: wy }, poly)) return face;
|
|
116
191
|
}
|
|
117
|
-
|
|
118
|
-
const poly = outline(sk, loop, loopEdges).pts;
|
|
119
|
-
if (polyArea(poly) <= 0) return null;
|
|
120
|
-
if (!pointInPoly({ x: wx, y: wy }, poly)) return null;
|
|
121
|
-
return { loop, edges: loopEdges };
|
|
192
|
+
return null;
|
|
122
193
|
}
|
|
123
194
|
|
|
124
195
|
export interface RoomGeom {
|
|
@@ -153,9 +224,12 @@ export function roomGeom(sk: Sketch, r: Room): RoomGeom {
|
|
|
153
224
|
const t = ((e2.p.x - e1.p.x) * e2.d.y - (e2.p.y - e1.p.y) * e2.d.x) / den;
|
|
154
225
|
inner.push({ x: e1.p.x + e1.d.x * t, y: e1.p.y + e1.d.y * t });
|
|
155
226
|
}
|
|
156
|
-
|
|
227
|
+
let area = Math.abs(polyArea(inner));
|
|
157
228
|
let per = 0;
|
|
158
229
|
for (let i = 0; i < inner.length; i++) { const a = inner[i], b = inner[(i + 1) % inner.length]; per += hyp(b.x - a.x, b.y - a.y); }
|
|
230
|
+
// a round room: its area and perimeter exactly, to the inside face of its wall (not the sampled outline's)
|
|
231
|
+
const whole = !r.loop.length && edges?.length === 1 ? circleEdge(edges[0]) : null, ci = whole && sk.circles[whole.cid];
|
|
232
|
+
if (ci) { const ri = Math.max(0, ci.r - edgeThick(sk, edges![0]) / 2); area = Math.PI * ri * ri; per = 2 * Math.PI * ri; }
|
|
159
233
|
let cx = 0, cy = 0, A = 0;
|
|
160
234
|
for (let i = 0; i < n; i++) {
|
|
161
235
|
const a = poly[i], b = poly[(i + 1) % n];
|
|
@@ -167,16 +241,30 @@ export function roomGeom(sk: Sketch, r: Room): RoomGeom {
|
|
|
167
241
|
}
|
|
168
242
|
|
|
169
243
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
244
|
+
* A room's outline for telling what's inside it: its corners (cheap, and what it's always been) — or, for one that
|
|
245
|
+
* runs round a circle (it has too few corners to say), its outline as drawn.
|
|
246
|
+
*/
|
|
247
|
+
export function roomOutline(sk: Sketch, r: Room): XY[] {
|
|
248
|
+
return r.loop.length >= 3 && !(r.edges ?? []).some(e => circleEdge(e)) ? r.loop.map(p => sk.points[p]) : roomGeom(sk, r).poly;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Add a room for a closed loop (or return the existing one). `edges` names the line/arc (or way round a circle) after
|
|
253
|
+
* each corner; leave it out for loops of straight lines. A whole circle has no corners: loop [] and its one edge. Null
|
|
254
|
+
* if it's under 1 sq ft.
|
|
172
255
|
*/
|
|
173
256
|
export function maybeRoom(sk: Sketch, loop: string[], edges?: string[]): string | null {
|
|
174
|
-
if (
|
|
257
|
+
if ('cabs' in sk) return null; // spaces are the plan's: not on a wall face
|
|
258
|
+
const round = !loop.length && edges?.length === 1 && !!circleEdge(edges[0]);
|
|
259
|
+
if (loop.length < 2 && !round) return null;
|
|
175
260
|
const es = edges || roomEdges(sk, { loop, name: '' });
|
|
176
261
|
if (!es) return null;
|
|
177
262
|
if (Math.abs(polyArea(outline(sk, loop, es).pts)) < 144) return null;
|
|
178
|
-
|
|
179
|
-
|
|
263
|
+
// (the same room: the same edges — round a circle, from the same corner: a circle cut by a line bounds two rooms
|
|
264
|
+
// of the same edges, the two ways round)
|
|
265
|
+
const keyOf = (l: string[], e: string[]) => e.map((id, i) => (circleEdge(id) ? id + '@' + (l[i] ?? '') : id)).sort().join('|');
|
|
266
|
+
const key = keyOf(loop, es);
|
|
267
|
+
for (const id in sk.rooms) { const r = sk.rooms[id], re = roomEdges(sk, r); if (re && keyOf(r.loop, re) === key) return id; }
|
|
180
268
|
const id = uid('r');
|
|
181
269
|
sk.rooms[id] = { loop: loop.slice(), name: 'Space ' + sk.seq.room++, ...(edges ? { edges: edges.slice() } : {}) };
|
|
182
270
|
return id;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { measure } from './constraints';
|
|
2
|
+
import { deep } from './ids';
|
|
3
|
+
import type { Constraint, Page, Sketch, View } from './types';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
A new page has no scale until its first length (the user's call, 2026-10-29: "first needing to set a dimension
|
|
7
|
+
before going into 3d, objects, or walls"). People sketch in proportion, not to size, so the inches a stroke gets are
|
|
8
|
+
only how far the finger went at that zoom. The first length entered (typed, lasered or linked) tells the editor what
|
|
9
|
+
they are: the whole sketch is scaled so that line is that long, and the view zoomed by the inverse at the same
|
|
10
|
+
moment, so nothing moves on the screen — only the numbers become real. Later lengths move geometry as before.
|
|
11
|
+
|
|
12
|
+
What's scaled is where things are: points, radii, notes, pen marks, link tiles, images, where dimensions sit. What
|
|
13
|
+
isn't is what has a real size of its own: a wall's thickness and height, a door's or window's size, a cabinet's.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The page's drawing has no scale yet. */
|
|
17
|
+
export const isUnscaled = (sk: Sketch): boolean => !!sk.unscaled;
|
|
18
|
+
|
|
19
|
+
/** A length a person enters (a line's, a distance, a diameter): what can give the drawing its scale. */
|
|
20
|
+
export const isEnteredLength = (c: Constraint): boolean =>
|
|
21
|
+
(c.type === 'dist' || c.type === 'pldist' || c.type === 'diam') && c.driving !== false && !c.auto && !c.elev && !c.anchor && (c.value ?? 0) > 0;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A drawing that comes in marked without its scale but with lengths entered all the same — edited by a version from
|
|
25
|
+
* before this, which kept a mark it didn't know — has its scale: the mark goes, so no length is taken for its first
|
|
26
|
+
* (that would scale a drawing that's already to size). Run on every drawing loaded or arriving.
|
|
27
|
+
*/
|
|
28
|
+
export function settleScale(sk: Sketch): Sketch {
|
|
29
|
+
if (sk?.unscaled && Object.values(sk.cons ?? {}).some(isEnteredLength)) delete sk.unscaled;
|
|
30
|
+
return sk;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Scale everything drawn on a sketch by k about the origin: where it is, not what has a size of its own. */
|
|
34
|
+
export function scaleSketch(sk: Sketch, k: number): void {
|
|
35
|
+
for (const p of Object.values(sk.points)) { p.x *= k; p.y *= k; }
|
|
36
|
+
for (const c of Object.values(sk.circles)) c.r *= k;
|
|
37
|
+
for (const o of Object.values(sk.openings)) {
|
|
38
|
+
o.at *= k;
|
|
39
|
+
if (o.wOff != null) o.wOff *= k;
|
|
40
|
+
for (const d of o.dims ?? []) { d.value *= k; if (d.off != null) d.off *= k; }
|
|
41
|
+
}
|
|
42
|
+
for (const n of sk.notes) {
|
|
43
|
+
if (n.pts) n.pts = n.pts.map(([x, y]) => [x * k, y * k]);
|
|
44
|
+
if (n.x != null) n.x *= k;
|
|
45
|
+
if (n.y != null) n.y *= k;
|
|
46
|
+
if (n.leader) n.leader = [n.leader[0] * k, n.leader[1] * k];
|
|
47
|
+
// (a note's letters and a tile's side are inches on the drawing: they zoom with it, so they scale with it)
|
|
48
|
+
if (n.ts != null) n.ts *= k;
|
|
49
|
+
}
|
|
50
|
+
// (and what the tiles' 100% is, so their percent holds)
|
|
51
|
+
if (sk.tileRef != null) sk.tileRef *= k;
|
|
52
|
+
for (const c of Object.values(sk.cons)) {
|
|
53
|
+
if (c.off != null) c.off *= k;
|
|
54
|
+
// (none should be here before the first length; one that is keeps measuring what it measures, met as it was)
|
|
55
|
+
if (c.type === 'dist' || c.type === 'pldist' || c.type === 'diam') c.value = measure(sk, c);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Zoom a view by the inverse of a scale of the drawing about the origin: everything stays where it was on the screen. */
|
|
60
|
+
export function zoomViewBy(v: View, k: number): void {
|
|
61
|
+
v.s /= k;
|
|
62
|
+
v.cx *= k;
|
|
63
|
+
v.cy *= k;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Scale a page — its drawing, its images and its view — by k, so nothing moves on the screen. */
|
|
67
|
+
export function scalePage(pg: Page, k: number): void {
|
|
68
|
+
scaleSketch(pg.sk, k);
|
|
69
|
+
for (const u of pg.underlays) {
|
|
70
|
+
u.x *= k; u.y *= k; u.w *= k;
|
|
71
|
+
if (u.h != null) u.h *= k;
|
|
72
|
+
}
|
|
73
|
+
if (pg.view) zoomViewBy(pg.view, k);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The factor that makes what `c` measures `v`: `v` over what it measures now — or, for a wall's face (which is its
|
|
78
|
+
* centreline's length or distance, less or more a thickness that doesn't scale), what makes that face `v`. What a
|
|
79
|
+
* dimension measures is linear in the scale (a + b·k), so two measures find it. Null when there's nothing to measure.
|
|
80
|
+
*/
|
|
81
|
+
export function factorFor(sk: Sketch, c: Constraint, v: number): number | null {
|
|
82
|
+
const now = measure(sk, c);
|
|
83
|
+
if (!(now > 1e-9) || !(v > 0)) return null;
|
|
84
|
+
const k1 = v / now;
|
|
85
|
+
if (Math.abs(k1 - 1) < 1e-12) return 1;
|
|
86
|
+
const trial = deep(sk);
|
|
87
|
+
scaleSketch(trial, k1);
|
|
88
|
+
const at1 = measure(trial, c), b = (at1 - now) / (k1 - 1), a = now - b, k = (v - a) / b;
|
|
89
|
+
return b > 1e-9 && k > 0 && Number.isFinite(k) ? k : k1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Give an unscaled page its scale: scaled by k (1: as it is), its view zoomed to match. */
|
|
93
|
+
export function setScale(pg: Page, k: number): void {
|
|
94
|
+
if (Math.abs(k - 1) > 1e-12) scalePage(pg, k);
|
|
95
|
+
delete pg.sk.unscaled;
|
|
96
|
+
pg.sk.scaledBy = k;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A drawing replaced by one from either side of the step that gave it its scale (undo, redo, or the scaled one
|
|
101
|
+
* arriving from elsewhere): the view zooms back, or again, so the drawing stays where it was on the screen.
|
|
102
|
+
*/
|
|
103
|
+
export function rezoomAcrossScale(pg: Page, was: Sketch, now: Sketch): void {
|
|
104
|
+
if (!pg.view || !!was.unscaled === !!now.unscaled) return;
|
|
105
|
+
const k = now.unscaled ? 1 / (was.scaledBy || 1) : now.scaledBy || 1;
|
|
106
|
+
if (Math.abs(k - 1) > 1e-12) zoomViewBy(pg.view, k);
|
|
107
|
+
}
|
package/src/model/settings.ts
CHANGED
|
@@ -5,7 +5,6 @@ export interface Layers {
|
|
|
5
5
|
underlay: boolean;
|
|
6
6
|
construction: boolean;
|
|
7
7
|
dims: boolean;
|
|
8
|
-
print: boolean;
|
|
9
8
|
rooms: boolean;
|
|
10
9
|
annot: boolean;
|
|
11
10
|
/** cabinets, doors and windows snap to corners and each other as they're placed and dragged */
|
|
@@ -27,7 +26,7 @@ export interface Settings extends UnitPrefs {
|
|
|
27
26
|
wallH: number;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
export const defaultLayers = (): Layers => ({ underlay: true, construction: true, dims: true,
|
|
29
|
+
export const defaultLayers = (): Layers => ({ underlay: true, construction: true, dims: true, rooms: true, annot: true, cabinets: true, objSnap: true });
|
|
31
30
|
|
|
32
31
|
export const settings = sessionState((): Settings => ({
|
|
33
32
|
unit: 'ftin', denom: 16, snapGrid: false, autoCons: true, toolTips: true, showCons: true, theme: 'system', welcomed: false,
|
package/src/model/sketch.ts
CHANGED
|
@@ -57,6 +57,21 @@ export function refExists(sk: Sketch, id: string): boolean {
|
|
|
57
57
|
export const pinnedPoints = new Set<string>();
|
|
58
58
|
onSessionReset(() => pinnedPoints.clear());
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* An arc whose ends are one point — dragged all the way round onto its start (the user's call, 2026-10-29: "if u fully
|
|
62
|
+
* rotate it to reach around - it should create a circle") — becomes that circle. It keeps the arc's id, so what refers
|
|
63
|
+
* to it (its radius, a tangent, Equal radius, Concentric) refers to the circle, and its look (a wall, its thickness, a
|
|
64
|
+
* guide). The point its ends were stays on it, for whatever else it's an end of (a line): gc lets it go if nothing is.
|
|
65
|
+
*/
|
|
66
|
+
export function closeArc(sk: Sketch, id: string): void {
|
|
67
|
+
const a = sk.arcs[id], o = sk.points[a.c], p = sk.points[a.a];
|
|
68
|
+
if (!o || !p) return;
|
|
69
|
+
const { c, constr, kind, thick } = a;
|
|
70
|
+
sk.circles[id] = { c, r: hyp(p.x - o.x, p.y - o.y), ...(constr ? { constr } : {}), ...(kind ? { kind } : {}), ...(thick ? { thick } : {}) };
|
|
71
|
+
delete sk.arcs[id];
|
|
72
|
+
addCons(sk, { type: 'oncircle', p: a.a, c: id });
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
/** Remove dangling entities, constraints and points, repeating until stable. */
|
|
61
76
|
export function gc(sk: Sketch): void {
|
|
62
77
|
let changed = true, guard = 0;
|
|
@@ -70,6 +85,8 @@ export function gc(sk: Sketch): void {
|
|
|
70
85
|
for (const id in sk.arcs) {
|
|
71
86
|
const a = sk.arcs[id];
|
|
72
87
|
if (!sk.points[a.c] || !sk.points[a.a] || !sk.points[a.b]) { delete sk.arcs[id]; changed = true; }
|
|
88
|
+
// its ends one point (dragged all the way round and joined): all the way round, a circle
|
|
89
|
+
else if (a.a === a.b) { closeArc(sk, id); changed = true; }
|
|
73
90
|
}
|
|
74
91
|
for (const id in sk.cons) {
|
|
75
92
|
const c = sk.cons[id];
|
|
@@ -167,12 +184,33 @@ export function linesBetween(sk: Sketch, a: string, b: string): string[] {
|
|
|
167
184
|
return out;
|
|
168
185
|
}
|
|
169
186
|
|
|
187
|
+
/**
|
|
188
|
+
* A room's edge round a circle (model/rooms.ts): '<circle id>+' runs counter-clockwise from the corner to the next,
|
|
189
|
+
* '<circle id>-' clockwise. Null for a line's or an arc's id.
|
|
190
|
+
*/
|
|
191
|
+
export function circleEdge(id: string): { cid: string; dir: 1 | -1 } | null {
|
|
192
|
+
const k = id[id.length - 1];
|
|
193
|
+
return k === '+' || k === '-' ? { cid: id.slice(0, -1), dir: k === '+' ? 1 : -1 } : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** A point is on a circle (not its centre): where a line or an arc ends on it, or what's held to it. */
|
|
197
|
+
export function onCircle(sk: Sketch, cid: string, pid: string): boolean {
|
|
198
|
+
const ci = sk.circles[cid], o = ci && sk.points[ci.c], p = sk.points[pid];
|
|
199
|
+
return !!o && !!p && pid !== ci.c && Math.abs(hyp(p.x - o.x, p.y - o.y) - ci.r) <= 1e-3;
|
|
200
|
+
}
|
|
201
|
+
|
|
170
202
|
export function roomValid(sk: Sketch, r: Room): boolean {
|
|
171
|
-
if (!r.loop
|
|
203
|
+
if (!r.loop) return false;
|
|
204
|
+
// a whole circle: no corners — the circle still there
|
|
205
|
+
if (!r.loop.length) { const ce = r.edges?.length === 1 ? circleEdge(r.edges[0]) : null; return !!ce && !!sk.circles[ce.cid] && !!sk.points[sk.circles[ce.cid].c]; }
|
|
206
|
+
if (r.loop.length < 2 || (!r.edges && r.loop.length < 3)) return false;
|
|
172
207
|
for (let i = 0; i < r.loop.length; i++) {
|
|
173
208
|
const a = r.loop[i], b = r.loop[(i + 1) % r.loop.length];
|
|
174
209
|
if (!sk.points[a]) return false;
|
|
175
210
|
if (r.edges) {
|
|
211
|
+
// round a circle: both corners still on it
|
|
212
|
+
const ce = circleEdge(r.edges[i]);
|
|
213
|
+
if (ce) { if (!onCircle(sk, ce.cid, a) || !onCircle(sk, ce.cid, b)) return false; continue; }
|
|
176
214
|
// each stored edge (line or arc) must still join these two corners
|
|
177
215
|
const id = r.edges[i], l = sk.lines[id], ar = sk.arcs[id];
|
|
178
216
|
const ends = l ? [l.a, l.b] : ar ? [ar.a, ar.b] : null;
|
|
@@ -254,13 +292,19 @@ export function lineLen(sk: Sketch, l: Line): number {
|
|
|
254
292
|
return hyp(b.x - a.x, b.y - a.y);
|
|
255
293
|
}
|
|
256
294
|
|
|
257
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* The length dimension attached to a line: its driving one, or else a reference one. (A drawing from before print
|
|
297
|
+
* dimensions were dropped can have both on a line: its print one now reads as a reference.)
|
|
298
|
+
*/
|
|
258
299
|
export function lengthDimOf(sk: Sketch, lid: string): string | null {
|
|
300
|
+
let ref: string | null = null;
|
|
259
301
|
for (const id in sk.cons) {
|
|
260
302
|
const c = sk.cons[id];
|
|
261
|
-
if (c.type
|
|
303
|
+
if (c.type !== 'dist' || c.line !== lid) continue;
|
|
304
|
+
if (c.driving !== false) return id;
|
|
305
|
+
ref ??= id;
|
|
262
306
|
}
|
|
263
|
-
return
|
|
307
|
+
return ref;
|
|
264
308
|
}
|
|
265
309
|
|
|
266
310
|
export interface PosSnapshot { p: Record<string, [number, number]>; r: Record<string, number> }
|
|
@@ -277,23 +321,30 @@ export function restorePos(sk: Sketch, s: PosSnapshot): void {
|
|
|
277
321
|
for (const id in s.r) if (sk.circles[id]) sk.circles[id].r = s.r[id];
|
|
278
322
|
}
|
|
279
323
|
|
|
280
|
-
/**
|
|
324
|
+
/**
|
|
325
|
+
* Every line and arc joined end to end with the given ones (through shared points), including them. Guides join only
|
|
326
|
+
* guides, and walls and lines only walls and lines: a guide drawn off a shape's corner isn't part of the shape, nor the
|
|
327
|
+
* shape part of the guides (the user's call, 2026-11-02).
|
|
328
|
+
*/
|
|
281
329
|
export function connectedGeometry(sk: Sketch, lineIds: string[], arcIds: string[] = []): { lines: string[]; arcs: string[] } {
|
|
330
|
+
// the points reached by walls and lines [0], and by guides [1]
|
|
331
|
+
const pts = [new Set<string>(), new Set<string>()], lines = new Set(lineIds), arcs = new Set(arcIds);
|
|
332
|
+
type End = { a: string; b: string; constr?: boolean };
|
|
282
333
|
// on a wall face, the plan's corners and edges aren't part of a shape: nothing joins through them
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
lineIds.forEach(id => { const l = sk.lines[id]; if (l)
|
|
286
|
-
arcIds.forEach(id => { const a = sk.arcs[id]; if (a)
|
|
334
|
+
const join = (o: End) => { for (const p of [o.a, o.b]) if (!sk.points[p]?.anchor) pts[+!!o.constr].add(p); };
|
|
335
|
+
const meets = (o: End) => pts[+!!o.constr].has(o.a) || pts[+!!o.constr].has(o.b);
|
|
336
|
+
lineIds.forEach(id => { const l = sk.lines[id]; if (l) join(l); });
|
|
337
|
+
arcIds.forEach(id => { const a = sk.arcs[id]; if (a) join(a); });
|
|
287
338
|
let grow = true;
|
|
288
339
|
while (grow) {
|
|
289
340
|
grow = false;
|
|
290
341
|
for (const id in sk.lines) {
|
|
291
342
|
const l = sk.lines[id];
|
|
292
|
-
if (!lines.has(id) && !l.anchor && !l.cab && (
|
|
343
|
+
if (!lines.has(id) && !l.anchor && !l.cab && meets(l)) { lines.add(id); join(l); grow = true; }
|
|
293
344
|
}
|
|
294
345
|
for (const id in sk.arcs) {
|
|
295
346
|
const a = sk.arcs[id];
|
|
296
|
-
if (!arcs.has(id) && (
|
|
347
|
+
if (!arcs.has(id) && meets(a)) { arcs.add(id); join(a); grow = true; }
|
|
297
348
|
}
|
|
298
349
|
}
|
|
299
350
|
return { lines: [...lines], arcs: [...arcs] };
|
package/src/model/stroke.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { hyp, rdp } from './geom';
|
|
|
2
2
|
import type { XY } from './types';
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
Stroke cleanup for Draw to shape
|
|
6
|
-
|
|
5
|
+
Stroke cleanup for Draw to shape: simplify a freehand stroke into the corners it most likely meant, then square it
|
|
6
|
+
up to level, plumb and 45° diagonals. Pure geometry (no sketch, no view).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/** Screen-pixel tolerances (the result doesn't depend on zoom). Generous, for wobbly lines drawn on site. */
|