@reekon-tools/boldr-cad 1.3.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 +21 -4
- 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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.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/host.d.ts +7 -0
- package/dist/react/host.js +14 -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/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/tools/survey.d.ts +6 -0
- package/dist/tools/survey.js +28 -0
- package/dist/tools/survey.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/index.ts +1 -1
- 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/host.ts +14 -1
- 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/tools/survey.ts +21 -0
- package/src/ui/ctx.ts +4 -3
- package/src/ui/drawer.ts +8 -2
- package/src/view3d/build.ts +11 -10
package/src/app/edit.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { pushedOut, syncFaces } from '../model/facesolve';
|
|
|
6
6
|
import { elevSyncFrame, fitState, overflow, pastWallMsg, restoreFit } from '../editor/elevation';
|
|
7
7
|
import { resolveOpenings } from '../model/openings';
|
|
8
8
|
import { gc, restorePos, snapshotPos } from '../model/sketch';
|
|
9
|
+
import { factorFor, isEnteredLength, isUnscaled, scalePage, setScale } from '../model/scale';
|
|
9
10
|
import { page, planSK, saveSoon, SK } from '../model/store';
|
|
10
11
|
import type { Constraint, SelItem, Sketch, WallFace } from '../model/types';
|
|
11
12
|
import { cabOfPoint, cabPoints } from '../model/faces';
|
|
@@ -141,11 +142,34 @@ export function holdGroups(sk: Sketch, c: Constraint): string[][] {
|
|
|
141
142
|
return groups;
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The drawing's first length (entered on an unscaled page's plan): the page is scaled so that what it measures is
|
|
147
|
+
* already that long, its view zoomed to match, so nothing moves on the screen (model/scale.ts). Returns how to take
|
|
148
|
+
* that back — the length was refused — or null when it isn't the first.
|
|
149
|
+
*/
|
|
150
|
+
function scaleForFirstLength(c: Constraint): (() => void) | null {
|
|
151
|
+
const pg = page();
|
|
152
|
+
if (!isUnscaled(pg.sk) || SK() !== pg.sk || !isEnteredLength(c)) return null;
|
|
153
|
+
const k = factorFor(pg.sk, c, c.value!);
|
|
154
|
+
if (k == null) return null;
|
|
155
|
+
setScale(pg, k);
|
|
156
|
+
requestDraw();
|
|
157
|
+
// (in place, not a copy put back: callers keep using the sketch they had)
|
|
158
|
+
return () => { scalePage(pg, 1 / k); pg.sk.unscaled = true; delete pg.sk.scaledBy; requestDraw(); };
|
|
159
|
+
}
|
|
160
|
+
|
|
144
161
|
/**
|
|
145
162
|
* Add a constraint through the solver and explain the outcome:
|
|
146
163
|
* reference dimension → toast, conflict → conflict sheet (unless silent).
|
|
147
164
|
*/
|
|
148
|
-
export function tryAddConstraint(c: Constraint, { silent
|
|
165
|
+
export function tryAddConstraint(c: Constraint, opts: { silent?: boolean; onResolved?: (() => void) | null } = {}): AddResult {
|
|
166
|
+
const unscale = scaleForFirstLength(c);
|
|
167
|
+
const r = addThroughSolver(c, opts);
|
|
168
|
+
if (unscale && !r.ok) unscale();
|
|
169
|
+
return r;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function addThroughSolver(c: Constraint, { silent = false, onResolved = null as (() => void) | null } = {}): AddResult {
|
|
149
173
|
// nothing may be pushed past its wall (in the wall view, past the wall; in the plan, a wall made shorter than
|
|
150
174
|
// what's on it): a constraint that would is taken back
|
|
151
175
|
const sk = SK(), was = overflow(sk), held = fitState(sk);
|
package/src/app/state.ts
CHANGED
|
@@ -12,13 +12,12 @@ export interface Hit {
|
|
|
12
12
|
/** 'cabfoot': a cabinet's footprint in the plan (id: its wall face's key, '#', its id) */
|
|
13
13
|
type: SelType | 'odim' | 'oanchor' | 'uhandle' | 'nhandle' | 'cabfoot';
|
|
14
14
|
id: string;
|
|
15
|
-
/** a print dimension label */
|
|
16
|
-
print?: boolean;
|
|
17
15
|
/** low-priority hit (room fill) */
|
|
18
16
|
weak?: boolean;
|
|
19
17
|
/**
|
|
20
18
|
* 'odim': 'w' width, 'a'/'b' distance to a wall end, or 'd:<id>' a placed dimension. 'oanchor': 's0'/'s1' edge.
|
|
21
|
-
* 'note', a link's tile: 'tip' its leader's tip, 'leader' along its leader (unset: its card).
|
|
19
|
+
* 'note', a link's tile: 'tip' its leader's tip, 'leader' along its leader (unset: its card). 'underlay', an expanded
|
|
20
|
+
* photo: 'link' its badge (a tap opens it).
|
|
22
21
|
*/
|
|
23
22
|
part?: string;
|
|
24
23
|
/** a wall picked whole: the side of its centreline the pointer was on (+1 the left of its A → B; a curved one's, outside) */
|
|
@@ -50,7 +49,7 @@ export interface Snap {
|
|
|
50
49
|
/** Screen-space hit region registered while drawing chips, badges and labels. */
|
|
51
50
|
export interface HitChip {
|
|
52
51
|
/** 'elev': one of the wall view's dimension chips (id: what it measures, as JSON) */
|
|
53
|
-
kind: 'dim' | '
|
|
52
|
+
kind: 'dim' | 'cons' | 'room' | 'odim' | 'elev';
|
|
54
53
|
part?: string;
|
|
55
54
|
id: string;
|
|
56
55
|
x: number; y: number; w: number; h: number;
|
|
@@ -83,7 +82,6 @@ export const ed = sessionState(() => ({
|
|
|
83
82
|
drawConstr: false,
|
|
84
83
|
/** the rail's Walls toggle: what's drawn (lines, rectangles, circles, arcs, Draw to shape) is walls */
|
|
85
84
|
drawWalls: false,
|
|
86
|
-
dimLayer: 'drive' as 'drive' | 'print',
|
|
87
85
|
openingType: 'door' as 'door' | 'window',
|
|
88
86
|
/** new markup's look (the stroke panel sets them; selected markup changes with them) */
|
|
89
87
|
noteColor: DEFAULT_NOTE_COLOR,
|
package/src/data/sync.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { newPage } from '../model/demo';
|
|
|
8
8
|
import { allCons } from '../model/faces';
|
|
9
9
|
import { syncFaces } from '../model/facesolve';
|
|
10
10
|
import { clearPageHistory, imageStored } from '../model/history';
|
|
11
|
+
import { rezoomAcrossScale, settleScale } from '../model/scale';
|
|
11
12
|
import { defaultLayers, settings } from '../model/settings';
|
|
12
13
|
import { store, type Persistence } from '../model/store';
|
|
13
14
|
import type { Constraint, Page, Project } from '../model/types';
|
|
@@ -61,7 +62,7 @@ export interface CadSync {
|
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* A driving dimension's value as BOLDR keeps it: lengths in micrometres, angles in degrees. Null for what isn't one of
|
|
64
|
-
* the drawing's own measurements: reference
|
|
65
|
+
* the drawing's own measurements: reference dimensions, constraints without a value, and dimensions linked
|
|
65
66
|
* to a measurement of the job (the job's, not the drawing's: see editor/links.ts).
|
|
66
67
|
*/
|
|
67
68
|
export function measurementOf(c: Constraint): CadMeasurementValue | null {
|
|
@@ -165,9 +166,12 @@ export function openCadDrawing(provider: CadDataProvider, opts: CadSyncOptions =
|
|
|
165
166
|
for (const [id, doc] of Object.entries(pages)) {
|
|
166
167
|
if (doc.rev?.clientId === clientId) continue; // our own write coming back
|
|
167
168
|
if (writing.has(id)) continue; // ours is on its way: this snapshot is older than what's here
|
|
169
|
+
settleScale(doc.sk); // (before it's compared: settled, it's what was saved, not an edit here)
|
|
168
170
|
const json = pageJson(doc), local = project.pages.find(p => p.id === id);
|
|
169
171
|
if (local) {
|
|
170
172
|
if (pageJson(local) !== saved.get(id) || json === saved.get(id)) continue; // edited here since, or nothing new
|
|
173
|
+
// (given its scale elsewhere, or taken back: the view here zooms to match, so the drawing stays put on screen)
|
|
174
|
+
rezoomAcrossScale(local, local.sk, doc.sk);
|
|
171
175
|
local.name = doc.name; local.sk = doc.sk; local.underlays = imagesOf(doc);
|
|
172
176
|
clearPageHistory(id);
|
|
173
177
|
if (id === store.curPageId) curChanged = true;
|
|
@@ -263,7 +267,8 @@ export function openCadDrawing(provider: CadDataProvider, opts: CadSyncOptions =
|
|
|
263
267
|
if (!file.meta && !opts.initialize) { stop(); reject(new NotACadDrawing('legacy')); return; }
|
|
264
268
|
const order = file.meta?.pageOrder ?? [];
|
|
265
269
|
const ids = [...order.filter(id => pages![id]), ...Object.keys(pages!).filter(id => !order.includes(id))];
|
|
266
|
-
|
|
270
|
+
// (each page's scale settled before what was saved is noted: model/scale.ts)
|
|
271
|
+
project = { name: file.name, pages: ids.map(id => ({ id, name: pages![id].name, sk: settleScale(pages![id].sk), underlays: imagesOf(pages![id]), view: null })) };
|
|
267
272
|
for (const id of ids) saved.set(id, pageJson(pages![id]));
|
|
268
273
|
if (file.meta) savedMeta = JSON.stringify(file.meta);
|
|
269
274
|
// a new drawing (or one whose pages are all gone) starts with one empty page, saved straight away; its id is
|
package/src/editor/actions.ts
CHANGED
|
@@ -2,14 +2,14 @@ import { afterEdit, holdFaces, solveSketch, tryAddConstraint, undo } from '../ap
|
|
|
2
2
|
import { ed, type OpeningDimPart } from '../app/state';
|
|
3
3
|
import { CONS_LABEL, measure } from '../model/constraints';
|
|
4
4
|
import { dropHistory, pushHistory } from '../model/history';
|
|
5
|
-
import { createsCycle, edgePoint, facingEdges, nearerEdge } from '../model/openings';
|
|
5
|
+
import { createsCycle, edgePoint, facingEdges, flipDoor, nearerEdge } from '../model/openings';
|
|
6
|
+
import { roomSideOf } from '../model/elevation';
|
|
6
7
|
import { cabOfPoint } from '../model/faces';
|
|
7
8
|
import { settings } from '../model/settings';
|
|
8
9
|
import { connectedGeometry, curveInfo, deleteSel, joinPoints } from '../model/sketch';
|
|
9
10
|
import { page, planSK, replaceSK, saveSoon, SK } from '../model/store';
|
|
10
|
-
import type { Constraint, ConsType, OpeningEdge, Sketch, Underlay
|
|
11
|
+
import type { Constraint, ConsType, OpeningEdge, Sketch, Underlay } from '../model/types';
|
|
11
12
|
import { fitView, requestDraw } from '../render/view';
|
|
12
|
-
import { convertNote } from '../tools/annotate';
|
|
13
13
|
import { cornerAngle, dimCurve, dimEdges, dimLine, dimPointLine, dimPoints, dimTwoLines, editDim, faceChoices, onChoice, setDimFace, toggleRef } from '../tools/dim';
|
|
14
14
|
import { edgeLabel, edgeOf } from '../model/planGeom';
|
|
15
15
|
import type { LineTool } from '../tools/line';
|
|
@@ -20,13 +20,13 @@ import { chamferPrompt } from '../tools/chamfer';
|
|
|
20
20
|
import { addOpeningDim, editOpeningDim, toggleOpeningDim } from '../tools/openings';
|
|
21
21
|
import { setTool, tools } from '../tools/registry';
|
|
22
22
|
import { fmtLen } from '../units';
|
|
23
|
-
import { elevOpen, fitState, OPENING_SIZES, openElevation,
|
|
23
|
+
import { elevOpen, fitState, OPENING_SIZES, openElevation, overflow, restoreFit, TOO_SHORT, wallAction, wallBar, wallDelete, wallPlus } from './elevation';
|
|
24
24
|
import { pushedOut } from '../model/facesolve';
|
|
25
|
-
import { editRoomName, openKeypad, openPalette, toast, toggleImageOpacity, toggleImageRotation, toggleTileSize, updateCtx, updateTopBar } from './ports';
|
|
25
|
+
import { dismissPopovers, editRoomName, openKeypad, openPalette, toast, toggleImageOpacity, toggleImageRotation, toggleTileSize, updateCtx, updateTopBar } from './ports';
|
|
26
26
|
import { cornerLines, edgePair, selInfo } from './selection';
|
|
27
27
|
import { restackImage } from './project';
|
|
28
28
|
import { imageById } from '../model/images';
|
|
29
|
-
import { canOpenLink, canSaveDim, linkSizeVisible, openDimLink, openSelectedLink, saveDimToGroup, selectedLink, toggleLeader, unlinkDim } from './linkActions';
|
|
29
|
+
import { canExpand, canOpenLink, canSaveDim, linkSizeVisible, openDimLink, openSelectedLink, saveDimToGroup, selectedLink, toggleExpand, toggleLeader, unlinkDim } from './linkActions';
|
|
30
30
|
|
|
31
31
|
/*
|
|
32
32
|
The contextual bottom bar's decisions: which actions fit the tool and the selection, what the yellow + measures,
|
|
@@ -34,7 +34,14 @@ import { canOpenLink, canSaveDim, linkSizeVisible, openDimLink, openSelectedLink
|
|
|
34
34
|
barItems() / plusVisible() and calls applyCtxAction() / plusAction() / deleteSelection().
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* A bar button; `lead`: the bar's first, its icon alone (see arrangeLead). `bare`: its icon alone too, wherever it is
|
|
39
|
+
* (Lock). Either way its name is its label for a screen reader.
|
|
40
|
+
*/
|
|
41
|
+
export interface CtxItem { ic: string; label: string; act: string; on?: boolean; key?: string; locked?: boolean; lead?: boolean; bare?: boolean }
|
|
42
|
+
|
|
43
|
+
/** A bar button shown as its icon alone, no name under it (the lead, and Lock). */
|
|
44
|
+
export const iconAlone = (it: CtxItem): boolean => !!(it.lead || it.bare);
|
|
38
45
|
|
|
39
46
|
/** Points a Lock on the current selection would pin (the origin is always fixed). */
|
|
40
47
|
function lockTargets(): string[] {
|
|
@@ -45,21 +52,24 @@ function lockTargets(): string[] {
|
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
55
|
+
* The bar leads with one button, its icon alone, set off by a divider (Figma's bars, 2528:101128: the user's call,
|
|
56
|
+
* 2026-10-27): Open, when what's selected is a link to something of the job's; otherwise Lock — an open padlock (white)
|
|
57
|
+
* when free, a closed one (red, "Locked") when locked. A link that can be locked too (an expanded photo) has Lock next,
|
|
58
|
+
* with a divider of its own. Lock is its icon alone wherever it is, locked or not (the user's call, 2026-10-30: "only
|
|
59
|
+
* display the icon - no text needed below it").
|
|
50
60
|
*/
|
|
51
|
-
function
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const out: CtxEntry[] = [
|
|
60
|
-
if (
|
|
61
|
-
//
|
|
62
|
-
for (const e of
|
|
61
|
+
function arrangeLead(items: CtxEntry[]): CtxEntry[] {
|
|
62
|
+
const find = (acts: string[]) => items.find((it): it is CtxItem => typeof it === 'object' && 'act' in it && acts.includes(it.act));
|
|
63
|
+
const open = find(['linkOpen', 'dimLinkOpen']), lock = find(['lock', 'imgLock']);
|
|
64
|
+
if (lock) {
|
|
65
|
+
const sk = SK();
|
|
66
|
+
const locked = lock.act === 'imgLock' ? selImages().length > 0 && selImages().every(u => u.locked) : (() => { const l = lockTargets(); return l.length > 0 && l.every(id => sk.points[id].fixed); })();
|
|
67
|
+
Object.assign(lock, { ic: locked ? 'lock' : 'unlock', label: locked ? 'Locked' : 'Lock', on: undefined, locked, bare: true });
|
|
68
|
+
}
|
|
69
|
+
const lead = open ?? lock, out: CtxEntry[] = [];
|
|
70
|
+
if (lead) { lead.lead = true; out.push(lead, 'sep'); if (open && lock) out.push(lock, 'sep'); }
|
|
71
|
+
// the rest, with no divider doubled, first or last
|
|
72
|
+
for (const e of items) if (e !== lead && !(open && e === lock) && !(e === 'sep' && (!out.length || out[out.length - 1] === 'sep'))) out.push(e);
|
|
63
73
|
while (out[out.length - 1] === 'sep') out.pop();
|
|
64
74
|
return out;
|
|
65
75
|
}
|
|
@@ -79,24 +89,22 @@ const PLACING = new Set(['door', 'window', 'wdoor', 'wwindow', 'cabinet']);
|
|
|
79
89
|
function ctxItems(): CtxEntry[] {
|
|
80
90
|
const items: CtxEntry[] = [], sk = SK(), s = selInfo();
|
|
81
91
|
const add = (ic: string, label: string, act: string, o: Partial<CtxItem> = {}) => items.push(Object.assign({ ic, label, act }, o));
|
|
82
|
-
/** To wall (all the selected lines and curves) — or To line + Thickness when they're all walls already. */
|
|
83
|
-
const wallItems = () => {
|
|
84
|
-
if (elevOpen()) return;
|
|
85
|
-
const kinds = [...s.lines.map(id => sk.lines[id].kind), ...s.curves.map(id => curveInfo(sk, id)?.o.kind)];
|
|
86
|
-
if (!kinds.length) return;
|
|
87
|
-
if (kinds.every(k => k === 'wall')) { add('thick', 'Thickness', 'thick'); add('line', 'To line', 'kind'); }
|
|
88
|
-
else add('wall', 'To wall', 'kind');
|
|
89
|
-
};
|
|
90
92
|
if (ed.mode === 'annotate') {
|
|
91
93
|
const ims = selImages();
|
|
92
94
|
if (ims.length && ims.length === s.n) {
|
|
93
95
|
// an image (or several): lock, its own scale, and which goes in front where they overlap
|
|
94
96
|
// (showing and hiding them all is the Layers menu's)
|
|
95
97
|
add('lock', 'Lock', 'imgLock');
|
|
96
|
-
|
|
98
|
+
// a photo of the job's, expanded from its tile: opening it, and Expand (on) to fold it back into its tile
|
|
99
|
+
const lk = ims.length === 1 ? ims[0].link : undefined;
|
|
100
|
+
if (lk && canOpenLink(lk)) add('open', 'Open', 'linkOpen');
|
|
101
|
+
// locked: unlocking it, and opening what it's a photo of — nothing that changes it (the user's call,
|
|
102
|
+
// 2026-10-30: "images in locked state: shouldn't have these options - only lock and link should be available")
|
|
103
|
+
if (ims.every(u => u.locked)) return items;
|
|
104
|
+
if (lk) add('expand', 'Expand', 'linkExpand', { on: true });
|
|
105
|
+
if (ims.length === 1) add('calib', 'Set scale', 'calibtool');
|
|
97
106
|
if (ims.length === 1) add('opacity', 'Opacity', 'imgOpacity');
|
|
98
|
-
|
|
99
|
-
if (ims.length === 1 && !ims[0].locked) add('rotate', 'Rotate', 'imgRotate');
|
|
107
|
+
if (ims.length === 1) add('rotate', 'Rotate', 'imgRotate');
|
|
100
108
|
if (ims.length === 1 && page().underlays.length > 1) {
|
|
101
109
|
const at = page().underlays.indexOf(ims[0]), last = page().underlays.length - 1;
|
|
102
110
|
if (at < last) add('tofront', 'To front', 'imgFront');
|
|
@@ -108,18 +116,17 @@ function ctxItems(): CtxEntry[] {
|
|
|
108
116
|
if (lk) {
|
|
109
117
|
// a link's tile: opening it, its leader, its size (its name is on the tile, not here: the user's call, 2026-10-21)
|
|
110
118
|
if (canOpenLink(lk.link)) add('open', 'Open', 'linkOpen');
|
|
119
|
+
// (a photo's: the photo whole, as one of the page's images)
|
|
120
|
+
if (canExpand(lk)) add('expand', 'Expand', 'linkExpand', { on: false });
|
|
111
121
|
add('leader', 'Leader', 'linkLeader', { on: !!lk.leader });
|
|
112
122
|
// (its leader's colour, as all markup's, is the rail's colour dot's)
|
|
113
123
|
// (every tile on the page: a slider above the bar)
|
|
114
124
|
add('tilesize', 'Size', 'linkSize');
|
|
115
125
|
return items;
|
|
116
126
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (n && n.type === 'pen' && s.notes.length === 1) add('convert', 'To lines', 'convert');
|
|
121
|
-
if (linkSizeVisible()) add('tilesize', 'Size', 'linkSize');
|
|
122
|
-
}
|
|
127
|
+
// markup: nothing of its own — a pen stroke as any other (no To lines: the user's call, 2026-10-27), and no Color
|
|
128
|
+
// (the rail's colour dot styles what's selected); with tiles among it, their size
|
|
129
|
+
if (s.notes.length && linkSizeVisible()) add('tilesize', 'Size', 'linkSize');
|
|
123
130
|
// nothing selected: no bar (the markup colour is the rail's colour dot, for what's selected too; the image's own
|
|
124
131
|
// options come with selecting it)
|
|
125
132
|
return items;
|
|
@@ -138,7 +145,6 @@ function ctxItems(): CtxEntry[] {
|
|
|
138
145
|
if (n >= 1) add('finish', 'Finish', 'finish');
|
|
139
146
|
return items;
|
|
140
147
|
}
|
|
141
|
-
if (ed.tool === 'dim') { add('dim', 'Driving', 'dimDrive', { on: ed.dimLayer === 'drive' }); add('ref', 'Print', 'dimPrint', { on: ed.dimLayer === 'print' }); return items; }
|
|
142
148
|
// nothing selected: the bar stays empty (Fit and Icons live in the top bar; grid snap and auto-constrain in Settings).
|
|
143
149
|
// Just placed a door, window or cabinet (its tool still on): its bar, for its sizes
|
|
144
150
|
if (!s.n || (ed.tool !== 'select' && !PLACING.has(ed.tool))) return items;
|
|
@@ -149,14 +155,31 @@ function ctxItems(): CtxEntry[] {
|
|
|
149
155
|
for (const it of wb.items) add(it.ic, it.label, it.act);
|
|
150
156
|
return items;
|
|
151
157
|
}
|
|
158
|
+
/*
|
|
159
|
+
Figma's bars (2528:101128, the user's call, 2026-10-27): Lock first, its icon alone (arrangeLead); then what the lines
|
|
160
|
+
and curves are (kind); then what ties them, or edits them. The yellow + measures: no Distance of its own here.
|
|
161
|
+
*/
|
|
152
162
|
// on a wall face, walls don't exist: no To wall / Thickness
|
|
153
163
|
const wall = elevOpen();
|
|
154
164
|
const P = s.points.length, L = s.lines.length, C = s.curves.length;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
/**
|
|
166
|
+
* What the selected lines and curves are, and its divider: walls — one straight wall's wall view, and their Thickness;
|
|
167
|
+
* otherwise To wall, and Guide (Solid, when they all are guides). A wall is always solid; on a wall face, Guide alone.
|
|
168
|
+
*/
|
|
169
|
+
const kind = () => {
|
|
170
|
+
const objs = [...s.lines.map(id => sk.lines[id]), ...s.curves.map(id => curveInfo(sk, id)?.o)].filter((o): o is NonNullable<typeof o> => !!o);
|
|
171
|
+
if (!objs.length) return;
|
|
172
|
+
if (objs.every(o => o.kind === 'wall' && !o.constr)) {
|
|
173
|
+
if (wall) return;
|
|
174
|
+
if (s.n === 1 && L === 1) add('elev', 'Elevation', 'elev');
|
|
175
|
+
add('thick', 'Thickness', 'thick');
|
|
176
|
+
} else {
|
|
177
|
+
if (!wall) add('wall', 'To wall', 'kind');
|
|
178
|
+
add('constr', objs.every(o => o.constr) ? 'Solid' : 'Guide', 'constr');
|
|
179
|
+
}
|
|
180
|
+
items.push('sep');
|
|
181
|
+
};
|
|
182
|
+
// a wall's edge: named, with its wall view (with another edge, a line or a point, the + is the distance between them)
|
|
160
183
|
if (s.wfaces.length) {
|
|
161
184
|
// (a curved wall has no wall view: just its name)
|
|
162
185
|
if (s.n === 1) {
|
|
@@ -164,24 +187,18 @@ function ctxItems(): CtxEntry[] {
|
|
|
164
187
|
items.push({ lbl: edgeLabel(sk, id, side) });
|
|
165
188
|
if (sk.lines[id]) add('elev', 'Elevation', 'elev');
|
|
166
189
|
}
|
|
167
|
-
else if (edgePair(s)) {
|
|
168
|
-
const one = s.wfaces.length === 2 && edgeOf(s.wfaces[0])[0] === edgeOf(s.wfaces[1])[0];
|
|
169
|
-
add(one ? 'thick' : 'dim', one ? 'Thickness' : 'Distance', 'edgeDist');
|
|
170
|
-
}
|
|
171
190
|
return items;
|
|
172
191
|
}
|
|
173
192
|
if (s.n === 1 && s.dims.length) {
|
|
174
193
|
const c = sk.cons[s.dims[0]];
|
|
175
194
|
// linked to a measurement of the job: opening it, letting go of it — no text saying what it's linked to (the user's
|
|
176
|
-
// call, 2026-10-21; its chip is drawn linked)
|
|
195
|
+
// call, 2026-10-21; its chip is drawn linked)
|
|
177
196
|
if (c.measurementRef) {
|
|
178
197
|
const t = c.measurementRef;
|
|
179
198
|
if (canOpenLink(t)) add('open', 'Open', 'dimLinkOpen');
|
|
180
199
|
add('unlink', 'Unlink', 'dimUnlink');
|
|
181
200
|
} else if (canSaveDim(s.dims[0])) add('savegroup', 'Save to group', 'dimSave');
|
|
182
|
-
// Reference
|
|
183
|
-
const cabSize = Object.values((sk as WallFace).cabs ?? {}).some(cb => cb.w === s.dims[0] || cb.h === s.dims[0]);
|
|
184
|
-
if (c.layer !== 'print' && !cabSize && !c.measurementRef) add('ref', c.driving === false ? 'Drive' : 'Reference', 'toggleref');
|
|
201
|
+
// (no Reference: Figma's dimension bars have none, 2026-10-27)
|
|
185
202
|
// measured on walls: which of their faces (the drawing stays as it is; the number becomes what it measures there)
|
|
186
203
|
const fc = faceChoices(sk, c);
|
|
187
204
|
if (fc.length && items.length) items.push('sep');
|
|
@@ -196,70 +213,55 @@ function ctxItems(): CtxEntry[] {
|
|
|
196
213
|
// a constraint badge: nothing to offer but delete (the trash), so the bar stays empty
|
|
197
214
|
if (s.n === 1 && s.cons.length) return items;
|
|
198
215
|
if (s.n === 1 && s.rooms.length) { add('rename', 'Rename', 'renameRoom'); return items; }
|
|
199
|
-
// one end of a door or window: named
|
|
216
|
+
// one end of a door or window: named (with another point, end or door/window, the + is the distance between them)
|
|
200
217
|
if (s.oanchors.length && s.n === 1) { items.push({ lbl: anchorLabel(s.oanchors[0]) }); return items; }
|
|
201
|
-
if (s.oanchors.length
|
|
202
|
-
// two doors/windows, or one and a point: the distance between them (locks the second one picked)
|
|
203
|
-
if (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1))) { add('dim', 'Distance', 'opDist'); return items; }
|
|
218
|
+
if (s.oanchors.length || (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1)))) return items;
|
|
204
219
|
if (s.n === 1 && s.openings.length) {
|
|
205
|
-
// its sizes
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
items.push({ lbl: openingLabel(o) });
|
|
220
|
+
// its sizes (no +: it isn't one dimension), with nothing saying them — the drawing does (the user's call, 2026-10-27);
|
|
221
|
+
// a door's Swing too, round its four ways (it was Flip: the user's call, 2026-10-27, "ok do swing instead"). (The
|
|
222
|
+
// type is fixed once placed: Door and Window are separate tools.)
|
|
209
223
|
for (const it of OPENING_SIZES) add(it.ic, it.label, it.act);
|
|
210
|
-
if (
|
|
224
|
+
if (sk.openings[s.openings[0]].type === 'door') { items.push('sep'); add('flip', 'Swing', 'opFlip'); }
|
|
211
225
|
return items;
|
|
212
226
|
}
|
|
227
|
+
// a cabinet's corner: nothing of its own (pick another point or line with it to dimension or constrain)
|
|
228
|
+
if (P === 1 && s.n === 1 && cabOfPoint(sk, s.points[0])) return items;
|
|
229
|
+
add('lock', 'Lock', 'lock', { key: '⇧F' });
|
|
213
230
|
if (L === 1 && s.n === 1) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
add('lock', 'Lock', 'lock', { key: '⇧F' }); add('split', 'Split', 'split'); add('offset', 'Offset', 'offset'); if (l.constr) add('constr', 'Solid', 'constr'); else guide();
|
|
218
|
-
if (!wall) {
|
|
219
|
-
if (l.kind === 'wall') { add('thick', 'Thickness', 'thick'); add('line', 'To line', 'kind'); } else add('wall', 'To wall', 'kind');
|
|
220
|
-
if (l.kind === 'wall' && !l.constr) { items.push('sep'); add('elev', 'Elevation', 'elev'); }
|
|
221
|
-
}
|
|
231
|
+
kind();
|
|
232
|
+
// (its length: the yellow +)
|
|
233
|
+
add('h', 'Horizontal', 'h', { key: '⇧H' }); add('v', 'Vertical', 'v', { key: '⇧V' }); add('split', 'Split', 'split'); add('offset', 'Offset', 'offset');
|
|
222
234
|
return items;
|
|
223
235
|
}
|
|
224
236
|
if (L === 2 && s.n === 2) {
|
|
225
|
-
// (no Angle
|
|
237
|
+
// (no Angle: the yellow + is the angle between them — or, parallel, the distance)
|
|
238
|
+
kind();
|
|
226
239
|
add('par', 'Parallel', 'par', { key: '⇧P' }); add('perp', 'Perpendicular', 'perp', { key: '⇧R' }); add('col', 'Collinear', 'col', { key: '⇧L' }); add('eq', 'Equal', 'eq', { key: '⇧E' });
|
|
227
240
|
// two lines that meet: round off (or cut off) the corner between them
|
|
228
|
-
if (filletCorner2()) { add('fillet', 'Fillet', 'fillet'); add('chamfer', 'Chamfer', 'chamfer'); }
|
|
229
|
-
wallItems();
|
|
241
|
+
if (filletCorner2()) { items.push('sep'); add('fillet', 'Fillet', 'fillet'); add('chamfer', 'Chamfer', 'chamfer'); }
|
|
230
242
|
return items;
|
|
231
243
|
}
|
|
232
|
-
// a
|
|
233
|
-
if (P === 1 && s.n === 1 && cabOfPoint(sk, s.points[0])) return items;
|
|
244
|
+
// a corner where exactly two lines meet: rounding it off or cutting it off (its angle is the yellow +)
|
|
234
245
|
if (P === 1 && s.n === 1) {
|
|
235
|
-
// a corner where exactly two lines meet: rounding it off or cutting it off (its angle is the yellow +)
|
|
236
246
|
if (filletable(sk, s.points[0])) { add('fillet', 'Fillet', 'fillet'); add('chamfer', 'Chamfer', 'chamfer'); }
|
|
237
|
-
add('lock', 'Lock', 'lock', { key: '⇧F' });
|
|
238
247
|
return items;
|
|
239
248
|
}
|
|
240
|
-
|
|
241
|
-
if (P
|
|
249
|
+
// points: joined into the first picked, or level or plumb with it
|
|
250
|
+
if (P >= 2 && s.n === P) { add('merge', 'Join', 'merge'); add('h', 'Horizontal', 'h'); add('v', 'Vertical', 'v'); return items; }
|
|
251
|
+
if (P === 1 && L === 1 && s.n === 2) { add('online', 'On line', 'online', { key: '⇧O' }); add('mid', 'Midpoint', 'mid', { key: '⇧M' }); return items; }
|
|
242
252
|
if (P === 1 && C === 1 && s.n === 2) { add('oncircle', 'On circle', 'oncircle'); return items; }
|
|
243
|
-
if (L === 1 && C === 1 && s.n === 2) { add('tan', 'Tangent', 'tan', { key: '⇧T' });
|
|
253
|
+
if (L === 1 && C === 1 && s.n === 2) { kind(); add('tan', 'Tangent', 'tan', { key: '⇧T' }); return items; }
|
|
244
254
|
// a circle or arc: the yellow + sets its diameter
|
|
245
|
-
if (C === 1 && s.n === 1) {
|
|
246
|
-
|
|
247
|
-
guide();
|
|
248
|
-
if (wall) return items;
|
|
249
|
-
if (ci && ci.o.kind === 'wall') { add('thick', 'Thickness', 'thick'); add('arc', 'To line', 'kind'); } else add('wall', 'To wall', 'kind');
|
|
250
|
-
return items;
|
|
251
|
-
}
|
|
252
|
-
if (C === 2 && s.n === 2) { add('eq', 'Equal', 'eqr'); add('conc', 'Concentric', 'conc'); wallItems(); return items; }
|
|
255
|
+
if (C === 1 && s.n === 1) { kind(); return items; }
|
|
256
|
+
if (C === 2 && s.n === 2) { kind(); add('conc', 'Concentric', 'conc'); add('eq', 'Equal', 'eqr'); return items; }
|
|
253
257
|
// 3+ lines (only lines): the constraints that make sense across many, each tied to the first line picked
|
|
254
258
|
if (L >= 3 && s.n === L) {
|
|
259
|
+
kind();
|
|
255
260
|
add('h', 'Horizontal', 'h', { key: '⇧H' }); add('v', 'Vertical', 'v', { key: '⇧V' });
|
|
256
261
|
add('par', 'Parallel', 'par', { key: '⇧P' }); add('col', 'Collinear', 'col', { key: '⇧L' }); add('eq', 'Equal', 'eq', { key: '⇧E' });
|
|
257
|
-
items.push('sep');
|
|
258
|
-
add('lock', 'Lock', 'lock'); guide(); wallItems();
|
|
259
262
|
return items;
|
|
260
263
|
}
|
|
261
|
-
if (L >= 2) {
|
|
262
|
-
add('lock', 'Lock', 'lock');
|
|
264
|
+
if (L >= 2) { kind(); add('eq', 'Equal', 'eqall'); }
|
|
263
265
|
return items;
|
|
264
266
|
}
|
|
265
267
|
|
|
@@ -296,7 +298,7 @@ function filletCorner2(): string | null {
|
|
|
296
298
|
}
|
|
297
299
|
|
|
298
300
|
/** Buttons for the bottom bar: they adapt to the tool and the selection (Lock first, when offered). */
|
|
299
|
-
export const barItems = (): CtxEntry[] =>
|
|
301
|
+
export const barItems = (): CtxEntry[] => arrangeLead(ctxItems());
|
|
300
302
|
|
|
301
303
|
/** The yellow + only appears for something it can measure: not for constraint badges or spaces alone. */
|
|
302
304
|
const DRAW_TOOLS = new Set(['line', 'freehand', 'rect', 'circle', 'arc']);
|
|
@@ -324,13 +326,13 @@ export function plusAction(): void {
|
|
|
324
326
|
if (ed.mode === 'annotate') return;
|
|
325
327
|
if (wallPlus()) return;
|
|
326
328
|
const s = selInfo(), sk = SK();
|
|
327
|
-
if (s.dims.length === 1 && s.n === 1)
|
|
329
|
+
if (s.dims.length === 1 && s.n === 1) editDim(s.dims[0]);
|
|
328
330
|
else if (s.odims.length === 1 && s.n === 1) { const [op, part] = s.odims[0].split('|'); editOpeningDim(op, part as OpeningDimPart); }
|
|
329
331
|
// a wall's edge: its length on that face; with another edge, a line or a point, the distance from it
|
|
330
332
|
// (a curved wall's: its diameter across that face)
|
|
331
|
-
else if (s.wfaces.length === 1 && s.n === 1) { const [id, side] = edgeOf(s.wfaces[0]); if (sk.lines[id]) dimLine(id,
|
|
333
|
+
else if (s.wfaces.length === 1 && s.n === 1) { const [id, side] = edgeOf(s.wfaces[0]); if (sk.lines[id]) dimLine(id, side); else dimCurve(id, side); }
|
|
332
334
|
else if (edgePair(s)) applyCtxAction('edgeDist');
|
|
333
|
-
else if (s.lines.length === 1 && s.n === 1) dimLine(s.lines[0]
|
|
335
|
+
else if (s.lines.length === 1 && s.n === 1) dimLine(s.lines[0]);
|
|
334
336
|
else if (s.curves.length === 1 && s.n === 1) dimCurve(s.curves[0]);
|
|
335
337
|
else if (s.openings.length === 1 && s.n === 1) editOpeningDim(s.openings[0], 'w');
|
|
336
338
|
// a corner where two lines meet: its angle
|
|
@@ -339,7 +341,7 @@ export function plusAction(): void {
|
|
|
339
341
|
else if (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1))) applyCtxAction('opDist');
|
|
340
342
|
// two things selected: the dimension between them
|
|
341
343
|
else if (s.n === 2 && s.lines.length === 2) dimTwoLines(s.lines[0], s.lines[1]);
|
|
342
|
-
else if (s.n === 2 && s.points.length === 2) dimPoints(s.points[0], s.points[1]
|
|
344
|
+
else if (s.n === 2 && s.points.length === 2) dimPoints(s.points[0], s.points[1]);
|
|
343
345
|
else if (s.n === 2 && s.points.length === 1 && s.lines.length === 1) dimPointLine(s.points[0], s.lines[0]);
|
|
344
346
|
else setTool('dim');
|
|
345
347
|
}
|
|
@@ -395,9 +397,13 @@ const NEEDS: Partial<Record<ConsType, string>> = {
|
|
|
395
397
|
online: 'a point and a line', oncircle: 'a point and a circle', tan: 'a line and a circle or arc', eqr: 'two circles', conc: 'two circles',
|
|
396
398
|
};
|
|
397
399
|
|
|
398
|
-
/**
|
|
400
|
+
/**
|
|
401
|
+
* H/V on each selected line, or parallel/collinear/equal from the first selected line to each of the others; three
|
|
402
|
+
* or more points, each level or plumb with the first picked.
|
|
403
|
+
*/
|
|
399
404
|
function multiLineCons(type: ConsType): Constraint[] | null {
|
|
400
405
|
const sk = SK(), s = selInfo();
|
|
406
|
+
if ((type === 'h' || type === 'v') && s.n === s.points.length && s.points.length >= 3) return s.points.slice(1).map(id => ({ type, a: s.points[0], b: id }));
|
|
401
407
|
if (s.n !== s.lines.length || s.lines.length < (type === 'h' || type === 'v' ? 2 : 3)) return null;
|
|
402
408
|
if (type === 'h' || type === 'v') return s.lines.map(id => ({ type, a: sk.lines[id].a, b: sk.lines[id].b }));
|
|
403
409
|
if (type === 'par' || type === 'col' || type === 'eq') return s.lines.slice(1).map(id => ({ type, l1: s.lines[0], l2: id }));
|
|
@@ -406,8 +412,11 @@ function multiLineCons(type: ConsType): Constraint[] | null {
|
|
|
406
412
|
|
|
407
413
|
const ADJ: Partial<Record<ConsType, string>> = { h: 'horizontal', v: 'vertical', par: 'parallel', col: 'collinear', eq: 'equal' };
|
|
408
414
|
|
|
409
|
-
/**
|
|
410
|
-
|
|
415
|
+
/**
|
|
416
|
+
* Apply one constraint type across several lines (or `points`, each tied to the first) as one undo step. A conflict
|
|
417
|
+
* opens the conflict sheet for the first one that fails.
|
|
418
|
+
*/
|
|
419
|
+
function applyMulti(type: ConsType, list: Constraint[], points = false): void {
|
|
411
420
|
pushHistory();
|
|
412
421
|
let added = 0;
|
|
413
422
|
const failed: Constraint[] = [];
|
|
@@ -418,9 +427,9 @@ function applyMulti(type: ConsType, list: Constraint[]): void {
|
|
|
418
427
|
}
|
|
419
428
|
if (!added && !failed.length) { dropHistory(); toast('They’re already ' + ADJ[type] + '.', { warn: true }); afterEdit(); return; }
|
|
420
429
|
if (!added) dropHistory();
|
|
421
|
-
// every selected line that now satisfies it (some may already have, through other constraints)
|
|
422
|
-
const lines = (type === 'h' || type === 'v' ? list.length : list.length + 1) - failed.length;
|
|
423
|
-
const nLines = lines + ' line' + (lines === 1 ? '' : 's');
|
|
430
|
+
// every selected line (or point) that now satisfies it (some may already have, through other constraints)
|
|
431
|
+
const lines = ((type === 'h' || type === 'v') && !points ? list.length : list.length + 1) - failed.length;
|
|
432
|
+
const nLines = lines + (points ? ' point' : ' line') + (lines === 1 ? '' : 's');
|
|
424
433
|
if (failed.length) {
|
|
425
434
|
// explain the first conflict; the sheet's Remove re-applies it
|
|
426
435
|
tryAddConstraint(failed[0], { onResolved: () => afterEdit() });
|
|
@@ -430,8 +439,8 @@ function applyMulti(type: ConsType, list: Constraint[]): void {
|
|
|
430
439
|
}
|
|
431
440
|
|
|
432
441
|
function applyCons(type: ConsType): void {
|
|
433
|
-
const multi = multiLineCons(type);
|
|
434
|
-
if (multi && (multi.length > 1 ||
|
|
442
|
+
const multi = multiLineCons(type), s = selInfo(), points = s.points.length > 2 && s.n === s.points.length;
|
|
443
|
+
if (multi && (multi.length > 1 || s.lines.length > 2 || points)) { applyMulti(type, multi, points); return; }
|
|
435
444
|
const c = consFromSel(type);
|
|
436
445
|
if (!c) { toast('Select ' + (NEEDS[type] || 'geometry') + ' first.', { warn: true }); return; }
|
|
437
446
|
pushHistory();
|
|
@@ -468,15 +477,13 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
468
477
|
// (the Line tool's, or the templator's: both close and finish a run of points)
|
|
469
478
|
case 'closeShape': (tools[ed.tool] as LineTool | SurveyTool).closeShape(); updateCtx(); return;
|
|
470
479
|
case 'finish': (tools[ed.tool] as LineTool | SurveyTool).finish(); updateCtx(); return;
|
|
471
|
-
case 'dimDrive': ed.dimLayer = 'drive'; break;
|
|
472
|
-
case 'dimPrint': ed.dimLayer = 'print'; break;
|
|
473
480
|
case 'h': case 'v': case 'par': case 'perp': case 'col': case 'eq': case 'mid': case 'online': case 'oncircle': case 'tan': case 'eqr': case 'conc':
|
|
474
481
|
applyCons(act); return;
|
|
475
482
|
case 'len':
|
|
476
|
-
if (s.lines.length === 1) dimLine(s.lines[0]
|
|
477
|
-
else if (s.wfaces.length === 1 && sk.lines[edgeOf(s.wfaces[0])[0]]) { const [lid, side] = edgeOf(s.wfaces[0]); dimLine(lid,
|
|
483
|
+
if (s.lines.length === 1) dimLine(s.lines[0]);
|
|
484
|
+
else if (s.wfaces.length === 1 && sk.lines[edgeOf(s.wfaces[0])[0]]) { const [lid, side] = edgeOf(s.wfaces[0]); dimLine(lid, side); }
|
|
478
485
|
return;
|
|
479
|
-
case 'pdist': if (s.points.length === 2) dimPoints(s.points[0], s.points[1]
|
|
486
|
+
case 'pdist': if (s.points.length === 2) dimPoints(s.points[0], s.points[1]); return;
|
|
480
487
|
case 'pldist': if (s.points.length === 1 && s.lines.length === 1) dimPointLine(s.points[0], s.lines[0]); return;
|
|
481
488
|
case 'diam': if (s.curves.length === 1) dimCurve(s.curves[0]); return;
|
|
482
489
|
case 'fillet': case 'chamfer': {
|
|
@@ -486,9 +493,13 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
486
493
|
}
|
|
487
494
|
case 'cornerAngle': cornerAngle(s.points[0]); return;
|
|
488
495
|
case 'merge': {
|
|
496
|
+
// (every one into the first picked)
|
|
497
|
+
if (s.points.length < 2) return;
|
|
489
498
|
pushHistory();
|
|
490
499
|
const snap = JSON.stringify(sk);
|
|
491
|
-
if (joinPoints(sk, s.points[0],
|
|
500
|
+
if (!s.points.slice(1).every(id => joinPoints(sk, s.points[0], id))) {
|
|
501
|
+
replaceSK(JSON.parse(snap) as Sketch); dropHistory(); toast('Two locked points can’t be joined. Unlock one first.', { warn: true });
|
|
502
|
+
} else {
|
|
492
503
|
const r = solveSketch();
|
|
493
504
|
if (!r.ok) { replaceSK(JSON.parse(snap) as Sketch); dropHistory(); toast('Can’t join those points — it conflicts with other constraints.', { warn: true }); }
|
|
494
505
|
else { ed.sel = [{ type: 'point', id: s.points[0] }]; toast('Points joined'); }
|
|
@@ -564,7 +575,8 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
564
575
|
case 'toggleref': toggleRef(s.dims[0]); return;
|
|
565
576
|
case 'odimRef': { const [op, part] = s.odims[0].split('|'); toggleOpeningDim(op, part.slice(2)); return; }
|
|
566
577
|
case 'renameRoom': editRoomName(s.rooms[0]); return;
|
|
567
|
-
|
|
578
|
+
// (the room is on the left of the wall's start → end when there's none either side of it)
|
|
579
|
+
case 'opFlip': { pushHistory(); const o = sk.openings[s.openings[0]]; flipDoor(o, roomSideOf(sk, o.line) === -1 ? -1 : 1); afterEdit(); return; }
|
|
568
580
|
case 'opDist': {
|
|
569
581
|
const ops = ed.sel.filter(x => x.type === 'opening').map(x => x.id);
|
|
570
582
|
if (ops.length === 2) {
|
|
@@ -601,8 +613,8 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
601
613
|
return;
|
|
602
614
|
}
|
|
603
615
|
case 'palette': openPalette(anchor); return;
|
|
604
|
-
case 'convert': convertNote(s.notes[0]); return;
|
|
605
616
|
case 'linkOpen': openSelectedLink(); return;
|
|
617
|
+
case 'linkExpand': toggleExpand(); return;
|
|
606
618
|
case 'linkSize': toggleTileSize(anchor); return;
|
|
607
619
|
case 'dimLinkOpen': if (s.dims.length === 1) openDimLink(s.dims[0]); return;
|
|
608
620
|
case 'dimUnlink': if (s.dims.length === 1) unlinkDim(s.dims[0]); return;
|
|
@@ -619,6 +631,8 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
619
631
|
const lock = !ims.every(u => u.locked), many = ims.length > 1;
|
|
620
632
|
pushHistory();
|
|
621
633
|
ims.forEach(u => { u.locked = lock; });
|
|
634
|
+
// (locked, its bar is Lock and Open alone: an opacity or turning slider open over it goes with the rest)
|
|
635
|
+
if (lock) dismissPopovers();
|
|
622
636
|
afterEdit();
|
|
623
637
|
toast(lock ? (many ? 'Images locked. They won’t move while you trace over them.' : 'Image locked. It won’t move while you trace over it.')
|
|
624
638
|
: many ? 'Images unlocked.' : 'Image unlocked. Drag it, or drag its handles to size it.');
|