@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
@@ -1,8 +1,8 @@
1
1
  import { ed, isSel } from '../app/state';
2
2
  import { consPoints, curveDefined, isDim, lineDefined, measure } from '../model/constraints';
3
3
  import { hyp, norm2, normAng, projT } from '../model/geom';
4
- import { drivingDim, edgePoint, measureOpeningDim, refPoint, refS, widthEntered } from '../model/openings';
5
- import { arcAngles, curveFrame, dimEnds, dimRadius, edgeOf, gridSteps, OPENING_TICK_EXT, openingFrame, outsideSide, wallThick } from '../model/planGeom';
4
+ import { drivingDim, edgePoint, hingeEdge, measureOpeningDim, refPoint, refS, widthEntered } from '../model/openings';
5
+ import { angleCorner, angleLabel, arcAngles, curveFrame, dimEnds, dimRadius, edgeOf, gridSteps, OPENING_TICK_EXT, openingFrame, outsideSide, wallThick } from '../model/planGeom';
6
6
  import { faceCorner } from '../model/wallfaces';
7
7
  import { roomGeom } from '../model/rooms';
8
8
  import { wallPieces } from '../model/wallshape';
@@ -15,12 +15,14 @@ import { cabPoints } from '../model/faces';
15
15
  import { drawKeyPlan, drawWallBackdrop, drawWallChips, drawWallSelection } from './elevation';
16
16
  import { fmtAng, fmtArea, fmtLen } from '../units';
17
17
  import { FONT, TH } from './theme';
18
- import { cvs, imgCache, keyPlanSize, mainT, nextThumbFrame, noteFont, noteHandles, noteTextBox, S, sketchBounds, underlayH, underlayHandles, underlayMid } from './view';
18
+ import { cvs, imageLinkBadge, imgCache, keyPlanSize, mainT, nextThumbFrame, noteFont, noteHandles, noteTextBox, S, sketchBounds, underlayH, underlayHandles, underlayMid, underlaySrc } from './view';
19
19
  import { dashFor, isSegment, NOTE_TEXT_SIZE, noteLook, shapeOutline } from '../model/markup';
20
20
  import { strokeMarkup, strokeOutline } from './markup';
21
21
  import { drawLink } from './linkTile';
22
22
  import { isLink, linkInfoOf } from '../model/links';
23
23
  import { smoothRectPath } from './squircle';
24
+ import { LINK_SVG } from './figmaIcons';
25
+ import { drawSvgIcon, svgIcon } from './svgPath';
24
26
  /** Tap targets registered while drawPlan() paints (null outside it: thumbnails and the elevation register none). */
25
27
  let hits = null;
