@reekon-tools/boldr-cad 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/README.md +21 -4
  2. package/dist/app/edit.d.ts +3 -3
  3. package/dist/app/edit.js +26 -1
  4. package/dist/app/edit.js.map +1 -1
  5. package/dist/app/state.d.ts +3 -5
  6. package/dist/app/state.js +0 -1
  7. package/dist/app/state.js.map +1 -1
  8. package/dist/data/sync.d.ts +1 -1
  9. package/dist/data/sync.js +7 -2
  10. package/dist/data/sync.js.map +1 -1
  11. package/dist/editor/actions.d.ts +8 -0
  12. package/dist/editor/actions.js +149 -169
  13. package/dist/editor/actions.js.map +1 -1
  14. package/dist/editor/elevation.d.ts +4 -1
  15. package/dist/editor/elevation.js +16 -17
  16. package/dist/editor/elevation.js.map +1 -1
  17. package/dist/editor/gestures.d.ts +21 -0
  18. package/dist/editor/gestures.js +92 -14
  19. package/dist/editor/gestures.js.map +1 -1
  20. package/dist/editor/keys.js +2 -2
  21. package/dist/editor/keys.js.map +1 -1
  22. package/dist/editor/linkActions.d.ts +22 -3
  23. package/dist/editor/linkActions.js +95 -13
  24. package/dist/editor/linkActions.js.map +1 -1
  25. package/dist/editor/mode.js +4 -3
  26. package/dist/editor/mode.js.map +1 -1
  27. package/dist/editor/project.js +1 -2
  28. package/dist/editor/project.js.map +1 -1
  29. package/dist/editor/scaleGate.d.ts +13 -0
  30. package/dist/editor/scaleGate.js +27 -0
  31. package/dist/editor/scaleGate.js.map +1 -0
  32. package/dist/editor/selection.js +13 -9
  33. package/dist/editor/selection.js.map +1 -1
  34. package/dist/editor/tooltips.js +9 -0
  35. package/dist/editor/tooltips.js.map +1 -1
  36. package/dist/editor/view3d.d.ts +4 -1
  37. package/dist/editor/view3d.js +6 -2
  38. package/dist/editor/view3d.js.map +1 -1
  39. package/dist/export/pdf.d.ts +5 -2
  40. package/dist/export/pdf.js +12 -8
  41. package/dist/export/pdf.js.map +1 -1
  42. package/dist/export/plan.d.ts +1 -1
  43. package/dist/export/plan.js +14 -9
  44. package/dist/export/plan.js.map +1 -1
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/model/demo.d.ts +1 -0
  49. package/dist/model/demo.js +2 -1
  50. package/dist/model/demo.js.map +1 -1
  51. package/dist/model/elevation.d.ts +1 -1
  52. package/dist/model/elevation.js +3 -3
  53. package/dist/model/elevation.js.map +1 -1
  54. package/dist/model/history.js +4 -1
  55. package/dist/model/history.js.map +1 -1
  56. package/dist/model/links.d.ts +4 -1
  57. package/dist/model/links.js +9 -2
  58. package/dist/model/links.js.map +1 -1
  59. package/dist/model/measurement.d.ts +2 -2
  60. package/dist/model/measurement.js +3 -3
  61. package/dist/model/measurement.js.map +1 -1
  62. package/dist/model/openings.d.ts +10 -0
  63. package/dist/model/openings.js +18 -0
  64. package/dist/model/openings.js.map +1 -1
  65. package/dist/model/planGeom.d.ts +37 -2
  66. package/dist/model/planGeom.js +72 -9
  67. package/dist/model/planGeom.js.map +1 -1
  68. package/dist/model/project.js +5 -2
  69. package/dist/model/project.js.map +1 -1
  70. package/dist/model/rooms.d.ts +18 -5
  71. package/dist/model/rooms.js +178 -73
  72. package/dist/model/rooms.js.map +1 -1
  73. package/dist/model/scale.d.ts +30 -0
  74. package/dist/model/scale.js +120 -0
  75. package/dist/model/scale.js.map +1 -0
  76. package/dist/model/settings.d.ts +0 -1
  77. package/dist/model/settings.js +1 -1
  78. package/dist/model/settings.js.map +1 -1
  79. package/dist/model/sketch.d.ts +21 -1
  80. package/dist/model/sketch.js +58 -4
  81. package/dist/model/sketch.js.map +1 -1
  82. package/dist/model/stroke.js +2 -2
  83. package/dist/model/types.d.ts +38 -7
  84. package/dist/react/BottomBar.d.ts +2 -1
  85. package/dist/react/BottomBar.js +94 -25
  86. package/dist/react/BottomBar.js.map +1 -1
  87. package/dist/react/Drawer.js +3 -2
  88. package/dist/react/Drawer.js.map +1 -1
  89. package/dist/react/Duck.js.map +1 -1
  90. package/dist/react/KeypadSheet.js.map +1 -1
  91. package/dist/react/MarkupPanel.js +1 -1
  92. package/dist/react/PlanCanvas.native.js +54 -7
  93. package/dist/react/PlanCanvas.native.js.map +1 -1
  94. package/dist/react/Popovers.js +14 -3
  95. package/dist/react/Popovers.js.map +1 -1
  96. package/dist/react/Rail.js +1 -1
  97. package/dist/react/Sheet.d.ts +2 -2
  98. package/dist/react/Sheet.js.map +1 -1
  99. package/dist/react/barScroll.d.ts +23 -0
  100. package/dist/react/barScroll.js +47 -0
  101. package/dist/react/barScroll.js.map +1 -0
  102. package/dist/react/corners.d.ts +6 -2
  103. package/dist/react/corners.js.map +1 -1
  104. package/dist/react/host.d.ts +7 -0
  105. package/dist/react/host.js +14 -1
  106. package/dist/react/host.js.map +1 -1
  107. package/dist/react/motion.d.ts +1 -1
  108. package/dist/react/motion.js +1 -1
  109. package/dist/react/motion.js.map +1 -1
  110. package/dist/render/draw.d.ts +21 -4
  111. package/dist/render/draw.js +184 -149
  112. package/dist/render/draw.js.map +1 -1
  113. package/dist/render/elevation.js +3 -3
  114. package/dist/render/elevation.js.map +1 -1
  115. package/dist/render/figmaIcons.d.ts +6 -0
  116. package/dist/render/figmaIcons.js +16 -1
  117. package/dist/render/figmaIcons.js.map +1 -1
  118. package/dist/render/hit.d.ts +8 -1
  119. package/dist/render/hit.js +23 -4
  120. package/dist/render/hit.js.map +1 -1
  121. package/dist/render/icons.js +27 -20
  122. package/dist/render/icons.js.map +1 -1
  123. package/dist/render/lens.d.ts +27 -0
  124. package/dist/render/lens.js +41 -0
  125. package/dist/render/lens.js.map +1 -0
  126. package/dist/render/linkTile.d.ts +20 -2
  127. package/dist/render/linkTile.js +476 -246
  128. package/dist/render/linkTile.js.map +1 -1
  129. package/dist/render/snap.d.ts +5 -1
  130. package/dist/render/snap.js +3 -1
  131. package/dist/render/snap.js.map +1 -1
  132. package/dist/render/svgPath.d.ts +29 -0
  133. package/dist/render/svgPath.js +114 -0
  134. package/dist/render/svgPath.js.map +1 -0
  135. package/dist/render/tileIcons.d.ts +18 -0
  136. package/dist/render/tileIcons.js +67 -0
  137. package/dist/render/tileIcons.js.map +1 -0
  138. package/dist/render/view.d.ts +18 -1
  139. package/dist/render/view.js +31 -4
  140. package/dist/render/view.js.map +1 -1
  141. package/dist/skia/SkiaDrawCtx.d.ts +8 -0
  142. package/dist/skia/SkiaDrawCtx.js +27 -5
  143. package/dist/skia/SkiaDrawCtx.js.map +1 -1
  144. package/dist/solver/equations.d.ts +6 -0
  145. package/dist/solver/equations.js +23 -6
  146. package/dist/solver/equations.js.map +1 -1
  147. package/dist/solver/gauss-newton.js +6 -1
  148. package/dist/solver/gauss-newton.js.map +1 -1
  149. package/dist/tools/annotate.d.ts +0 -2
  150. package/dist/tools/annotate.js +1 -22
  151. package/dist/tools/annotate.js.map +1 -1
  152. package/dist/tools/chamfer.js +4 -0
  153. package/dist/tools/chamfer.js.map +1 -1
  154. package/dist/tools/dim.d.ts +3 -3
  155. package/dist/tools/dim.js +26 -65
  156. package/dist/tools/dim.js.map +1 -1
  157. package/dist/tools/fillet.js +4 -0
  158. package/dist/tools/fillet.js.map +1 -1
  159. package/dist/tools/freehand.js +1 -4
  160. package/dist/tools/freehand.js.map +1 -1
  161. package/dist/tools/line.js +15 -11
  162. package/dist/tools/line.js.map +1 -1
  163. package/dist/tools/modify.js +6 -1
  164. package/dist/tools/modify.js.map +1 -1
  165. package/dist/tools/registry.d.ts +5 -0
  166. package/dist/tools/registry.js +6 -1
  167. package/dist/tools/registry.js.map +1 -1
  168. package/dist/tools/select.d.ts +19 -0
  169. package/dist/tools/select.js +116 -17
  170. package/dist/tools/select.js.map +1 -1
  171. package/dist/tools/shapes.d.ts +1 -1
  172. package/dist/tools/shapes.js +12 -5
  173. package/dist/tools/shapes.js.map +1 -1
  174. package/dist/tools/survey.d.ts +6 -0
  175. package/dist/tools/survey.js +28 -0
  176. package/dist/tools/survey.js.map +1 -1
  177. package/dist/view3d/build.js +9 -8
  178. package/dist/view3d/build.js.map +1 -1
  179. package/package.json +9 -9
  180. package/src/app/edit.ts +25 -1
  181. package/src/app/state.ts +3 -5
  182. package/src/data/sync.ts +7 -2
  183. package/src/editor/actions.ts +126 -112
  184. package/src/editor/elevation.ts +13 -16
  185. package/src/editor/gestures.ts +83 -13
  186. package/src/editor/keys.ts +2 -2
  187. package/src/editor/linkActions.ts +90 -13
  188. package/src/editor/mode.ts +4 -3
  189. package/src/editor/project.ts +1 -2
  190. package/src/editor/scaleGate.ts +32 -0
  191. package/src/editor/selection.ts +13 -7
  192. package/src/editor/tooltips.ts +8 -0
  193. package/src/editor/view3d.ts +6 -2
  194. package/src/export/pdf.ts +7 -4
  195. package/src/export/plan.ts +14 -10
  196. package/src/index.ts +1 -1
  197. package/src/model/demo.ts +2 -1
  198. package/src/model/elevation.ts +4 -4
  199. package/src/model/history.ts +4 -1
  200. package/src/model/links.ts +8 -2
  201. package/src/model/measurement.ts +3 -3
  202. package/src/model/openings.ts +18 -0
  203. package/src/model/planGeom.ts +70 -10
  204. package/src/model/project.ts +6 -2
  205. package/src/model/rooms.ts +151 -63
  206. package/src/model/scale.ts +105 -0
  207. package/src/model/settings.ts +1 -2
  208. package/src/model/sketch.ts +48 -4
  209. package/src/model/stroke.ts +2 -2
  210. package/src/model/types.ts +38 -7
  211. package/src/react/BottomBar.tsx +114 -29
  212. package/src/react/Drawer.tsx +14 -10
  213. package/src/react/Duck.tsx +3 -3
  214. package/src/react/KeypadSheet.tsx +2 -2
  215. package/src/react/MarkupPanel.tsx +1 -1
  216. package/src/react/PlanCanvas.native.tsx +60 -6
  217. package/src/react/Popovers.tsx +24 -9
  218. package/src/react/Rail.tsx +1 -1
  219. package/src/react/Sheet.tsx +3 -3
  220. package/src/react/barScroll.ts +57 -0
  221. package/src/react/corners.ts +6 -3
  222. package/src/react/host.ts +14 -1
  223. package/src/react/motion.tsx +6 -6
  224. package/src/render/draw.ts +156 -116
  225. package/src/render/elevation.ts +5 -4
  226. package/src/render/figmaIcons.ts +17 -1
  227. package/src/render/hit.ts +21 -4
  228. package/src/render/icons.ts +27 -20
  229. package/src/render/lens.ts +52 -0
  230. package/src/render/linkTile.ts +436 -190
  231. package/src/render/snap.ts +7 -2
  232. package/src/render/svgPath.ts +95 -0
  233. package/src/render/tileIcons.ts +75 -0
  234. package/src/render/view.ts +37 -5
  235. package/src/skia/SkiaDrawCtx.ts +26 -4
  236. package/src/solver/equations.ts +22 -6
  237. package/src/solver/gauss-newton.ts +4 -1
  238. package/src/styles.css +3 -2
  239. package/src/tools/annotate.ts +1 -18
  240. package/src/tools/chamfer.ts +3 -0
  241. package/src/tools/dim.ts +26 -52
  242. package/src/tools/fillet.ts +3 -0
  243. package/src/tools/freehand.ts +1 -3
  244. package/src/tools/line.ts +12 -11
  245. package/src/tools/modify.ts +5 -1
  246. package/src/tools/registry.ts +10 -1
  247. package/src/tools/select.ts +99 -18
  248. package/src/tools/shapes.ts +9 -4
  249. package/src/tools/survey.ts +21 -0
  250. package/src/ui/ctx.ts +4 -3
  251. package/src/ui/drawer.ts +8 -2
  252. package/src/view3d/build.ts +11 -10
