@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/solver/equations.ts
CHANGED
|
@@ -20,10 +20,42 @@ type DFn = (id: string) => number[];
|
|
|
20
20
|
type RVFn = (X: Vec, cid: string) => number;
|
|
21
21
|
type RDFn = (cid: string) => number[];
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The side of its line a tangent keeps the curve's centre on (1: left of the line's a → b): the side it was made
|
|
25
|
+
* tangent from (`sign`: the fillet tool and the Tangent action set it), or, for one saved before tangents had a side,
|
|
26
|
+
* the side it's on now.
|
|
27
|
+
*/
|
|
28
|
+
export function tanSide(sk: Sketch, c: Constraint): 1 | -1 {
|
|
29
|
+
if (c.sign) return c.sign < 0 ? -1 : 1;
|
|
30
|
+
const l = c.l ? sk.lines[c.l] : undefined, ci = curveInfo(sk, c.c);
|
|
31
|
+
const A = l && sk.points[l.a], B = l && sk.points[l.b], o = ci && sk.points[ci.o.c];
|
|
32
|
+
if (!A || !B || !o) return 1;
|
|
33
|
+
return (B.x - A.x) * (o.y - A.y) - (B.y - A.y) * (o.x - A.x) < 0 ? -1 : 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
/** Residual equations for one constraint, each with the variable indices it depends on. */
|
|
24
37
|
export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD: RDFn): [number[], EqFn][] {
|
|
25
38
|
const L = (id: string | undefined) => (id ? sk.lines[id] : undefined);
|
|
26
39
|
const out: [number[], EqFn][] = [];
|
|
40
|
+
/**
|
|
41
|
+
* Line `id`'s length, and what it depends on — for a wall's `face`, along that face corner to corner: the walls at its
|
|
42
|
+
* ends cut it short (an inside corner) or carry it on (an outside one), so they're part of it too.
|
|
43
|
+
*/
|
|
44
|
+
const len = (id: string, face?: number): { deps: number[]; f: EqFn } => {
|
|
45
|
+
const l = sk.lines[id];
|
|
46
|
+
if (face && l.kind === 'wall') {
|
|
47
|
+
const ends = faceEnds(sk, id), side = face < 0 ? -1 : 1;
|
|
48
|
+
return {
|
|
49
|
+
deps: [...D(l.a), ...D(l.b), ...[...ends.a, ...ends.b].flatMap(e => D(e.far ?? e.arc!.c))],
|
|
50
|
+
f: X => {
|
|
51
|
+
const at = (p: string) => { const q = P(X, p); return { x: q[0], y: q[1] }; };
|
|
52
|
+
const fs = faceSpan(at, at(l.a), at(l.b), l.thick, side, ends);
|
|
53
|
+
return fs ? fs.s1 - fs.s0 : 0;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { deps: [...D(l.a), ...D(l.b)], f: X => { const a = P(X, l.a), b = P(X, l.b); return hyp(b[0] - a[0], b[1] - a[1]); } };
|
|
58
|
+
};
|
|
27
59
|
const unit = (a: [number, number], b: [number, number]): [number, number] => {
|
|
28
60
|
const x = b[0] - a[0], y = b[1] - a[1], n = hyp(x, y) || 1e-12;
|
|
29
61
|
return [x / n, y / n];
|
|
@@ -49,24 +81,27 @@ export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD:
|
|
|
49
81
|
}
|
|
50
82
|
case 'tan': {
|
|
51
83
|
// line tangent to a circle/arc: the centre's distance from the (infinite) line equals the radius,
|
|
52
|
-
// on the side the centre started on (
|
|
84
|
+
// on the side the centre started on (tanSide), so it never flips across the line
|
|
53
85
|
const l = L(c.l), r = rad(c.c);
|
|
54
86
|
if (!l || !r) break;
|
|
87
|
+
const s = tanSide(sk, c);
|
|
55
88
|
// an arc that ends on one of the line's ends (a fillet, a smooth join): tangent there is its radius to that end
|
|
56
|
-
// standing square to the line. (The distance form has no slope at that point, so the solver
|
|
57
|
-
// second such tangent from one that's already true.)
|
|
89
|
+
// standing square to the line, from its own side. (The distance form has no slope at that point, so the solver
|
|
90
|
+
// couldn't tell a second such tangent from one that's already true.) Square alone is true of the arc's mirror
|
|
91
|
+
// image across the line too, which a fast drag could land on — and the arc, drawn counter-clockwise from its
|
|
92
|
+
// start to its end, then went the long way round. So it's the angle from the line's normal on the centre's side
|
|
93
|
+
// to the radius: a slope at the answer, as square had, and half a turn out on the wrong side, never zero.
|
|
58
94
|
const ci = curveInfo(sk, c.c);
|
|
59
95
|
const joint = ci && ci.kind === 'arc' ? [ci.o.a, ci.o.b].find(p => p === l.a || p === l.b) : undefined;
|
|
60
96
|
if (ci && joint) {
|
|
61
97
|
const o = ci.o.c;
|
|
62
98
|
out.push([[...D(l.a), ...D(l.b), ...D(o), ...D(joint)], X => {
|
|
63
99
|
const a = P(X, l.a), b = P(X, l.b), m = P(X, o), e = P(X, joint);
|
|
64
|
-
const ux = b[0] - a[0], uy = b[1] - a[1],
|
|
65
|
-
return (ux *
|
|
100
|
+
const ux = b[0] - a[0], uy = b[1] - a[1], wx = m[0] - e[0], wy = m[1] - e[1];
|
|
101
|
+
return Math.atan2(ux * wx + uy * wy, s * (ux * wy - uy * wx));
|
|
66
102
|
}]);
|
|
67
103
|
break;
|
|
68
104
|
}
|
|
69
|
-
const s = c.sign || 1;
|
|
70
105
|
out.push([[...D(l.a), ...D(l.b), ...D(r.c), ...r.deps], X => {
|
|
71
106
|
const o = P(X, r.c), a = P(X, l.a), b = P(X, l.b), n = hyp(b[0] - a[0], b[1] - a[1]) || 1e-12;
|
|
72
107
|
return (s * ((b[0] - a[0]) * (o[1] - a[1]) - (b[1] - a[1]) * (o[0] - a[0]))) / n - r.f(X);
|
|
@@ -86,12 +121,10 @@ export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD:
|
|
|
86
121
|
break;
|
|
87
122
|
}
|
|
88
123
|
case 'eq': {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return hyp(b[0] - a[0], b[1] - a[1]) - hyp(q[0] - p[0], q[1] - p[1]);
|
|
94
|
-
}]);
|
|
124
|
+
if (!L(c.l1) || !L(c.l2)) break;
|
|
125
|
+
// (a wall's edge picked: that face's length — faces)
|
|
126
|
+
const a = len(c.l1!, c.faces?.[0]), b = len(c.l2!, c.faces?.[1]);
|
|
127
|
+
out.push([[...a.deps, ...b.deps], X => a.f(X) - b.f(X)]);
|
|
95
128
|
break;
|
|
96
129
|
}
|
|
97
130
|
case 'eqr': {
|
|
@@ -111,9 +144,12 @@ export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD:
|
|
|
111
144
|
case 'online': {
|
|
112
145
|
const l = L(c.l);
|
|
113
146
|
if (!l) break;
|
|
147
|
+
// on one face of a wall: half its thickness to that side of the centreline — read when solved, so a wall made
|
|
148
|
+
// thicker carries what's on its face with it
|
|
149
|
+
const faceOff = () => (c.face && l.kind === 'wall' ? (c.face * l.thick) / 2 : 0);
|
|
114
150
|
out.push([[...D(c.p!), ...D(l.a), ...D(l.b)], X => {
|
|
115
151
|
const p = P(X, c.p!), a = P(X, l.a), b = P(X, l.b), dx = b[0] - a[0], dy = b[1] - a[1], n = hyp(dx, dy) || 1e-12;
|
|
116
|
-
return ((p[0] - a[0]) * dy - (p[1] - a[1]) * dx) / n;
|
|
152
|
+
return ((p[0] - a[0]) * dy - (p[1] - a[1]) * dx) / n + faceOff();
|
|
117
153
|
}]);
|
|
118
154
|
break;
|
|
119
155
|
}
|
|
@@ -126,15 +162,9 @@ export function consEqs(sk: Sketch, c: Constraint, P: PFn, D: DFn, RV: RVFn, RD:
|
|
|
126
162
|
case 'dist': {
|
|
127
163
|
const fw = c.face && c.line && (!c.kind || c.kind === 'al') ? L(c.line) : undefined;
|
|
128
164
|
if (fw && fw.kind === 'wall') {
|
|
129
|
-
// along one face of a wall, corner to corner
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const deps = [...D(fw.a), ...D(fw.b), ...[...ends.a, ...ends.b].flatMap(e => D(e.far ?? e.arc!.c))];
|
|
133
|
-
out.push([deps, X => {
|
|
134
|
-
const at = (id: string) => { const q = P(X, id); return { x: q[0], y: q[1] }; };
|
|
135
|
-
const f = faceSpan(at, at(fw.a), at(fw.b), fw.thick, side, ends);
|
|
136
|
-
return (f ? f.s1 - f.s0 : 0) - c.value!;
|
|
137
|
-
}]);
|
|
165
|
+
// along one face of a wall, corner to corner
|
|
166
|
+
const f = len(c.line!, c.face);
|
|
167
|
+
out.push([f.deps, X => f.f(X) - c.value!]);
|
|
138
168
|
break;
|
|
139
169
|
}
|
|
140
170
|
const deps = [...D(c.a!), ...D(c.b!)], s = c.sign || 1;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isDim, measure, relatedCons } from '../model/constraints';
|
|
2
2
|
import { addCons, restorePos, snapshotPos } from '../model/sketch';
|
|
3
3
|
import type { Analysis, Constraint, Sketch, XY } from '../model/types';
|
|
4
|
-
import { buildSys, getX, type System, type Vec } from './equations';
|
|
4
|
+
import { buildSys, getX, tanSide, type System, type Vec } from './equations';
|
|
5
5
|
import { blocks, compactRows, envAt, envelopeOf, jac, rref, rrefTol, solveDense, solveEnvelope, sq, type Envelope, type Row } from './linalg';
|
|
6
6
|
import type { AddResult, SolveResult, Solver } from './types';
|
|
7
7
|
|
|
@@ -180,6 +180,9 @@ export class GaussNewtonSolver implements Solver {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
drag(sk: Sketch, ids: string[], targets: XY[]): boolean {
|
|
183
|
+
// (a tangent saved before tangents kept a side keeps the one it's on now, read before the dragged points jump to
|
|
184
|
+
// the pointer: read after, a moved end of its line could put it on the wrong one)
|
|
185
|
+
for (const c of Object.values(sk.cons)) if (c.type === 'tan' && !c.sign) c.sign = tanSide(sk, c);
|
|
183
186
|
const snap = snapshotPos(sk);
|
|
184
187
|
ids.forEach((id, i) => { sk.points[id].x = targets[i].x; sk.points[id].y = targets[i].y; });
|
|
185
188
|
const s = this.solve(sk, ids);
|
package/src/styles.css
CHANGED
|
@@ -96,7 +96,7 @@ input{font:inherit}
|
|
|
96
96
|
|
|
97
97
|
/* bottom contextual bar */
|
|
98
98
|
.bottom{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);display:flex;align-items:center;gap:12px;z-index:24;max-width:calc(100% - 180px)}
|
|
99
|
-
.ctx{display:flex;align-items:center;gap:2px;background:var(--g2);border-radius:
|
|
99
|
+
.ctx{display:flex;align-items:center;gap:2px;background:var(--g2);border-radius:30px;min-height:60px;padding:3px 6px;overflow-x:auto;scrollbar-width:none;box-shadow:0 6px 22px rgba(0,0,0,.26);max-width:100%}
|
|
100
100
|
.ctx::-webkit-scrollbar{display:none}
|
|
101
101
|
.cb{min-width:58px;height:54px;border-radius:27px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:#fff;font-size:10.5px;font-weight:500;padding:0 8px;flex-shrink:0;white-space:nowrap;transition:background .12s}
|
|
102
102
|
.cb:hover{background:rgba(255,255,255,.08)}
|
|
@@ -104,9 +104,10 @@ input{font:inherit}
|
|
|
104
104
|
.cb[disabled]{opacity:.35;pointer-events:none}
|
|
105
105
|
.cb.warn{color:#FFB4AE}
|
|
106
106
|
.cb.locked{color:var(--red)}
|
|
107
|
+
.cb.lead{min-width:48px;width:48px;height:48px;border-radius:50%;padding:0}
|
|
107
108
|
.ctx .sep{width:1px;height:32px;background:var(--g3);margin:0 4px;flex-shrink:0}
|
|
108
109
|
.ctx .lbl{font-size:12px;color:var(--lg1);padding:0 10px;white-space:nowrap;flex-shrink:0}
|
|
109
|
-
.addbtn{width:48px;height:48px;border-radius:50%;background:var(--yellow);color:var(--dark);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:4px
|
|
110
|
+
.addbtn{width:48px;height:48px;border-radius:50%;background:var(--yellow);color:var(--dark);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:4px}
|
|
110
111
|
/* the + stays pinned at the end of the bar while the other actions scroll under it (phone) */
|
|
111
112
|
.ctx .addbtn{position:sticky;right:0;z-index:1;box-shadow:0 0 0 6px var(--g2),-16px 0 10px -2px var(--g2)}
|
|
112
113
|
.ctx:has(.addbtn){padding-right:6px}
|
package/src/tools/annotate.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { targetImage } from '../editor/project';
|
|
2
2
|
import { imageById } from '../model/images';
|
|
3
|
-
import { strokeToLines } from './freehand';
|
|
4
3
|
import { afterEdit } from '../app/edit';
|
|
5
4
|
import { ed } from '../app/state';
|
|
6
5
|
import { hyp } from '../model/geom';
|
|
7
|
-
import {
|
|
6
|
+
import { pushHistory } from '../model/history';
|
|
8
7
|
import { uid } from '../model/ids';
|
|
9
8
|
import { page, SK } from '../model/store';
|
|
10
9
|
import { dashFor, DEFAULT_NOTE_WIDTH, NOTE_TEXT_IN, noteLook, shapeOutline } from '../model/markup';
|
|
@@ -16,7 +15,6 @@ import { fitView, mainT, requestDraw, requestOverlayDraw, S, s2w, underlayH } fr
|
|
|
16
15
|
import { tools, type PEv, type Tool } from './registry';
|
|
17
16
|
import { hitAny } from '../render/hit';
|
|
18
17
|
import { selectTool } from './select';
|
|
19
|
-
import { setMode } from '../editor/mode';
|
|
20
18
|
import { hideNoteInput, openKeypad, setCursor, showNoteInput, toast, updateHint } from '../editor/ports';
|
|
21
19
|
import { selectionHint } from '../editor/selection';
|
|
22
20
|
|
|
@@ -194,21 +192,6 @@ export const calibTool = {
|
|
|
194
192
|
hint() { return ['Tap the first end of something you know the length of.', 'Tap the other end.'][this.st.pts.length] || ''; },
|
|
195
193
|
};
|
|
196
194
|
|
|
197
|
-
/** Turn a pen stroke into straight lines, squaring near-horizontal/vertical segments. */
|
|
198
|
-
export function convertNote(id: string): void {
|
|
199
|
-
const sk = SK();
|
|
200
|
-
const n = sk.notes.find(n => n.id === id);
|
|
201
|
-
if (!n || !n.pts) return;
|
|
202
|
-
pushHistory();
|
|
203
|
-
const r = strokeToLines(n.pts.map(p => ({ x: p[0], y: p[1] })));
|
|
204
|
-
if (!r) { dropHistory(); toast('That stroke is too small to turn into lines.', { warn: true }); return; }
|
|
205
|
-
sk.notes = sk.notes.filter(x => x.id !== id);
|
|
206
|
-
setMode('draft');
|
|
207
|
-
ed.sel = r.lines.map(id => ({ type: 'line' as const, id }));
|
|
208
|
-
afterEdit();
|
|
209
|
-
toast('Turned into ' + r.lines.length + ' line' + (r.lines.length > 1 ? 's' : '') + '. Select a line and tap + to add its length.');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
195
|
/**
|
|
213
196
|
* Lets a markup drawing tool (pen, arrow, cloud) reshape what was just drawn: a press on a handle of the selected
|
|
214
197
|
* markup is handed to Select markup (reshape); anywhere else draws as usual.
|
package/src/tools/chamfer.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { requestDraw } from '../render/view';
|
|
|
8
8
|
import { solver } from '../solver';
|
|
9
9
|
import { fmtLen } from '../units';
|
|
10
10
|
import { closeKeypad, openKeypad, toast, updateCtx } from '../editor/ports';
|
|
11
|
+
import { waitForScale } from '../editor/scaleGate';
|
|
11
12
|
import { cornerAt, cutCorner } from './corner';
|
|
12
13
|
|
|
13
14
|
/*
|
|
@@ -33,6 +34,8 @@ function showSide(lid: string | null): void { ed.sel = lid ? [{ type: 'line', id
|
|
|
33
34
|
export function chamferPrompt(pid: string): void {
|
|
34
35
|
const sk = SK(), k = cornerAt(sk, pid);
|
|
35
36
|
if (!k) { toast('Select a corner where two lines meet to cut it off.', { warn: true }); return; }
|
|
37
|
+
// (a distance typed is real: nothing drawn says what it is in a sketch without its scale yet — editor/scaleGate.ts)
|
|
38
|
+
if (waitForScale()) return;
|
|
36
39
|
const most = Math.min(k.len1, k.len2), start = Math.round(Math.min(6, most * 0.5) * 16) / 16;
|
|
37
40
|
openKeypad({
|
|
38
41
|
title: 'Chamfer', sub: 'Back from the corner along each side — up to ' + fmtLen(most), initial: fmtLen(start), value: start, selectAll: true, laser: true,
|
package/src/tools/dim.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { afterEdit, holdGroups, tryAddConstraint } from '../app/edit';
|
|
2
2
|
import { ed } from '../app/state';
|
|
3
3
|
import { measure } from '../model/constraints';
|
|
4
|
-
import { hyp, norm2,
|
|
4
|
+
import { hyp, norm2, projT } from '../model/geom';
|
|
5
5
|
import { dropHistory, pushHistory } from '../model/history';
|
|
6
|
-
import {
|
|
6
|
+
import { lengthDimOf, lineLen } from '../model/sketch';
|
|
7
7
|
import { SK } from '../model/store';
|
|
8
8
|
import type { Constraint, LinkTarget, OpeningEdge, Sketch, WallFace, XY } from '../model/types';
|
|
9
9
|
import { askLinkedChange, changesValue } from '../editor/linkWrite';
|
|
@@ -25,6 +25,7 @@ import type { PEv } from './registry';
|
|
|
25
25
|
import { keypadFresh, keypadOpen, keypadSetReading } from '../editor/keypad';
|
|
26
26
|
import { cornerLines } from '../editor/selection';
|
|
27
27
|
import { closeKeypad, kpTyped, openKeypad, showConflict, toast, updateCtx, updateHint, type KeypadSpec } from '../editor/ports';
|
|
28
|
+
import { noScaleYet, pendingLabel, startingValue } from '../editor/scaleGate';
|
|
28
29
|
|
|
29
30
|
export const dimTool = {
|
|
30
31
|
/** angle: the first pick is a corner whose angle keypad is open (the next tap closes it) */
|
|
@@ -66,7 +67,7 @@ export const dimTool = {
|
|
|
66
67
|
move(ev: PEv) { this.hover(ev); },
|
|
67
68
|
up(ev: PEv) {
|
|
68
69
|
const h = pickForDim(ev);
|
|
69
|
-
const
|
|
70
|
+
const st = this.st;
|
|
70
71
|
// a corner's angle keypad is open: the next tap closes it and goes on (a second point measures from the corner)
|
|
71
72
|
if (st.angle) { st.angle = false; if (keypadOpen()) closeKeypad(); }
|
|
72
73
|
if (!h || h.type === 'room') { st.first = null; ed.pickedAnchor = null; ed.sel = []; ed.preview = null; updateHint(); requestDraw(); return; }
|
|
@@ -74,7 +75,6 @@ export const dimTool = {
|
|
|
74
75
|
const picked = ed.pickedAnchor;
|
|
75
76
|
if (h.type === 'oanchor') {
|
|
76
77
|
const edge = h.part as OpeningEdge;
|
|
77
|
-
if (print) { toast('Print dimensions can’t attach to doors or windows yet. Switch to Driving in the bar below.', { warn: true }); return; }
|
|
78
78
|
if (st.first) {
|
|
79
79
|
const p = st.first; st.first = null; ed.sel = [];
|
|
80
80
|
addOpeningDim(h.id, h.weak ? nearerEdge(SK(), h.id, p) : edge, { pt: p });
|
|
@@ -101,10 +101,10 @@ export const dimTool = {
|
|
|
101
101
|
if (!st.first) {
|
|
102
102
|
st.first = h.id; ed.sel = [{ type: 'point', id: h.id }];
|
|
103
103
|
// a corner: its angle straight away (close the keypad to measure from the corner to another point instead)
|
|
104
|
-
|
|
104
|
+
st.angle = cornerAngle(h.id, () => { this.st.first = null; this.st.angle = false; ed.sel = []; updateHint(); requestDraw(); }, true);
|
|
105
105
|
updateHint(); requestDraw(); return;
|
|
106
106
|
}
|
|
107
|
-
if (h.id !== st.first) dimPoints(st.first, h.id
|
|
107
|
+
if (h.id !== st.first) dimPoints(st.first, h.id);
|
|
108
108
|
st.first = null; updateHint();
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
@@ -113,14 +113,14 @@ export const dimTool = {
|
|
|
113
113
|
if (h.type === 'wface') {
|
|
114
114
|
const [lid, side] = edgeOf(h.id);
|
|
115
115
|
if (!SK().lines[lid]) { dimCurve(lid, side); return; }
|
|
116
|
-
if (st.first) { const p = st.first; st.first = null;
|
|
117
|
-
dimLine(lid,
|
|
116
|
+
if (st.first) { const p = st.first; st.first = null; dimEdgePoint(lid, side, p); updateHint(); return; }
|
|
117
|
+
dimLine(lid, side);
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
if (h.type === 'line') {
|
|
121
121
|
if (h.side) ed.wallTap = { id: h.id, side: h.side };
|
|
122
122
|
if (st.first) { dimPointLine(st.first, h.id); st.first = null; updateHint(); return; }
|
|
123
|
-
dimLine(h.id
|
|
123
|
+
dimLine(h.id);
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
if (h.type === 'circle' || h.type === 'arc') {
|
|
@@ -131,7 +131,7 @@ export const dimTool = {
|
|
|
131
131
|
hint() {
|
|
132
132
|
if (ed.pickedAnchor) return 'Tap a corner or point to measure from. The door or window locks at that distance.';
|
|
133
133
|
if (this.st.first) return 'Tap a second point, a door or window edge, or a line for the offset distance.';
|
|
134
|
-
return
|
|
134
|
+
return 'Tap a line, a circle, or two points — door and window edges count as points. Then type the value.';
|
|
135
135
|
},
|
|
136
136
|
};
|
|
137
137
|
|
|
@@ -153,34 +153,15 @@ function pickForDim(ev: PEv): Hit | null {
|
|
|
153
153
|
return a ? { type: 'oanchor', id: a.id, part: a.edge, weak: true } : h;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
/** +1 or -1: the side of a line that faces away from any room (for print dimension offsets). */
|
|
157
|
-
function sideAwayFromRooms(lid: string): number {
|
|
158
|
-
const sk = SK(), l = sk.lines[lid], a = sk.points[l.a], b = sk.points[l.b];
|
|
159
|
-
const n = norm2(-(b.y - a.y), b.x - a.x);
|
|
160
|
-
const m = { x: (a.x + b.x) / 2 + n[0] * 12, y: (a.y + b.y) / 2 + n[1] * 12 };
|
|
161
|
-
for (const id in sk.rooms) { const r = sk.rooms[id]; if (roomValid(sk, r) && pointInPoly(m, r.loop.map(p => sk.points[p]))) return -1; }
|
|
162
|
-
return 1;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
156
|
/**
|
|
166
157
|
* Tap on a line (or its + chip): open the keypad for its length, or edit the existing length. A wall's is measured on
|
|
167
158
|
* `side`, the edge picked — or, picked whole, on the side a space is on (the side it was tapped on, between two), and
|
|
168
159
|
* along its centerline where there's no space; the keypad can switch it.
|
|
169
160
|
*/
|
|
170
|
-
export function dimLine(lid: string,
|
|
161
|
+
export function dimLine(lid: string, side?: 1 | -1): void {
|
|
171
162
|
const sk = SK(), l = sk.lines[lid];
|
|
172
163
|
if (!l) return;
|
|
173
164
|
const wall = faced(sk, lid);
|
|
174
|
-
if (print) {
|
|
175
|
-
pushHistory();
|
|
176
|
-
// (on a wall's edge: that face's length, labelled on that side)
|
|
177
|
-
const f = wall ? side : undefined;
|
|
178
|
-
const off = (36 / V().s) * (f ?? sideAwayFromRooms(lid));
|
|
179
|
-
addCons(sk, { type: 'dist', a: l.a, b: l.b, kind: 'al', value: f ? faceLength(sk, lid, f) : lineLen(sk, l), line: lid, driving: false, layer: 'print', off, ...(f ? { face: f } : {}) });
|
|
180
|
-
afterEdit();
|
|
181
|
-
toast('Print dimension added. Drag it to move it.');
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
165
|
const ex = lengthDimOf(sk, lid);
|
|
185
166
|
if (ex) { editDim(ex, undefined, false, wall && side ? { face: side } : undefined); return; }
|
|
186
167
|
let face: 0 | 1 | -1 = wall ? side ?? defaultFace(sk, lid, ed.wallTap?.id === lid ? ed.wallTap.side : undefined) ?? 0 : 0;
|
|
@@ -190,10 +171,10 @@ export function dimLine(lid: string, print: boolean, side?: 1 | -1): void {
|
|
|
190
171
|
const show = () => { ed.sel = face ? [{ type: 'wface', id: edgeId(lid, face) }] : [{ type: 'line', id: lid }]; requestDraw(); };
|
|
191
172
|
show();
|
|
192
173
|
const spec: KeypadSpec = {
|
|
193
|
-
title: 'Length', sub: wall ? lengthSub(sk, lid, face) : '',
|
|
174
|
+
title: 'Length', sub: wall ? lengthSub(sk, lid, face) : '', ...startingValue(len()), selectAll: true, laser: true,
|
|
194
175
|
extras: wall ? () => lengthChoices(sk, lid).map(ch => ({
|
|
195
176
|
label: ch.label!, on: ch.face === face,
|
|
196
|
-
onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, lid, face); show(); if (keypadFresh()) keypadSetReading(len()); },
|
|
177
|
+
onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, lid, face); show(); if (keypadFresh() && !noScaleYet()) keypadSetReading(len()); },
|
|
197
178
|
})) : undefined,
|
|
198
179
|
onClose: () => { ed.sel = was; updateCtx(); requestDraw(); },
|
|
199
180
|
onDone: (v, capture) => { if (v > 0) addLength(v, capture !== undefined ? { capture } : {}); },
|
|
@@ -305,7 +286,7 @@ export function faceChoices(sk: Sketch, c: Constraint): FaceChoice[] {
|
|
|
305
286
|
const l = sk.lines[c.l], a = sk.points[l.a], b = sk.points[l.b], H = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y) ? 'H' : '';
|
|
306
287
|
return [
|
|
307
288
|
{ label: 'Inside faces', short: 'Inside', icon: 'spanIn' + H, faces: k(1) },
|
|
308
|
-
{ label: 'Centerlines', short: '
|
|
289
|
+
{ label: 'Centerlines', short: 'Center', icon: 'spanMid' + H, faces: [0, 0] },
|
|
309
290
|
{ label: 'Outside faces', short: 'Outside', icon: 'spanOut' + H, faces: k(-1) },
|
|
310
291
|
];
|
|
311
292
|
}
|
|
@@ -408,22 +389,16 @@ export function dimEdges(items: { type: string; id: string }[], thickness: (lid:
|
|
|
408
389
|
else if (other.type === 'line') dimTwoLines(lid, other.id, { [lid]: side });
|
|
409
390
|
}
|
|
410
391
|
|
|
411
|
-
export function dimPoints(a: string, b: string
|
|
392
|
+
export function dimPoints(a: string, b: string): void {
|
|
412
393
|
const sk = SK(), A = sk.points[a], B = sk.points[b];
|
|
413
394
|
const L = hyp(B.x - A.x, B.y - A.y);
|
|
414
|
-
if (print) {
|
|
415
|
-
pushHistory();
|
|
416
|
-
addCons(sk, { type: 'dist', a, b, kind: 'al', value: L, driving: false, layer: 'print', off: 30 / V().s });
|
|
417
|
-
afterEdit();
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
395
|
ed.sel = [];
|
|
421
396
|
const n = norm2(-(B.y - A.y), B.x - A.x);
|
|
422
|
-
ed.preview = (c, T) => drawPendingDim(c, T, { from: A, to: B, a: A, b: B, nx: n[0], ny: n[1], off: 0, label:
|
|
397
|
+
ed.preview = (c, T) => drawPendingDim(c, T, { from: A, to: B, a: A, b: B, nx: n[0], ny: n[1], off: 0, label: pendingLabel(kpTyped(), L) });
|
|
423
398
|
requestDraw();
|
|
424
399
|
openKeypad({
|
|
425
400
|
onClose: () => { ed.preview = null; requestDraw(); },
|
|
426
|
-
title: 'Distance',
|
|
401
|
+
title: 'Distance', ...startingValue(L), selectAll: true, laser: true,
|
|
427
402
|
onDone: (v, capture) => {
|
|
428
403
|
pushHistory();
|
|
429
404
|
const r = tryAddConstraint({ type: 'dist', a, b, kind: 'al', value: v, driving: true, off: 0, ...(capture !== undefined ? { capture } : {}) });
|
|
@@ -449,7 +424,7 @@ export function dimTwoLines(l1: string, l2: string, sides: Record<string, 0 | 1
|
|
|
449
424
|
const kind = parallel ? 'pldist' : 'angle';
|
|
450
425
|
const existing = Object.keys(sk.cons).find(id => {
|
|
451
426
|
const c = sk.cons[id];
|
|
452
|
-
if (c.type !== kind
|
|
427
|
+
if (c.type !== kind) return false;
|
|
453
428
|
if (kind === 'angle') return (c.l1 === l1 && c.l2 === l2) || (c.l1 === l2 && c.l2 === l1);
|
|
454
429
|
return (c.l === l1 && (c.p === L2.a || c.p === L2.b)) || (c.l === l2 && (c.p === L1.a || c.p === L1.b));
|
|
455
430
|
});
|
|
@@ -487,15 +462,15 @@ export function dimPointLine(p: string, lid: string, opts: { keepParallel?: stri
|
|
|
487
462
|
applyFace(c, { faces: opts.faces ?? [faceToward(sk, lid, P), 0] });
|
|
488
463
|
const choices = faceChoices(sk, c);
|
|
489
464
|
ed.sel = [];
|
|
490
|
-
ed.preview = (cx, T) => { const e = dimEnds(sk, c); if (e) drawPendingDim(cx, T, { from: e.a, to: e.b, a: e.a, b: e.b, nx: 0, ny: 0, off: 0, label:
|
|
465
|
+
ed.preview = (cx, T) => { const e = dimEnds(sk, c); if (e) drawPendingDim(cx, T, { from: e.a, to: e.b, a: e.a, b: e.b, nx: 0, ny: 0, off: 0, label: pendingLabel(kpTyped(), measure(sk, c)) }); };
|
|
491
466
|
requestDraw();
|
|
492
467
|
const spec: KeypadSpec = {
|
|
493
468
|
onClose: () => { ed.preview = null; requestDraw(); },
|
|
494
469
|
// (which faces it's measured between: the choices beside the keys)
|
|
495
|
-
title: opts.title || 'Distance to line', sub: opts.keepParallel ? 'Measured square to the lines' : '',
|
|
470
|
+
title: opts.title || 'Distance to line', sub: opts.keepParallel ? 'Measured square to the lines' : '', ...startingValue(measure(sk, c)), selectAll: true,
|
|
496
471
|
extras: choices.length ? () => choices.map(ch => ({
|
|
497
472
|
label: ch.label!, on: onChoice(c, ch),
|
|
498
|
-
onPress: () => { applyFace(c, ch); requestOverlayDraw(); if (keypadFresh()) keypadSetReading(measure(sk, c)); },
|
|
473
|
+
onPress: () => { applyFace(c, ch); requestOverlayDraw(); if (keypadFresh() && !noScaleYet()) keypadSetReading(measure(sk, c)); },
|
|
499
474
|
})) : undefined,
|
|
500
475
|
onDone: v => {
|
|
501
476
|
pushHistory();
|
|
@@ -525,10 +500,10 @@ export function dimCurve(id: string, side?: 1 | -1): void {
|
|
|
525
500
|
const show = () => { ed.sel = face ? [{ type: 'wface', id: edgeId(id, face) }] : [{ type, id }]; requestDraw(); };
|
|
526
501
|
show();
|
|
527
502
|
const spec: KeypadSpec = {
|
|
528
|
-
title: 'Diameter', sub: wall ? lengthSub(sk, id, face) : '',
|
|
503
|
+
title: 'Diameter', sub: wall ? lengthSub(sk, id, face) : '', ...startingValue(now()), selectAll: true,
|
|
529
504
|
extras: wall ? () => curveChoices(sk, id).map(ch => ({
|
|
530
505
|
label: ch.label!, on: ch.face === face,
|
|
531
|
-
onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, id, face); show(); if (keypadFresh()) keypadSetReading(now()); },
|
|
506
|
+
onPress: () => { face = ch.face!; spec.sub = lengthSub(sk, id, face); show(); if (keypadFresh() && !noScaleYet()) keypadSetReading(now()); },
|
|
532
507
|
})) : undefined,
|
|
533
508
|
onClose: () => { ed.sel = was; requestDraw(); },
|
|
534
509
|
onDone: v => {
|
|
@@ -549,7 +524,7 @@ export function cornerAngle(pid: string, done?: () => void, modeless = false): b
|
|
|
549
524
|
const sk = SK(), pair = cornerLines(sk, pid);
|
|
550
525
|
if (!pair) return false;
|
|
551
526
|
// edit the corner's existing angle dimension rather than stacking a second one
|
|
552
|
-
const ex = Object.keys(sk.cons).find(id => { const c = sk.cons[id]; return c.type === 'angle' &&
|
|
527
|
+
const ex = Object.keys(sk.cons).find(id => { const c = sk.cons[id]; return c.type === 'angle' && ((c.l1 === pair[0] && c.l2 === pair[1]) || (c.l1 === pair[1] && c.l2 === pair[0])); });
|
|
553
528
|
if (ex) editDim(ex, done, modeless); else dimAngle(pair[0], pair[1], done, modeless);
|
|
554
529
|
return true;
|
|
555
530
|
}
|
|
@@ -605,7 +580,7 @@ export function editDim(id: string, done?: () => void, modeless = false, pick?:
|
|
|
605
580
|
// (which faces it's measured on: the choices beside the keys)
|
|
606
581
|
const spec: KeypadSpec = {
|
|
607
582
|
title,
|
|
608
|
-
sub: c.driving === false ?
|
|
583
|
+
sub: c.driving === false ? 'Reference — not driving the shape' : 'Driving dimension',
|
|
609
584
|
numeric: isAng, initial: isAng ? String(Math.round(now() * 10) / 10) : fmtLen(now()), selectAll: true, laser: !isAng, modeless,
|
|
610
585
|
// (just the faces it's measured on: Reference and Delete are the bar's, for a dimension selected)
|
|
611
586
|
extras: () => faceChoices(sk, withFace(c, face)).map(ch => ({
|
|
@@ -615,7 +590,7 @@ export function editDim(id: string, done?: () => void, modeless = false, pick?:
|
|
|
615
590
|
onDone: (v, capture) => { setDimValue(id, v * k, face ?? undefined, { capture }); done?.(); },
|
|
616
591
|
// a line's length or an angle: or one of the job's, from its list — the dimension takes it, linked to it (a
|
|
617
592
|
// dimension already linked to another, re-linked)
|
|
618
|
-
measurements: ((c.type === 'dist' && c.line) || isAng) &&
|
|
593
|
+
measurements: ((c.type === 'dist' && c.line) || isAng) && offersJobMeasurements() ? {
|
|
619
594
|
kind: isAng ? 'angle' : 'length', pick: t => {
|
|
620
595
|
const v = jobValue(t, isAng ? 'angle' : 'length');
|
|
621
596
|
if (v == null) return;
|
|
@@ -671,7 +646,6 @@ function takeLink(c: Constraint, o: DimValueOpts): void {
|
|
|
671
646
|
export function setDimValue(id: string, v: number, face?: FaceChoice, opts: DimValueOpts = {}): boolean {
|
|
672
647
|
const sk = SK(), c = sk.cons[id];
|
|
673
648
|
if (!c) return false;
|
|
674
|
-
if (c.layer === 'print') { toast('Print dimensions follow the drawing. Change a driving dimension instead.', { warn: true }); return false; }
|
|
675
649
|
if (c.measurementRef && opts.link === undefined && !opts.asked && !opts.quiet && changesValue(c, v)) {
|
|
676
650
|
askLinkedChange(c, v, unlink => setDimValue(id, v, face, { ...opts, asked: true, ...(unlink ? { link: null } : {}) }), opts.capture);
|
|
677
651
|
return false;
|
package/src/tools/fillet.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { requestDraw } from '../render/view';
|
|
|
9
9
|
import { solver } from '../solver';
|
|
10
10
|
import { fmtLen } from '../units';
|
|
11
11
|
import { openKeypad, toast, updateCtx } from '../editor/ports';
|
|
12
|
+
import { waitForScale } from '../editor/scaleGate';
|
|
12
13
|
import { cornerAt, cutCorner } from './corner';
|
|
13
14
|
|
|
14
15
|
/*
|
|
@@ -33,6 +34,8 @@ export function maxFillet(sk: Sketch, pid: string): number {
|
|
|
33
34
|
export function filletPrompt(pid: string): void {
|
|
34
35
|
const sk = SK();
|
|
35
36
|
if (!cornerAt(sk, pid)) { toast('Select a corner where two lines meet to round it off.', { warn: true }); return; }
|
|
37
|
+
// (a radius typed is real: nothing drawn says what it is in a sketch without its scale yet — editor/scaleGate.ts)
|
|
38
|
+
if (waitForScale()) return;
|
|
36
39
|
const most = maxFillet(sk, pid), start = Math.min(6, most * 0.5);
|
|
37
40
|
openKeypad({
|
|
38
41
|
title: 'Fillet radius', sub: 'Up to ' + fmtLen(most) + ' fits this corner', initial: fmtLen(Math.round(start * 16) / 16), value: Math.round(start * 16) / 16, selectAll: true, laser: true,
|
package/src/tools/freehand.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { ed } from '../app/state';
|
|
|
3
3
|
import { wallBand, type PEv } from './registry';
|
|
4
4
|
import { hyp } from '../model/geom';
|
|
5
5
|
import { dropHistory, pushHistory } from '../model/history';
|
|
6
|
-
import { maybeRoom } from '../model/rooms';
|
|
7
6
|
import { settings } from '../model/settings';
|
|
8
7
|
import { addLine, addPoint } from '../model/sketch';
|
|
9
8
|
import { deg, JOIN_PX, simplifyStroke, SQUARE_DEG, squareUp, turn } from '../model/stroke';
|
|
@@ -20,7 +19,6 @@ import { drawsWalls } from '../editor/mode';
|
|
|
20
19
|
simplified, corners cleaned up, closed if it ends where it began, every segment within 15° of level, plumb or a
|
|
21
20
|
45° diagonal snapped exactly to it (most site sketches are 90s and 45s), joined to points it starts or ends on,
|
|
22
21
|
and (with auto-constrain on) level/plumb segments and square corners constrained, ready to adjust.
|
|
23
|
-
The same conversion turns a markup pen stroke into lines (convertNote).
|
|
24
22
|
*/
|
|
25
23
|
|
|
26
24
|
/**
|
|
@@ -57,7 +55,7 @@ export function strokeToLines(stroke: XY[], opts: { constr?: boolean; kind?: Lin
|
|
|
57
55
|
if (Math.abs(turn(p, q, r) - 90) < SQUARE_DEG) add({ type: 'perp', l1: lines[j], l2: lines[i] });
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
|
-
|
|
58
|
+
// (a closed shape is only a shape: the Space tool makes it a space, as the line tool's)
|
|
61
59
|
return { lines, closed };
|
|
62
60
|
}
|
|
63
61
|
|
package/src/tools/line.ts
CHANGED
|
@@ -2,15 +2,14 @@ import { afterEdit, analyze, tryAddConstraint } from '../app/edit';
|
|
|
2
2
|
import { ed, type Guide, type Snap } from '../app/state';
|
|
3
3
|
import { hyp, norm2 } from '../model/geom';
|
|
4
4
|
import { pushHistory } from '../model/history';
|
|
5
|
-
import { roomGeom, maybeRoom } from '../model/rooms';
|
|
6
5
|
import { settings } from '../model/settings';
|
|
7
6
|
import { addLine, gc, pinnedPoints } from '../model/sketch';
|
|
8
|
-
import {
|
|
7
|
+
import { isUnscaled, setScale } from '../model/scale';
|
|
8
|
+
import { page, SK, store } from '../model/store';
|
|
9
9
|
import type { LineKind } from '../model/types';
|
|
10
10
|
import { TH } from '../render/theme';
|
|
11
11
|
import { shiftSnap, snap45, snapAt } from '../render/snap';
|
|
12
12
|
import { requestDraw, requestOverlayDraw, S } from '../render/view';
|
|
13
|
-
import { fmtArea } from '../units';
|
|
14
13
|
import { commitPoint, applyPending, lengthPreview, PT, rememberHover, wallBand, type PEv, type Tool } from './registry';
|
|
15
14
|
import { openKeypad, toast, updateCtx, updateHint } from '../editor/ports';
|
|
16
15
|
import type { KeyIn } from '../editor/keys';
|
|
@@ -138,19 +137,17 @@ export function makeLineTool(): LineTool {
|
|
|
138
137
|
const closed = b.id === st.first && st.chain.length >= 3;
|
|
139
138
|
st.chain.push(b.id); st.prev = lid; st.start = b.id;
|
|
140
139
|
pinnedPoints.clear(); pinnedPoints.add(b.id);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const rid = kindNow() === 'wall' || !ed.drawConstr ? maybeRoom(sk, loop) : null;
|
|
145
|
-
this.reset();
|
|
146
|
-
if (rid) toast(sk.rooms[rid].name + ' added — ' + fmtArea(roomGeom(sk, sk.rooms[rid]).area));
|
|
147
|
-
}
|
|
140
|
+
// a closed shape is only a shape: it's made a space by the Space tool, when the person picks it (the user's call,
|
|
141
|
+
// 2026-10-26: "By default i dont want spaces create at all")
|
|
142
|
+
if (closed) this.reset();
|
|
148
143
|
},
|
|
149
144
|
typeLength(initial: string) {
|
|
150
145
|
const st = this.st;
|
|
151
146
|
if (!st.start) return;
|
|
152
147
|
const dirs: [string, [number, number]][] = [['→', [1, 0]], ['←', [-1, 0]], ['↑', [0, 1]], ['↓', [0, -1]]];
|
|
153
148
|
let dir = st.dir || [1, 0];
|
|
149
|
+
// how long the segment is drawn so far (to the pointer: nothing, on a touch screen, after a tap)
|
|
150
|
+
const from = PT(st.start), drawn = ed.snapState ? hyp(ed.snapState.x - from.x, ed.snapState.y - from.y) : 0;
|
|
154
151
|
openKeypad({
|
|
155
152
|
title: 'Segment length', sub: 'Direction sets where it goes', initial: initial || '',
|
|
156
153
|
extras: () => dirs.map(([label, d]) => ({
|
|
@@ -161,11 +158,15 @@ export function makeLineTool(): LineTool {
|
|
|
161
158
|
})),
|
|
162
159
|
onDone: v => {
|
|
163
160
|
if (v <= 0) return;
|
|
161
|
+
pushHistory();
|
|
162
|
+
// the drawing's first length: the sketch is scaled so the segment, as drawn, is that long — as it is, when
|
|
163
|
+
// nothing was drawn to go by (model/scale.ts). Before it's read where the segment starts: that moves too.
|
|
164
|
+
const pg = page();
|
|
165
|
+
if (isUnscaled(pg.sk) && SK() === pg.sk) { setScale(pg, drawn > 1e-6 ? v / drawn : 1); requestDraw(); }
|
|
164
166
|
const a = PT(st.start!);
|
|
165
167
|
const guides: Guide[] = [];
|
|
166
168
|
if (Math.abs(dir[1]) < 1e-9) guides.push({ kind: 'H', from: st.start! });
|
|
167
169
|
else if (Math.abs(dir[0]) < 1e-9) guides.push({ kind: 'V', from: st.start! });
|
|
168
|
-
pushHistory();
|
|
169
170
|
this.segmentTo({ x: a.x + dir[0] * v, y: a.y + dir[1] * v, kind: 'free', guides }, v);
|
|
170
171
|
afterEdit();
|
|
171
172
|
},
|