26
28
  const addHit = (h) => { if (hits)
@@ -39,26 +41,29 @@ export function linkBlue() {
39
41
  const dark = !!m && (parseInt(m[1], 16) * 299 + parseInt(m[2], 16) * 587 + parseInt(m[3], 16) * 114) / 1000 < 128;
40
42
  return dark ? TH.lblue : TH.blue;
41
43
  }
42
- /** A small chain-link mark centred on (x, y). */
43
- export function linkMark(c, x, y, color) {
44
- c.save();
45
- c.translate(x, y);
46
- c.rotate(-Math.PI / 4);
47
- c.strokeStyle = color;
48
- c.lineWidth = 1.4;
49
- c.beginPath();
50
- c.roundRect(-6.5, -2.25, 7.5, 4.5, 2.25);
51
- c.roundRect(-1, -2.25, 7.5, 4.5, 2.25);
52
- c.stroke();
53
- c.restore();
44
+ /** Figma's link icon (render/figmaIcons.ts), read once. */
45
+ const LINK_ICON = svgIcon(LINK_SVG);
46
+ /** The link icon, `size` across, centred on (x, y): in its own dark grey, or `color`. */
47
+ export function linkIcon(c, x, y, size, color) {
48
+ drawSvgIcon(c, LINK_ICON, 'link', x - size / 2, y - size / 2, size, color);
54
49
  }
55
- /** What a link mark takes before a chip's text. */
56
- const MARK_W = 14;
57
- /** Rounded label on the canvas; registers a hit region when `kind` is given. */
58
- export function chip(c, text, cx, cy, { bg, fg, border, font = '600 12px', pad = 7, h = 22, minW = 0, kind, id, rot = 0, mark }) {
50
+ /**
51
+ * A linked measurement's badge, as the frame draws it on its 30 px chip (Figma 2795:77149): 5 in from the chip's edges,
52
+ * 20 across with the 12 px link icon in its middle, 10 before the value, and the value 5 from the chip's end. A linked
53
+ * chip is the size of a plain one (the user's call, 2026-10-25: "the same size as the regular measurement tiles"), so
54
+ * the badge is drawn at the chip's own height: all of it scaled by h / 30.
55
+ */
56
+ const LINKED = { h: 30, pad: 5, badge: 20, badgeR: 3.826, icon: 12, gap: 10 };
57
+ /** The width and height chip() draws `text` at (and its font, set on `c`). */
58
+ export function chipSize(c, text, { font = '600 12px', pad = 7, h = 22, minW = 0, mark }) {
59
59
  c.font = font + ' ' + FONT;
60
- const mw = mark ? MARK_W : 0, w = Math.max(minW, c.measureText(text).width + pad * 2 + mw);
61
- const x = cx - w / 2, y = cy - h / 2;
60
+ const room = mark ? (LINKED.pad * 2 + LINKED.badge + LINKED.gap) * (h / LINKED.h) : pad * 2;
61
+ return { w: Math.max(minW, c.measureText(text).width + room), h };
62
+ }
63
+ /** Rounded label on the canvas; registers a hit region when `kind` is given. */
64
+ export function chip(c, text, cx, cy, opts) {
65
+ const { bg, fg, border, kind, id, rot = 0, mark } = opts;
66
+ const { w, h } = chipSize(c, text, opts), x = cx - w / 2, y = cy - h / 2;
62
67
  c.save();
63
68
  if (rot) {
64
69
  c.translate(cx, cy);
@@ -76,9 +81,16 @@ export function chip(c, text, cx, cy, { bg, fg, border, font = '600 12px', pad =
76
81
  c.fillStyle = fg;
77
82
  c.textAlign = 'center';
78
83
  c.textBaseline = 'middle';
79
- c.fillText(text, cx + mw / 2, cy + 0.5);
80
- if (mark)
81
- linkMark(c, x + pad + 4.5, cy, fg);
84
+ if (mark) {
85
+ const k = h / LINKED.h, inset = LINKED.pad * k, bw = LINKED.badge * k, bx = x + inset, tx = bx + bw + LINKED.gap * k;
86
+ c.fillText(text, (tx + x + w - inset) / 2, cy + 0.5);
87
+ rr(c, bx, cy - bw / 2, bw, bw, LINKED.badgeR * k);
88
+ c.fillStyle = '#FFFFFF';
89
+ c.fill();
90
+ linkIcon(c, bx + bw / 2, cy, LINKED.icon * k);
91
+ }
92
+ else
93
+ c.fillText(text, cx, cy + 0.5);
82
94
  c.restore();
83
95
  if (kind) {
84
96
  const ext = rot ? Math.max(w, h) / 2 : 0;
@@ -118,8 +130,9 @@ function drawUnderlay(c, T) {
118
130
  if (!settings.layers.underlay)
119
131
  return;
120
132
  for (const u of page().underlays) {
121
- const im = imgCache(u.src);
122
- if (!im || !im.complete || !im.naturalWidth)
133
+ const im = imgCache(underlaySrc(u)), ready = !!im && im.complete && !!im.naturalWidth;
134
+ // (an expanded photo whose picture hasn't come yet, or is gone: where it is, as its tile's card)
135
+ if (!ready && !u.link)
123
136
  continue;
124
137
  // drawn about its middle, turned as it's set (clockwise as seen: the canvas turns that way)
125
138
  const h = underlayH(u), m = underlayMid(u), [x, y] = S(T, m.x, m.y), w = u.w * T.s, hh = h * T.s;
@@ -128,7 +141,48 @@ function drawUnderlay(c, T) {
128
141
  c.translate(x, y);
129
142
  if (u.rot)
130
143
  c.rotate((u.rot * Math.PI) / 180);
131
- c.drawImage(im, -w / 2, -hh / 2, w, hh);
144
+ if (ready)
145
+ c.drawImage(im, -w / 2, -hh / 2, w, hh);
146
+ else {
147
+ c.fillStyle = '#2B2C2E';
148
+ c.fillRect(-w / 2, -hh / 2, w, hh);
149
+ if (linkInfoOf(u.link)?.exists === false) {
150
+ c.font = '500 13px ' + FONT;
151
+ c.fillStyle = '#9FA0A2';
152
+ c.textAlign = 'center';
153
+ c.textBaseline = 'middle';
154
+ c.fillText('Removed', 0, 0);
155
+ }
156
+ }
157
+ c.restore();
158
+ }
159
+ }
160
+ /**
161
+ * Each expanded photo's badge: its link icon in a white disc in its top-right corner (imageLinkBadge), over the drawing
162
+ * — always, as Skia's, selected or not, locked or not: it says the photo is the job's, and a tap on it opens it.
163
+ */
164
+ function drawImageLinks(c, T) {
165
+ if (!settings.layers.underlay)
166
+ return;
167
+ const { W, H } = cvs;
168
+ for (const u of page().underlays) {
169
+ const b = imageLinkBadge(T, u);
170
+ if (!b || b.x < -b.r || b.y < -b.r || b.x > W + b.r || b.y > H + b.r)
171
+ continue;
172
+ const r = b.r, disc = (cc, X, Y) => { cc.beginPath(); cc.arc(X + r, Y + r, r, 0, Math.PI * 2); };
173
+ c.save();
174
+ if (ed.mode === 'draft')
175
+ c.globalAlpha = 0.75;
176
+ if (c.shape)
177
+ c.shape('image link ' + r, b.x - r, b.y - r, disc);
178
+ else
179
+ disc(c, b.x - r, b.y - r);
180
+ c.fillStyle = '#FFFFFF';
181
+ c.fill();
182
+ c.lineWidth = 1;
183
+ c.strokeStyle = 'rgba(24,26,28,.2)';
184
+ c.stroke();
185
+ linkIcon(c, b.x, b.y, r * 1.2, '#181A1C');
132
186
  c.restore();
133
187
  }
134
188
  }
@@ -147,20 +201,49 @@ function drawRooms(c, T, sk, thumb) {
147
201
  if (thumb)
148
202
  continue;
149
203
  const [cx, cy] = S(T, g.c.x, g.c.y);
204
+ // (its area and perimeter once the drawing has its scale: before, only placeholders — just its name)
205
+ const nm = r.name, info = sk.unscaled ? '' : fmtArea(g.area) + ' · ' + fmtLen(g.per) + ' perim.';
206
+ c.font = '600 14px ' + FONT;
207
+ const nw = c.measureText(nm).width;
208
+ c.font = '500 12px ' + FONT;
209
+ const iw = info ? c.measureText(info).width : 0;
210
+ const show = spaceLabel(T, g.inner, nw, iw);
211
+ if (!show.name)
212
+ continue;
150
213
  c.textAlign = 'center';
151
214
  c.textBaseline = 'middle';
152
215
  c.fillStyle = TH.label;
153
216
  c.font = '600 14px ' + FONT;
154
- const nm = r.name;
155
- c.fillText(nm, cx, cy - 9);
156
- c.font = '500 12px ' + FONT;
157
- c.fillStyle = TH.ink2;
158
- c.fillText(fmtArea(g.area) + ' · ' + fmtLen(g.per) + ' perim.', cx, cy + 9);
159
- c.font = '600 14px ' + FONT;
160
- const w = Math.max(c.measureText(nm).width, 140);
217
+ c.fillText(nm, cx, show.info ? cy - 9 : cy);
218
+ if (show.info) {
219
+ c.font = '500 12px ' + FONT;
220
+ c.fillStyle = TH.ink2;
221
+ c.fillText(info, cx, cy + 9);
222
+ }
223
+ const w = Math.max(nw, 140);
161
224
  addHit({ kind: 'room', id, x: cx - w / 2, y: cy - 20, w, h: 40 });
162
225
  }
163
226
  }
227
+ /** Room left round a space's label on the screen, each side, points. */
228
+ const SPACE_LABEL_PAD = 8;
229
+ /**
230
+ * Which of a space's label shows: only what fits inside the space on the screen, so zoomed out it goes rather than
231
+ * cluttering the drawing (the user's call, 2026-10-29, as a tile's name goes: LINK_FULL_PX) — its area and perimeter
232
+ * first, then its name. `nameW`, `infoW`: their widths (no area line: 0).
233
+ */
234
+ export function spaceLabel(T, inner, nameW, infoW) {
235
+ let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
236
+ for (const p of inner) {
237
+ x0 = Math.min(x0, p.x);
238
+ x1 = Math.max(x1, p.x);
239
+ y0 = Math.min(y0, p.y);
240
+ y1 = Math.max(y1, p.y);
241
+ }
242
+ const w = (x1 - x0) * T.s, h = (y1 - y0) * T.s, pad = 2 * SPACE_LABEL_PAD;
243
+ // (one line of 14 pt; two, 18 apart, of 14 and 12)
244
+ const name = w >= nameW + pad && h >= 18 + pad;
245
+ return { name, info: name && infoW > 0 && w >= infoW + pad && h >= 36 + pad };
246
+ }
164
247
  function curveWalls(sk) {
165
248
  const out = [];
166
249
  const show = (constr) => !(constr && !settings.layers.construction);
@@ -443,14 +526,16 @@ function drawOpenings(c, T, sk) {
443
526
  c.stroke();
444
527
  }
445
528
  else {
446
- const hinge = P(s0, (f * t) / 2), leaf = P(s0, f * (t / 2 + w));
529
+ // the leaf, open square to the wall from the jamb it hangs on, and its swing back to the other
530
+ const [hs, ls] = hingeEdge(o) === 's1' ? [s1, s0] : [s0, s1];
531
+ const hinge = P(hs, (f * t) / 2), leaf = P(hs, f * (t / 2 + w));
447
532
  c.beginPath();
448
533
  c.moveTo(hinge[0], hinge[1]);
449
534
  c.lineTo(leaf[0], leaf[1]);
450
535
  c.stroke();
451
536
  const [hx, hy] = hinge, r = w * T.s;
452
537
  const a0 = Math.atan2(leaf[1] - hy, leaf[0] - hx);
453
- const end = P(s1, (f * t) / 2);
538
+ const end = P(ls, (f * t) / 2);
454
539
  const a1 = Math.atan2(end[1] - hy, end[0] - hx);
455
540
  c.beginPath();
456
541
  c.setLineDash([4, 3]);
@@ -578,12 +663,31 @@ function drawLock(c, x, y) {
578
663
  c.fillRect(x - 3.3, y - 1.2, 6.6, 4.6);
579
664
  c.restore();
580
665
  }
666
+ /** The selected points and the selected lines' ends: what a constraint's icon still shows for with the icons off. */
667
+ function selPoints(sk) {
668
+ const out = new Set();
669
+ for (const s of ed.sel) {
670
+ if (s.type === 'line') {
671
+ const l = sk.lines[s.id];
672
+ if (l) {
673
+ out.add(l.a);
674
+ out.add(l.b);
675
+ }
676
+ }
677
+ if (s.type === 'point')
678
+ out.add(s.id);
679
+ }
680
+ return out;
681
+ }
581
682
  /** Points: filled = fully defined, hollow = still free (shape cue paired with color). */
582
683
  function drawPoints(c, T, sk) {
583
684
  const { W, H } = cvs;
584
685
  const touch = ed.lastPointerTouch;
585
686
  // a cabinet's corners aren't grab points: it moves as one, like a door or window
586
687
  const cabCorners = new Set(Object.values(sk.cabs ?? {}).flatMap(cabPoints));
688
+ // a locked point's padlock is a constraint icon: with the icons off, it shows only as the badges do, on what's
689
+ // selected (the user's call, 2026-10-26)
690
+ const locks = settings.showCons ? null : selPoints(sk);
587
691
  for (const id in sk.points) {
588
692
  const p = sk.points[id];
589
693
  const [x, y] = S(T, p.x, p.y);
@@ -626,14 +730,14 @@ function drawPoints(c, T, sk) {
626
730
  c.strokeStyle = TH.ink;
627
731
  c.stroke();
628
732
  }
629
- if (p.fixed && !p.origin && !p.anchor)
733
+ if (p.fixed && !p.origin && !p.anchor && (!locks || locks.has(id) || ed.highlight.has(id)))
630
734
  drawLock(c, x + 9, y - 14);
631
735
  }
632
736
  }
633
737
  function dimAnchor(sk, c) {
634
- const P = (id) => sk.points[id];
635
- if (c.type === 'dist') {
636
- // (measured on a wall's face: from its corner to corner, the label off the face on that side)
738
+ if (c.type === 'dist' || c.type === 'pldist') {
739
+ // (a length measured on a wall's face: from its corner to corner, the label off the face on that side. Between
740
+ // parallel lines or walls: from face to face, slid along them.)
637
741
  const e = dimEnds(sk, c);
638
742
  if (!e)
639
743
  return null;
@@ -641,29 +745,13 @@ function dimAnchor(sk, c) {
641
745
  const dx = b.x - a.x, dy = b.y - a.y, L = hyp(dx, dy) || 1e-9, nx = -dy / L, ny = dx / L, off = c.off || 0;
642
746
  return { m: { x: (a.x + b.x) / 2 + nx * off, y: (a.y + b.y) / 2 + ny * off }, a, b, nx, ny, L };
643
747
  }
644
- if (c.type === 'pldist') {
645
- // (between walls' faces: from face to face)
646
- const e = dimEnds(sk, c);
647
- if (!e)
648
- return null;
649
- return { m: { x: (e.a.x + e.b.x) / 2, y: (e.a.y + e.b.y) / 2 }, a: e.a, b: e.b };
650
- }
651
748
  if (c.type === 'diam') {
652
749
  // (across a curved wall's face: its arrows on that face)
653
750
  const g = dimRadius(sk, c);
654
751
  return g ? { m: { x: g.o.x, y: g.o.y }, o: g.o, r: g.r } : null;
655
752
  }
656
- if (c.type === 'angle') {
657
- const l1 = sk.lines[c.l1], l2 = sk.lines[c.l2];
658
- const sh = [l1.a, l1.b].find(p => p === l2.a || p === l2.b);
659
- if (sh) {
660
- const s = P(sh), o1 = P(l1.a === sh ? l1.b : l1.a), o2 = P(l2.a === sh ? l2.b : l2.a);
661
- const u1 = norm2(o1.x - s.x, o1.y - s.y), u2 = norm2(o2.x - s.x, o2.y - s.y);
662
- return { vtx: s, bis: norm2(u1[0] + u2[0], u1[1] + u2[1]), u1, u2 };
663
- }
664
- const a = P(l1.a), b = P(l1.b);
665
- return { vtx: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, bis: [0, 1] };
666
- }
753
+ if (c.type === 'angle')
754
+ return angleCorner(sk, c);
667
755
  return null;
668
756
  }
669
757
  /** Whether a dimension, or something it measures (its lines, points or curves), is selected or hovered. */
@@ -683,7 +771,7 @@ function drawDims(c, T, sk) {
683
771
  return;
684
772
  for (const id in sk.cons) {
685
773
  const d = sk.cons[id];
686
- if (!isDim(d) || d.layer === 'print' || d.elev)
774
+ if (!isDim(d) || d.elev)
687
775
  continue; // the wall view's own dimensions are its chips
688
776
  const A = dimAnchor(sk, d);
689
777
  if (!A)
@@ -693,22 +781,22 @@ function drawDims(c, T, sk) {
693
781
  const bad = ed.highlight.has(id) || ed.ana.bad.has(id);
694
782
  if (ref && !bad && !dimLive(sk, id, d))
695
783
  continue;
696
- // a measurement of the job sets it: outlined, with a link mark — red once the job's measurement is gone
784
+ // a measurement of the job sets it: the link badge before its value (chip()), on the chip's own colour — red once
785
+ // the job's measurement is gone
697
786
  const linked = !!d.measurementRef && !ref && !bad, gone = linked && linkInfoOf(d.measurementRef)?.exists === false;
698
787
  let bg = ref ? TH.ref : TH.blue, fg = ref ? TH.reftext : '#fff';
699
- if (bad) {
788
+ if (bad || gone) {
700
789
  bg = TH.red;
701
790
  fg = '#fff';
702
791
  }
703
- if (linked) {
704
- bg = TH.bg;
705
- fg = gone ? TH.red : d.type === 'angle' ? TH.purple : linkBlue();
706
- }
707
- const border = selected ? [TH.yellow, 2.5] : linked ? [fg, 1.5] : null;
792
+ const border = selected ? [TH.yellow, 2.5] : null;
708
793
  const mark = linked ? 'link' : undefined;
709
794
  const wrap = (s) => (ref ? '(' : '') + s + (ref ? ')' : '');
710
795
  if (d.type === 'angle' && 'vtx' in A) {
711
796
  const [vx, vy] = S(T, A.vtx.x, A.vtx.y);
797
+ // pulled off its corner: its arc out where the label is, the label riding on it; else a little arc at the
798
+ // corner, the label out along the bisector
799
+ const moved = angleLabel(sk, d), R = moved ? moved.r * T.s : 22;
712
800
  if (A.u1 && A.u2) {
713
801
  // arc between the two lines, so the angle reads as belonging to that corner
714
802
  const t1 = Math.atan2(-A.u1[1], A.u1[0]), t2 = Math.atan2(-A.u2[1], A.u2[0]);
@@ -717,18 +805,34 @@ function drawDims(c, T, sk) {
717
805
  sweep -= 2 * Math.PI;
718
806
  while (sweep < -Math.PI)
719
807
  sweep += 2 * Math.PI;
808
+ // (blue, as every other dimension is — the user's call, 2026-10-29; purple is the inference guides')
720
809
  c.save();
721
- c.strokeStyle = bad ? TH.red : ref ? TH.chipline : TH.purple;
810
+ c.strokeStyle = bad ? TH.red : ref ? TH.chipline : TH.blue;
722
811
  c.lineWidth = 1.2;
723
812
  if (ref)
724
813
  c.setLineDash([3, 3]);
725
814
  c.beginPath();
726
- c.arc(vx, vy, 22, t1, t1 + sweep, sweep < 0);
815
+ c.arc(vx, vy, R, t1, t1 + sweep, sweep < 0);
727
816
  c.stroke();
817
+ // (pulled out past where a line ends: an extension line along it, out to the arc)
818
+ c.lineWidth = 1;
819
+ for (const [u, len] of [[A.u1, A.len1], [A.u2, A.len2]]) {
820
+ const from = (len ?? 0) * T.s;
821
+ if (moved && R > from + 2) {
822
+ c.beginPath();
823
+ c.moveTo(vx + u[0] * from, vy - u[1] * from);
824
+ c.lineTo(vx + u[0] * (R + 4), vy - u[1] * (R + 4));
825
+ c.stroke();
826
+ }
827
+ }
728
828
  c.restore();
729
829
  }
730
- chip(c, wrap(fmtAng(val)), vx + A.bis[0] * 50, vy - A.bis[1] * 50, linked ? { bg, fg, border, kind: 'dim', id, mark }
731
- : { bg: bad ? TH.red : ref ? TH.ref : TH.purple, fg: ref ? TH.reftext : '#fff', border, kind: 'dim', id });
830
+ const text = wrap(fmtAng(val)), opts = { bg, fg, border, kind: 'dim', id, mark };
831
+ // (left where it was drawn: 50 px out along the bisector, or as far as its corners stay clear of the corner — a
832
+ // linked chip is wider)
833
+ const sz = chipSize(c, text, opts), out = Math.max(50, Math.hypot(sz.w, sz.h) / 2 + 28);
834
+ const [lx, ly] = moved ? [vx + Math.cos(moved.ang) * R, vy - Math.sin(moved.ang) * R] : [vx + A.bis[0] * out, vy - A.bis[1] * out];
835
+ chip(c, text, lx, ly, opts);
732
836
  continue;
733
837
  }
734
838
  if (d.type === 'diam' && 'o' in A) {
@@ -739,7 +843,7 @@ function drawDims(c, T, sk) {
739
843
  continue;
740
844
  const [mx, my] = S(T, A.m.x, A.m.y);
741
845
  const lineColor = bad ? TH.red : ref ? TH.chipline : TH.blue;
742
- if (d.type === 'dist' && A.nx != null && Math.abs((d.off || 0) * T.s) > 10) {
846
+ if ((d.type === 'dist' || d.type === 'pldist') && A.nx != null && Math.abs((d.off || 0) * T.s) > 10) {
743
847
  drawDimLeaders(c, T, A.a, A.b, A.nx, A.ny, d.off, lineColor, ref);
744
848
  }
745
849
  else if (d.type === 'pldist' || !d.line) {
@@ -999,7 +1103,8 @@ export function drawRubberBand(c, T, from, to, snapped) {
999
1103
  endRing(c, x2, y2, TH.blue);
1000
1104
  c.restore();
1001
1105
  const L = hyp(to.x - from.x, to.y - from.y);
1002
- if (L * T.s > 24)
1106
+ // (no distance on a drawing without its scale yet: it's only a placeholder)
1107
+ if (L * T.s > 24 && !planSK().unscaled)
1003
1108
  chip(c, fmtLen(L), (x1 + x2) / 2, (y1 + y2) / 2 - 16, { bg: '#fff', fg: TH.blue, border: [TH.blue, 1.5] });
1004
1109
  }
1005
1110
  /**
@@ -1047,7 +1152,9 @@ export function drawPendingDim(c, T, p) {
1047
1152
  }
1048
1153
  c.restore();
1049
1154
  const [mx, my] = S(T, (p.a.x + p.b.x) / 2 + p.nx * p.off, (p.a.y + p.b.y) / 2 + p.ny * p.off);
1050
- chip(c, p.label, mx, my - (Math.abs(p.off * T.s) > 1 ? 0 : 16), { bg: TH.blue, fg: '#fff', border: [TH.yellow, 2] });
1155
+ // (no label until one's typed, on a page whose sizes are still placeholders)
1156
+ if (p.label)
1157
+ chip(c, p.label, mx, my - (Math.abs(p.off * T.s) > 1 ? 0 : 16), { bg: TH.blue, fg: '#fff', border: [TH.yellow, 2] });
1051
1158
  }
1052
1159
  /**
1053
1160
  * A linear dimension pulled off its line: extension lines from both ends out to a dimension line with
@@ -1112,70 +1219,9 @@ function drawDiameter(c, T, o, r, d, text, color, chipOpts, dashed = false) {
1112
1219
  if (!d.rad)
1113
1220
  arrowHead(c, p2, -sx, -sy);
1114
1221
  c.restore();
1115
- c.font = (chipOpts.font || '600 12px') + ' ' + FONT;
1116
- const w = c.measureText(text).width + 14 + (chipOpts.mark ? MARK_W : 0);
1222
+ const { w } = chipSize(c, text, chipOpts);
1117
1223
  chip(c, text, end[0] + side * (w / 2 + 2), end[1], chipOpts);
1118
1224
  }
1119
- /** Architectural dimension strings on the Print dimensions layer (reference-only). */
1120
- function drawPrintDims(c, T, sk) {
1121
- if (!settings.layers.print)
1122
- return;
1123
- for (const id in sk.cons) {
1124
- const d = sk.cons[id];
1125
- if (d.layer !== 'print' || d.type !== 'dist')
1126
- continue;
1127
- const a = sk.points[d.a], b = sk.points[d.b];
1128
- if (!a || !b)
1129
- continue;
1130
- const dx = b.x - a.x, dy = b.y - a.y, L = hyp(dx, dy);
1131
- if (L < 1e-6)
1132
- continue;
1133
- const nx = -dy / L, ny = dx / L, off = d.off || 24;
1134
- const pa = { x: a.x + nx * off, y: a.y + ny * off }, pb = { x: b.x + nx * off, y: b.y + ny * off };
1135
- const [ax, ay] = S(T, a.x, a.y), [bx, by] = S(T, b.x, b.y), [qax, qay] = S(T, pa.x, pa.y), [qbx, qby] = S(T, pb.x, pb.y);
1136
- const sgn = Math.sign(off) || 1, ex = nx * sgn * 6, ey = -ny * sgn * 6;
1137
- const selected = isSel('dim', id);
1138
- c.strokeStyle = selected ? TH.yellow : TH.ink;
1139
- c.lineWidth = 1;
1140
- c.beginPath();
1141
- const gap = ax - qax || ay - qay ? 4 : 0, ga = norm2(qax - ax, qay - ay);
1142
- c.moveTo(ax + ga[0] * gap, ay + ga[1] * gap);
1143
- c.lineTo(qax + ex, qay + ey);
1144
- c.moveTo(bx + ga[0] * gap, by + ga[1] * gap);
1145
- c.lineTo(qbx + ex, qby + ey);
1146
- c.moveTo(qax, qay);
1147
- c.lineTo(qbx, qby);
1148
- c.stroke();
1149
- const u = norm2(qbx - qax, qby - qay);
1150
- c.lineWidth = 1.8;
1151
- c.beginPath();
1152
- for (const [x, y] of [[qax, qay], [qbx, qby]]) {
1153
- const tx = ((u[0] - u[1]) * 5) / Math.SQRT2, ty = ((u[1] + u[0]) * 5) / Math.SQRT2;
1154
- c.moveTo(x - tx, y - ty);
1155
- c.lineTo(x + tx, y + ty);
1156
- }
1157
- c.stroke();
1158
- let ang = Math.atan2(qby - qay, qbx - qax);
1159
- if (ang > Math.PI / 2)
1160
- ang -= Math.PI;
1161
- if (ang < -Math.PI / 2)
1162
- ang += Math.PI;
1163
- const mx = (qax + qbx) / 2, my = (qay + qby) / 2, txt = fmtLen(L);
1164
- c.save();
1165
- c.translate(mx, my);
1166
- c.rotate(ang);
1167
- c.font = '600 12px ' + FONT;
1168
- const w = c.measureText(txt).width + 8;
1169
- c.fillStyle = TH.bg;
1170
- c.fillRect(-w / 2, -17, w, 14);
1171
- c.fillStyle = selected ? TH.yellow : TH.ink;
1172
- c.textAlign = 'center';
1173
- c.textBaseline = 'alphabetic';
1174
- c.fillText(txt, 0, -6);
1175
- c.restore();
1176
- addHit({ kind: 'pdim', id, x: mx - w / 2 - 4, y: my - 18, w: w + 8, h: 24 });
1177
- }
1178
- }
1179
1225
  /**
1180
1226
  * A badge's symbol drawn as lines rather than a character: ∥ ⊥ ∘ ◎ aren't in every typeface — on a device the
1181
1227
  * app's own (Inter) lacks them and Skia has nothing to fall back on, so they came out as the font's "missing glyph"
@@ -1275,18 +1321,7 @@ function badgeSpots(T, sk, c) {
1275
1321
  }
1276
1322
  function drawBadges(c, T, sk) {
1277
1323
  const { W, H } = cvs;
1278
- const selIds = new Set(ed.sel.map(s => s.id)), selPts = new Set();
1279
- for (const s of ed.sel) {
1280
- if (s.type === 'line') {
1281
- const l = sk.lines[s.id];
1282
- if (l) {
1283
- selPts.add(l.a);
1284
- selPts.add(l.b);
1285
- }
1286
- }
1287
- if (s.type === 'point')
1288
- selPts.add(s.id);
1289
- }
1324
+ const selIds = new Set(ed.sel.map(s => s.id)), selPts = selPoints(sk);
1290
1325
  for (const id in sk.cons) {
1291
1326
  const k = sk.cons[id];
1292
1327
  if (isDim(k) || k.elev)
@@ -1633,7 +1668,6 @@ function paintPlanView(ctx, sk, m, under = true) {
1633
1668
  drawOpenings(ctx, T, sk);
1634
1669
  drawLines(ctx, T, sk);
1635
1670
  drawCurves(ctx, T, sk);
1636
- drawPrintDims(ctx, T, sk);
1637
1671
  if (ed.mode === 'draft')
1638
1672
  drawBadges(ctx, T, sk);
1639
1673
  // (the drawing's points only in Draft: Annotate can't move them, and shouldn't look as if it could — the user's
@@ -1643,6 +1677,8 @@ function paintPlanView(ctx, sk, m, under = true) {
1643
1677
  drawOpeningAnchors(ctx, T, sk);
1644
1678
  drawDims(ctx, T, sk);
1645
1679
  drawOpeningDims(ctx, T, sk);
1680
+ // (an expanded photo's badge over the drawing, under the markup: as it's tapped)
1681
+ drawImageLinks(ctx, T);
1646
1682
  drawNotes(ctx, T, sk);
1647
1683
  drawNoteSelection(ctx, T);
1648
1684
  }
@@ -1665,7 +1701,6 @@ function paintWall(ctx, W, fixed) {
1665
1701
  drawCurves(ctx, T, sk);
1666
1702
  // (over the edges — its own and its neighbours' — so a selected cabinet's outline shows all the way round)
1667
1703
  drawWallSelection(ctx, T, plan, f, sk);
1668
- drawPrintDims(ctx, T, sk);
1669
1704
  drawBadges(ctx, T, sk);
1670
1705
  if (ed.mode === 'draft')
1671
1706
  drawPoints(ctx, T, sk);