@@ -2,13 +2,13 @@ import { afterEdit, holdFaces, solveSketch, tryAddConstraint, undo } from '../ap
2
2
  import { ed } from '../app/state';
3
3
  import { CONS_LABEL, measure } from '../model/constraints';
4
4
  import { dropHistory, pushHistory } from '../model/history';
5
- import { createsCycle, edgePoint, facingEdges, nearerEdge } from '../model/openings';
5
+ import { createsCycle, edgePoint, facingEdges, flipDoor, nearerEdge } from '../model/openings';
6
+ import { roomSideOf } from '../model/elevation';
6
7
  import { cabOfPoint } from '../model/faces';
7
8
  import { settings } from '../model/settings';
8
9
  import { connectedGeometry, curveInfo, deleteSel, joinPoints } from '../model/sketch';
9
10
  import { page, planSK, replaceSK, saveSoon, SK } from '../model/store';
10
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,13 +17,15 @@ 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, openingLabel, overflow, restoreFit, TOO_SHORT, wallAction, wallBar, wallDelete, wallPlus } from './elevation';
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);
@@ -33,24 +35,30 @@ function lockTargets() {
33
35
  return [...ids].filter(id => sk.points[id] && !sk.points[id].origin && !sk.points[id].anchor);
34
36
  }
