@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/dist/editor/actions.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { afterEdit, holdFaces, solveSketch, tryAddConstraint, undo } from '../app/edit';
|
|
2
2
|
import { ed } 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
11
|
import { fitView, requestDraw } from '../render/view';
|
|
11
|
-
import { convertNote } from '../tools/annotate';
|
|
12
12
|
import { cornerAngle, dimCurve, dimEdges, dimLine, dimPointLine, dimPoints, dimTwoLines, editDim, faceChoices, onChoice, setDimFace, toggleRef } from '../tools/dim';
|
|
13
13
|
import { edgeLabel, edgeOf } from '../model/planGeom';
|
|
14
14
|
import { offsetPrompt, splitLine } from '../tools/modify';
|
|
@@ -17,40 +17,49 @@ import { chamferPrompt } from '../tools/chamfer';
|
|
|
17
17
|
import { addOpeningDim, editOpeningDim, toggleOpeningDim } from '../tools/openings';
|
|
18
18
|
import { setTool, tools } from '../tools/registry';
|
|
19
19
|
import { fmtLen } from '../units';
|
|
20
|
-
import { elevOpen, fitState, OPENING_SIZES, openElevation,
|
|
20
|
+
import { elevOpen, fitState, OPENING_SIZES, openElevation, overflow, restoreFit, TOO_SHORT, wallAction, wallBar, wallDelete, wallPlus } from './elevation';
|
|
21
21
|
import { pushedOut } from '../model/facesolve';
|
|
22
|
-
import { editRoomName, openKeypad, openPalette, toast, toggleImageOpacity, toggleImageRotation, toggleTileSize, updateCtx, updateTopBar } from './ports';
|
|
22
|
+
import { dismissPopovers, editRoomName, openKeypad, openPalette, toast, toggleImageOpacity, toggleImageRotation, toggleTileSize, updateCtx, updateTopBar } from './ports';
|
|
23
23
|
import { cornerLines, edgePair, selInfo } from './selection';
|
|
24
24
|
import { restackImage } from './project';
|
|
25
25
|
import { imageById } from '../model/images';
|
|
26
|
-
import { canOpenLink, canSaveDim, linkSizeVisible, openDimLink, openSelectedLink, saveDimToGroup, selectedLink, toggleLeader, unlinkDim } from './linkActions';
|
|
26
|
+
import { canExpand, canOpenLink, canSaveDim, linkSizeVisible, openDimLink, openSelectedLink, saveDimToGroup, selectedLink, toggleExpand, toggleLeader, unlinkDim } from './linkActions';
|
|
27
|
+
/** A bar button shown as its icon alone, no name under it (the lead, and Lock). */
|
|
28
|
+
export const iconAlone = (it) => !!(it.lead || it.bare);
|
|
27
29
|
/** Points a Lock on the current selection would pin (the origin is always fixed). */
|
|
28
30
|
function lockTargets() {
|
|
29
31
|
const sk = SK(), s = selInfo(), ids = new Set(s.points);
|
|
30
|
-
|
|
32
|
+
// (a straight wall's edge: its wall, as if picked whole)
|
|
33
|
+
withEdges(sk, s).forEach(id => { ids.add(sk.lines[id].a); ids.add(sk.lines[id].b); });
|
|
31
34
|
s.curves.forEach(id => { const ci = curveInfo(sk, id); if (ci)
|
|
32
35
|
ids.add(ci.o.c); });
|
|
33
36
|
return [...ids].filter(id => sk.points[id] && !sk.points[id].origin && !sk.points[id].anchor);
|
|
34
37
|
}
|
|
35
38
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
39
|
+
* The bar leads with one button, its icon alone, set off by a divider (Figma's bars, 2528:101128: the user's call,
|
|
40
|
+
* 2026-10-27): Open, when what's selected is a link to something of the job's; otherwise Lock — an open padlock (white)
|
|
41
|
+
* when free, a closed one (red, "Locked") when locked. A link that can be locked too (an expanded photo) has Lock next,
|
|
42
|
+
* 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
|
|
43
|
+
* display the icon - no text needed below it").
|
|
38
44
|
*/
|
|
39
|
-
function
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
out.push('sep');
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
function arrangeLead(items) {
|
|
46
|
+
const find = (acts) => items.find((it) => typeof it === 'object' && 'act' in it && acts.includes(it.act));
|
|
47
|
+
const open = find(['linkOpen', 'dimLinkOpen']), lock = find(['lock', 'imgLock']);
|
|
48
|
+
if (lock) {
|
|
49
|
+
const sk = SK();
|
|
50
|
+
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); })();
|
|
51
|
+
Object.assign(lock, { ic: locked ? 'lock' : 'unlock', label: locked ? 'Locked' : 'Lock', on: undefined, locked, bare: true });
|
|
52
|
+
}
|
|
53
|
+
const lead = open ?? lock, out = [];
|
|
54
|
+
if (lead) {
|
|
55
|
+
lead.lead = true;
|
|
56
|
+
out.push(lead, 'sep');
|
|
57
|
+
if (open && lock)
|
|
58
|
+
out.push(lock, 'sep');
|
|
59
|
+
}
|
|
60
|
+
// the rest, with no divider doubled, first or last
|
|
61
|
+
for (const e of items)
|
|
62
|
+
if (e !== lead && !(open && e === lock) && !(e === 'sep' && (!out.length || out[out.length - 1] === 'sep')))
|
|
54
63
|
out.push(e);
|
|
55
64
|
while (out[out.length - 1] === 'sep')
|
|
56
65
|
out.pop();
|
|
@@ -67,32 +76,27 @@ const PLACING = new Set(['door', 'window', 'wdoor', 'wwindow', 'cabinet']);
|
|
|
67
76
|
function ctxItems() {
|
|
68
77
|
const items = [], sk = SK(), s = selInfo();
|
|
69
78
|
const add = (ic, label, act, o = {}) => items.push(Object.assign({ ic, label, act }, o));
|
|
70
|
-
/** To wall (all the selected lines and curves) — or To line + Thickness when they're all walls already. */
|
|
71
|
-
const wallItems = () => {
|
|
72
|
-
if (elevOpen())
|
|
73
|
-
return;
|
|
74
|
-
const kinds = [...s.lines.map(id => sk.lines[id].kind), ...s.curves.map(id => curveInfo(sk, id)?.o.kind)];
|
|
75
|
-
if (!kinds.length)
|
|
76
|
-
return;
|
|
77
|
-
if (kinds.every(k => k === 'wall')) {
|
|
78
|
-
add('thick', 'Thickness', 'thick');
|
|
79
|
-
add('line', 'To line', 'kind');
|
|
80
|
-
}
|
|
81
|
-
else
|
|
82
|
-
add('wall', 'To wall', 'kind');
|
|
83
|
-
};
|
|
84
79
|
if (ed.mode === 'annotate') {
|
|
85
80
|
const ims = selImages();
|
|
86
81
|
if (ims.length && ims.length === s.n) {
|
|
87
82
|
// an image (or several): lock, its own scale, and which goes in front where they overlap
|
|
88
83
|
// (showing and hiding them all is the Layers menu's)
|
|
89
84
|
add('lock', 'Lock', 'imgLock');
|
|
90
|
-
|
|
85
|
+
// a photo of the job's, expanded from its tile: opening it, and Expand (on) to fold it back into its tile
|
|
86
|
+
const lk = ims.length === 1 ? ims[0].link : undefined;
|
|
87
|
+
if (lk && canOpenLink(lk))
|
|
88
|
+
add('open', 'Open', 'linkOpen');
|
|
89
|
+
// locked: unlocking it, and opening what it's a photo of — nothing that changes it (the user's call,
|
|
90
|
+
// 2026-10-30: "images in locked state: shouldn't have these options - only lock and link should be available")
|
|
91
|
+
if (ims.every(u => u.locked))
|
|
92
|
+
return items;
|
|
93
|
+
if (lk)
|
|
94
|
+
add('expand', 'Expand', 'linkExpand', { on: true });
|
|
95
|
+
if (ims.length === 1)
|
|
91
96
|
add('calib', 'Set scale', 'calibtool');
|
|
92
97
|
if (ims.length === 1)
|
|
93
98
|
add('opacity', 'Opacity', 'imgOpacity');
|
|
94
|
-
|
|
95
|
-
if (ims.length === 1 && !ims[0].locked)
|
|
99
|
+
if (ims.length === 1)
|
|
96
100
|
add('rotate', 'Rotate', 'imgRotate');
|
|
97
101
|
if (ims.length === 1 && page().underlays.length > 1) {
|
|
98
102
|
const at = page().underlays.indexOf(ims[0]), last = page().underlays.length - 1;
|
|
@@ -108,20 +112,19 @@ function ctxItems() {
|
|
|
108
112
|
// 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)
|
|
109
113
|
if (canOpenLink(lk.link))
|
|
110
114
|
add('open', 'Open', 'linkOpen');
|
|
115
|
+
// (a photo's: the photo whole, as one of the page's images)
|
|
116
|
+
if (canExpand(lk))
|
|
117
|
+
add('expand', 'Expand', 'linkExpand', { on: false });
|
|
111
118
|
add('leader', 'Leader', 'linkLeader', { on: !!lk.leader });
|
|
112
119
|
// (its leader's colour, as all markup's, is the rail's colour dot's)
|
|
113
120
|
// (every tile on the page: a slider above the bar)
|
|
114
121
|
add('tilesize', 'Size', 'linkSize');
|
|
115
122
|
return items;
|
|
116
123
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
add('convert', 'To lines', 'convert');
|
|
122
|
-
if (linkSizeVisible())
|
|
123
|
-
add('tilesize', 'Size', 'linkSize');
|
|
124
|
-
}
|
|
124
|
+
// markup: nothing of its own — a pen stroke as any other (no To lines: the user's call, 2026-10-27), and no Color
|
|
125
|
+
// (the rail's colour dot styles what's selected); with tiles among it, their size
|
|
126
|
+
if (s.notes.length && linkSizeVisible())
|
|
127
|
+
add('tilesize', 'Size', 'linkSize');
|
|
125
128
|
// nothing selected: no bar (the markup colour is the rail's colour dot, for what's selected too; the image's own
|
|
126
129
|
// options come with selecting it)
|
|
127
130
|
return items;
|
|
@@ -143,11 +146,6 @@ function ctxItems() {
|
|
|
143
146
|
add('finish', 'Finish', 'finish');
|
|
144
147
|
return items;
|
|
145
148
|
}
|
|
146
|
-
if (ed.tool === 'dim') {
|
|
147
|
-
add('dim', 'Driving', 'dimDrive', { on: ed.dimLayer === 'drive' });
|
|
148
|
-
add('ref', 'Print', 'dimPrint', { on: ed.dimLayer === 'print' });
|
|
149
|
-
return items;
|
|
150
|
-
}
|
|
151
149
|
// nothing selected: the bar stays empty (Fit and Icons live in the top bar; grid snap and auto-constrain in Settings).
|
|
152
150
|
// Just placed a door, window or cabinet (its tool still on): its bar, for its sizes
|
|
153
151
|
if (!s.n || (ed.tool !== 'select' && !PLACING.has(ed.tool)))
|
|
@@ -161,15 +159,41 @@ function ctxItems() {
|
|
|
161
159
|
add(it.ic, it.label, it.act);
|
|
162
160
|
return items;
|
|
163
161
|
}
|
|
162
|
+
/*
|
|
163
|
+
Figma's bars (2528:101128, the user's call, 2026-10-27): Lock first, its icon alone (arrangeLead); then what the lines
|
|
164
|
+
and curves are (kind); then what ties them, or edits them. The yellow + measures: no Distance of its own here.
|
|
165
|
+
*/
|
|
164
166
|
// on a wall face, walls don't exist: no To wall / Thickness
|
|
165
167
|
const wall = elevOpen();
|
|
166
168
|
const P = s.points.length, L = s.lines.length, C = s.curves.length;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
/**
|
|
170
|
+
* What the selected lines and curves are, and its divider: walls — one straight wall's wall view, and their Thickness;
|
|
171
|
+
* otherwise To wall, and Guide (Solid, when they all are guides). A wall is always solid, so with one among them
|
|
172
|
+
* there's no Guide (the user's call, 2026-11-03: a wall and a line picked together offered it) — only Solid, for a
|
|
173
|
+
* wall that's a guide from before; on a wall face, Guide alone. `walls`: the walls of picked edges, as if picked whole.
|
|
174
|
+
*/
|
|
175
|
+
const kind = (walls = []) => {
|
|
176
|
+
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);
|
|
177
|
+
if (!objs.length)
|
|
178
|
+
return;
|
|
179
|
+
if (objs.every(o => o.kind === 'wall')) {
|
|
180
|
+
if (wall)
|
|
181
|
+
return;
|
|
182
|
+
if (s.n === 1 && L === 1)
|
|
183
|
+
add('elev', 'Elevation', 'elev');
|
|
184
|
+
add('thick', 'Thickness', 'thick');
|
|
185
|
+
if (objs.some(o => o.constr))
|
|
186
|
+
add('constr', 'Solid', 'constr');
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
if (!wall)
|
|
190
|
+
add('wall', 'To wall', 'kind');
|
|
191
|
+
if (!objs.some(o => o.kind === 'wall'))
|
|
192
|
+
add('constr', objs.every(o => o.constr) ? 'Solid' : 'Guide', 'constr');
|
|
193
|
+
}
|
|
194
|
+
items.push('sep');
|
|
195
|
+
};
|
|
196
|
+
// a wall's edge: named, with its wall view (with another edge, a line or a point, the + is the distance between them)
|
|
173
197
|
if (s.wfaces.length) {
|
|
174
198
|
// (a curved wall has no wall view: just its name)
|
|
175
199
|
if (s.n === 1) {
|
|
@@ -178,16 +202,34 @@ function ctxItems() {
|
|
|
178
202
|
if (sk.lines[id])
|
|
179
203
|
add('elev', 'Elevation', 'elev');
|
|
180
204
|
}
|
|
181
|
-
else if (
|
|
182
|
-
|
|
183
|
-
|
|
205
|
+
else if (s.n === 2 && straightEdges(sk, s).length === s.wfaces.length) {
|
|
206
|
+
/*
|
|
207
|
+
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:
|
|
208
|
+
"add equal, lock, and to wall to both bottom bars"), each acting on what's picked — the point On face; Parallel
|
|
209
|
+
and Perpendicular, the wall (a face runs with its centreline); Collinear, the line along the face (both its ends
|
|
210
|
+
on it); Equal, the face's length, corner to corner. (Two edges: no Collinear — their faces flush is another thing.)
|
|
211
|
+
*/
|
|
212
|
+
// (the two edges of one wall: nothing but the + — its thickness)
|
|
213
|
+
const pair = new Set(withEdges(sk, s)).size === 2;
|
|
214
|
+
if (P === 1 || pair)
|
|
215
|
+
add('lock', 'Lock', 'lock', { key: '⇧F' });
|
|
216
|
+
if (P === 1)
|
|
217
|
+
add('online', 'On face', 'online', { key: '⇧O' });
|
|
218
|
+
else if (pair) {
|
|
219
|
+
kind(straightEdges(sk, s).map(([id]) => id));
|
|
220
|
+
add('par', 'Parallel', 'par', { key: '⇧P' });
|
|
221
|
+
add('perp', 'Perpendicular', 'perp', { key: '⇧R' });
|
|
222
|
+
if (L === 1)
|
|
223
|
+
add('col', 'Collinear', 'col', { key: '⇧L' });
|
|
224
|
+
add('eq', 'Equal', 'eq', { key: '⇧E' });
|
|
225
|
+
}
|
|
184
226
|
}
|
|
185
227
|
return items;
|
|
186
228
|
}
|
|
187
229
|
if (s.n === 1 && s.dims.length) {
|
|
188
230
|
const c = sk.cons[s.dims[0]];
|
|
189
231
|
// linked to a measurement of the job: opening it, letting go of it — no text saying what it's linked to (the user's
|
|
190
|
-
// call, 2026-10-21; its chip is drawn linked)
|
|
232
|
+
// call, 2026-10-21; its chip is drawn linked)
|
|
191
233
|
if (c.measurementRef) {
|
|
192
234
|
const t = c.measurementRef;
|
|
193
235
|
if (canOpenLink(t))
|
|
@@ -196,10 +238,7 @@ function ctxItems() {
|
|
|
196
238
|
}
|
|
197
239
|
else if (canSaveDim(s.dims[0]))
|
|
198
240
|
add('savegroup', 'Save to group', 'dimSave');
|
|
199
|
-
// Reference
|
|
200
|
-
const cabSize = Object.values(sk.cabs ?? {}).some(cb => cb.w === s.dims[0] || cb.h === s.dims[0]);
|
|
201
|
-
if (c.layer !== 'print' && !cabSize && !c.measurementRef)
|
|
202
|
-
add('ref', c.driving === false ? 'Drive' : 'Reference', 'toggleref');
|
|
241
|
+
// (no Reference: Figma's dimension bars have none, 2026-10-27)
|
|
203
242
|
// measured on walls: which of their faces (the drawing stays as it is; the number becomes what it measures there)
|
|
204
243
|
const fc = faceChoices(sk, c);
|
|
205
244
|
if (fc.length && items.length)
|
|
@@ -220,96 +259,71 @@ function ctxItems() {
|
|
|
220
259
|
add('rename', 'Rename', 'renameRoom');
|
|
221
260
|
return items;
|
|
222
261
|
}
|
|
223
|
-
// one end of a door or window: named
|
|
262
|
+
// one end of a door or window: named (with another point, end or door/window, the + is the distance between them)
|
|
224
263
|
if (s.oanchors.length && s.n === 1) {
|
|
225
264
|
items.push({ lbl: anchorLabel(s.oanchors[0]) });
|
|
226
265
|
return items;
|
|
227
266
|
}
|
|
228
|
-
if (s.oanchors.length
|
|
229
|
-
add('dim', 'Distance', 'anchorDist');
|
|
230
|
-
return items;
|
|
231
|
-
}
|
|
232
|
-
// two doors/windows, or one and a point: the distance between them (locks the second one picked)
|
|
233
|
-
if (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1))) {
|
|
234
|
-
add('dim', 'Distance', 'opDist');
|
|
267
|
+
if (s.oanchors.length || (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1))))
|
|
235
268
|
return items;
|
|
236
|
-
}
|
|
237
269
|
if (s.n === 1 && s.openings.length) {
|
|
238
|
-
// its sizes
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
items.push({ lbl: openingLabel(o) });
|
|
270
|
+
// its sizes (no +: it isn't one dimension), with nothing saying them — the drawing does (the user's call, 2026-10-27);
|
|
271
|
+
// a door's Swing too, round its four ways (it was Flip: the user's call, 2026-10-27, "ok do swing instead"). (The
|
|
272
|
+
// type is fixed once placed: Door and Window are separate tools.)
|
|
242
273
|
for (const it of OPENING_SIZES)
|
|
243
274
|
add(it.ic, it.label, it.act);
|
|
244
|
-
if (
|
|
275
|
+
if (sk.openings[s.openings[0]].type === 'door') {
|
|
245
276
|
items.push('sep');
|
|
246
|
-
add('flip', '
|
|
277
|
+
add('flip', 'Swing', 'opFlip');
|
|
247
278
|
}
|
|
248
279
|
return items;
|
|
249
280
|
}
|
|
281
|
+
// a cabinet's corner: nothing of its own (pick another point or line with it to dimension or constrain)
|
|
282
|
+
if (P === 1 && s.n === 1 && cabOfPoint(sk, s.points[0]))
|
|
283
|
+
return items;
|
|
284
|
+
add('lock', 'Lock', 'lock', { key: '⇧F' });
|
|
250
285
|
if (L === 1 && s.n === 1) {
|
|
251
|
-
|
|
286
|
+
kind();
|
|
287
|
+
// (its length: the yellow +)
|
|
252
288
|
add('h', 'Horizontal', 'h', { key: '⇧H' });
|
|
253
|
-
add('v', 'Vertical', 'v', { key: '⇧V' });
|
|
254
|
-
items.push('sep');
|
|
255
|
-
add('lock', 'Lock', 'lock', { key: '⇧F' });
|
|
289
|
+
add('v', 'Vertical', 'v', { key: '⇧V' });
|
|
256
290
|
add('split', 'Split', 'split');
|
|
257
291
|
add('offset', 'Offset', 'offset');
|
|
258
|
-
if (l.constr)
|
|
259
|
-
add('constr', 'Solid', 'constr');
|
|
260
|
-
else
|
|
261
|
-
guide();
|
|
262
|
-
if (!wall) {
|
|
263
|
-
if (l.kind === 'wall') {
|
|
264
|
-
add('thick', 'Thickness', 'thick');
|
|
265
|
-
add('line', 'To line', 'kind');
|
|
266
|
-
}
|
|
267
|
-
else
|
|
268
|
-
add('wall', 'To wall', 'kind');
|
|
269
|
-
if (l.kind === 'wall' && !l.constr) {
|
|
270
|
-
items.push('sep');
|
|
271
|
-
add('elev', 'Elevation', 'elev');
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
292
|
return items;
|
|
275
293
|
}
|
|
276
294
|
if (L === 2 && s.n === 2) {
|
|
277
|
-
// (no Angle
|
|
295
|
+
// (no Angle: the yellow + is the angle between them — or, parallel, the distance)
|
|
296
|
+
kind();
|
|
278
297
|
add('par', 'Parallel', 'par', { key: '⇧P' });
|
|
279
298
|
add('perp', 'Perpendicular', 'perp', { key: '⇧R' });
|
|
280
299
|
add('col', 'Collinear', 'col', { key: '⇧L' });
|
|
281
300
|
add('eq', 'Equal', 'eq', { key: '⇧E' });
|
|
282
301
|
// two lines that meet: round off (or cut off) the corner between them
|
|
283
302
|
if (filletCorner2()) {
|
|
303
|
+
items.push('sep');
|
|
284
304
|
add('fillet', 'Fillet', 'fillet');
|
|
285
305
|
add('chamfer', 'Chamfer', 'chamfer');
|
|
286
306
|
}
|
|
287
|
-
wallItems();
|
|
288
307
|
return items;
|
|
289
308
|
}
|
|
290
|
-
// a
|
|
291
|
-
if (P === 1 && s.n === 1 && cabOfPoint(sk, s.points[0]))
|
|
292
|
-
return items;
|
|
309
|
+
// a corner where exactly two lines meet: rounding it off or cutting it off (its angle is the yellow +)
|
|
293
310
|
if (P === 1 && s.n === 1) {
|
|
294
|
-
// a corner where exactly two lines meet: rounding it off or cutting it off (its angle is the yellow +)
|
|
295
311
|
if (filletable(sk, s.points[0])) {
|
|
296
312
|
add('fillet', 'Fillet', 'fillet');
|
|
297
313
|
add('chamfer', 'Chamfer', 'chamfer');
|
|
298
314
|
}
|
|
299
|
-
add('lock', 'Lock', 'lock', { key: '⇧F' });
|
|
300
315
|
return items;
|
|
301
316
|
}
|
|
302
|
-
|
|
317
|
+
// points: joined into the first picked, or level or plumb with it
|
|
318
|
+
if (P >= 2 && s.n === P) {
|
|
319
|
+
add('merge', 'Join', 'merge');
|
|
303
320
|
add('h', 'Horizontal', 'h');
|
|
304
321
|
add('v', 'Vertical', 'v');
|
|
305
|
-
add('dim', 'Distance', 'pdist');
|
|
306
|
-
add('merge', 'Join', 'merge');
|
|
307
322
|
return items;
|
|
308
323
|
}
|
|
309
324
|
if (P === 1 && L === 1 && s.n === 2) {
|
|
310
|
-
add('mid', 'Midpoint', 'mid', { key: '⇧M' });
|
|
311
325
|
add('online', 'On line', 'online', { key: '⇧O' });
|
|
312
|
-
add('
|
|
326
|
+
add('mid', 'Midpoint', 'mid', { key: '⇧M' });
|
|
313
327
|
return items;
|
|
314
328
|
}
|
|
315
329
|
if (P === 1 && C === 1 && s.n === 2) {
|
|
@@ -317,51 +331,35 @@ function ctxItems() {
|
|
|
317
331
|
return items;
|
|
318
332
|
}
|
|
319
333
|
if (L === 1 && C === 1 && s.n === 2) {
|
|
334
|
+
kind();
|
|
320
335
|
add('tan', 'Tangent', 'tan', { key: '⇧T' });
|
|
321
|
-
wallItems();
|
|
322
336
|
return items;
|
|
323
337
|
}
|
|
324
338
|
// a circle or arc: the yellow + sets its diameter
|
|
325
339
|
if (C === 1 && s.n === 1) {
|
|
326
|
-
|
|
327
|
-
guide();
|
|
328
|
-
if (wall)
|
|
329
|
-
return items;
|
|
330
|
-
if (ci && ci.o.kind === 'wall') {
|
|
331
|
-
add('thick', 'Thickness', 'thick');
|
|
332
|
-
add('arc', 'To line', 'kind');
|
|
333
|
-
}
|
|
334
|
-
else
|
|
335
|
-
add('wall', 'To wall', 'kind');
|
|
340
|
+
kind();
|
|
336
341
|
return items;
|
|
337
342
|
}
|
|
338
343
|
if (C === 2 && s.n === 2) {
|
|
339
|
-
|
|
344
|
+
kind();
|
|
340
345
|
add('conc', 'Concentric', 'conc');
|
|
341
|
-
|
|
346
|
+
add('eq', 'Equal', 'eqr');
|
|
342
347
|
return items;
|
|
343
348
|
}
|
|
344
349
|
// 3+ lines (only lines): the constraints that make sense across many, each tied to the first line picked
|
|
345
350
|
if (L >= 3 && s.n === L) {
|
|
351
|
+
kind();
|
|
346
352
|
add('h', 'Horizontal', 'h', { key: '⇧H' });
|
|
347
353
|
add('v', 'Vertical', 'v', { key: '⇧V' });
|
|
348
354
|
add('par', 'Parallel', 'par', { key: '⇧P' });
|
|
349
355
|
add('col', 'Collinear', 'col', { key: '⇧L' });
|
|
350
356
|
add('eq', 'Equal', 'eq', { key: '⇧E' });
|
|
351
|
-
items.push('sep');
|
|
352
|
-
add('lock', 'Lock', 'lock');
|
|
353
|
-
guide();
|
|
354
|
-
wallItems();
|
|
355
357
|
return items;
|
|
356
358
|
}
|
|
357
359
|
if (L >= 2) {
|
|
360
|
+
kind();
|
|
358
361
|
add('eq', 'Equal', 'eqall');
|
|
359
|
-
add('lock', 'Lock', 'lock');
|
|
360
|
-
guide();
|
|
361
|
-
wallItems();
|
|
362
|
-
return items;
|
|
363
362
|
}
|
|
364
|
-
add('lock', 'Lock', 'lock');
|
|
365
363
|
return items;
|
|
366
364
|
}
|
|
367
365
|
/** The keypad for walls' thickness, finished face to face. */
|
|
@@ -400,7 +398,7 @@ function filletCorner2() {
|
|
|
400
398
|
return p && filletable(sk, p) ? p : null;
|
|
401
399
|
}
|
|
402
400
|
/** Buttons for the bottom bar: they adapt to the tool and the selection (Lock first, when offered). */
|
|
403
|
-
export const barItems = () =>
|
|
401
|
+
export const barItems = () => arrangeLead(ctxItems());
|
|
404
402
|
/** The yellow + only appears for something it can measure: not for constraint badges or spaces alone. */
|
|
405
403
|
const DRAW_TOOLS = new Set(['line', 'freehand', 'rect', 'circle', 'arc']);
|
|
406
404
|
export function plusVisible() {
|
|
@@ -431,12 +429,8 @@ export function plusAction() {
|
|
|
431
429
|
if (wallPlus())
|
|
432
430
|
return;
|
|
433
431
|
const s = selInfo(), sk = SK();
|
|
434
|
-
if (s.dims.length === 1 && s.n === 1)
|
|
435
|
-
|
|
436
|
-
toast('Print dimensions are labels only — drag to move them.', { warn: true });
|
|
437
|
-
else
|
|
438
|
-
editDim(s.dims[0]);
|
|
439
|
-
}
|
|
432
|
+
if (s.dims.length === 1 && s.n === 1)
|
|
433
|
+
editDim(s.dims[0]);
|
|
440
434
|
else if (s.odims.length === 1 && s.n === 1) {
|
|
441
435
|
const [op, part] = s.odims[0].split('|');
|
|
442
436
|
editOpeningDim(op, part);
|
|
@@ -446,14 +440,14 @@ export function plusAction() {
|
|
|
446
440
|
else if (s.wfaces.length === 1 && s.n === 1) {
|
|
447
441
|
const [id, side] = edgeOf(s.wfaces[0]);
|
|
448
442
|
if (sk.lines[id])
|
|
449
|
-
dimLine(id,
|
|
443
|
+
dimLine(id, side);
|
|
450
444
|
else
|
|
451
445
|
dimCurve(id, side);
|
|
452
446
|
}
|
|
453
447
|
else if (edgePair(s))
|
|
454
448
|
applyCtxAction('edgeDist');
|
|
455
449
|
else if (s.lines.length === 1 && s.n === 1)
|
|
456
|
-
dimLine(s.lines[0]
|
|
450
|
+
dimLine(s.lines[0]);
|
|
457
451
|
else if (s.curves.length === 1 && s.n === 1)
|
|
458
452
|
dimCurve(s.curves[0]);
|
|
459
453
|
else if (s.openings.length === 1 && s.n === 1)
|
|
@@ -469,7 +463,7 @@ export function plusAction() {
|
|
|
469
463
|
else if (s.n === 2 && s.lines.length === 2)
|
|
470
464
|
dimTwoLines(s.lines[0], s.lines[1]);
|
|
471
465
|
else if (s.n === 2 && s.points.length === 2)
|
|
472
|
-
dimPoints(s.points[0], s.points[1]
|
|
466
|
+
dimPoints(s.points[0], s.points[1]);
|
|
473
467
|
else if (s.n === 2 && s.points.length === 1 && s.lines.length === 1)
|
|
474
468
|
dimPointLine(s.points[0], s.lines[0]);
|
|
475
469
|
else
|
|
@@ -499,6 +493,10 @@ export function deleteSelection() {
|
|
|
499
493
|
afterEdit();
|
|
500
494
|
toast(imgIds.size && !rest.length ? (imgIds.size > 1 ? 'Images removed' : 'Image removed') : 'Deleted', { action: { label: 'Undo', fn: () => undo() } });
|
|
501
495
|
}
|
|
496
|
+
/** The picked edges of straight walls, as [wall, side] (a curved wall's edge has no line to stand for it). */
|
|
497
|
+
const straightEdges = (sk, s) => s.wfaces.map(edgeOf).filter(([id]) => !!sk.lines[id]);
|
|
498
|
+
/** The picked lines, and the walls of the picked straight edges. */
|
|
499
|
+
const withEdges = (sk, s) => [...s.lines, ...straightEdges(sk, s).map(([id]) => id)];
|
|
502
500
|
function consFromSel(type) {
|
|
503
501
|
const sk = SK(), s = selInfo(), L = s.lines.map(id => sk.lines[id]);
|
|
504
502
|
switch (type) {
|
|
@@ -510,13 +508,28 @@ function consFromSel(type) {
|
|
|
510
508
|
return { type, a: s.points[0], b: s.points[1] };
|
|
511
509
|
return null;
|
|
512
510
|
case 'par':
|
|
513
|
-
case 'perp':
|
|
511
|
+
case 'perp': {
|
|
512
|
+
// (a wall's edge: its wall — the same thing, square to something)
|
|
513
|
+
const ls = withEdges(sk, s);
|
|
514
|
+
return ls.length === 2 && ls[0] !== ls[1] ? { type, l1: ls[0], l2: ls[1] } : null;
|
|
515
|
+
}
|
|
514
516
|
case 'col':
|
|
515
|
-
case 'eq':
|
|
516
517
|
return s.lines.length === 2 ? { type, l1: s.lines[0], l2: s.lines[1] } : null;
|
|
518
|
+
case 'eq': {
|
|
519
|
+
// (a wall's edge: that face's length, corner to corner)
|
|
520
|
+
const ls = withEdges(sk, s), f = straightEdges(sk, s);
|
|
521
|
+
if (ls.length !== 2 || ls[0] === ls[1])
|
|
522
|
+
return null;
|
|
523
|
+
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])) } : { type, l1: ls[0], l2: ls[1] };
|
|
524
|
+
}
|
|
517
525
|
case 'mid':
|
|
518
|
-
case 'online':
|
|
526
|
+
case 'online': {
|
|
527
|
+
// (a point and a wall's edge: on that face)
|
|
528
|
+
const f = straightEdges(sk, s);
|
|
529
|
+
if (type === 'online' && s.n === 2 && s.points.length === 1 && f.length === 1)
|
|
530
|
+
return { type, p: s.points[0], l: f[0][0], face: f[0][1] };
|
|
519
531
|
return s.points.length === 1 && s.lines.length === 1 ? { type, p: s.points[0], l: s.lines[0] } : null;
|
|
532
|
+
}
|
|
520
533
|
case 'oncircle':
|
|
521
534
|
return s.points.length === 1 && s.curves.length === 1 ? { type, p: s.points[0], c: s.curves[0] } : null;
|
|
522
535
|
case 'tan': {
|
|
@@ -540,9 +553,14 @@ const NEEDS = {
|
|
|
540
553
|
h: 'a line or two points', v: 'a line or two points', par: 'two lines', perp: 'two lines', col: 'two lines', eq: 'two lines', mid: 'a point and a line',
|
|
541
554
|
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',
|
|
542
555
|
};
|
|
543
|
-
/**
|
|
556
|
+
/**
|
|
557
|
+
* H/V on each selected line, or parallel/collinear/equal from the first selected line to each of the others; three
|
|
558
|
+
* or more points, each level or plumb with the first picked.
|
|
559
|
+
*/
|
|
544
560
|
function multiLineCons(type) {
|
|
545
561
|
const sk = SK(), s = selInfo();
|
|
562
|
+
if ((type === 'h' || type === 'v') && s.n === s.points.length && s.points.length >= 3)
|
|
563
|
+
return s.points.slice(1).map(id => ({ type, a: s.points[0], b: id }));
|
|
546
564
|
if (s.n !== s.lines.length || s.lines.length < (type === 'h' || type === 'v' ? 2 : 3))
|
|
547
565
|
return null;
|
|
548
566
|
if (type === 'h' || type === 'v')
|
|
@@ -552,8 +570,11 @@ function multiLineCons(type) {
|
|
|
552
570
|
return null;
|
|
553
571
|
}
|
|
554
572
|
const ADJ = { h: 'horizontal', v: 'vertical', par: 'parallel', col: 'collinear', eq: 'equal' };
|
|
555
|
-
/**
|
|
556
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Apply one constraint type across several lines (or `points`, each tied to the first) as one undo step. A conflict
|
|
575
|
+
* opens the conflict sheet for the first one that fails.
|
|
576
|
+
*/
|
|
577
|
+
function applyMulti(type, list, points = false) {
|
|
557
578
|
pushHistory();
|
|
558
579
|
let added = 0;
|
|
559
580
|
const failed = [];
|
|
@@ -572,9 +593,9 @@ function applyMulti(type, list) {
|
|
|
572
593
|
}
|
|
573
594
|
if (!added)
|
|
574
595
|
dropHistory();
|
|
575
|
-
// every selected line that now satisfies it (some may already have, through other constraints)
|
|
576
|
-
const lines = (type === 'h' || type === 'v' ? list.length : list.length + 1) - failed.length;
|
|
577
|
-
const nLines = lines + ' line' + (lines === 1 ? '' : 's');
|
|
596
|
+
// every selected line (or point) that now satisfies it (some may already have, through other constraints)
|
|
597
|
+
const lines = ((type === 'h' || type === 'v') && !points ? list.length : list.length + 1) - failed.length;
|
|
598
|
+
const nLines = lines + (points ? ' point' : ' line') + (lines === 1 ? '' : 's');
|
|
578
599
|
if (failed.length) {
|
|
579
600
|
// explain the first conflict; the sheet's Remove re-applies it
|
|
580
601
|
tryAddConstraint(failed[0], { onResolved: () => afterEdit() });
|
|
@@ -585,10 +606,49 @@ function applyMulti(type, list) {
|
|
|
585
606
|
toast(CONS_LABEL[type] + ' added to ' + nLines + '.');
|
|
586
607
|
afterEdit();
|
|
587
608
|
}
|
|
609
|
+
/** A line and a straight wall's edge picked: the line along that face, both its ends on it (Collinear, with a face). */
|
|
610
|
+
function alongFace() {
|
|
611
|
+
const sk = SK(), s = selInfo(), f = straightEdges(sk, s);
|
|
612
|
+
if (s.n !== 2 || s.lines.length !== 1 || f.length !== 1 || f[0][0] === s.lines[0])
|
|
613
|
+
return null;
|
|
614
|
+
const [l, face] = f[0], ln = sk.lines[s.lines[0]];
|
|
615
|
+
return [ln.a, ln.b].map(p => ({ type: 'online', p, l, face }));
|
|
616
|
+
}
|
|
617
|
+
/** 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. */
|
|
618
|
+
function applyAlong(list) {
|
|
619
|
+
pushHistory();
|
|
620
|
+
let added = 0;
|
|
621
|
+
const failed = [];
|
|
622
|
+
for (const c of list) {
|
|
623
|
+
const r = tryAddConstraint(c, { silent: true });
|
|
624
|
+
if (r.ok)
|
|
625
|
+
added++;
|
|
626
|
+
else if (r.conflict)
|
|
627
|
+
failed.push(c);
|
|
628
|
+
}
|
|
629
|
+
if (!added && !failed.length) {
|
|
630
|
+
dropHistory();
|
|
631
|
+
toast('It’s already along that face.', { warn: true });
|
|
632
|
+
afterEdit();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (!added)
|
|
636
|
+
dropHistory();
|
|
637
|
+
if (failed.length)
|
|
638
|
+
tryAddConstraint(failed[0], { onResolved: () => afterEdit() });
|
|
639
|
+
else
|
|
640
|
+
toast(CONS_LABEL.col + ' added');
|
|
641
|
+
afterEdit();
|
|
642
|
+
}
|
|
588
643
|
function applyCons(type) {
|
|
589
|
-
const
|
|
590
|
-
if (
|
|
591
|
-
|
|
644
|
+
const along = type === 'col' ? alongFace() : null;
|
|
645
|
+
if (along) {
|
|
646
|
+
applyAlong(along);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const multi = multiLineCons(type), s = selInfo(), points = s.points.length > 2 && s.n === s.points.length;
|
|
650
|
+
if (multi && (multi.length > 1 || s.lines.length > 2 || points)) {
|
|
651
|
+
applyMulti(type, multi, points);
|
|
592
652
|
return;
|
|
593
653
|
}
|
|
594
654
|
const c = consFromSel(type);
|
|
@@ -601,7 +661,7 @@ function applyCons(type) {
|
|
|
601
661
|
if (!r.ok)
|
|
602
662
|
dropHistory();
|
|
603
663
|
else
|
|
604
|
-
toast(
|
|
664
|
+
toast(consLabel(c) + ' added');
|
|
605
665
|
afterEdit();
|
|
606
666
|
}
|
|
607
667
|
/** Run a bottom-bar action. `anchor` is the button pressed, for actions that open a popover next to it. */
|
|
@@ -654,12 +714,6 @@ export function applyCtxAction(act, anchor) {
|
|
|
654
714
|
tools[ed.tool].finish();
|
|
655
715
|
updateCtx();
|
|
656
716
|
return;
|
|
657
|
-
case 'dimDrive':
|
|
658
|
-
ed.dimLayer = 'drive';
|
|
659
|
-
break;
|
|
660
|
-
case 'dimPrint':
|
|
661
|
-
ed.dimLayer = 'print';
|
|
662
|
-
break;
|
|
663
717
|
case 'h':
|
|
664
718
|
case 'v':
|
|
665
719
|
case 'par':
|
|
@@ -676,15 +730,15 @@ export function applyCtxAction(act, anchor) {
|
|
|
676
730
|
return;
|
|
677
731
|
case 'len':
|
|
678
732
|
if (s.lines.length === 1)
|
|
679
|
-
dimLine(s.lines[0]
|
|
733
|
+
dimLine(s.lines[0]);
|
|
680
734
|
else if (s.wfaces.length === 1 && sk.lines[edgeOf(s.wfaces[0])[0]]) {
|
|
681
735
|
const [lid, side] = edgeOf(s.wfaces[0]);
|
|
682
|
-
dimLine(lid,
|
|
736
|
+
dimLine(lid, side);
|
|
683
737
|
}
|
|
684
738
|
return;
|
|
685
739
|
case 'pdist':
|
|
686
740
|
if (s.points.length === 2)
|
|
687
|
-
dimPoints(s.points[0], s.points[1]
|
|
741
|
+
dimPoints(s.points[0], s.points[1]);
|
|
688
742
|
return;
|
|
689
743
|
case 'pldist':
|
|
690
744
|
if (s.points.length === 1 && s.lines.length === 1)
|
|
@@ -705,9 +759,17 @@ export function applyCtxAction(act, anchor) {
|
|
|
705
759
|
cornerAngle(s.points[0]);
|
|
706
760
|
return;
|
|
707
761
|
case 'merge': {
|
|
762
|
+
// (every one into the first picked)
|
|
763
|
+
if (s.points.length < 2)
|
|
764
|
+
return;
|
|
708
765
|
pushHistory();
|
|
709
766
|
const snap = JSON.stringify(sk);
|
|
710
|
-
if (joinPoints(sk, s.points[0],
|
|
767
|
+
if (!s.points.slice(1).every(id => joinPoints(sk, s.points[0], id))) {
|
|
768
|
+
replaceSK(JSON.parse(snap));
|
|
769
|
+
dropHistory();
|
|
770
|
+
toast('Two locked points can’t be joined. Unlock one first.', { warn: true });
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
711
773
|
const r = solveSketch();
|
|
712
774
|
if (!r.ok) {
|
|
713
775
|
replaceSK(JSON.parse(snap));
|
|
@@ -747,14 +809,22 @@ export function applyCtxAction(act, anchor) {
|
|
|
747
809
|
requestDraw();
|
|
748
810
|
break;
|
|
749
811
|
}
|
|
750
|
-
case 'constr':
|
|
812
|
+
case 'constr': {
|
|
813
|
+
// all guides, or all solid again if they all are — one setting for the whole selection, as To wall is (flipping
|
|
814
|
+
// each swapped a guide and a line picked together). A wall is always solid: never made a guide, and one that's a
|
|
815
|
+
// guide from before goes back to solid
|
|
816
|
+
const objs = [...s.lines.filter(id => !sk.lines[id].anchor).map(id => sk.lines[id]), ...s.curves.map(id => curveInfo(sk, id)?.o)]
|
|
817
|
+
.filter((o) => !!o);
|
|
818
|
+
const lines = objs.filter(o => o.kind !== 'wall'), walls = objs.filter(o => o.kind === 'wall' && o.constr);
|
|
819
|
+
if (!lines.length && !walls.length)
|
|
820
|
+
return;
|
|
821
|
+
const guide = !lines.every(o => o.constr);
|
|
751
822
|
pushHistory();
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
s.curves.forEach(id => { const ci = curveInfo(sk, id); if (ci)
|
|
755
|
-
ci.o.constr = !ci.o.constr; });
|
|
823
|
+
lines.forEach(o => { o.constr = guide; });
|
|
824
|
+
walls.forEach(o => { o.constr = false; });
|
|
756
825
|
afterEdit();
|
|
757
826
|
return;
|
|
827
|
+
}
|
|
758
828
|
case 'kind': {
|
|
759
829
|
// one setting for the whole selection: all walls, unless they all are already (then all lines)
|
|
760
830
|
const targets = [...s.lines.map(id => sk.lines[id]), ...s.curves.map(id => curveInfo(sk, id)?.o).filter((o) => !!o)];
|
|
@@ -797,9 +867,9 @@ export function applyCtxAction(act, anchor) {
|
|
|
797
867
|
return;
|
|
798
868
|
}
|
|
799
869
|
case 'thick':
|
|
800
|
-
// straight and curved walls in the selection
|
|
870
|
+
// straight and curved walls in the selection (an edge's: its wall)
|
|
801
871
|
thicknessPrompt([
|
|
802
|
-
...s.
|
|
872
|
+
...withEdges(sk, s).map(id => sk.lines[id]).filter(l => l.kind === 'wall'),
|
|
803
873
|
...s.curves.map(id => curveInfo(sk, id)?.o).filter((o) => !!o && o.kind === 'wall'),
|
|
804
874
|
]);
|
|
805
875
|
return;
|
|
@@ -824,10 +894,11 @@ export function applyCtxAction(act, anchor) {
|
|
|
824
894
|
case 'renameRoom':
|
|
825
895
|
editRoomName(s.rooms[0]);
|
|
826
896
|
return;
|
|
897
|
+
// (the room is on the left of the wall's start → end when there's none either side of it)
|
|
827
898
|
case 'opFlip': {
|
|
828
899
|
pushHistory();
|
|
829
900
|
const o = sk.openings[s.openings[0]];
|
|
830
|
-
o
|
|
901
|
+
flipDoor(o, roomSideOf(sk, o.line) === -1 ? -1 : 1);
|
|
831
902
|
afterEdit();
|
|
832
903
|
return;
|
|
833
904
|
}
|
|
@@ -876,12 +947,12 @@ export function applyCtxAction(act, anchor) {
|
|
|
876
947
|
case 'palette':
|
|
877
948
|
openPalette(anchor);
|
|
878
949
|
return;
|
|
879
|
-
case 'convert':
|
|
880
|
-
convertNote(s.notes[0]);
|
|
881
|
-
return;
|
|
882
950
|
case 'linkOpen':
|
|
883
951
|
openSelectedLink();
|
|
884
952
|
return;
|
|
953
|
+
case 'linkExpand':
|
|
954
|
+
toggleExpand();
|
|
955
|
+
return;
|
|
885
956
|
case 'linkSize':
|
|
886
957
|
toggleTileSize(anchor);
|
|
887
958
|
return;
|
|
@@ -916,6 +987,9 @@ export function applyCtxAction(act, anchor) {
|
|
|
916
987
|
const lock = !ims.every(u => u.locked), many = ims.length > 1;
|
|
917
988
|
pushHistory();
|
|
918
989
|
ims.forEach(u => { u.locked = lock; });
|
|
990
|
+
// (locked, its bar is Lock and Open alone: an opacity or turning slider open over it goes with the rest)
|
|
991
|
+
if (lock)
|
|
992
|
+
dismissPopovers();
|
|
919
993
|
afterEdit();
|
|
920
994
|
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.')
|
|
921
995
|
: many ? 'Images unlocked.' : 'Image unlocked. Drag it, or drag its handles to size it.');
|