@reekon-tools/boldr-cad 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/app/edit.d.ts +3 -3
- package/dist/app/edit.js +26 -1
- package/dist/app/edit.js.map +1 -1
- package/dist/app/state.d.ts +6 -8
- package/dist/app/state.js +0 -1
- package/dist/app/state.js.map +1 -1
- package/dist/data/sync.d.ts +1 -1
- package/dist/data/sync.js +7 -2
- package/dist/data/sync.js.map +1 -1
- package/dist/editor/actions.d.ts +8 -0
- package/dist/editor/actions.js +255 -181
- package/dist/editor/actions.js.map +1 -1
- package/dist/editor/elevation.d.ts +4 -1
- package/dist/editor/elevation.js +16 -17
- package/dist/editor/elevation.js.map +1 -1
- package/dist/editor/gestures.d.ts +21 -0
- package/dist/editor/gestures.js +92 -14
- package/dist/editor/gestures.js.map +1 -1
- package/dist/editor/keys.js +4 -3
- package/dist/editor/keys.js.map +1 -1
- package/dist/editor/laser.d.ts +4 -2
- package/dist/editor/laser.js +4 -2
- package/dist/editor/laser.js.map +1 -1
- package/dist/editor/linkActions.d.ts +49 -12
- package/dist/editor/linkActions.js +164 -35
- package/dist/editor/linkActions.js.map +1 -1
- package/dist/editor/links.d.ts +13 -0
- package/dist/editor/links.js +7 -1
- package/dist/editor/links.js.map +1 -1
- package/dist/editor/mode.d.ts +2 -1
- package/dist/editor/mode.js +8 -5
- package/dist/editor/mode.js.map +1 -1
- package/dist/editor/project.js +1 -2
- package/dist/editor/project.js.map +1 -1
- package/dist/editor/scaleGate.d.ts +13 -0
- package/dist/editor/scaleGate.js +27 -0
- package/dist/editor/scaleGate.js.map +1 -0
- package/dist/editor/selection.js +13 -9
- package/dist/editor/selection.js.map +1 -1
- package/dist/editor/tooltips.js +10 -0
- package/dist/editor/tooltips.js.map +1 -1
- package/dist/editor/view3d.d.ts +4 -1
- package/dist/editor/view3d.js +6 -2
- package/dist/editor/view3d.js.map +1 -1
- package/dist/export/pdf.d.ts +5 -2
- package/dist/export/pdf.js +12 -8
- package/dist/export/pdf.js.map +1 -1
- package/dist/export/plan.d.ts +1 -1
- package/dist/export/plan.js +14 -9
- package/dist/export/plan.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model/constraints.js +2 -0
- package/dist/model/constraints.js.map +1 -1
- package/dist/model/demo.d.ts +1 -0
- package/dist/model/demo.js +2 -1
- package/dist/model/demo.js.map +1 -1
- package/dist/model/elevation.d.ts +1 -1
- package/dist/model/elevation.js +3 -3
- package/dist/model/elevation.js.map +1 -1
- package/dist/model/geom.d.ts +2 -0
- package/dist/model/geom.js +11 -0
- package/dist/model/geom.js.map +1 -1
- package/dist/model/history.js +4 -1
- package/dist/model/history.js.map +1 -1
- package/dist/model/links.d.ts +10 -2
- package/dist/model/links.js +15 -3
- package/dist/model/links.js.map +1 -1
- package/dist/model/measurement.d.ts +2 -2
- package/dist/model/measurement.js +3 -3
- package/dist/model/measurement.js.map +1 -1
- package/dist/model/openings.d.ts +10 -0
- package/dist/model/openings.js +18 -0
- package/dist/model/openings.js.map +1 -1
- package/dist/model/planGeom.d.ts +37 -2
- package/dist/model/planGeom.js +72 -9
- package/dist/model/planGeom.js.map +1 -1
- package/dist/model/project.js +5 -2
- package/dist/model/project.js.map +1 -1
- package/dist/model/rooms.d.ts +18 -5
- package/dist/model/rooms.js +178 -73
- package/dist/model/rooms.js.map +1 -1
- package/dist/model/scale.d.ts +30 -0
- package/dist/model/scale.js +123 -0
- package/dist/model/scale.js.map +1 -0
- package/dist/model/settings.d.ts +0 -1
- package/dist/model/settings.js +1 -1
- package/dist/model/settings.js.map +1 -1
- package/dist/model/sketch.d.ts +26 -2
- package/dist/model/sketch.js +77 -22
- package/dist/model/sketch.js.map +1 -1
- package/dist/model/stroke.js +2 -2
- package/dist/model/trim.d.ts +54 -0
- package/dist/model/trim.js +219 -0
- package/dist/model/trim.js.map +1 -0
- package/dist/model/types.d.ts +62 -9
- package/dist/react/BottomBar.d.ts +2 -1
- package/dist/react/BottomBar.js +144 -46
- package/dist/react/BottomBar.js.map +1 -1
- package/dist/react/CadEditor.js +18 -9
- package/dist/react/CadEditor.js.map +1 -1
- package/dist/react/Drawer.js +6 -4
- package/dist/react/Drawer.js.map +1 -1
- package/dist/react/Duck.js.map +1 -1
- package/dist/react/KeypadSheet.js.map +1 -1
- package/dist/react/MarkupPanel.js +1 -1
- package/dist/react/PlanCanvas.native.js +54 -7
- package/dist/react/PlanCanvas.native.js.map +1 -1
- package/dist/react/PlanCanvas.web.js +17 -5
- package/dist/react/PlanCanvas.web.js.map +1 -1
- package/dist/react/Popovers.js +14 -3
- package/dist/react/Popovers.js.map +1 -1
- package/dist/react/Rail.js +1 -1
- package/dist/react/ShapeMenu.js +3 -2
- package/dist/react/ShapeMenu.js.map +1 -1
- package/dist/react/Sheet.d.ts +2 -2
- package/dist/react/Sheet.js.map +1 -1
- package/dist/react/ToolTip.js +3 -2
- package/dist/react/ToolTip.js.map +1 -1
- package/dist/react/TopBar.js +4 -3
- package/dist/react/TopBar.js.map +1 -1
- package/dist/react/barScroll.d.ts +23 -0
- package/dist/react/barScroll.js +47 -0
- package/dist/react/barScroll.js.map +1 -0
- package/dist/react/corners.d.ts +6 -2
- package/dist/react/corners.js.map +1 -1
- package/dist/react/editorBox.d.ts +8 -0
- package/dist/react/editorBox.js +10 -0
- package/dist/react/editorBox.js.map +1 -0
- package/dist/react/host.d.ts +1 -1
- package/dist/react/host.js +1 -1
- package/dist/react/host.js.map +1 -1
- package/dist/react/motion.d.ts +1 -1
- package/dist/react/motion.js +1 -1
- package/dist/react/motion.js.map +1 -1
- package/dist/react/pageLock.native.d.ts +2 -0
- package/dist/react/pageLock.native.js +2 -0
- package/dist/react/pageLock.native.js.map +1 -1
- package/dist/react/pageLock.web.d.ts +6 -0
- package/dist/react/pageLock.web.js +12 -3
- package/dist/react/pageLock.web.js.map +1 -1
- package/dist/react/ports.js +8 -3
- package/dist/react/ports.js.map +1 -1
- package/dist/react/saving.d.ts +7 -1
- package/dist/react/saving.js +10 -2
- package/dist/react/saving.js.map +1 -1
- package/dist/react/store.d.ts +5 -2
- package/dist/react/store.js +4 -1
- package/dist/react/store.js.map +1 -1
- package/dist/render/draw.d.ts +21 -4
- package/dist/render/draw.js +203 -151
- package/dist/render/draw.js.map +1 -1
- package/dist/render/elevation.js +3 -3
- package/dist/render/elevation.js.map +1 -1
- package/dist/render/figmaIcons.d.ts +6 -0
- package/dist/render/figmaIcons.js +16 -1
- package/dist/render/figmaIcons.js.map +1 -1
- package/dist/render/hit.d.ts +8 -1
- package/dist/render/hit.js +23 -4
- package/dist/render/hit.js.map +1 -1
- package/dist/render/icons.js +27 -20
- package/dist/render/icons.js.map +1 -1
- package/dist/render/lens.d.ts +27 -0
- package/dist/render/lens.js +41 -0
- package/dist/render/lens.js.map +1 -0
- package/dist/render/linkTile.d.ts +20 -2
- package/dist/render/linkTile.js +476 -246
- package/dist/render/linkTile.js.map +1 -1
- package/dist/render/snap.d.ts +10 -1
- package/dist/render/snap.js +87 -15
- package/dist/render/snap.js.map +1 -1
- package/dist/render/svgPath.d.ts +29 -0
- package/dist/render/svgPath.js +114 -0
- package/dist/render/svgPath.js.map +1 -0
- package/dist/render/tileIcons.d.ts +18 -0
- package/dist/render/tileIcons.js +67 -0
- package/dist/render/tileIcons.js.map +1 -0
- package/dist/render/view.d.ts +18 -1
- package/dist/render/view.js +38 -6
- package/dist/render/view.js.map +1 -1
- package/dist/skia/SkiaDrawCtx.d.ts +8 -0
- package/dist/skia/SkiaDrawCtx.js +27 -5
- package/dist/skia/SkiaDrawCtx.js.map +1 -1
- package/dist/solver/equations.d.ts +6 -0
- package/dist/solver/equations.js +53 -22
- package/dist/solver/equations.js.map +1 -1
- package/dist/solver/gauss-newton.js +6 -1
- package/dist/solver/gauss-newton.js.map +1 -1
- package/dist/tools/annotate.d.ts +0 -2
- package/dist/tools/annotate.js +1 -22
- package/dist/tools/annotate.js.map +1 -1
- package/dist/tools/chamfer.js +4 -0
- package/dist/tools/chamfer.js.map +1 -1
- package/dist/tools/dim.d.ts +3 -3
- package/dist/tools/dim.js +26 -65
- package/dist/tools/dim.js.map +1 -1
- package/dist/tools/fillet.js +4 -0
- package/dist/tools/fillet.js.map +1 -1
- package/dist/tools/freehand.js +1 -4
- package/dist/tools/freehand.js.map +1 -1
- package/dist/tools/line.js +15 -11
- package/dist/tools/line.js.map +1 -1
- package/dist/tools/modify.d.ts +32 -4
- package/dist/tools/modify.js +253 -98
- package/dist/tools/modify.js.map +1 -1
- package/dist/tools/registry.d.ts +7 -0
- package/dist/tools/registry.js +14 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/select.d.ts +30 -8
- package/dist/tools/select.js +165 -34
- package/dist/tools/select.js.map +1 -1
- package/dist/tools/shapes.d.ts +1 -1
- package/dist/tools/shapes.js +12 -5
- package/dist/tools/shapes.js.map +1 -1
- package/dist/view3d/build.js +9 -8
- package/dist/view3d/build.js.map +1 -1
- package/package.json +9 -9
- package/src/app/edit.ts +25 -1
- package/src/app/state.ts +6 -8
- package/src/data/sync.ts +7 -2
- package/src/editor/actions.ts +213 -122
- package/src/editor/elevation.ts +13 -16
- package/src/editor/gestures.ts +83 -13
- package/src/editor/keys.ts +3 -2
- package/src/editor/laser.ts +7 -4
- package/src/editor/linkActions.ts +169 -31
- package/src/editor/links.ts +17 -1
- package/src/editor/mode.ts +8 -5
- package/src/editor/project.ts +1 -2
- package/src/editor/scaleGate.ts +32 -0
- package/src/editor/selection.ts +13 -7
- package/src/editor/tooltips.ts +9 -0
- package/src/editor/view3d.ts +6 -2
- package/src/export/pdf.ts +7 -4
- package/src/export/plan.ts +14 -10
- package/src/index.ts +1 -0
- package/src/model/constraints.ts +1 -0
- package/src/model/demo.ts +2 -1
- package/src/model/elevation.ts +4 -4
- package/src/model/geom.ts +10 -0
- package/src/model/history.ts +4 -1
- package/src/model/links.ts +14 -3
- package/src/model/measurement.ts +3 -3
- package/src/model/openings.ts +18 -0
- package/src/model/planGeom.ts +70 -10
- package/src/model/project.ts +6 -2
- package/src/model/rooms.ts +151 -63
- package/src/model/scale.ts +107 -0
- package/src/model/settings.ts +1 -2
- package/src/model/sketch.ts +62 -11
- package/src/model/stroke.ts +2 -2
- package/src/model/trim.ts +188 -0
- package/src/model/types.ts +57 -9
- package/src/react/BottomBar.tsx +177 -52
- package/src/react/CadEditor.tsx +19 -9
- package/src/react/Drawer.tsx +17 -12
- package/src/react/Duck.tsx +3 -3
- package/src/react/KeypadSheet.tsx +2 -2
- package/src/react/MarkupPanel.tsx +1 -1
- package/src/react/PlanCanvas.native.tsx +60 -6
- package/src/react/PlanCanvas.web.tsx +17 -5
- package/src/react/Popovers.tsx +24 -9
- package/src/react/Rail.tsx +1 -1
- package/src/react/ShapeMenu.tsx +3 -2
- package/src/react/Sheet.tsx +3 -3
- package/src/react/ToolTip.tsx +3 -2
- package/src/react/TopBar.tsx +4 -3
- package/src/react/barScroll.ts +57 -0
- package/src/react/corners.ts +6 -3
- package/src/react/editorBox.ts +20 -0
- package/src/react/host.ts +1 -1
- package/src/react/motion.tsx +6 -6
- package/src/react/pageLock.native.ts +2 -0
- package/src/react/pageLock.web.ts +12 -3
- package/src/react/ports.ts +7 -2
- package/src/react/saving.ts +10 -2
- package/src/react/store.ts +5 -2
- package/src/render/draw.ts +169 -118
- package/src/render/elevation.ts +5 -4
- package/src/render/figmaIcons.ts +17 -1
- package/src/render/hit.ts +21 -4
- package/src/render/icons.ts +27 -20
- package/src/render/lens.ts +52 -0
- package/src/render/linkTile.ts +436 -190
- package/src/render/snap.ts +87 -17
- package/src/render/svgPath.ts +95 -0
- package/src/render/tileIcons.ts +75 -0
- package/src/render/view.ts +43 -7
- package/src/skia/SkiaDrawCtx.ts +26 -4
- package/src/solver/equations.ts +52 -22
- package/src/solver/gauss-newton.ts +4 -1
- package/src/styles.css +3 -2
- package/src/tools/annotate.ts +1 -18
- package/src/tools/chamfer.ts +3 -0
- package/src/tools/dim.ts +26 -52
- package/src/tools/fillet.ts +3 -0
- package/src/tools/freehand.ts +1 -3
- package/src/tools/line.ts +12 -11
- package/src/tools/modify.ts +210 -70
- package/src/tools/registry.ts +18 -1
- package/src/tools/select.ts +144 -36
- package/src/tools/shapes.ts +9 -4
- package/src/ui/ctx.ts +4 -3
- package/src/ui/drawer.ts +8 -2
- package/src/view3d/build.ts +11 -10
- package/dist/react/LaserTray.d.ts +0 -4
- package/dist/react/LaserTray.js +0 -26
- package/dist/react/LaserTray.js.map +0 -1
- package/src/react/LaserTray.tsx +0 -42
package/src/editor/actions.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { afterEdit, holdFaces, solveSketch, tryAddConstraint, undo } from '../app/edit';
|
|
2
2
|
import { ed, type OpeningDimPart } from '../app/state';
|
|
3
|
-
import { CONS_LABEL, measure } from '../model/constraints';
|
|
3
|
+
import { CONS_LABEL, consLabel, 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,32 +34,43 @@ 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[] {
|
|
41
48
|
const sk = SK(), s = selInfo(), ids = new Set(s.points);
|
|
42
|
-
|
|
49
|
+
// (a straight wall's edge: its wall, as if picked whole)
|
|
50
|
+
withEdges(sk, s).forEach(id => { ids.add(sk.lines[id].a); ids.add(sk.lines[id].b); });
|
|
43
51
|
s.curves.forEach(id => { const ci = curveInfo(sk, id); if (ci) ids.add(ci.o.c); });
|
|
44
52
|
return [...ids].filter(id => sk.points[id] && !sk.points[id].origin && !sk.points[id].anchor);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
56
|
+
* The bar leads with one button, its icon alone, set off by a divider (Figma's bars, 2528:101128: the user's call,
|
|
57
|
+
* 2026-10-27): Open, when what's selected is a link to something of the job's; otherwise Lock — an open padlock (white)
|
|
58
|
+
* when free, a closed one (red, "Locked") when locked. A link that can be locked too (an expanded photo) has Lock next,
|
|
59
|
+
* 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
|
|
60
|
+
* display the icon - no text needed below it").
|
|
50
61
|
*/
|
|
51
|
-
function
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const out: CtxEntry[] = [
|
|
60
|
-
if (
|
|
61
|
-
//
|
|
62
|
-
for (const e of
|
|
62
|
+
function arrangeLead(items: CtxEntry[]): CtxEntry[] {
|
|
63
|
+
const find = (acts: string[]) => items.find((it): it is CtxItem => typeof it === 'object' && 'act' in it && acts.includes(it.act));
|
|
64
|
+
const open = find(['linkOpen', 'dimLinkOpen']), lock = find(['lock', 'imgLock']);
|
|
65
|
+
if (lock) {
|
|
66
|
+
const sk = SK();
|
|
67
|
+
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); })();
|
|
68
|
+
Object.assign(lock, { ic: locked ? 'lock' : 'unlock', label: locked ? 'Locked' : 'Lock', on: undefined, locked, bare: true });
|
|
69
|
+
}
|
|
70
|
+
const lead = open ?? lock, out: CtxEntry[] = [];
|
|
71
|
+
if (lead) { lead.lead = true; out.push(lead, 'sep'); if (open && lock) out.push(lock, 'sep'); }
|
|
72
|
+
// the rest, with no divider doubled, first or last
|
|
73
|
+
for (const e of items) if (e !== lead && !(open && e === lock) && !(e === 'sep' && (!out.length || out[out.length - 1] === 'sep'))) out.push(e);
|
|
63
74
|
while (out[out.length - 1] === 'sep') out.pop();
|
|
64
75
|
return out;
|
|
65
76
|
}
|
|
@@ -79,24 +90,22 @@ const PLACING = new Set(['door', 'window', 'wdoor', 'wwindow', 'cabinet']);
|
|
|
79
90
|
function ctxItems(): CtxEntry[] {
|
|
80
91
|
const items: CtxEntry[] = [], sk = SK(), s = selInfo();
|
|
81
92
|
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
93
|
if (ed.mode === 'annotate') {
|
|
91
94
|
const ims = selImages();
|
|
92
95
|
if (ims.length && ims.length === s.n) {
|
|
93
96
|
// an image (or several): lock, its own scale, and which goes in front where they overlap
|
|
94
97
|
// (showing and hiding them all is the Layers menu's)
|
|
95
98
|
add('lock', 'Lock', 'imgLock');
|
|
96
|
-
|
|
99
|
+
// a photo of the job's, expanded from its tile: opening it, and Expand (on) to fold it back into its tile
|
|
100
|
+
const lk = ims.length === 1 ? ims[0].link : undefined;
|
|
101
|
+
if (lk && canOpenLink(lk)) add('open', 'Open', 'linkOpen');
|
|
102
|
+
// locked: unlocking it, and opening what it's a photo of — nothing that changes it (the user's call,
|
|
103
|
+
// 2026-10-30: "images in locked state: shouldn't have these options - only lock and link should be available")
|
|
104
|
+
if (ims.every(u => u.locked)) return items;
|
|
105
|
+
if (lk) add('expand', 'Expand', 'linkExpand', { on: true });
|
|
106
|
+
if (ims.length === 1) add('calib', 'Set scale', 'calibtool');
|
|
97
107
|
if (ims.length === 1) add('opacity', 'Opacity', 'imgOpacity');
|
|
98
|
-
|
|
99
|
-
if (ims.length === 1 && !ims[0].locked) add('rotate', 'Rotate', 'imgRotate');
|
|
108
|
+
if (ims.length === 1) add('rotate', 'Rotate', 'imgRotate');
|
|
100
109
|
if (ims.length === 1 && page().underlays.length > 1) {
|
|
101
110
|
const at = page().underlays.indexOf(ims[0]), last = page().underlays.length - 1;
|
|
102
111
|
if (at < last) add('tofront', 'To front', 'imgFront');
|
|
@@ -108,18 +117,17 @@ function ctxItems(): CtxEntry[] {
|
|
|
108
117
|
if (lk) {
|
|
109
118
|
// 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
119
|
if (canOpenLink(lk.link)) add('open', 'Open', 'linkOpen');
|
|
120
|
+
// (a photo's: the photo whole, as one of the page's images)
|
|
121
|
+
if (canExpand(lk)) add('expand', 'Expand', 'linkExpand', { on: false });
|
|
111
122
|
add('leader', 'Leader', 'linkLeader', { on: !!lk.leader });
|
|
112
123
|
// (its leader's colour, as all markup's, is the rail's colour dot's)
|
|
113
124
|
// (every tile on the page: a slider above the bar)
|
|
114
125
|
add('tilesize', 'Size', 'linkSize');
|
|
115
126
|
return items;
|
|
116
127
|
}
|
|
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
|
-
}
|
|
128
|
+
// markup: nothing of its own — a pen stroke as any other (no To lines: the user's call, 2026-10-27), and no Color
|
|
129
|
+
// (the rail's colour dot styles what's selected); with tiles among it, their size
|
|
130
|
+
if (s.notes.length && linkSizeVisible()) add('tilesize', 'Size', 'linkSize');
|
|
123
131
|
// nothing selected: no bar (the markup colour is the rail's colour dot, for what's selected too; the image's own
|
|
124
132
|
// options come with selecting it)
|
|
125
133
|
return items;
|
|
@@ -138,7 +146,6 @@ function ctxItems(): CtxEntry[] {
|
|
|
138
146
|
if (n >= 1) add('finish', 'Finish', 'finish');
|
|
139
147
|
return items;
|
|
140
148
|
}
|
|
141
|
-
if (ed.tool === 'dim') { add('dim', 'Driving', 'dimDrive', { on: ed.dimLayer === 'drive' }); add('ref', 'Print', 'dimPrint', { on: ed.dimLayer === 'print' }); return items; }
|
|
142
149
|
// nothing selected: the bar stays empty (Fit and Icons live in the top bar; grid snap and auto-constrain in Settings).
|
|
143
150
|
// Just placed a door, window or cabinet (its tool still on): its bar, for its sizes
|
|
144
151
|
if (!s.n || (ed.tool !== 'select' && !PLACING.has(ed.tool))) return items;
|
|
@@ -149,39 +156,70 @@ function ctxItems(): CtxEntry[] {
|
|
|
149
156
|
for (const it of wb.items) add(it.ic, it.label, it.act);
|
|
150
157
|
return items;
|
|
151
158
|
}
|
|
159
|
+
/*
|
|
160
|
+
Figma's bars (2528:101128, the user's call, 2026-10-27): Lock first, its icon alone (arrangeLead); then what the lines
|
|
161
|
+
and curves are (kind); then what ties them, or edits them. The yellow + measures: no Distance of its own here.
|
|
162
|
+
*/
|
|
152
163
|
// on a wall face, walls don't exist: no To wall / Thickness
|
|
153
164
|
const wall = elevOpen();
|
|
154
165
|
const P = s.points.length, L = s.lines.length, C = s.curves.length;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
/**
|
|
167
|
+
* What the selected lines and curves are, and its divider: walls — one straight wall's wall view, and their Thickness;
|
|
168
|
+
* otherwise To wall, and Guide (Solid, when they all are guides). A wall is always solid, so with one among them
|
|
169
|
+
* there's no Guide (the user's call, 2026-11-03: a wall and a line picked together offered it) — only Solid, for a
|
|
170
|
+
* wall that's a guide from before; on a wall face, Guide alone. `walls`: the walls of picked edges, as if picked whole.
|
|
171
|
+
*/
|
|
172
|
+
const kind = (walls: string[] = []) => {
|
|
173
|
+
const objs = [...s.lines.map(id => sk.lines[id]), ...walls.map(id => sk.lines[id]), ...s.curves.map(id => curveInfo(sk, id)?.o)].filter((o): o is NonNullable<typeof o> => !!o);
|
|
174
|
+
if (!objs.length) return;
|
|
175
|
+
if (objs.every(o => o.kind === 'wall')) {
|
|
176
|
+
if (wall) return;
|
|
177
|
+
if (s.n === 1 && L === 1) add('elev', 'Elevation', 'elev');
|
|
178
|
+
add('thick', 'Thickness', 'thick');
|
|
179
|
+
if (objs.some(o => o.constr)) add('constr', 'Solid', 'constr');
|
|
180
|
+
} else {
|
|
181
|
+
if (!wall) add('wall', 'To wall', 'kind');
|
|
182
|
+
if (!objs.some(o => o.kind === 'wall')) add('constr', objs.every(o => o.constr) ? 'Solid' : 'Guide', 'constr');
|
|
183
|
+
}
|
|
184
|
+
items.push('sep');
|
|
185
|
+
};
|
|
186
|
+
// a wall's edge: named, with its wall view (with another edge, a line or a point, the + is the distance between them)
|
|
160
187
|
if (s.wfaces.length) {
|
|
161
188
|
// (a curved wall has no wall view: just its name)
|
|
162
189
|
if (s.n === 1) {
|
|
163
190
|
const [id, side] = edgeOf(s.wfaces[0]);
|
|
164
191
|
items.push({ lbl: edgeLabel(sk, id, side) });
|
|
165
192
|
if (sk.lines[id]) add('elev', 'Elevation', 'elev');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
193
|
+
} else if (s.n === 2 && straightEdges(sk, s).length === s.wfaces.length) {
|
|
194
|
+
/*
|
|
195
|
+
A straight wall's edge with a point or a line: the bar the wall picked whole has (the user's call, 2026-11-03:
|
|
196
|
+
"add equal, lock, and to wall to both bottom bars"), each acting on what's picked — the point On face; Parallel
|
|
197
|
+
and Perpendicular, the wall (a face runs with its centreline); Collinear, the line along the face (both its ends
|
|
198
|
+
on it); Equal, the face's length, corner to corner. (Two edges: no Collinear — their faces flush is another thing.)
|
|
199
|
+
*/
|
|
200
|
+
// (the two edges of one wall: nothing but the + — its thickness)
|
|
201
|
+
const pair = new Set(withEdges(sk, s)).size === 2;
|
|
202
|
+
if (P === 1 || pair) add('lock', 'Lock', 'lock', { key: '⇧F' });
|
|
203
|
+
if (P === 1) add('online', 'On face', 'online', { key: '⇧O' });
|
|
204
|
+
else if (pair) {
|
|
205
|
+
kind(straightEdges(sk, s).map(([id]) => id));
|
|
206
|
+
add('par', 'Parallel', 'par', { key: '⇧P' }); add('perp', 'Perpendicular', 'perp', { key: '⇧R' });
|
|
207
|
+
if (L === 1) add('col', 'Collinear', 'col', { key: '⇧L' });
|
|
208
|
+
add('eq', 'Equal', 'eq', { key: '⇧E' });
|
|
209
|
+
}
|
|
170
210
|
}
|
|
171
211
|
return items;
|
|
172
212
|
}
|
|
173
213
|
if (s.n === 1 && s.dims.length) {
|
|
174
214
|
const c = sk.cons[s.dims[0]];
|
|
175
215
|
// 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)
|
|
216
|
+
// call, 2026-10-21; its chip is drawn linked)
|
|
177
217
|
if (c.measurementRef) {
|
|
178
218
|
const t = c.measurementRef;
|
|
179
219
|
if (canOpenLink(t)) add('open', 'Open', 'dimLinkOpen');
|
|
180
220
|
add('unlink', 'Unlink', 'dimUnlink');
|
|
181
221
|
} 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');
|
|
222
|
+
// (no Reference: Figma's dimension bars have none, 2026-10-27)
|
|
185
223
|
// measured on walls: which of their faces (the drawing stays as it is; the number becomes what it measures there)
|
|
186
224
|
const fc = faceChoices(sk, c);
|
|
187
225
|
if (fc.length && items.length) items.push('sep');
|
|
@@ -196,70 +234,55 @@ function ctxItems(): CtxEntry[] {
|
|
|
196
234
|
// a constraint badge: nothing to offer but delete (the trash), so the bar stays empty
|
|
197
235
|
if (s.n === 1 && s.cons.length) return items;
|
|
198
236
|
if (s.n === 1 && s.rooms.length) { add('rename', 'Rename', 'renameRoom'); return items; }
|
|
199
|
-
// one end of a door or window: named
|
|
237
|
+
// one end of a door or window: named (with another point, end or door/window, the + is the distance between them)
|
|
200
238
|
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; }
|
|
239
|
+
if (s.oanchors.length || (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1)))) return items;
|
|
204
240
|
if (s.n === 1 && s.openings.length) {
|
|
205
|
-
// its sizes
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
items.push({ lbl: openingLabel(o) });
|
|
241
|
+
// its sizes (no +: it isn't one dimension), with nothing saying them — the drawing does (the user's call, 2026-10-27);
|
|
242
|
+
// a door's Swing too, round its four ways (it was Flip: the user's call, 2026-10-27, "ok do swing instead"). (The
|
|
243
|
+
// type is fixed once placed: Door and Window are separate tools.)
|
|
209
244
|
for (const it of OPENING_SIZES) add(it.ic, it.label, it.act);
|
|
210
|
-
if (
|
|
245
|
+
if (sk.openings[s.openings[0]].type === 'door') { items.push('sep'); add('flip', 'Swing', 'opFlip'); }
|
|
211
246
|
return items;
|
|
212
247
|
}
|
|
248
|
+
// a cabinet's corner: nothing of its own (pick another point or line with it to dimension or constrain)
|
|
249
|
+
if (P === 1 && s.n === 1 && cabOfPoint(sk, s.points[0])) return items;
|
|
250
|
+
add('lock', 'Lock', 'lock', { key: '⇧F' });
|
|
213
251
|
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
|
-
}
|
|
252
|
+
kind();
|
|
253
|
+
// (its length: the yellow +)
|
|
254
|
+
add('h', 'Horizontal', 'h', { key: '⇧H' }); add('v', 'Vertical', 'v', { key: '⇧V' }); add('split', 'Split', 'split'); add('offset', 'Offset', 'offset');
|
|
222
255
|
return items;
|
|
223
256
|
}
|
|
224
257
|
if (L === 2 && s.n === 2) {
|
|
225
|
-
// (no Angle
|
|
258
|
+
// (no Angle: the yellow + is the angle between them — or, parallel, the distance)
|
|
259
|
+
kind();
|
|
226
260
|
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
261
|
// 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();
|
|
262
|
+
if (filletCorner2()) { items.push('sep'); add('fillet', 'Fillet', 'fillet'); add('chamfer', 'Chamfer', 'chamfer'); }
|
|
230
263
|
return items;
|
|
231
264
|
}
|
|
232
|
-
// a
|
|
233
|
-
if (P === 1 && s.n === 1 && cabOfPoint(sk, s.points[0])) return items;
|
|
265
|
+
// a corner where exactly two lines meet: rounding it off or cutting it off (its angle is the yellow +)
|
|
234
266
|
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
267
|
if (filletable(sk, s.points[0])) { add('fillet', 'Fillet', 'fillet'); add('chamfer', 'Chamfer', 'chamfer'); }
|
|
237
|
-
add('lock', 'Lock', 'lock', { key: '⇧F' });
|
|
238
268
|
return items;
|
|
239
269
|
}
|
|
240
|
-
|
|
241
|
-
if (P
|
|
270
|
+
// points: joined into the first picked, or level or plumb with it
|
|
271
|
+
if (P >= 2 && s.n === P) { add('merge', 'Join', 'merge'); add('h', 'Horizontal', 'h'); add('v', 'Vertical', 'v'); return items; }
|
|
272
|
+
if (P === 1 && L === 1 && s.n === 2) { add('online', 'On line', 'online', { key: '⇧O' }); add('mid', 'Midpoint', 'mid', { key: '⇧M' }); return items; }
|
|
242
273
|
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' });
|
|
274
|
+
if (L === 1 && C === 1 && s.n === 2) { kind(); add('tan', 'Tangent', 'tan', { key: '⇧T' }); return items; }
|
|
244
275
|
// 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; }
|
|
276
|
+
if (C === 1 && s.n === 1) { kind(); return items; }
|
|
277
|
+
if (C === 2 && s.n === 2) { kind(); add('conc', 'Concentric', 'conc'); add('eq', 'Equal', 'eqr'); return items; }
|
|
253
278
|
// 3+ lines (only lines): the constraints that make sense across many, each tied to the first line picked
|
|
254
279
|
if (L >= 3 && s.n === L) {
|
|
280
|
+
kind();
|
|
255
281
|
add('h', 'Horizontal', 'h', { key: '⇧H' }); add('v', 'Vertical', 'v', { key: '⇧V' });
|
|
256
282
|
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
283
|
return items;
|
|
260
284
|
}
|
|
261
|
-
if (L >= 2) {
|
|
262
|
-
add('lock', 'Lock', 'lock');
|
|
285
|
+
if (L >= 2) { kind(); add('eq', 'Equal', 'eqall'); }
|
|
263
286
|
return items;
|
|
264
287
|
}
|
|
265
288
|
|
|
@@ -296,7 +319,7 @@ function filletCorner2(): string | null {
|
|
|
296
319
|
}
|
|
297
320
|
|
|
298
321
|
/** Buttons for the bottom bar: they adapt to the tool and the selection (Lock first, when offered). */
|
|
299
|
-
export const barItems = (): CtxEntry[] =>
|
|
322
|
+
export const barItems = (): CtxEntry[] => arrangeLead(ctxItems());
|
|
300
323
|
|
|
301
324
|
/** The yellow + only appears for something it can measure: not for constraint badges or spaces alone. */
|
|
302
325
|
const DRAW_TOOLS = new Set(['line', 'freehand', 'rect', 'circle', 'arc']);
|
|
@@ -324,13 +347,13 @@ export function plusAction(): void {
|
|
|
324
347
|
if (ed.mode === 'annotate') return;
|
|
325
348
|
if (wallPlus()) return;
|
|
326
349
|
const s = selInfo(), sk = SK();
|
|
327
|
-
if (s.dims.length === 1 && s.n === 1)
|
|
350
|
+
if (s.dims.length === 1 && s.n === 1) editDim(s.dims[0]);
|
|
328
351
|
else if (s.odims.length === 1 && s.n === 1) { const [op, part] = s.odims[0].split('|'); editOpeningDim(op, part as OpeningDimPart); }
|
|
329
352
|
// a wall's edge: its length on that face; with another edge, a line or a point, the distance from it
|
|
330
353
|
// (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,
|
|
354
|
+
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
355
|
else if (edgePair(s)) applyCtxAction('edgeDist');
|
|
333
|
-
else if (s.lines.length === 1 && s.n === 1) dimLine(s.lines[0]
|
|
356
|
+
else if (s.lines.length === 1 && s.n === 1) dimLine(s.lines[0]);
|
|
334
357
|
else if (s.curves.length === 1 && s.n === 1) dimCurve(s.curves[0]);
|
|
335
358
|
else if (s.openings.length === 1 && s.n === 1) editOpeningDim(s.openings[0], 'w');
|
|
336
359
|
// a corner where two lines meet: its angle
|
|
@@ -339,7 +362,7 @@ export function plusAction(): void {
|
|
|
339
362
|
else if (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1))) applyCtxAction('opDist');
|
|
340
363
|
// two things selected: the dimension between them
|
|
341
364
|
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]
|
|
365
|
+
else if (s.n === 2 && s.points.length === 2) dimPoints(s.points[0], s.points[1]);
|
|
343
366
|
else if (s.n === 2 && s.points.length === 1 && s.lines.length === 1) dimPointLine(s.points[0], s.lines[0]);
|
|
344
367
|
else setTool('dim');
|
|
345
368
|
}
|
|
@@ -362,6 +385,11 @@ export function deleteSelection(): void {
|
|
|
362
385
|
toast(imgIds.size && !rest.length ? (imgIds.size > 1 ? 'Images removed' : 'Image removed') : 'Deleted', { action: { label: 'Undo', fn: () => undo() } });
|
|
363
386
|
}
|
|
364
387
|
|
|
388
|
+
/** The picked edges of straight walls, as [wall, side] (a curved wall's edge has no line to stand for it). */
|
|
389
|
+
const straightEdges = (sk: Sketch, s: ReturnType<typeof selInfo>): [string, 1 | -1][] => s.wfaces.map(edgeOf).filter(([id]) => !!sk.lines[id]);
|
|
390
|
+
/** The picked lines, and the walls of the picked straight edges. */
|
|
391
|
+
const withEdges = (sk: Sketch, s: ReturnType<typeof selInfo>): string[] => [...s.lines, ...straightEdges(sk, s).map(([id]) => id)];
|
|
392
|
+
|
|
365
393
|
function consFromSel(type: ConsType): Constraint | null {
|
|
366
394
|
const sk = SK(), s = selInfo(), L = s.lines.map(id => sk.lines[id]);
|
|
367
395
|
switch (type) {
|
|
@@ -369,10 +397,25 @@ function consFromSel(type: ConsType): Constraint | null {
|
|
|
369
397
|
if (s.lines.length === 1) return { type, a: L[0].a, b: L[0].b };
|
|
370
398
|
if (s.points.length === 2) return { type, a: s.points[0], b: s.points[1] };
|
|
371
399
|
return null;
|
|
372
|
-
case 'par': case 'perp':
|
|
400
|
+
case 'par': case 'perp': {
|
|
401
|
+
// (a wall's edge: its wall — the same thing, square to something)
|
|
402
|
+
const ls = withEdges(sk, s);
|
|
403
|
+
return ls.length === 2 && ls[0] !== ls[1] ? { type, l1: ls[0], l2: ls[1] } : null;
|
|
404
|
+
}
|
|
405
|
+
case 'col':
|
|
373
406
|
return s.lines.length === 2 ? { type, l1: s.lines[0], l2: s.lines[1] } : null;
|
|
374
|
-
case '
|
|
407
|
+
case 'eq': {
|
|
408
|
+
// (a wall's edge: that face's length, corner to corner)
|
|
409
|
+
const ls = withEdges(sk, s), f = straightEdges(sk, s);
|
|
410
|
+
if (ls.length !== 2 || ls[0] === ls[1]) return null;
|
|
411
|
+
return f.length ? { type, l1: ls[0], l2: ls[1], faces: ls.map((_, i) => (i < s.lines.length ? 0 : f[i - s.lines.length][1])) as [number, number] } : { type, l1: ls[0], l2: ls[1] };
|
|
412
|
+
}
|
|
413
|
+
case 'mid': case 'online': {
|
|
414
|
+
// (a point and a wall's edge: on that face)
|
|
415
|
+
const f = straightEdges(sk, s);
|
|
416
|
+
if (type === 'online' && s.n === 2 && s.points.length === 1 && f.length === 1) return { type, p: s.points[0], l: f[0][0], face: f[0][1] };
|
|
375
417
|
return s.points.length === 1 && s.lines.length === 1 ? { type, p: s.points[0], l: s.lines[0] } : null;
|
|
418
|
+
}
|
|
376
419
|
case 'oncircle':
|
|
377
420
|
return s.points.length === 1 && s.curves.length === 1 ? { type, p: s.points[0], c: s.curves[0] } : null;
|
|
378
421
|
case 'tan': {
|
|
@@ -395,9 +438,13 @@ const NEEDS: Partial<Record<ConsType, string>> = {
|
|
|
395
438
|
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
439
|
};
|
|
397
440
|
|
|
398
|
-
/**
|
|
441
|
+
/**
|
|
442
|
+
* H/V on each selected line, or parallel/collinear/equal from the first selected line to each of the others; three
|
|
443
|
+
* or more points, each level or plumb with the first picked.
|
|
444
|
+
*/
|
|
399
445
|
function multiLineCons(type: ConsType): Constraint[] | null {
|
|
400
446
|
const sk = SK(), s = selInfo();
|
|
447
|
+
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
448
|
if (s.n !== s.lines.length || s.lines.length < (type === 'h' || type === 'v' ? 2 : 3)) return null;
|
|
402
449
|
if (type === 'h' || type === 'v') return s.lines.map(id => ({ type, a: sk.lines[id].a, b: sk.lines[id].b }));
|
|
403
450
|
if (type === 'par' || type === 'col' || type === 'eq') return s.lines.slice(1).map(id => ({ type, l1: s.lines[0], l2: id }));
|
|
@@ -406,8 +453,11 @@ function multiLineCons(type: ConsType): Constraint[] | null {
|
|
|
406
453
|
|
|
407
454
|
const ADJ: Partial<Record<ConsType, string>> = { h: 'horizontal', v: 'vertical', par: 'parallel', col: 'collinear', eq: 'equal' };
|
|
408
455
|
|
|
409
|
-
/**
|
|
410
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Apply one constraint type across several lines (or `points`, each tied to the first) as one undo step. A conflict
|
|
458
|
+
* opens the conflict sheet for the first one that fails.
|
|
459
|
+
*/
|
|
460
|
+
function applyMulti(type: ConsType, list: Constraint[], points = false): void {
|
|
411
461
|
pushHistory();
|
|
412
462
|
let added = 0;
|
|
413
463
|
const failed: Constraint[] = [];
|
|
@@ -418,9 +468,9 @@ function applyMulti(type: ConsType, list: Constraint[]): void {
|
|
|
418
468
|
}
|
|
419
469
|
if (!added && !failed.length) { dropHistory(); toast('They’re already ' + ADJ[type] + '.', { warn: true }); afterEdit(); return; }
|
|
420
470
|
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');
|
|
471
|
+
// every selected line (or point) that now satisfies it (some may already have, through other constraints)
|
|
472
|
+
const lines = ((type === 'h' || type === 'v') && !points ? list.length : list.length + 1) - failed.length;
|
|
473
|
+
const nLines = lines + (points ? ' point' : ' line') + (lines === 1 ? '' : 's');
|
|
424
474
|
if (failed.length) {
|
|
425
475
|
// explain the first conflict; the sheet's Remove re-applies it
|
|
426
476
|
tryAddConstraint(failed[0], { onResolved: () => afterEdit() });
|
|
@@ -429,15 +479,42 @@ function applyMulti(type: ConsType, list: Constraint[]): void {
|
|
|
429
479
|
afterEdit();
|
|
430
480
|
}
|
|
431
481
|
|
|
482
|
+
/** A line and a straight wall's edge picked: the line along that face, both its ends on it (Collinear, with a face). */
|
|
483
|
+
function alongFace(): Constraint[] | null {
|
|
484
|
+
const sk = SK(), s = selInfo(), f = straightEdges(sk, s);
|
|
485
|
+
if (s.n !== 2 || s.lines.length !== 1 || f.length !== 1 || f[0][0] === s.lines[0]) return null;
|
|
486
|
+
const [l, face] = f[0], ln = sk.lines[s.lines[0]];
|
|
487
|
+
return [ln.a, ln.b].map(p => ({ type: 'online' as const, p, l, face }));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** A line along a face (alongFace), as one undo step: its ends on it, or the conflict sheet for the first that can't be. */
|
|
491
|
+
function applyAlong(list: Constraint[]): void {
|
|
492
|
+
pushHistory();
|
|
493
|
+
let added = 0;
|
|
494
|
+
const failed: Constraint[] = [];
|
|
495
|
+
for (const c of list) {
|
|
496
|
+
const r = tryAddConstraint(c, { silent: true });
|
|
497
|
+
if (r.ok) added++;
|
|
498
|
+
else if (r.conflict) failed.push(c);
|
|
499
|
+
}
|
|
500
|
+
if (!added && !failed.length) { dropHistory(); toast('It’s already along that face.', { warn: true }); afterEdit(); return; }
|
|
501
|
+
if (!added) dropHistory();
|
|
502
|
+
if (failed.length) tryAddConstraint(failed[0], { onResolved: () => afterEdit() });
|
|
503
|
+
else toast(CONS_LABEL.col + ' added');
|
|
504
|
+
afterEdit();
|
|
505
|
+
}
|
|
506
|
+
|
|
432
507
|
function applyCons(type: ConsType): void {
|
|
433
|
-
const
|
|
434
|
-
if (
|
|
508
|
+
const along = type === 'col' ? alongFace() : null;
|
|
509
|
+
if (along) { applyAlong(along); return; }
|
|
510
|
+
const multi = multiLineCons(type), s = selInfo(), points = s.points.length > 2 && s.n === s.points.length;
|
|
511
|
+
if (multi && (multi.length > 1 || s.lines.length > 2 || points)) { applyMulti(type, multi, points); return; }
|
|
435
512
|
const c = consFromSel(type);
|
|
436
513
|
if (!c) { toast('Select ' + (NEEDS[type] || 'geometry') + ' first.', { warn: true }); return; }
|
|
437
514
|
pushHistory();
|
|
438
515
|
const r = tryAddConstraint(c, { onResolved: () => afterEdit() });
|
|
439
516
|
if (!r.ok) dropHistory();
|
|
440
|
-
else toast(
|
|
517
|
+
else toast(consLabel(c) + ' added');
|
|
441
518
|
afterEdit();
|
|
442
519
|
}
|
|
443
520
|
|
|
@@ -468,15 +545,13 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
468
545
|
// (the Line tool's, or the templator's: both close and finish a run of points)
|
|
469
546
|
case 'closeShape': (tools[ed.tool] as LineTool | SurveyTool).closeShape(); updateCtx(); return;
|
|
470
547
|
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
548
|
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
549
|
applyCons(act); return;
|
|
475
550
|
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,
|
|
551
|
+
if (s.lines.length === 1) dimLine(s.lines[0]);
|
|
552
|
+
else if (s.wfaces.length === 1 && sk.lines[edgeOf(s.wfaces[0])[0]]) { const [lid, side] = edgeOf(s.wfaces[0]); dimLine(lid, side); }
|
|
478
553
|
return;
|
|
479
|
-
case 'pdist': if (s.points.length === 2) dimPoints(s.points[0], s.points[1]
|
|
554
|
+
case 'pdist': if (s.points.length === 2) dimPoints(s.points[0], s.points[1]); return;
|
|
480
555
|
case 'pldist': if (s.points.length === 1 && s.lines.length === 1) dimPointLine(s.points[0], s.lines[0]); return;
|
|
481
556
|
case 'diam': if (s.curves.length === 1) dimCurve(s.curves[0]); return;
|
|
482
557
|
case 'fillet': case 'chamfer': {
|
|
@@ -486,9 +561,13 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
486
561
|
}
|
|
487
562
|
case 'cornerAngle': cornerAngle(s.points[0]); return;
|
|
488
563
|
case 'merge': {
|
|
564
|
+
// (every one into the first picked)
|
|
565
|
+
if (s.points.length < 2) return;
|
|
489
566
|
pushHistory();
|
|
490
567
|
const snap = JSON.stringify(sk);
|
|
491
|
-
if (joinPoints(sk, s.points[0],
|
|
568
|
+
if (!s.points.slice(1).every(id => joinPoints(sk, s.points[0], id))) {
|
|
569
|
+
replaceSK(JSON.parse(snap) as Sketch); dropHistory(); toast('Two locked points can’t be joined. Unlock one first.', { warn: true });
|
|
570
|
+
} else {
|
|
492
571
|
const r = solveSketch();
|
|
493
572
|
if (!r.ok) { replaceSK(JSON.parse(snap) as Sketch); dropHistory(); toast('Can’t join those points — it conflicts with other constraints.', { warn: true }); }
|
|
494
573
|
else { ed.sel = [{ type: 'point', id: s.points[0] }]; toast('Points joined'); }
|
|
@@ -514,12 +593,21 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
514
593
|
requestDraw();
|
|
515
594
|
break;
|
|
516
595
|
}
|
|
517
|
-
case 'constr':
|
|
596
|
+
case 'constr': {
|
|
597
|
+
// all guides, or all solid again if they all are — one setting for the whole selection, as To wall is (flipping
|
|
598
|
+
// each swapped a guide and a line picked together). A wall is always solid: never made a guide, and one that's a
|
|
599
|
+
// guide from before goes back to solid
|
|
600
|
+
const objs = [...s.lines.filter(id => !sk.lines[id].anchor).map(id => sk.lines[id]), ...s.curves.map(id => curveInfo(sk, id)?.o)]
|
|
601
|
+
.filter((o): o is NonNullable<typeof o> => !!o);
|
|
602
|
+
const lines = objs.filter(o => o.kind !== 'wall'), walls = objs.filter(o => o.kind === 'wall' && o.constr);
|
|
603
|
+
if (!lines.length && !walls.length) return;
|
|
604
|
+
const guide = !lines.every(o => o.constr);
|
|
518
605
|
pushHistory();
|
|
519
|
-
|
|
520
|
-
|
|
606
|
+
lines.forEach(o => { o.constr = guide; });
|
|
607
|
+
walls.forEach(o => { o.constr = false; });
|
|
521
608
|
afterEdit();
|
|
522
609
|
return;
|
|
610
|
+
}
|
|
523
611
|
case 'kind': {
|
|
524
612
|
// one setting for the whole selection: all walls, unless they all are already (then all lines)
|
|
525
613
|
const targets: { kind?: 'line' | 'wall'; thick?: number }[] = [...s.lines.map(id => sk.lines[id]), ...s.curves.map(id => curveInfo(sk, id)?.o).filter((o): o is NonNullable<typeof o> => !!o)];
|
|
@@ -548,9 +636,9 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
548
636
|
return;
|
|
549
637
|
}
|
|
550
638
|
case 'thick':
|
|
551
|
-
// straight and curved walls in the selection
|
|
639
|
+
// straight and curved walls in the selection (an edge's: its wall)
|
|
552
640
|
thicknessPrompt([
|
|
553
|
-
...s.
|
|
641
|
+
...withEdges(sk, s).map(id => sk.lines[id]).filter(l => l.kind === 'wall'),
|
|
554
642
|
...s.curves.map(id => curveInfo(sk, id)?.o).filter((o): o is NonNullable<typeof o> => !!o && o.kind === 'wall'),
|
|
555
643
|
]);
|
|
556
644
|
return;
|
|
@@ -564,7 +652,8 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
564
652
|
case 'toggleref': toggleRef(s.dims[0]); return;
|
|
565
653
|
case 'odimRef': { const [op, part] = s.odims[0].split('|'); toggleOpeningDim(op, part.slice(2)); return; }
|
|
566
654
|
case 'renameRoom': editRoomName(s.rooms[0]); return;
|
|
567
|
-
|
|
655
|
+
// (the room is on the left of the wall's start → end when there's none either side of it)
|
|
656
|
+
case 'opFlip': { pushHistory(); const o = sk.openings[s.openings[0]]; flipDoor(o, roomSideOf(sk, o.line) === -1 ? -1 : 1); afterEdit(); return; }
|
|
568
657
|
case 'opDist': {
|
|
569
658
|
const ops = ed.sel.filter(x => x.type === 'opening').map(x => x.id);
|
|
570
659
|
if (ops.length === 2) {
|
|
@@ -601,8 +690,8 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
601
690
|
return;
|
|
602
691
|
}
|
|
603
692
|
case 'palette': openPalette(anchor); return;
|
|
604
|
-
case 'convert': convertNote(s.notes[0]); return;
|
|
605
693
|
case 'linkOpen': openSelectedLink(); return;
|
|
694
|
+
case 'linkExpand': toggleExpand(); return;
|
|
606
695
|
case 'linkSize': toggleTileSize(anchor); return;
|
|
607
696
|
case 'dimLinkOpen': if (s.dims.length === 1) openDimLink(s.dims[0]); return;
|
|
608
697
|
case 'dimUnlink': if (s.dims.length === 1) unlinkDim(s.dims[0]); return;
|
|
@@ -619,6 +708,8 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
|
|
|
619
708
|
const lock = !ims.every(u => u.locked), many = ims.length > 1;
|
|
620
709
|
pushHistory();
|
|
621
710
|
ims.forEach(u => { u.locked = lock; });
|
|
711
|
+
// (locked, its bar is Lock and Open alone: an opacity or turning slider open over it goes with the rest)
|
|
712
|
+
if (lock) dismissPopovers();
|
|
622
713
|
afterEdit();
|
|
623
714
|
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
715
|
: many ? 'Images unlocked.' : 'Image unlocked. Drag it, or drag its handles to size it.');
|