35
37
  /**
36
- * Lock is always the first action, set off by a divider, and shows its state:
37
- * open padlock "Lock" (white) when free, closed padlock "Locked" (red) when locked.
38
+ * The bar leads with one button, its icon alone, set off by a divider (Figma's bars, 2528:101128: the user's call,
39
+ * 2026-10-27): Open, when what's selected is a link to something of the job's; otherwise Lock — an open padlock (white)
40
+ * when free, a closed one (red, "Locked") when locked. A link that can be locked too (an expanded photo) has Lock next,
41
+ * 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
42
+ * display the icon - no text needed below it").
38
43
  */
39
- function arrangeLock(items) {
40
- const i = items.findIndex(it => typeof it === 'object' && 'act' in it && (it.act === 'lock' || it.act === 'imgLock'));
41
- if (i < 0)
42
- return items;
43
- const it = items[i];
44
- const sk = SK();
45
- const locked = it.act === 'imgLock' ? selImages().length > 0 && selImages().every(u => u.locked) : (() => { const l = lockTargets(); return l.length > 0 && l.every(id => sk.points[id].fixed); })();
46
- Object.assign(it, { ic: locked ? 'lock' : 'unlock', label: locked ? 'Locked' : 'Lock', on: undefined, locked });
47
- const rest = items.filter((_, k) => k !== i);
48
- const out = [it];
49
- if (rest.length)
50
- out.push('sep');
51
- // tidy separators: none doubled, none leading or trailing
52
- for (const e of rest)
53
- if (!(e === 'sep' && (out[out.length - 1] === 'sep')))
44
+ function arrangeLead(items) {
45
+ const find = (acts) => items.find((it) => typeof it === 'object' && 'act' in it && acts.includes(it.act));
46
+ const open = find(['linkOpen', 'dimLinkOpen']), lock = find(['lock', 'imgLock']);
47
+ if (lock) {
48
+ const sk = SK();
49
+ 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); })();
50
+ Object.assign(lock, { ic: locked ? 'lock' : 'unlock', label: locked ? 'Locked' : 'Lock', on: undefined, locked, bare: true });
51
+ }
52
+ const lead = open ?? lock, out = [];
53
+ if (lead) {
54
+ lead.lead = true;
55
+ out.push(lead, 'sep');
56
+ if (open && lock)
57
+ out.push(lock, 'sep');
58
+ }
59
+ // the rest, with no divider doubled, first or last
60
+ for (const e of items)
61
+ if (e !== lead && !(open && e === lock) && !(e === 'sep' && (!out.length || out[out.length - 1] === 'sep')))
54
62
  out.push(e);
