@reekon-tools/boldr-cad 1.4.0 → 1.5.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 +3 -5
- 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 +149 -169
- 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 +2 -2
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/linkActions.d.ts +22 -3
- package/dist/editor/linkActions.js +95 -13
- package/dist/editor/linkActions.js.map +1 -1
- package/dist/editor/mode.js +4 -3
- 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 +9 -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/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/history.js +4 -1
- package/dist/model/history.js.map +1 -1
- package/dist/model/links.d.ts +4 -1
- package/dist/model/links.js +9 -2
- 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 +120 -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 +21 -1
- package/dist/model/sketch.js +58 -4
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/stroke.js +2 -2
- package/dist/model/types.d.ts +38 -7
- package/dist/react/BottomBar.d.ts +2 -1
- package/dist/react/BottomBar.js +94 -25
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/Drawer.js +3 -2
- 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/Popovers.js +14 -3
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/Rail.js +1 -1
- package/dist/react/Sheet.d.ts +2 -2
- package/dist/react/Sheet.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/motion.d.ts +1 -1
- package/dist/react/motion.js +1 -1
- package/dist/react/motion.js.map +1 -1
- package/dist/render/draw.d.ts +21 -4
- package/dist/render/draw.js +184 -149
- 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 +5 -1
- package/dist/render/snap.js +3 -1
- 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 +31 -4
- 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 +23 -6
- 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.js +6 -1
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +5 -0
- package/dist/tools/registry.js +6 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +19 -0
- package/dist/tools/select.js +116 -17
- 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 +3 -5
- package/src/data/sync.ts +7 -2
- package/src/editor/actions.ts +126 -112
- package/src/editor/elevation.ts +13 -16
- package/src/editor/gestures.ts +83 -13
- package/src/editor/keys.ts +2 -2
- package/src/editor/linkActions.ts +90 -13
- package/src/editor/mode.ts +4 -3
- 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 +8 -0
- package/src/editor/view3d.ts +6 -2
- package/src/export/pdf.ts +7 -4
- package/src/export/plan.ts +14 -10
- package/src/model/demo.ts +2 -1
- package/src/model/elevation.ts +4 -4
- package/src/model/history.ts +4 -1
- package/src/model/links.ts +8 -2
- 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 +105 -0
- package/src/model/settings.ts +1 -2
- package/src/model/sketch.ts +48 -4
- package/src/model/stroke.ts +2 -2
- package/src/model/types.ts +38 -7
- package/src/react/BottomBar.tsx +114 -29
- package/src/react/Drawer.tsx +14 -10
- 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/Popovers.tsx +24 -9
- package/src/react/Rail.tsx +1 -1
- package/src/react/Sheet.tsx +3 -3
- package/src/react/barScroll.ts +57 -0
- package/src/react/corners.ts +6 -3
- package/src/react/motion.tsx +6 -6
- package/src/render/draw.ts +156 -116
- 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 +7 -2
- package/src/render/svgPath.ts +95 -0
- package/src/render/tileIcons.ts +75 -0
- package/src/render/view.ts +37 -5
- package/src/skia/SkiaDrawCtx.ts +26 -4
- package/src/solver/equations.ts +22 -6
- 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 +5 -1
- package/src/tools/registry.ts +10 -1
- package/src/tools/select.ts +99 -18
- 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/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
|
},
|
package/src/tools/modify.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { placeTargets, snapSlide } from '../editor/elevation';
|
|
|
15
15
|
import { fmtArea, fmtLen } from '../units';
|
|
16
16
|
import type { PEv } from './registry';
|
|
17
17
|
import { openKeypad, toast } from '../editor/ports';
|
|
18
|
+
import { noScaleYet, waitForScale } from '../editor/scaleGate';
|
|
18
19
|
|
|
19
20
|
/** Hover helper: highlight only geometry of the given types. */
|
|
20
21
|
const hoverOnly = (types: string[]) => (ev: PEv) => {
|
|
@@ -118,6 +119,8 @@ export const offsetTool = {
|
|
|
118
119
|
};
|
|
119
120
|
|
|
120
121
|
export function offsetPrompt(lid: string, side: number): void {
|
|
122
|
+
// (a distance typed is real: nothing drawn says what it is in a sketch without its scale yet — editor/scaleGate.ts)
|
|
123
|
+
if (waitForScale()) return;
|
|
121
124
|
ed.sel = [{ type: 'line', id: lid }];
|
|
122
125
|
requestDraw();
|
|
123
126
|
openKeypad({ title: 'Offset distance', sub: 'Copies the line, kept parallel', initial: '', onDone: v => { if (!v) return; offsetLine(lid, v * side); } });
|
|
@@ -185,7 +188,8 @@ export const roomTool = {
|
|
|
185
188
|
if (!id) { dropHistory(); toast('That area is too small for a space.', { warn: true }); return; }
|
|
186
189
|
ed.sel = [];
|
|
187
190
|
afterEdit();
|
|
188
|
-
|
|
191
|
+
// (its area once the drawing has its scale: before, only a placeholder)
|
|
192
|
+
toast(sk.rooms[id].name + (noScaleYet() ? '' : ' — ' + fmtArea(roomGeom(sk, sk.rooms[id]).area)));
|
|
189
193
|
},
|
|
190
194
|
hint() { return 'Tap inside a closed shape to make a space. Area is measured to the inside face of walls.'; },
|
|
191
195
|
};
|
package/src/tools/registry.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { requestDraw, S, type Xform } from '../render/view';
|
|
|
10
10
|
import { fmtLen } from '../units';
|
|
11
11
|
import { onSessionReset } from '../model/session';
|
|
12
12
|
import { updateCtx, updateHint, updateRail } from '../editor/ports';
|
|
13
|
+
import { noScaleYet, toolWaitsForScale, waitForScale } from '../editor/scaleGate';
|
|
13
14
|
import type { KeyIn } from '../editor/keys';
|
|
14
15
|
import type { DrawCtx } from '../render/ctx';
|
|
15
16
|
import { TH } from '../render/theme';
|
|
@@ -36,6 +37,11 @@ export interface Tool {
|
|
|
36
37
|
hint?(): string;
|
|
37
38
|
/** something half-done that Esc should cancel first (a line in progress, a picked first point…) */
|
|
38
39
|
busy?(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* whether a finger or pencil pressed and moving is placing or dragging something, for a device's magnifier
|
|
42
|
+
* (render/lens.ts). Unset: whenever one is (a drawing tool places at it all the time).
|
|
43
|
+
*/
|
|
44
|
+
magnify?(): boolean;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export const tools: Record<string, Tool> = {};
|
|
@@ -53,6 +59,8 @@ export function cancelTool(): void {
|
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
export function setTool(t: string): void {
|
|
62
|
+
// (doors, windows, cabinets and offsets have real sizes: they wait for the page's first length — editor/scaleGate.ts)
|
|
63
|
+
if (toolWaitsForScale(t) && waitForScale()) return;
|
|
56
64
|
if (t === ed.tool && t !== 'select' && t !== 'aselect') { cancelTool(); updateCtx(); updateHint(); return; }
|
|
57
65
|
cancelTool();
|
|
58
66
|
ed.tool = t;
|
|
@@ -88,7 +96,8 @@ export function rememberHover(s: Snap | null): void {
|
|
|
88
96
|
export function lengthPreview(c: DrawCtx, T: Xform, a: XY, b: XY, col?: string): void {
|
|
89
97
|
const [x1, y1] = S(T, a.x, a.y), [x2, y2] = S(T, b.x, b.y);
|
|
90
98
|
const L = hyp(b.x - a.x, b.y - a.y);
|
|
91
|
-
|
|
99
|
+
// (not on a page without its scale: the length is only a placeholder)
|
|
100
|
+
if (L * T.s < 6 || noScaleYet()) return;
|
|
92
101
|
chip(c, fmtLen(L), (x1 + x2) / 2, (y1 + y2) / 2 - 16, { bg: '#fff', fg: '#0066FF', border: [col || '#0066FF', 1.5] });
|
|
93
102
|
}
|
|
94
103
|
|
package/src/tools/select.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { dimEnds, dimRadius, edgeOf, gridSteps, openingFrame } from '../model/planGeom';
|
|
1
|
+
import { angleCorner, angleSweep, arcAngles, dimEnds, dimRadius, edgeOf, gridSteps, inSweep, openingFrame } from '../model/planGeom';
|
|
2
2
|
import { imageById } from '../model/images';
|
|
3
3
|
import { editNoteText } from '../editor/markup';
|
|
4
4
|
import { afterEdit, resolveOpeningsNow, solveSketch, tryAddConstraint } from '../app/edit';
|
|
5
5
|
import { ed, isSel, type BoxSel, type Hit, type Snap } from '../app/state';
|
|
6
|
-
import { clamp, hyp, norm2, projT, segInter } from '../model/geom';
|
|
6
|
+
import { clamp, hyp, norm2, normAng, projT, segInter } from '../model/geom';
|
|
7
7
|
import { dropHistory, pushHistory } from '../model/history';
|
|
8
8
|
import { deep } from '../model/ids';
|
|
9
9
|
import { settings } from '../model/settings';
|
|
10
10
|
import { openingSpan, slideGroup, type SlideGroup } from '../model/openings';
|
|
11
|
-
import { connectedGeometry, joinPoints, mergePoints, restorePos, snapshotPos } from '../model/sketch';
|
|
11
|
+
import { circleEdge, connectedGeometry, joinPoints, mergePoints, restorePos, snapshotPos } from '../model/sketch';
|
|
12
12
|
import { planSK, replaceSK, SK } from '../model/store';
|
|
13
|
-
import type { Note, SelItem, Sketch, Underlay, WallFace } from '../model/types';
|
|
13
|
+
import type { Note, SelItem, Sketch, Underlay, WallFace, XY } from '../model/types';
|
|
14
14
|
import { cabOfPoint, cabPoints, faceEdit, openingOfPoint } from '../model/faces';
|
|
15
15
|
import { pushedOut, syncFace, type Overflow } from '../model/facesolve';
|
|
16
16
|
import { snapCabinetMove } from '../editor/face';
|
|
@@ -24,9 +24,16 @@ import type { PEv } from './registry';
|
|
|
24
24
|
import { onSessionReset } from '../model/session';
|
|
25
25
|
import { openDrawer, setCursor, toast, updateCtx, updateHint } from '../editor/ports';
|
|
26
26
|
import { selectionHint } from '../editor/selection';
|
|
27
|
+
import { openImageLink } from '../editor/linkActions';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An arc the dragged point is an end of: its other end, which end the point is, how far round it goes (kept as it's
|
|
31
|
+
* dragged), and whether it's closed — dragged all the way round onto its other end (see closeArcs).
|
|
32
|
+
*/
|
|
33
|
+
interface ArcEnd { id: string; other: string; end: 'a' | 'b'; sweep: number; closed: boolean }
|
|
27
34
|
|
|
28
35
|
type Drag =
|
|
29
|
-
| { kind: 'point'; id: string; start: { x: number; y: number }; grab: { x: number; y: number }; snap: Snap | null }
|
|
36
|
+
| { kind: 'point'; id: string; start: { x: number; y: number }; grab: { x: number; y: number }; snap: Snap | null; arcs: ArcEnd[] }
|
|
30
37
|
/** cab: dragging a cabinet (its box where it started): it snaps to corners, the floor, other cabinets */
|
|
31
38
|
| { kind: 'multi'; ids: string[]; start: { x: number; y: number }[]; grab: { x: number; y: number }; cab?: { id: string; box: { u0: number; u1: number; z0: number; z1: number } }; via?: string; gst?: number; gx?: number; gy?: number }
|
|
32
39
|
/** size a circle by its edge; grab: how far out from its centreline it was picked up (a wall's outer face, say) */
|
|
@@ -34,6 +41,8 @@ type Drag =
|
|
|
34
41
|
| { kind: 'dimoff'; id: string }
|
|
35
42
|
/** swing a diameter's line around its circle and move the label in/out */
|
|
36
43
|
| { kind: 'diamlabel'; id: string }
|
|
44
|
+
/** pull an angle's label off its corner: out (its arc's radius) and round between its two lines */
|
|
45
|
+
| { kind: 'anglelabel'; id: string }
|
|
37
46
|
/** slide a door/window dimension (width or a placed one) toward or away from its wall */
|
|
38
47
|
| { kind: 'odimoff'; id: string; part: string }
|
|
39
48
|
/** slide a door/window (or the free chain of openings it's locked into) along its wall; grab: s where it was picked up */
|
|
@@ -179,6 +188,8 @@ export const selectTool = {
|
|
|
179
188
|
else if (st.dragging && ed.boxSel) { boxSelect(ed.boxSel, ev.shift); ed.boxSel = null; }
|
|
180
189
|
else if (st.hit) {
|
|
181
190
|
const h = st.hit, now = performance.now();
|
|
191
|
+
// an expanded photo's badge, tapped: open what it's for
|
|
192
|
+
if (h.type === 'underlay' && h.part === 'link') { this.st = {}; openImageLink(h.id); updateCtx(); requestDraw(); return; }
|
|
182
193
|
// a cabinet's footprint in the plan: open its wall, the cabinet selected
|
|
183
194
|
if (h.type === 'cabfoot') { const [key, cab] = h.id.split('#'); this.st = {}; openCabinet(key, cab); return; }
|
|
184
195
|
// with a finger or pencil, tapping the selected line again selects everything joined to it (as a double tap does)
|
|
@@ -220,6 +231,8 @@ export const selectTool = {
|
|
|
220
231
|
this.st = {};
|
|
221
232
|
updateCtx(); updateHint(); requestDraw();
|
|
222
233
|
},
|
|
234
|
+
/** (something being dragged: not a box drawn round things, nor a press that hasn't moved anything) */
|
|
235
|
+
magnify() { return !!this.st.drag; },
|
|
223
236
|
hint() {
|
|
224
237
|
if (ed.sel.length) return selectionHint();
|
|
225
238
|
return ed.lastPointerTouch
|
|
@@ -355,7 +368,11 @@ function beginDrag(h: Hit, down: PEv): Drag | null {
|
|
|
355
368
|
if (it.type === 'point') s.add(it.id);
|
|
356
369
|
if (it.type === 'arc') addArc(it.id);
|
|
357
370
|
if (it.type === 'circle') { const ci = sk.circles[it.id]; if (ci) s.add(ci.c); }
|
|
358
|
-
if (it.type === 'room') {
|
|
371
|
+
if (it.type === 'room') {
|
|
372
|
+
const r = sk.rooms[it.id];
|
|
373
|
+
// (its corners — and the centre of a circle it runs round, which has none of its own)
|
|
374
|
+
if (r) { r.loop.forEach(p => s.add(p)); for (const e of r.edges ?? []) { const ce = circleEdge(e), ci = ce && sk.circles[ce.cid]; if (ci) s.add(ci.c); } }
|
|
375
|
+
}
|
|
359
376
|
}
|
|
360
377
|
} else {
|
|
361
378
|
if (h.type === 'line') addL(h.id);
|
|
@@ -373,7 +390,7 @@ function beginDrag(h: Hit, down: PEv): Drag | null {
|
|
|
373
390
|
if (p.fixed) { toast(p.origin ? 'The origin stays put. Lock or unlock other points from the bar below.' : p.anchor ? 'This corner is set by the plan — move the wall, door or window there.' : 'This point is locked. Unlock it to move it.', { warn: true }); return null; }
|
|
374
391
|
// (one of several selected: the selection moves, as by one of its lines)
|
|
375
392
|
if (several) return multi(ptsOf());
|
|
376
|
-
return { kind: 'point', id: h.id, start: { x: p.x, y: p.y }, grab: { x: wx, y: wy }, snap: null };
|
|
393
|
+
return { kind: 'point', id: h.id, start: { x: p.x, y: p.y }, grab: { x: wx, y: wy }, snap: null, arcs: arcEnds(sk, h.id) };
|
|
377
394
|
}
|
|
378
395
|
if (h.type === 'cab') {
|
|
379
396
|
// a cabinet moves as a whole (and the rest of the selection with it, when it's part of it)
|
|
@@ -403,7 +420,10 @@ function beginDrag(h: Hit, down: PEv): Drag | null {
|
|
|
403
420
|
if (h.type === 'dim') {
|
|
404
421
|
const c = sk.cons[h.id];
|
|
405
422
|
if (c.type === 'diam') return { kind: 'diamlabel', id: h.id };
|
|
406
|
-
if (c.type
|
|
423
|
+
if (c.type === 'angle') return { kind: 'anglelabel', id: h.id };
|
|
424
|
+
// (a distance between two parallel lines or walls slides along them: the user's call, 2026-10-24, when one made
|
|
425
|
+
// by selecting two walls across from each other wouldn't leave where it was first drawn)
|
|
426
|
+
if (c.type !== 'dist' && c.type !== 'pldist') return null;
|
|
407
427
|
return { kind: 'dimoff', id: h.id };
|
|
408
428
|
}
|
|
409
429
|
if (h.type === 'opening') {
|
|
@@ -449,6 +469,41 @@ function groupDrag(sk: Sketch, h: Hit, wx: number, wy: number): Drag | null {
|
|
|
449
469
|
/** How far things ran past their walls when each drag began: it may not go further. */
|
|
450
470
|
const dragBase = new WeakMap<Drag, Overflow>();
|
|
451
471
|
|
|
472
|
+
/** The arcs a point is an end of (not their centre), each with how far round it goes now. */
|
|
473
|
+
function arcEnds(sk: Sketch, pid: string): ArcEnd[] {
|
|
474
|
+
const out: ArcEnd[] = [];
|
|
475
|
+
for (const id in sk.arcs) {
|
|
476
|
+
const a = sk.arcs[id];
|
|
477
|
+
if (a.a === a.b || a.c === pid || (a.a !== pid && a.b !== pid)) continue;
|
|
478
|
+
const g = arcAngles(sk, a);
|
|
479
|
+
out.push({ id, other: a.a === pid ? a.b : a.a, end: a.a === pid ? 'a' : 'b', sweep: g.full ? 2 * Math.PI : normAng(g.t1 - g.t0), closed: false });
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* An arc's end dragged all the way round onto its other end — or on past it — closes it (the user's call, 2026-10-29:
|
|
486
|
+
* "if u fully rotate it to reach around - it should create a circle"): it's held on the other end, drawn all the way
|
|
487
|
+
* round (arcAngles' `full`), and joined into a circle when it's let go (endDrag). Back the way it came, it opens again.
|
|
488
|
+
* Only from more than half way round: dragged back onto its start the short way, an arc shrinks — it isn't snapped onto
|
|
489
|
+
* its own other end, which would close it. `s` is where the end would go otherwise, `raw` the pointer; returns where it goes.
|
|
490
|
+
*/
|
|
491
|
+
function closeArcs(sk: Sketch, arcs: ArcEnd[], s: Snap, raw: XY, tol: number): Snap {
|
|
492
|
+
for (const t of arcs) {
|
|
493
|
+
const a = sk.arcs[t.id], o = a && sk.points[a.c], q = a && sk.points[t.other];
|
|
494
|
+
if (!a || !o || !q) continue;
|
|
495
|
+
// how far round it would go with the end at the pointer (from its other end, the way it runs: counter-clockwise a → b)
|
|
496
|
+
const th = Math.atan2(raw.y - o.y, raw.x - o.x), tq = Math.atan2(q.y - o.y, q.x - o.x);
|
|
497
|
+
const sweep = t.end === 'b' ? normAng(th - tq) : normAng(tq - th), near = hyp(raw.x - q.x, raw.y - q.y) <= tol;
|
|
498
|
+
if (t.closed) { if (sweep > 1.5 * Math.PI && !near) t.closed = false; }
|
|
499
|
+
else if ((near && t.sweep > Math.PI) || (t.sweep > 1.5 * Math.PI && sweep < 0.5 * Math.PI)) t.closed = true;
|
|
500
|
+
if (t.closed) return { x: q.x, y: q.y, kind: 'point', ref: t.other, label: 'Endpoint', guides: [] };
|
|
501
|
+
t.sweep = sweep;
|
|
502
|
+
if (s.kind === 'point' && s.ref === t.other) return { x: raw.x, y: raw.y, kind: 'free', ref: null, guides: [] };
|
|
503
|
+
}
|
|
504
|
+
return s;
|
|
505
|
+
}
|
|
506
|
+
|
|
452
507
|
function dragTo(d: Drag, ev: PEv): void {
|
|
453
508
|
const sk = SK();
|
|
454
509
|
const [wx, wy] = s2w(ev.sx, ev.sy);
|
|
@@ -482,16 +537,22 @@ function dragTo(d: Drag, ev: PEv): void {
|
|
|
482
537
|
// (not a line already held level or plumb: its direction can't change)
|
|
483
538
|
const squared = (l: { a: string; b: string }) => Object.values(sk.cons).some(c => (c.type === 'h' || c.type === 'v') && ((c.a === l.a && c.b === l.b) || (c.a === l.b && c.b === l.a)));
|
|
484
539
|
const ends = ev.shift ? Object.values(sk.lines).filter(l => !l.anchor && (l.a === d.id || l.b === d.id) && !squared(l)).map(l => sk.points[l.a === d.id ? l.b : l.a]) : [];
|
|
540
|
+
let locked = false;
|
|
485
541
|
if (ends.length) {
|
|
486
542
|
const q = ends.map(o => snap45(o, { x: wx, y: wy })).reduce((m, c) => (hyp(c.x - wx, c.y - wy) < hyp(m.x - wx, m.y - wy) ? c : m));
|
|
487
543
|
// where the drawing can't take it there (other constraints), it's an ordinary drag — never stuck
|
|
488
|
-
|
|
489
|
-
restoreFit(sk, held!);
|
|
544
|
+
locked = go([d.id], [{ x: q.x, y: q.y }]);
|
|
545
|
+
if (locked) { d.snap = null; ed.snapState = null; } else restoreFit(sk, held!);
|
|
546
|
+
}
|
|
547
|
+
// (either way on to what every step of a drag does below — its doors and windows, and drawing it again: returning
|
|
548
|
+
// here left a Shift-drag frozen on the screen until the pointer lifted)
|
|
549
|
+
if (!locked) {
|
|
550
|
+
// (an arc's end, round onto its other end: closed — see closeArcs)
|
|
551
|
+
const s = closeArcs(sk, d.arcs, snapAt(ev.sx, ev.sy, { exclude: new Set([d.id]), touch: ev.touch }), { x: wx, y: wy }, (ev.touch ? 16 : 10) / mainT().s);
|
|
552
|
+
d.snap = s.kind === 'point' || s.kind === 'mid' || s.kind === 'online' || s.kind === 'oncircle' ? s : null;
|
|
553
|
+
ed.snapState = d.snap ? s : null;
|
|
554
|
+
go([d.id], [{ x: s.x, y: s.y }]);
|
|
490
555
|
}
|
|
491
|
-
const s = snapAt(ev.sx, ev.sy, { exclude: new Set([d.id]), touch: ev.touch });
|
|
492
|
-
d.snap = s.kind === 'point' || s.kind === 'mid' || s.kind === 'online' || s.kind === 'oncircle' ? s : null;
|
|
493
|
-
ed.snapState = d.snap ? s : null;
|
|
494
|
-
go([d.id], [{ x: s.x, y: s.y }]);
|
|
495
556
|
} else if (d.kind === 'multi') {
|
|
496
557
|
let mx = wx - d.grab.x, my = wy - d.grab.y;
|
|
497
558
|
// Shift: moved level, plumb or 45° only
|
|
@@ -529,10 +590,20 @@ function dragTo(d: Drag, ev: PEv): void {
|
|
|
529
590
|
c.ang = Math.atan2(wy - g.o.y, wx - g.o.x);
|
|
530
591
|
c.off = Math.max(0, hyp(wx - g.o.x, wy - g.o.y) - g.r - 40 / mainT().s);
|
|
531
592
|
}
|
|
593
|
+
} else if (d.kind === 'anglelabel') {
|
|
594
|
+
// (where the label's dragged: as far out from its corner, and round it as far as its two lines; never into it)
|
|
595
|
+
const c = sk.cons[d.id], g = angleCorner(sk, c);
|
|
596
|
+
if (g) {
|
|
597
|
+
const a = Math.atan2(wy - g.vtx.y, wx - g.vtx.x);
|
|
598
|
+
c.off = Math.max(24 / mainT().s, hyp(wx - g.vtx.x, wy - g.vtx.y));
|
|
599
|
+
c.ang = g.u1 && g.u2 ? (({ t1, sweep }) => inSweep(a, t1, sweep))(angleSweep(g.u1, g.u2)) : a;
|
|
600
|
+
}
|
|
532
601
|
} else if (d.kind === 'dimoff') {
|
|
533
|
-
// (off the
|
|
534
|
-
|
|
535
|
-
const
|
|
602
|
+
// (off the span it measures: a line, a wall's face for a length measured on one — or, for a distance between
|
|
603
|
+
// parallel lines or walls, along them)
|
|
604
|
+
const c = sk.cons[d.id], e = dimEnds(sk, c) ?? (c.a && c.b ? { a: sk.points[c.a], b: sk.points[c.b] } : null);
|
|
605
|
+
if (!e) return;
|
|
606
|
+
const { a, b } = e, n = norm2(-(b.y - a.y), b.x - a.x);
|
|
536
607
|
c.off = (wx - (a.x + b.x) / 2) * n[0] + (wy - (a.y + b.y) / 2) * n[1];
|
|
537
608
|
} else if (d.kind === 'wopening') {
|
|
538
609
|
// a locked door won't move: say so once, then nothing
|
|
@@ -594,7 +665,17 @@ function endDrag(d: Drag): void {
|
|
|
594
665
|
const sk = SK();
|
|
595
666
|
ed.snapState = null;
|
|
596
667
|
if (d.kind === 'wopening' && !d.drag.started) dropHistory(); // it didn't move
|
|
597
|
-
|
|
668
|
+
// an arc dragged all the way round onto its start: its ends joined, it's a circle (gc: closeArc). It's what the drag
|
|
669
|
+
// was, so whether or not auto-constrain is on
|
|
670
|
+
const closed = d.kind === 'point' ? d.arcs.find(t => t.closed) : undefined;
|
|
671
|
+
if (closed && d.kind === 'point') {
|
|
672
|
+
const was = JSON.stringify(sk);
|
|
673
|
+
if (mergePoints(sk, closed.other, d.id)) {
|
|
674
|
+
const r = solveSketch();
|
|
675
|
+
if (!r.ok) { replaceSK(JSON.parse(was) as Sketch); toast('Can’t close that arc — it conflicts with other constraints.', { warn: true }); }
|
|
676
|
+
else { ed.sel = sk.circles[closed.id] ? [{ type: 'circle', id: closed.id }] : []; toast('Closed into a circle'); }
|
|
677
|
+
}
|
|
678
|
+
} else if (d.kind === 'point' && d.snap && settings.autoCons) {
|
|
598
679
|
const s = d.snap;
|
|
599
680
|
if (s.kind === 'point') {
|
|
600
681
|
const snap = JSON.stringify(sk);
|