55
63
  while (out[out.length - 1] === 'sep')
56
64
  out.pop();
@@ -67,32 +75,27 @@ const PLACING = new Set(['door', 'window', 'wdoor', 'wwindow', 'cabinet']);
67
75
  function ctxItems() {
68
76
  const items = [], sk = SK(), s = selInfo();
69
77
  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
78
  if (ed.mode === 'annotate') {
85
79
  const ims = selImages();
86
80
  if (ims.length && ims.length === s.n) {
87
81
  // an image (or several): lock, its own scale, and which goes in front where they overlap
88
82
  // (showing and hiding them all is the Layers menu's)
89
83
  add('lock', 'Lock', 'imgLock');
90
- if (ims.length === 1 && !ims[0].locked)
84
+ // a photo of the job's, expanded from its tile: opening it, and Expand (on) to fold it back into its tile
85
+ const lk = ims.length === 1 ? ims[0].link : undefined;
86
+ if (lk && canOpenLink(lk))
87
+ add('open', 'Open', 'linkOpen');
88
+ // locked: unlocking it, and opening what it's a photo of — nothing that changes it (the user's call,
89
+ // 2026-10-30: "images in locked state: shouldn't have these options - only lock and link should be available")
90
+ if (ims.every(u => u.locked))
91
+ return items;
92
+ if (lk)
93
+ add('expand', 'Expand', 'linkExpand', { on: true });
94
+ if (ims.length === 1)
91
95
  add('calib', 'Set scale', 'calibtool');
92
96
  if (ims.length === 1)
93
97
  add('opacity', 'Opacity', 'imgOpacity');
94
- // (turning moves it: not while it's locked, as it can't be moved or sized then)
95
- if (ims.length === 1 && !ims[0].locked)
98
+ if (ims.length === 1)
96
99
  add('rotate', 'Rotate', 'imgRotate');
97
100
  if (ims.length === 1 && page().underlays.length > 1) {
98
101
  const at = page().underlays.indexOf(ims[0]), last = page().underlays.length - 1;
@@ -108,20 +111,19 @@ function ctxItems() {
108
111
  // 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
112
  if (canOpenLink(lk.link))
110
113
  add('open', 'Open', 'linkOpen');
114
+ // (a photo's: the photo whole, as one of the page's images)
115
+ if (canExpand(lk))
116
+ add('expand', 'Expand', 'linkExpand', { on: false });
111
117
  add('leader', 'Leader', 'linkLeader', { on: !!lk.leader });
112
118
  // (its leader's colour, as all markup's, is the rail's colour dot's)
113
119
  // (every tile on the page: a slider above the bar)
114
120
  add('tilesize', 'Size', 'linkSize');
115
121
  return items;
116
122
  }
117
- if (s.notes.length) {
118
- // (no Color: the rail's colour dot styles what's selected)
119
- const n = sk.notes.find(n => n.id === s.notes[0]);
120
- if (n && n.type === 'pen' && s.notes.length === 1)
121
- add('convert', 'To lines', 'convert');
122
- if (linkSizeVisible())
123
- add('tilesize', 'Size', 'linkSize');
124
- }
123
+ // markup: nothing of its own — a pen stroke as any other (no To lines: the user's call, 2026-10-27), and no Color
124
+ // (the rail's colour dot styles what's selected); with tiles among it, their size
125
+ if (s.notes.length && linkSizeVisible())
126
+ add('tilesize', 'Size', 'linkSize');
125
127
  // nothing selected: no bar (the markup colour is the rail's colour dot, for what's selected too; the image's own
126
128
  // options come with selecting it)
127
129
  return items;
@@ -143,11 +145,6 @@ function ctxItems() {
143
145
  add('finish', 'Finish', 'finish');
144
146
  return items;
145
147
  }
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
148
  // nothing selected: the bar stays empty (Fit and Icons live in the top bar; grid snap and auto-constrain in Settings).
152
149
  // Just placed a door, window or cabinet (its tool still on): its bar, for its sizes
153
150
  if (!s.n || (ed.tool !== 'select' && !PLACING.has(ed.tool)))
@@ -161,15 +158,36 @@ function ctxItems() {
161
158
  add(it.ic, it.label, it.act);
162
159
  return items;
163
160
  }
161
+ /*
162
+ Figma's bars (2528:101128, the user's call, 2026-10-27): Lock first, its icon alone (arrangeLead); then what the lines
163
+ and curves are (kind); then what ties them, or edits them. The yellow + measures: no Distance of its own here.
164
+ */
164
165
  // on a wall face, walls don't exist: no To wall / Thickness
165
166
  const wall = elevOpen();
166
167
  const P = s.points.length, L = s.lines.length, C = s.curves.length;
167
- // Guide (dashed) is for lines: a wall is always solid (one that's a guide from before can still go back to Solid)
168
- const allWalls = L + C > 0 && s.lines.every(id => sk.lines[id].kind === 'wall' && !sk.lines[id].constr) && s.curves.every(id => { const o = curveInfo(sk, id)?.o; return o?.kind === 'wall' && !o.constr; });
169
- const guide = () => { if (!allWalls)
170
- add('constr', 'Guide', 'constr'); };
171
- // a wall's edge: named, with its wall view; with another edge, a line or a point, the distance between them (two
172
- // edges of one wall: its thickness)
168
+ /**
169
+ * What the selected lines and curves are, and its divider: walls — one straight wall's wall view, and their Thickness;
170
+ * otherwise To wall, and Guide (Solid, when they all are guides). A wall is always solid; on a wall face, Guide alone.
171
+ */
172
+ const kind = () => {
173
+ const objs = [...s.lines.map(id => sk.lines[id]), ...s.curves.map(id => curveInfo(sk, id)?.o)].filter((o) => !!o);
174
+ if (!objs.length)
175
+ return;
176
+ if (objs.every(o => o.kind === 'wall' && !o.constr)) {
177
+ if (wall)
178
+ return;
179
+ if (s.n === 1 && L === 1)
180
+ add('elev', 'Elevation', 'elev');
181
+ add('thick', 'Thickness', 'thick');
182
+ }
183
+ else {
184
+ if (!wall)
185
+ add('wall', 'To wall', 'kind');
186
+ add('constr', objs.every(o => o.constr) ? 'Solid' : 'Guide', 'constr');
187
+ }
188
+ items.push('sep');
189
+ };
190
+ // a wall's edge: named, with its wall view (with another edge, a line or a point, the + is the distance between them)
173
191
  if (s.wfaces.length) {
174
192
  // (a curved wall has no wall view: just its name)
175
193
  if (s.n === 1) {
@@ -178,16 +196,12 @@ function ctxItems() {
178
196
  if (sk.lines[id])
179
197
  add('elev', 'Elevation', 'elev');
180
198
  }
181
- else if (edgePair(s)) {
182
- const one = s.wfaces.length === 2 && edgeOf(s.wfaces[0])[0] === edgeOf(s.wfaces[1])[0];
183
- add(one ? 'thick' : 'dim', one ? 'Thickness' : 'Distance', 'edgeDist');
184
- }
185
199
  return items;
186
200
  }
187
201
  if (s.n === 1 && s.dims.length) {
188
202
  const c = sk.cons[s.dims[0]];
189
203
  // 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), and no Reference (a measurement drives its line, or it isn't linked)
204
+ // call, 2026-10-21; its chip is drawn linked)
191
205
  if (c.measurementRef) {
192
206
  const t = c.measurementRef;
193
207
  if (canOpenLink(t))
@@ -196,10 +210,7 @@ function ctxItems() {
196
210
  }
197
211
  else if (canSaveDim(s.dims[0]))
198
212
  add('savegroup', 'Save to group', 'dimSave');
199
- // Reference frees what a dimension holds — a distance, say; not a cabinet's own width or height, which it keeps
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');
213
+ // (no Reference: Figma's dimension bars have none, 2026-10-27)
203
214
  // measured on walls: which of their faces (the drawing stays as it is; the number becomes what it measures there)
204
215
  const fc = faceChoices(sk, c);
205
216
  if (fc.length && items.length)
@@ -220,96 +231,71 @@ function ctxItems() {
220
231
  add('rename', 'Rename', 'renameRoom');
221
232
  return items;
222
233
  }
223
- // one end of a door or window: named; with another point, end or door/window, the distance between them
234
+ // one end of a door or window: named (with another point, end or door/window, the + is the distance between them)
224
235
  if (s.oanchors.length && s.n === 1) {
225
236
  items.push({ lbl: anchorLabel(s.oanchors[0]) });
226
237
  return items;
227
238
  }
228
- if (s.oanchors.length && s.n === 2 && s.oanchors.length + s.points.length + s.openings.length === 2) {
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');
239
+ if (s.oanchors.length || (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1))))
235
240
  return items;
236
- }
237
241
  if (s.n === 1 && s.openings.length) {
238
- // its sizes by name, as in the wall view (no +: it isn't one dimension); a door's swing too. (The type is fixed
239
- // once placed: Door and Window are separate tools.)
240
- const o = sk.openings[s.openings[0]];
241
- items.push({ lbl: openingLabel(o) });
242
+ // its sizes (no +: it isn't one dimension), with nothing saying them — the drawing does (the user's call, 2026-10-27);
243
+ // a door's Swing too, round its four ways (it was Flip: the user's call, 2026-10-27, "ok do swing instead"). (The
244
+ // type is fixed once placed: Door and Window are separate tools.)
242
245
  for (const it of OPENING_SIZES)
243
246
  add(it.ic, it.label, it.act);
244
- if (o.type === 'door') {
247
+ if (sk.openings[s.openings[0]].type === 'door') {
245
248
  items.push('sep');
246
- add('flip', 'Flip', 'opFlip');
249
+ add('flip', 'Swing', 'opFlip');
247
250
  }
248
251
  return items;
249
252
  }
253
+ // a cabinet's corner: nothing of its own (pick another point or line with it to dimension or constrain)
254
+ if (P === 1 && s.n === 1 && cabOfPoint(sk, s.points[0]))
255
+ return items;
256
+ add('lock', 'Lock', 'lock', { key: '⇧F' });
250
257
  if (L === 1 && s.n === 1) {
251
- const l = sk.lines[s.lines[0]];
258
+ kind();
259
+ // (its length: the yellow +)
252
260
  add('h', 'Horizontal', 'h', { key: '⇧H' });
253
- add('v', 'Vertical', 'v', { key: '⇧V' }); // (its length: the yellow +)
254
- items.push('sep');
255
- add('lock', 'Lock', 'lock', { key: '⇧F' });
261
+ add('v', 'Vertical', 'v', { key: '⇧V' });
256
262
  add('split', 'Split', 'split');
257
263
  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
264
  return items;
275
265
  }
276
266
  if (L === 2 && s.n === 2) {
277
- // (no Angle here: the yellow + is the angle between them — or, parallel, the distance)
267
+ // (no Angle: the yellow + is the angle between them — or, parallel, the distance)
268
+ kind();
278
269
  add('par', 'Parallel', 'par', { key: '⇧P' });
279
270
  add('perp', 'Perpendicular', 'perp', { key: '⇧R' });
280
271
  add('col', 'Collinear', 'col', { key: '⇧L' });
281
272
  add('eq', 'Equal', 'eq', { key: '⇧E' });
282
273
  // two lines that meet: round off (or cut off) the corner between them
283
274
  if (filletCorner2()) {
275
+ items.push('sep');
284
276
  add('fillet', 'Fillet', 'fillet');
285
277
  add('chamfer', 'Chamfer', 'chamfer');
286
278
  }
287
- wallItems();
288
279
  return items;
289
280
  }
290
- // a cabinet's corner: nothing of its own (pick another point or line with it to dimension or constrain)
291
- if (P === 1 && s.n === 1 && cabOfPoint(sk, s.points[0]))
292
- return items;
281
+ // a corner where exactly two lines meet: rounding it off or cutting it off (its angle is the yellow +)
293
282
  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
283
  if (filletable(sk, s.points[0])) {
296
284
  add('fillet', 'Fillet', 'fillet');
297
285
  add('chamfer', 'Chamfer', 'chamfer');
298
286
  }
299
- add('lock', 'Lock', 'lock', { key: '⇧F' });
300
287
  return items;
301
288
  }
302
- if (P === 2 && s.n === 2) {
289
+ // points: joined into the first picked, or level or plumb with it
290
+ if (P >= 2 && s.n === P) {
291
+ add('merge', 'Join', 'merge');
303
292
  add('h', 'Horizontal', 'h');
304
293
  add('v', 'Vertical', 'v');
305
- add('dim', 'Distance', 'pdist');
306
- add('merge', 'Join', 'merge');
307
294
  return items;
308
295
  }
309
296
  if (P === 1 && L === 1 && s.n === 2) {
310
- add('mid', 'Midpoint', 'mid', { key: '⇧M' });
311
297
  add('online', 'On line', 'online', { key: '⇧O' });
312
- add('dim', 'Distance', 'pldist');
298
+ add('mid', 'Midpoint', 'mid', { key: '⇧M' });
313
299
  return items;
314
300
  }
315
301
  if (P === 1 && C === 1 && s.n === 2) {
@@ -317,51 +303,35 @@ function ctxItems() {
317
303
  return items;
318
304
  }
319
305
  if (L === 1 && C === 1 && s.n === 2) {
306
+ kind();
320
307
  add('tan', 'Tangent', 'tan', { key: '⇧T' });
321
- wallItems();
322
308
  return items;
323
309
  }
324
310
  // a circle or arc: the yellow + sets its diameter
325
311
  if (C === 1 && s.n === 1) {
326
- const ci = curveInfo(sk, s.curves[0]);
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');
312
+ kind();
336
313
  return items;
337
314
  }
338
315
  if (C === 2 && s.n === 2) {
339
- add('eq', 'Equal', 'eqr');
316
+ kind();
340
317
  add('conc', 'Concentric', 'conc');
341
- wallItems();
318
+ add('eq', 'Equal', 'eqr');
342
319
  return items;
343
320
  }
344
321
  // 3+ lines (only lines): the constraints that make sense across many, each tied to the first line picked
345
322
  if (L >= 3 && s.n === L) {
323
+ kind();
346
324
  add('h', 'Horizontal', 'h', { key: '⇧H' });
347
325
  add('v', 'Vertical', 'v', { key: '⇧V' });
348
326
  add('par', 'Parallel', 'par', { key: '⇧P' });
349
327
  add('col', 'Collinear', 'col', { key: '⇧L' });
350
328
  add('eq', 'Equal', 'eq', { key: '⇧E' });
351
- items.push('sep');
352
- add('lock', 'Lock', 'lock');
353
- guide();
354
- wallItems();
355
329
  return items;
356
330
  }
357
331
  if (L >= 2) {
332
+ kind();
358
333
  add('eq', 'Equal', 'eqall');
359
- add('lock', 'Lock', 'lock');
360
- guide();
361
- wallItems();
362
- return items;
363
334
  }
364
- add('lock', 'Lock', 'lock');
365
335
  return items;
366
336
  }
367
337
  /** The keypad for walls' thickness, finished face to face. */
@@ -400,7 +370,7 @@ function filletCorner2() {
400
370
  return p && filletable(sk, p) ? p : null;
401
371
  }
402
372
  /** Buttons for the bottom bar: they adapt to the tool and the selection (Lock first, when offered). */
403
- export const barItems = () => arrangeLock(ctxItems());
373
+ export const barItems = () => arrangeLead(ctxItems());
404
374
  /** The yellow + only appears for something it can measure: not for constraint badges or spaces alone. */
405
375
  const DRAW_TOOLS = new Set(['line', 'freehand', 'rect', 'circle', 'arc']);
406
376
  export function plusVisible() {
@@ -431,12 +401,8 @@ export function plusAction() {
431
401
  if (wallPlus())
432
402
  return;
433
403
  const s = selInfo(), sk = SK();
434
- if (s.dims.length === 1 && s.n === 1) {
435
- if (sk.cons[s.dims[0]]?.layer === 'print')
436
- toast('Print dimensions are labels only — drag to move them.', { warn: true });
437
- else
438
- editDim(s.dims[0]);
439
- }
404
+ if (s.dims.length === 1 && s.n === 1)
405
+ editDim(s.dims[0]);
440
406
  else if (s.odims.length === 1 && s.n === 1) {
441
407
  const [op, part] = s.odims[0].split('|');
442
408
  editOpeningDim(op, part);
@@ -446,14 +412,14 @@ export function plusAction() {
446
412
  else if (s.wfaces.length === 1 && s.n === 1) {
447
413
  const [id, side] = edgeOf(s.wfaces[0]);
448
414
  if (sk.lines[id])
449
- dimLine(id, false, side);
415
+ dimLine(id, side);
450
416
  else
451
417
  dimCurve(id, side);
452
418
  }
453
419
  else if (edgePair(s))
454
420
  applyCtxAction('edgeDist');
455
421
  else if (s.lines.length === 1 && s.n === 1)
456
- dimLine(s.lines[0], false);
422
+ dimLine(s.lines[0]);
457
423
  else if (s.curves.length === 1 && s.n === 1)
458
424
  dimCurve(s.curves[0]);
459
425
  else if (s.openings.length === 1 && s.n === 1)
@@ -469,7 +435,7 @@ export function plusAction() {
469
435
  else if (s.n === 2 && s.lines.length === 2)
470
436
  dimTwoLines(s.lines[0], s.lines[1]);
471
437
  else if (s.n === 2 && s.points.length === 2)
472
- dimPoints(s.points[0], s.points[1], false);
438
+ dimPoints(s.points[0], s.points[1]);
473
439
  else if (s.n === 2 && s.points.length === 1 && s.lines.length === 1)
474
440
  dimPointLine(s.points[0], s.lines[0]);
475
441
  else
@@ -540,9 +506,14 @@ const NEEDS = {
540
506
  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
507
  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
508
  };
543
- /** H/V on each selected line, or parallel/collinear/equal from the first selected line to each of the others. */
509
+ /**
510
+ * H/V on each selected line, or parallel/collinear/equal from the first selected line to each of the others; three
511
+ * or more points, each level or plumb with the first picked.
512
+ */
544
513
  function multiLineCons(type) {
545
514
  const sk = SK(), s = selInfo();
515
+ if ((type === 'h' || type === 'v') && s.n === s.points.length && s.points.length >= 3)
516
+ return s.points.slice(1).map(id => ({ type, a: s.points[0], b: id }));
546
517
  if (s.n !== s.lines.length || s.lines.length < (type === 'h' || type === 'v' ? 2 : 3))
547
518
  return null;
548
519
  if (type === 'h' || type === 'v')
@@ -552,8 +523,11 @@ function multiLineCons(type) {
552
523
  return null;
553
524
  }
554
525
  const ADJ = { h: 'horizontal', v: 'vertical', par: 'parallel', col: 'collinear', eq: 'equal' };
555
- /** Apply one constraint type across several lines as one undo step. A conflict opens the conflict sheet for the first one that fails. */
556
- function applyMulti(type, list) {
526
+ /**
527
+ * Apply one constraint type across several lines (or `points`, each tied to the first) as one undo step. A conflict
528
+ * opens the conflict sheet for the first one that fails.
529
+ */
530
+ function applyMulti(type, list, points = false) {
557
531
  pushHistory();
558
532
  let added = 0;
559
533
  const failed = [];
@@ -572,9 +546,9 @@ function applyMulti(type, list) {
572
546
  }
573
547
  if (!added)
574
548
  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');
549
+ // every selected line (or point) that now satisfies it (some may already have, through other constraints)
550
+ const lines = ((type === 'h' || type === 'v') && !points ? list.length : list.length + 1) - failed.length;
551
+ const nLines = lines + (points ? ' point' : ' line') + (lines === 1 ? '' : 's');
578
552
  if (failed.length) {
579
553
  // explain the first conflict; the sheet's Remove re-applies it
580
554
  tryAddConstraint(failed[0], { onResolved: () => afterEdit() });
@@ -586,9 +560,9 @@ function applyMulti(type, list) {
586
560
  afterEdit();
587
561
  }
588
562
  function applyCons(type) {
589
- const multi = multiLineCons(type);
590
- if (multi && (multi.length > 1 || selInfo().lines.length > 2)) {
591
- applyMulti(type, multi);
563
+ const multi = multiLineCons(type), s = selInfo(), points = s.points.length > 2 && s.n === s.points.length;
564
+ if (multi && (multi.length > 1 || s.lines.length > 2 || points)) {
565
+ applyMulti(type, multi, points);
592
566
  return;
593
567
  }
594
568
  const c = consFromSel(type);
@@ -654,12 +628,6 @@ export function applyCtxAction(act, anchor) {
654
628
  tools[ed.tool].finish();
655
629
  updateCtx();
656
630
  return;
657
- case 'dimDrive':
658
- ed.dimLayer = 'drive';
659
- break;
660
- case 'dimPrint':
661
- ed.dimLayer = 'print';
662
- break;
663
631
  case 'h':
664
632
  case 'v':
665
633
  case 'par':
@@ -676,15 +644,15 @@ export function applyCtxAction(act, anchor) {
676
644
  return;
677
645
  case 'len':
678
646
  if (s.lines.length === 1)
679
- dimLine(s.lines[0], false);
647
+ dimLine(s.lines[0]);
680
648
  else if (s.wfaces.length === 1 && sk.lines[edgeOf(s.wfaces[0])[0]]) {
681
649
  const [lid, side] = edgeOf(s.wfaces[0]);
682
- dimLine(lid, false, side);
650
+ dimLine(lid, side);
683
651
  }
684
652
  return;
685
653
  case 'pdist':
686
654
  if (s.points.length === 2)
687
- dimPoints(s.points[0], s.points[1], false);
655
+ dimPoints(s.points[0], s.points[1]);
688
656
  return;
689
657
  case 'pldist':
690
658
  if (s.points.length === 1 && s.lines.length === 1)
@@ -705,9 +673,17 @@ export function applyCtxAction(act, anchor) {
705
673
  cornerAngle(s.points[0]);
706
674
  return;
707
675
  case 'merge': {
676
+ // (every one into the first picked)
677
+ if (s.points.length < 2)
678
+ return;
708
679
  pushHistory();
709
680
  const snap = JSON.stringify(sk);
710
- if (joinPoints(sk, s.points[0], s.points[1])) {
681
+ if (!s.points.slice(1).every(id => joinPoints(sk, s.points[0], id))) {
682
+ replaceSK(JSON.parse(snap));
683
+ dropHistory();
684
+ toast('Two locked points can’t be joined. Unlock one first.', { warn: true });
685
+ }
686
+ else {
711
687
  const r = solveSketch();
712
688
  if (!r.ok) {
713
689
  replaceSK(JSON.parse(snap));
@@ -824,10 +800,11 @@ export function applyCtxAction(act, anchor) {
824
800
  case 'renameRoom':
825
801
  editRoomName(s.rooms[0]);
826
802
  return;
803
+ // (the room is on the left of the wall's start → end when there's none either side of it)
827
804
  case 'opFlip': {
828
805
  pushHistory();
829
806
  const o = sk.openings[s.openings[0]];
830
- o.flip = !o.flip;
807
+ flipDoor(o, roomSideOf(sk, o.line) === -1 ? -1 : 1);
831
808
  afterEdit();
832
809
  return;
833
810
  }
@@ -876,12 +853,12 @@ export function applyCtxAction(act, anchor) {
876
853
  case 'palette':
877
854
  openPalette(anchor);
878
855
  return;
879
- case 'convert':
880
- convertNote(s.notes[0]);
881
- return;
882
856
  case 'linkOpen':
883
857
  openSelectedLink();
884
858
  return;
859
+ case 'linkExpand':
860
+ toggleExpand();
861
+ return;
885
862
  case 'linkSize':
886
863
  toggleTileSize(anchor);
887
864
  return;
@@ -916,6 +893,9 @@ export function applyCtxAction(act, anchor) {
916
893
  const lock = !ims.every(u => u.locked), many = ims.length > 1;
917
894
  pushHistory();
918
895
  ims.forEach(u => { u.locked = lock; });
896
+ // (locked, its bar is Lock and Open alone: an opacity or turning slider open over it goes with the rest)
897
+ if (lock)
898
+ dismissPopovers();
919
899
  afterEdit();
920
900
  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
901
  : many ? 'Images unlocked.' : 'Image unlocked. Drag it, or drag its handles to size it.');