@reekon-tools/boldr-cad 0.1.0 → 1.0.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 (547) hide show
  1. package/README.md +81 -9
  2. package/dist/app/edit.d.ts +12 -1
  3. package/dist/app/edit.js +96 -11
  4. package/dist/app/edit.js.map +1 -1
  5. package/dist/app/state.d.ts +31 -4
  6. package/dist/app/state.js +31 -2
  7. package/dist/app/state.js.map +1 -1
  8. package/dist/data/memory.d.ts +5 -0
  9. package/dist/data/memory.js +2 -2
  10. package/dist/data/memory.js.map +1 -1
  11. package/dist/data/provider.d.ts +19 -7
  12. package/dist/data/sync.d.ts +7 -2
  13. package/dist/data/sync.js +115 -64
  14. package/dist/data/sync.js.map +1 -1
  15. package/dist/editor/actions.d.ts +1 -2
  16. package/dist/editor/actions.js +391 -113
  17. package/dist/editor/actions.js.map +1 -1
  18. package/dist/editor/confirm.d.ts +23 -0
  19. package/dist/editor/confirm.js +35 -0
  20. package/dist/editor/confirm.js.map +1 -0
  21. package/dist/editor/elevation.d.ts +191 -90
  22. package/dist/editor/elevation.js +812 -616
  23. package/dist/editor/elevation.js.map +1 -1
  24. package/dist/editor/face.d.ts +73 -0
  25. package/dist/editor/face.js +269 -0
  26. package/dist/editor/face.js.map +1 -0
  27. package/dist/editor/files.d.ts +6 -0
  28. package/dist/editor/files.js +50 -4
  29. package/dist/editor/files.js.map +1 -1
  30. package/dist/editor/gestures.d.ts +18 -3
  31. package/dist/editor/gestures.js +145 -30
  32. package/dist/editor/gestures.js.map +1 -1
  33. package/dist/editor/keypad.d.ts +19 -4
  34. package/dist/editor/keypad.js +39 -9
  35. package/dist/editor/keypad.js.map +1 -1
  36. package/dist/editor/keys.js +36 -4
  37. package/dist/editor/keys.js.map +1 -1
  38. package/dist/editor/laser.d.ts +20 -4
  39. package/dist/editor/laser.js +104 -21
  40. package/dist/editor/laser.js.map +1 -1
  41. package/dist/editor/linkActions.d.ts +49 -0
  42. package/dist/editor/linkActions.js +210 -0
  43. package/dist/editor/linkActions.js.map +1 -0
  44. package/dist/editor/linkFollow.d.ts +2 -0
  45. package/dist/editor/linkFollow.js +53 -0
  46. package/dist/editor/linkFollow.js.map +1 -0
  47. package/dist/editor/linkWrite.d.ts +13 -0
  48. package/dist/editor/linkWrite.js +84 -0
  49. package/dist/editor/linkWrite.js.map +1 -0
  50. package/dist/editor/links.d.ts +84 -0
  51. package/dist/editor/links.js +130 -0
  52. package/dist/editor/links.js.map +1 -0
  53. package/dist/editor/markup.d.ts +29 -3
  54. package/dist/editor/markup.js +95 -10
  55. package/dist/editor/markup.js.map +1 -1
  56. package/dist/editor/mode.d.ts +40 -2
  57. package/dist/editor/mode.js +71 -4
  58. package/dist/editor/mode.js.map +1 -1
  59. package/dist/editor/ports.d.ts +36 -6
  60. package/dist/editor/ports.js +10 -1
  61. package/dist/editor/ports.js.map +1 -1
  62. package/dist/editor/project.d.ts +38 -6
  63. package/dist/editor/project.js +167 -27
  64. package/dist/editor/project.js.map +1 -1
  65. package/dist/editor/selection.d.ts +10 -0
  66. package/dist/editor/selection.js +62 -8
  67. package/dist/editor/selection.js.map +1 -1
  68. package/dist/editor/status.js +3 -0
  69. package/dist/editor/status.js.map +1 -1
  70. package/dist/editor/tooltips.d.ts +5 -0
  71. package/dist/editor/tooltips.js +48 -0
  72. package/dist/editor/tooltips.js.map +1 -0
  73. package/dist/editor/view3d.d.ts +17 -0
  74. package/dist/editor/view3d.js +135 -0
  75. package/dist/editor/view3d.js.map +1 -0
  76. package/dist/editor/view3dState.d.ts +39 -0
  77. package/dist/editor/view3dState.js +44 -0
  78. package/dist/editor/view3dState.js.map +1 -0
  79. package/dist/export/dxf.js +1 -1
  80. package/dist/export/dxf.js.map +1 -1
  81. package/dist/export/pdf.js +17 -12
  82. package/dist/export/pdf.js.map +1 -1
  83. package/dist/export/plan.d.ts +23 -6
  84. package/dist/export/plan.js +127 -44
  85. package/dist/export/plan.js.map +1 -1
  86. package/dist/index.d.ts +5 -0
  87. package/dist/index.js +2 -0
  88. package/dist/index.js.map +1 -1
  89. package/dist/model/constraints.js +15 -4
  90. package/dist/model/constraints.js.map +1 -1
  91. package/dist/model/demo.js +3 -3
  92. package/dist/model/demo.js.map +1 -1
  93. package/dist/model/elevation.d.ts +6 -9
  94. package/dist/model/elevation.js +5 -13
  95. package/dist/model/elevation.js.map +1 -1
  96. package/dist/model/faces.d.ts +58 -0
  97. package/dist/model/faces.js +197 -0
  98. package/dist/model/faces.js.map +1 -0
  99. package/dist/model/facesolve.d.ts +72 -0
  100. package/dist/model/facesolve.js +369 -0
  101. package/dist/model/facesolve.js.map +1 -0
  102. package/dist/model/history.d.ts +3 -2
  103. package/dist/model/history.js +17 -11
  104. package/dist/model/history.js.map +1 -1
  105. package/dist/model/images.d.ts +18 -0
  106. package/dist/model/images.js +26 -0
  107. package/dist/model/images.js.map +1 -0
  108. package/dist/model/links.d.ts +46 -0
  109. package/dist/model/links.js +73 -0
  110. package/dist/model/links.js.map +1 -0
  111. package/dist/model/markup.d.ts +46 -0
  112. package/dist/model/markup.js +71 -0
  113. package/dist/model/markup.js.map +1 -0
  114. package/dist/model/measurement.d.ts +11 -0
  115. package/dist/model/measurement.js +24 -0
  116. package/dist/model/measurement.js.map +1 -0
  117. package/dist/model/openings.d.ts +4 -0
  118. package/dist/model/openings.js +4 -0
  119. package/dist/model/openings.js.map +1 -1
  120. package/dist/model/planGeom.d.ts +72 -2
  121. package/dist/model/planGeom.js +184 -10
  122. package/dist/model/planGeom.js.map +1 -1
  123. package/dist/model/project.js +3 -2
  124. package/dist/model/project.js.map +1 -1
  125. package/dist/model/projectfile.js +2 -1
  126. package/dist/model/projectfile.js.map +1 -1
  127. package/dist/model/rooms.js +2 -2
  128. package/dist/model/rooms.js.map +1 -1
  129. package/dist/model/settings.d.ts +6 -0
  130. package/dist/model/settings.js +2 -2
  131. package/dist/model/settings.js.map +1 -1
  132. package/dist/model/sketch.d.ts +14 -0
  133. package/dist/model/sketch.js +143 -24
  134. package/dist/model/sketch.js.map +1 -1
  135. package/dist/model/smooth.d.ts +6 -0
  136. package/dist/model/smooth.js +52 -0
  137. package/dist/model/smooth.js.map +1 -0
  138. package/dist/model/store.d.ts +12 -0
  139. package/dist/model/store.js +35 -3
  140. package/dist/model/store.js.map +1 -1
  141. package/dist/model/types.d.ts +173 -8
  142. package/dist/model/wallfaces.d.ts +32 -0
  143. package/dist/model/wallfaces.js +133 -20
  144. package/dist/model/wallfaces.js.map +1 -1
  145. package/dist/model/wallshape.d.ts +15 -0
  146. package/dist/model/wallshape.js +89 -0
  147. package/dist/model/wallshape.js.map +1 -0
  148. package/dist/react/BottomBar.d.ts +9 -5
  149. package/dist/react/BottomBar.js +213 -24
  150. package/dist/react/BottomBar.js.map +1 -1
  151. package/dist/react/Buttons.d.ts +13 -2
  152. package/dist/react/Buttons.js +37 -5
  153. package/dist/react/Buttons.js.map +1 -1
  154. package/dist/react/CadEditor.d.ts +46 -3
  155. package/dist/react/CadEditor.js +77 -20
  156. package/dist/react/CadEditor.js.map +1 -1
  157. package/dist/react/ConfirmSheet.d.ts +4 -0
  158. package/dist/react/ConfirmSheet.js +31 -0
  159. package/dist/react/ConfirmSheet.js.map +1 -0
  160. package/dist/react/ConflictSheet.d.ts +2 -1
  161. package/dist/react/ConflictSheet.js +16 -8
  162. package/dist/react/ConflictSheet.js.map +1 -1
  163. package/dist/react/Drawer.d.ts +8 -2
  164. package/dist/react/Drawer.js +167 -38
  165. package/dist/react/Drawer.js.map +1 -1
  166. package/dist/react/Duck.d.ts +13 -0
  167. package/dist/react/Duck.js +45 -0
  168. package/dist/react/Duck.js.map +1 -0
  169. package/dist/react/Empty3D.d.ts +2 -0
  170. package/dist/react/Empty3D.js +39 -0
  171. package/dist/react/Empty3D.js.map +1 -0
  172. package/dist/react/FitScroll.d.ts +6 -0
  173. package/dist/react/FitScroll.js +14 -0
  174. package/dist/react/FitScroll.js.map +1 -0
  175. package/dist/react/Icon.native.d.ts +6 -0
  176. package/dist/react/Icon.native.js +4 -0
  177. package/dist/react/Icon.native.js.map +1 -1
  178. package/dist/react/Icon.web.d.ts +6 -0
  179. package/dist/react/Icon.web.js +4 -0
  180. package/dist/react/Icon.web.js.map +1 -1
  181. package/dist/react/KeypadSheet.d.ts +9 -2
  182. package/dist/react/KeypadSheet.js +174 -42
  183. package/dist/react/KeypadSheet.js.map +1 -1
  184. package/dist/react/LaserTray.js +8 -5
  185. package/dist/react/LaserTray.js.map +1 -1
  186. package/dist/react/MarkupPanel.d.ts +8 -0
  187. package/dist/react/MarkupPanel.js +67 -0
  188. package/dist/react/MarkupPanel.js.map +1 -0
  189. package/dist/react/PageThumb.native.d.ts +7 -2
  190. package/dist/react/PageThumb.native.js +19 -7
  191. package/dist/react/PageThumb.native.js.map +1 -1
  192. package/dist/react/PageThumb.web.d.ts +3 -2
  193. package/dist/react/PageThumb.web.js +4 -4
  194. package/dist/react/PageThumb.web.js.map +1 -1
  195. package/dist/react/PlanCanvas.native.d.ts +1 -1
  196. package/dist/react/PlanCanvas.native.js +134 -36
  197. package/dist/react/PlanCanvas.native.js.map +1 -1
  198. package/dist/react/PlanCanvas.web.js +16 -5
  199. package/dist/react/PlanCanvas.web.js.map +1 -1
  200. package/dist/react/Popovers.d.ts +12 -6
  201. package/dist/react/Popovers.js +53 -26
  202. package/dist/react/Popovers.js.map +1 -1
  203. package/dist/react/Rail.d.ts +24 -0
  204. package/dist/react/Rail.js +324 -34
  205. package/dist/react/Rail.js.map +1 -1
  206. package/dist/react/ShapeMenu.d.ts +7 -0
  207. package/dist/react/ShapeMenu.js +58 -0
  208. package/dist/react/ShapeMenu.js.map +1 -0
  209. package/dist/react/Sheet.d.ts +14 -4
  210. package/dist/react/Sheet.js +54 -9
  211. package/dist/react/Sheet.js.map +1 -1
  212. package/dist/react/Slider.d.ts +18 -0
  213. package/dist/react/Slider.js +27 -0
  214. package/dist/react/Slider.js.map +1 -0
  215. package/dist/react/Toast.js +14 -6
  216. package/dist/react/Toast.js.map +1 -1
  217. package/dist/react/ToolTip.d.ts +7 -0
  218. package/dist/react/ToolTip.js +43 -0
  219. package/dist/react/ToolTip.js.map +1 -0
  220. package/dist/react/TopBar.d.ts +6 -3
  221. package/dist/react/TopBar.js +48 -17
  222. package/dist/react/TopBar.js.map +1 -1
  223. package/dist/react/Tween.d.ts +7 -0
  224. package/dist/react/Tween.js +26 -0
  225. package/dist/react/Tween.js.map +1 -0
  226. package/dist/react/Tween.native.d.ts +6 -0
  227. package/dist/react/Tween.native.js +43 -0
  228. package/dist/react/Tween.native.js.map +1 -0
  229. package/dist/react/View3D.native.d.ts +4 -0
  230. package/dist/react/View3D.native.js +103 -0
  231. package/dist/react/View3D.native.js.map +1 -0
  232. package/dist/react/View3D.web.d.ts +4 -0
  233. package/dist/react/View3D.web.js +151 -0
  234. package/dist/react/View3D.web.js.map +1 -0
  235. package/dist/react/View3DHost.d.ts +143 -0
  236. package/dist/react/View3DHost.js +377 -0
  237. package/dist/react/View3DHost.js.map +1 -0
  238. package/dist/react/corners.d.ts +7 -0
  239. package/dist/react/corners.js +51 -0
  240. package/dist/react/corners.js.map +1 -0
  241. package/dist/react/easing.d.ts +15 -0
  242. package/dist/react/easing.js +10 -0
  243. package/dist/react/easing.js.map +1 -0
  244. package/dist/react/host.d.ts +7 -2
  245. package/dist/react/host.js +8 -3
  246. package/dist/react/host.js.map +1 -1
  247. package/dist/react/hostKeypad.d.ts +56 -0
  248. package/dist/react/hostKeypad.js +5 -0
  249. package/dist/react/hostKeypad.js.map +1 -0
  250. package/dist/react/motion.d.ts +77 -0
  251. package/dist/react/motion.js +173 -0
  252. package/dist/react/motion.js.map +1 -0
  253. package/dist/react/ports.js +90 -8
  254. package/dist/react/ports.js.map +1 -1
  255. package/dist/react/saving.d.ts +15 -0
  256. package/dist/react/saving.js +76 -0
  257. package/dist/react/saving.js.map +1 -0
  258. package/dist/react/store.d.ts +38 -1
  259. package/dist/react/store.js +31 -1
  260. package/dist/react/store.js.map +1 -1
  261. package/dist/react/theme.d.ts +10 -0
  262. package/dist/react/theme.js +8 -0
  263. package/dist/react/theme.js.map +1 -1
  264. package/dist/react/topRow.d.ts +22 -0
  265. package/dist/react/topRow.js +61 -0
  266. package/dist/react/topRow.js.map +1 -0
  267. package/dist/react/touch.d.ts +6 -0
  268. package/dist/react/touch.js +9 -0
  269. package/dist/react/touch.js.map +1 -0
  270. package/dist/render/ctx.d.ts +8 -0
  271. package/dist/render/draw.d.ts +33 -1
  272. package/dist/render/draw.js +593 -215
  273. package/dist/render/draw.js.map +1 -1
  274. package/dist/render/elevation.d.ts +65 -43
  275. package/dist/render/elevation.js +414 -140
  276. package/dist/render/elevation.js.map +1 -1
  277. package/dist/render/figmaIcons.d.ts +12 -0
  278. package/dist/render/figmaIcons.js +21 -0
  279. package/dist/render/figmaIcons.js.map +1 -0
  280. package/dist/render/hit.d.ts +22 -2
  281. package/dist/render/hit.js +168 -25
  282. package/dist/render/hit.js.map +1 -1
  283. package/dist/render/icons.js +69 -10
  284. package/dist/render/icons.js.map +1 -1
  285. package/dist/render/linkTile.d.ts +15 -0
  286. package/dist/render/linkTile.js +369 -0
  287. package/dist/render/linkTile.js.map +1 -0
  288. package/dist/render/markup.d.ts +11 -0
  289. package/dist/render/markup.js +112 -0
  290. package/dist/render/markup.js.map +1 -0
  291. package/dist/render/navigate.d.ts +88 -0
  292. package/dist/render/navigate.js +100 -0
  293. package/dist/render/navigate.js.map +1 -0
  294. package/dist/render/snap.d.ts +20 -1
  295. package/dist/render/snap.js +118 -25
  296. package/dist/render/snap.js.map +1 -1
  297. package/dist/render/squircle.d.ts +8 -0
  298. package/dist/render/squircle.js +71 -0
  299. package/dist/render/squircle.js.map +1 -0
  300. package/dist/render/view.d.ts +115 -2
  301. package/dist/render/view.js +244 -19
  302. package/dist/render/view.js.map +1 -1
  303. package/dist/skia/SkiaDrawCtx.d.ts +30 -2
  304. package/dist/skia/SkiaDrawCtx.js +198 -43
  305. package/dist/skia/SkiaDrawCtx.js.map +1 -1
  306. package/dist/skia/host.d.ts +18 -2
  307. package/dist/skia/host.js +46 -1
  308. package/dist/skia/host.js.map +1 -1
  309. package/dist/solver/equations.js +37 -2
  310. package/dist/solver/equations.js.map +1 -1
  311. package/dist/solver/gauss-newton.d.ts +17 -0
  312. package/dist/solver/gauss-newton.js +144 -36
  313. package/dist/solver/gauss-newton.js.map +1 -1
  314. package/dist/solver/linalg.d.ts +45 -2
  315. package/dist/solver/linalg.js +164 -10
  316. package/dist/solver/linalg.js.map +1 -1
  317. package/dist/solver/types.d.ts +5 -0
  318. package/dist/tools/annotate.d.ts +57 -1
  319. package/dist/tools/annotate.js +64 -43
  320. package/dist/tools/annotate.js.map +1 -1
  321. package/dist/tools/chamfer.d.ts +12 -0
  322. package/dist/tools/chamfer.js +114 -0
  323. package/dist/tools/chamfer.js.map +1 -0
  324. package/dist/tools/corner.d.ts +38 -0
  325. package/dist/tools/corner.js +99 -0
  326. package/dist/tools/corner.js.map +1 -0
  327. package/dist/tools/dim.d.ts +86 -9
  328. package/dist/tools/dim.js +581 -96
  329. package/dist/tools/dim.js.map +1 -1
  330. package/dist/tools/elevation.js +1 -1
  331. package/dist/tools/elevation.js.map +1 -1
  332. package/dist/tools/fillet.d.ts +9 -0
  333. package/dist/tools/fillet.js +86 -0
  334. package/dist/tools/fillet.js.map +1 -0
  335. package/dist/tools/freehand.d.ts +3 -2
  336. package/dist/tools/freehand.js +19 -12
  337. package/dist/tools/freehand.js.map +1 -1
  338. package/dist/tools/index.js +14 -4
  339. package/dist/tools/index.js.map +1 -1
  340. package/dist/tools/line.d.ts +8 -3
  341. package/dist/tools/line.js +66 -30
  342. package/dist/tools/line.js.map +1 -1
  343. package/dist/tools/modify.d.ts +13 -0
  344. package/dist/tools/modify.js +105 -25
  345. package/dist/tools/modify.js.map +1 -1
  346. package/dist/tools/openings.js +22 -15
  347. package/dist/tools/openings.js.map +1 -1
  348. package/dist/tools/registry.d.ts +8 -0
  349. package/dist/tools/registry.js +42 -2
  350. package/dist/tools/registry.js.map +1 -1
  351. package/dist/tools/select.d.ts +77 -3
  352. package/dist/tools/select.js +560 -74
  353. package/dist/tools/select.js.map +1 -1
  354. package/dist/tools/shapes.d.ts +14 -0
  355. package/dist/tools/shapes.js +154 -75
  356. package/dist/tools/shapes.js.map +1 -1
  357. package/dist/units/format.d.ts +10 -2
  358. package/dist/units/format.js +18 -3
  359. package/dist/units/format.js.map +1 -1
  360. package/dist/view3d/build.d.ts +35 -0
  361. package/dist/view3d/build.js +630 -0
  362. package/dist/view3d/build.js.map +1 -0
  363. package/dist/view3d/camera.d.ts +60 -0
  364. package/dist/view3d/camera.js +84 -0
  365. package/dist/view3d/camera.js.map +1 -0
  366. package/dist/view3d/fatLines.d.ts +11 -0
  367. package/dist/view3d/fatLines.js +83 -0
  368. package/dist/view3d/fatLines.js.map +1 -0
  369. package/dist/view3d/lines.d.ts +17 -0
  370. package/dist/view3d/lines.js +271 -0
  371. package/dist/view3d/lines.js.map +1 -0
  372. package/dist/view3d/mesh.d.ts +51 -0
  373. package/dist/view3d/mesh.js +101 -0
  374. package/dist/view3d/mesh.js.map +1 -0
  375. package/dist/view3d/poly.d.ts +16 -0
  376. package/dist/view3d/poly.js +109 -0
  377. package/dist/view3d/poly.js.map +1 -0
  378. package/dist/view3d/viewer.d.ts +86 -0
  379. package/dist/view3d/viewer.js +350 -0
  380. package/dist/view3d/viewer.js.map +1 -0
  381. package/package.json +15 -2
  382. package/src/app/edit.ts +83 -12
  383. package/src/app/main.ts +11 -5
  384. package/src/app/state.ts +38 -5
  385. package/src/data/memory.ts +2 -2
  386. package/src/data/provider.ts +15 -7
  387. package/src/data/sync.ts +87 -50
  388. package/src/editor/actions.ts +292 -83
  389. package/src/editor/confirm.ts +58 -0
  390. package/src/editor/elevation.ts +698 -478
  391. package/src/editor/face.ts +227 -0
  392. package/src/editor/files.ts +40 -4
  393. package/src/editor/gestures.ts +147 -31
  394. package/src/editor/keypad.ts +43 -11
  395. package/src/editor/keys.ts +25 -5
  396. package/src/editor/laser.ts +98 -20
  397. package/src/editor/linkActions.ts +194 -0
  398. package/src/editor/linkFollow.ts +43 -0
  399. package/src/editor/linkWrite.ts +77 -0
  400. package/src/editor/links.ts +181 -0
  401. package/src/editor/markup.ts +92 -8
  402. package/src/editor/mode.ts +83 -7
  403. package/src/editor/ports.ts +37 -6
  404. package/src/editor/project.ts +155 -26
  405. package/src/editor/selection.ts +54 -8
  406. package/src/editor/status.ts +2 -0
  407. package/src/editor/tooltips.ts +47 -0
  408. package/src/editor/view3d.ts +119 -0
  409. package/src/editor/view3dState.ts +61 -0
  410. package/src/export/dxf.ts +1 -1
  411. package/src/export/pdf.ts +15 -7
  412. package/src/export/plan.ts +119 -35
  413. package/src/index.ts +5 -0
  414. package/src/input/keyboard.ts +2 -4
  415. package/src/model/constraints.ts +13 -4
  416. package/src/model/demo.ts +3 -3
  417. package/src/model/elevation.ts +6 -16
  418. package/src/model/faces.ts +153 -0
  419. package/src/model/facesolve.ts +322 -0
  420. package/src/model/history.ts +19 -11
  421. package/src/model/images.ts +35 -0
  422. package/src/model/links.ts +77 -0
  423. package/src/model/markup.ts +80 -0
  424. package/src/model/measurement.ts +26 -0
  425. package/src/model/openings.ts +6 -0
  426. package/src/model/planGeom.ts +174 -8
  427. package/src/model/project.ts +3 -2
  428. package/src/model/projectfile.ts +2 -1
  429. package/src/model/rooms.ts +1 -1
  430. package/src/model/settings.ts +8 -2
  431. package/src/model/sketch.ts +101 -14
  432. package/src/model/smooth.ts +51 -0
  433. package/src/model/store.ts +35 -4
  434. package/src/model/types.ts +170 -8
  435. package/src/model/wallfaces.ts +126 -17
  436. package/src/model/wallshape.ts +97 -0
  437. package/src/react/BottomBar.tsx +263 -46
  438. package/src/react/Buttons.tsx +50 -9
  439. package/src/react/CadEditor.tsx +133 -30
  440. package/src/react/ConfirmSheet.tsx +38 -0
  441. package/src/react/ConflictSheet.tsx +19 -12
  442. package/src/react/Drawer.tsx +213 -110
  443. package/src/react/Duck.tsx +50 -0
  444. package/src/react/Empty3D.tsx +32 -0
  445. package/src/react/FitScroll.tsx +19 -0
  446. package/src/react/Icon.native.tsx +5 -0
  447. package/src/react/Icon.web.tsx +5 -0
  448. package/src/react/KeypadSheet.tsx +253 -60
  449. package/src/react/LaserTray.tsx +12 -8
  450. package/src/react/MarkupPanel.tsx +150 -0
  451. package/src/react/PageThumb.native.tsx +18 -7
  452. package/src/react/PageThumb.web.tsx +4 -4
  453. package/src/react/PlanCanvas.native.tsx +151 -34
  454. package/src/react/PlanCanvas.web.tsx +15 -5
  455. package/src/react/Popovers.tsx +116 -51
  456. package/src/react/Rail.tsx +360 -27
  457. package/src/react/ShapeMenu.tsx +66 -0
  458. package/src/react/Sheet.tsx +56 -11
  459. package/src/react/Slider.tsx +48 -0
  460. package/src/react/Toast.tsx +16 -9
  461. package/src/react/ToolTip.tsx +49 -0
  462. package/src/react/TopBar.tsx +97 -39
  463. package/src/react/Tween.native.tsx +36 -0
  464. package/src/react/Tween.tsx +24 -0
  465. package/src/react/View3D.native.tsx +131 -0
  466. package/src/react/View3D.web.tsx +131 -0
  467. package/src/react/View3DHost.ts +367 -0
  468. package/src/react/corners.ts +52 -0
  469. package/src/react/easing.ts +14 -0
  470. package/src/react/host.ts +8 -3
  471. package/src/react/hostKeypad.ts +66 -0
  472. package/src/react/motion.tsx +239 -0
  473. package/src/react/ports.ts +78 -9
  474. package/src/react/saving.ts +80 -0
  475. package/src/react/store.ts +35 -3
  476. package/src/react/theme.ts +8 -0
  477. package/src/react/topRow.ts +71 -0
  478. package/src/react/touch.ts +10 -0
  479. package/src/render/ctx.ts +8 -0
  480. package/src/render/draw.ts +437 -175
  481. package/src/render/elevation.ts +349 -143
  482. package/src/render/figmaIcons.ts +22 -0
  483. package/src/render/hit.ts +140 -21
  484. package/src/render/icons.ts +69 -10
  485. package/src/render/linkTile.ts +297 -0
  486. package/src/render/markup.ts +91 -0
  487. package/src/render/navigate.ts +114 -0
  488. package/src/render/snap.ts +105 -22
  489. package/src/render/squircle.ts +73 -0
  490. package/src/render/view.ts +255 -19
  491. package/src/skia/SkiaDrawCtx.ts +163 -41
  492. package/src/skia/host.ts +46 -1
  493. package/src/solver/equations.ts +37 -2
  494. package/src/solver/gauss-newton.ts +125 -28
  495. package/src/solver/linalg.ts +141 -5
  496. package/src/solver/types.ts +5 -0
  497. package/src/styles.css +91 -42
  498. package/src/tools/annotate.ts +58 -29
  499. package/src/tools/chamfer.ts +103 -0
  500. package/src/tools/corner.ts +99 -0
  501. package/src/tools/dim.ts +524 -92
  502. package/src/tools/elevation.ts +1 -1
  503. package/src/tools/fillet.ts +82 -0
  504. package/src/tools/freehand.ts +20 -16
  505. package/src/tools/index.ts +17 -4
  506. package/src/tools/line.ts +59 -23
  507. package/src/tools/modify.ts +86 -24
  508. package/src/tools/openings.ts +8 -13
  509. package/src/tools/registry.ts +27 -2
  510. package/src/tools/select.ts +424 -75
  511. package/src/tools/shapes.ts +119 -58
  512. package/src/ui/ctx.ts +2 -1
  513. package/src/ui/domPorts.ts +23 -4
  514. package/src/ui/drawer.ts +6 -26
  515. package/src/ui/elevation.ts +10 -101
  516. package/src/ui/keypad.ts +8 -14
  517. package/src/ui/laser.ts +2 -2
  518. package/src/ui/menu.ts +63 -17
  519. package/src/ui/noteinput.ts +3 -3
  520. package/src/ui/opacity.ts +108 -0
  521. package/src/ui/palette.ts +67 -12
  522. package/src/ui/rail.ts +21 -7
  523. package/src/ui/topbar.ts +4 -14
  524. package/src/units/format.ts +18 -3
  525. package/src/view3d/build.ts +607 -0
  526. package/src/view3d/camera.ts +113 -0
  527. package/src/view3d/fatLines.ts +83 -0
  528. package/src/view3d/lines.ts +258 -0
  529. package/src/view3d/mesh.ts +111 -0
  530. package/src/view3d/poly.ts +104 -0
  531. package/src/view3d/viewer.ts +348 -0
  532. package/dist/react/ElevCanvas.native.d.ts +0 -3
  533. package/dist/react/ElevCanvas.native.js +0 -58
  534. package/dist/react/ElevCanvas.native.js.map +0 -1
  535. package/dist/react/ElevCanvas.web.d.ts +0 -4
  536. package/dist/react/ElevCanvas.web.js +0 -44
  537. package/dist/react/ElevCanvas.web.js.map +0 -1
  538. package/dist/react/ElevationSheet.d.ts +0 -2
  539. package/dist/react/ElevationSheet.js +0 -38
  540. package/dist/react/ElevationSheet.js.map +0 -1
  541. package/dist/render/elevationView.d.ts +0 -3
  542. package/dist/render/elevationView.js +0 -100
  543. package/dist/render/elevationView.js.map +0 -1
  544. package/src/react/ElevCanvas.native.tsx +0 -68
  545. package/src/react/ElevCanvas.web.tsx +0 -42
  546. package/src/react/ElevationSheet.tsx +0 -88
  547. package/src/render/elevationView.ts +0 -73
@@ -1,17 +1,28 @@
1
1
  import { ed, isSel, type HitChip } from '../app/state';
2
- import { curveDefined, isDim, lineDefined, measure } from '../model/constraints';
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 } from '../model/openings';
5
- import { arcAngles, gridSteps, OPENING_TICK_EXT, openingFrame, outsideSide } from '../model/planGeom';
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';
6
+ import { faceCorner } from '../model/wallfaces';
6
7
  import { roomGeom } from '../model/rooms';
8
+ import { wallPieces } from '../model/wallshape';
9
+ import { cabinetFootprints } from '../model/facesolve';
7
10
  import { settings } from '../model/settings';
8
- import { curveInfo, linesBetween, roomValid } from '../model/sketch';
9
- import { page, SK } from '../model/store';
10
- import type { Constraint, Line, Page, Sketch, XY } from '../model/types';
11
+ import { curveInfo, frameMemo, lineBetween, roomValid, withLineIndex } from '../model/sketch';
12
+ import { page, planSK, SK } from '../model/store';
13
+ import { elev, elevOpen } from '../editor/elevation';
14
+ import { cabPoints } from '../model/faces';
15
+ import { drawKeyPlan, drawWallBackdrop, drawWallChips, drawWallSelection, type ElevAct } from './elevation';
16
+ import type { Constraint, Page, Sketch, WallFace, XY } from '../model/types';
11
17
  import { fmtAng, fmtArea, fmtLen } from '../units';
12
18
  import { FONT, TH } from './theme';
13
- import { cvs, imgCache, mainT, noteHandles, S, sketchBounds, underlayH, underlayHandles, type Xform } from './view';
19
+ import { cvs, imgCache, keyPlanSize, mainT, nextThumbFrame, noteFont, noteHandles, noteTextBox, S, sketchBounds, underlayH, underlayHandles, underlayMid, type Xform } from './view';
14
20
  import type { DrawCtx } from './ctx';
21
+ import { dashFor, isSegment, NOTE_TEXT_SIZE, noteLook, shapeOutline } from '../model/markup';
22
+ import { strokeMarkup, strokeOutline } from './markup';
23
+ import { drawLink } from './linkTile';
24
+ import { isLink, linkInfoOf } from '../model/links';
25
+ import { smoothRectPath } from './squircle';
15
26
 
16
27
  type C2D = DrawCtx;
17
28
 
@@ -19,34 +30,52 @@ type C2D = DrawCtx;
19
30
  let hits: HitChip[] | null = null;
20
31
  const addHit = (h: HitChip): void => { if (hits) hits.push(h); };
21
32
 
22
- const OPENING_PAD = 4;
23
33
 
34
+ /** A rounded rectangle's path, its corners smoothed as Figma's 60% ("iOS"): render/squircle.ts. */
24
35
  export function rr(c: C2D, x: number, y: number, w: number, h: number, r: number): void {
25
- c.beginPath();
26
- c.moveTo(x + r, y);
27
- c.arcTo(x + w, y, x + w, y + h, r);
28
- c.arcTo(x + w, y + h, x, y + h, r);
29
- c.arcTo(x, y + h, x, y, r);
30
- c.arcTo(x, y, x + w, y, r);
31
- c.closePath();
36
+ // (one shape for each size, moved into place where the engine can: hundreds of these are drawn every frame)
37
+ if (c.shape) c.shape('rr ' + w + ' ' + h + ' ' + r, x, y, (cc, X, Y) => smoothRectPath(cc, X, Y, w, h, r));
38
+ else smoothRectPath(c, x, y, w, h, r);
32
39
  }
33
40
 
34
41
  export interface ChipOpts {
35
42
  bg: string; fg: string; border?: [string, number?] | null; font?: string; pad?: number; h?: number; minW?: number;
36
43
  kind?: HitChip['kind']; id?: string; rot?: number;
44
+ /** a link mark before the text (a value set elsewhere: another view, or a measurement of the job) */
45
+ mark?: 'link';
46
+ }
47
+
48
+ /** Blue for outlined (linked) chips: the lighter blue on a dark canvas, so the text stays readable. */
49
+ export function linkBlue(): string {
50
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i.exec(TH.bg);
51
+ const dark = !!m && (parseInt(m[1], 16) * 299 + parseInt(m[2], 16) * 587 + parseInt(m[3], 16) * 114) / 1000 < 128;
52
+ return dark ? TH.lblue : TH.blue;
53
+ }
54
+
55
+ /** A small chain-link mark centred on (x, y). */
56
+ export function linkMark(c: C2D, x: number, y: number, color: string): void {
57
+ c.save();
58
+ c.translate(x, y); c.rotate(-Math.PI / 4);
59
+ c.strokeStyle = color; c.lineWidth = 1.4;
60
+ c.beginPath(); c.roundRect(-6.5, -2.25, 7.5, 4.5, 2.25); c.roundRect(-1, -2.25, 7.5, 4.5, 2.25); c.stroke();
61
+ c.restore();
37
62
  }
38
63
 
64
+ /** What a link mark takes before a chip's text. */
65
+ const MARK_W = 14;
66
+
39
67
  /** Rounded label on the canvas; registers a hit region when `kind` is given. */
40
- export function chip(c: C2D, text: string, cx: number, cy: number, { bg, fg, border, font = '600 12px', pad = 7, h = 22, minW = 0, kind, id, rot = 0 }: ChipOpts): number {
68
+ export function chip(c: C2D, text: string, cx: number, cy: number, { bg, fg, border, font = '600 12px', pad = 7, h = 22, minW = 0, kind, id, rot = 0, mark }: ChipOpts): number {
41
69
  c.font = font + ' ' + FONT;
42
- const w = Math.max(minW, c.measureText(text).width + pad * 2);
70
+ const mw = mark ? MARK_W : 0, w = Math.max(minW, c.measureText(text).width + pad * 2 + mw);
43
71
  const x = cx - w / 2, y = cy - h / 2;
44
72
  c.save();
45
73
  if (rot) { c.translate(cx, cy); c.rotate(rot); c.translate(-cx, -cy); }
46
74
  rr(c, x, y, w, h, 5);
47
75
  c.fillStyle = bg; c.fill();
48
76
  if (border) { c.lineWidth = border[1] || 1; c.strokeStyle = border[0]; c.stroke(); }
49
- c.fillStyle = fg; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(text, cx, cy + 0.5);
77
+ c.fillStyle = fg; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(text, cx + mw / 2, cy + 0.5);
78
+ if (mark) linkMark(c, x + pad + 4.5, cy, fg);
50
79
  c.restore();
51
80
  if (kind) {
52
81
  const ext = rot ? Math.max(w, h) / 2 : 0;
@@ -55,29 +84,37 @@ export function chip(c: C2D, text: string, cx: number, cy: number, { bg, fg, bor
55
84
  return w;
56
85
  }
57
86
 
58
- function drawGrid(c: C2D, T: Xform): void {
87
+ /** The grid over the screen, and `m` px past each edge (see KeepOpts). */
88
+ function drawGrid(c: C2D, T: Xform, m = 0): void {
59
89
  const { W, H } = cvs;
60
90
  const { minor, major } = gridSteps(T.s);
61
- const x0 = (0 - T.ox) / T.s, x1 = (W - T.ox) / T.s, y1 = (T.oy - 0) / T.s, y0 = (T.oy - H) / T.s;
91
+ const x0 = (-m - T.ox) / T.s, x1 = (W + m - T.ox) / T.s, y1 = (T.oy + m) / T.s, y0 = (T.oy - H - m) / T.s;
62
92
  const lines = (step: number, color: string, w: number) => {
63
- if ((x1 - x0) / step > 600) return;
93
+ // (too dense to draw: judged on the screen's width, whatever is drawn past it)
94
+ if (W / T.s / step > 600) return;
64
95
  c.beginPath(); c.strokeStyle = color; c.lineWidth = w;
65
- for (let x = Math.floor(x0 / step) * step; x <= x1; x += step) { const sx = Math.round(x * T.s + T.ox) + 0.5; c.moveTo(sx, 0); c.lineTo(sx, H); }
66
- for (let y = Math.floor(y0 / step) * step; y <= y1; y += step) { const sy = Math.round(-y * T.s + T.oy) + 0.5; c.moveTo(0, sy); c.lineTo(W, sy); }
96
+ for (let x = Math.floor(x0 / step) * step; x <= x1; x += step) { const sx = Math.round(x * T.s + T.ox) + 0.5; c.moveTo(sx, -m); c.lineTo(sx, H + m); }
97
+ for (let y = Math.floor(y0 / step) * step; y <= y1; y += step) { const sy = Math.round(-y * T.s + T.oy) + 0.5; c.moveTo(-m, sy); c.lineTo(W + m, sy); }
67
98
  c.stroke();
68
99
  };
69
100
  lines(minor, TH.grid, 1);
70
101
  lines(major, TH.grid2, 1);
71
102
  }
72
103
 
104
+ /** The page's photos and plans, in order (the last on top). */
73
105
  function drawUnderlay(c: C2D, T: Xform): void {
74
- const u = page().underlay;
75
- if (!u || !settings.layers.underlay) return;
76
- const im = imgCache(u.src);
77
- if (!im || !im.complete || !im.naturalWidth) return;
78
- const h = underlayH(u);
79
- const [x, y] = S(T, u.x, u.y);
80
- c.save(); c.globalAlpha = u.opacity ?? 0.6; c.drawImage(im, x, y, u.w * T.s, h * T.s); c.restore();
106
+ if (!settings.layers.underlay) return;
107
+ for (const u of page().underlays) {
108
+ const im = imgCache(u.src);
109
+ if (!im || !im.complete || !im.naturalWidth) continue;
110
+ // drawn about its middle, turned as it's set (clockwise as seen: the canvas turns that way)
111
+ const h = underlayH(u), m = underlayMid(u), [x, y] = S(T, m.x, m.y), w = u.w * T.s, hh = h * T.s;
112
+ c.save(); c.globalAlpha = u.opacity ?? 0.6;
113
+ c.translate(x, y);
114
+ if (u.rot) c.rotate((u.rot * Math.PI) / 180);
115
+ c.drawImage(im, -w / 2, -hh / 2, w, hh);
116
+ c.restore();
117
+ }
81
118
  }
82
119
 
83
120
  const inkFor = (defined: boolean, constr: boolean | undefined) => (constr ? TH.constr : defined ? TH.def : TH.ink);
@@ -106,30 +143,6 @@ function drawRooms(c: C2D, T: Xform, sk: Sketch, thumb?: boolean): void {
106
143
  }
107
144
  }
108
145
 
109
- function wallDegree(sk: Sketch): Record<string, number> {
110
- const deg: Record<string, number> = {};
111
- for (const id in sk.lines) {
112
- const l = sk.lines[id];
113
- if (l.kind !== 'wall') continue;
114
- deg[l.a] = (deg[l.a] || 0) + 1; deg[l.b] = (deg[l.b] || 0) + 1;
115
- }
116
- for (const id in sk.arcs) {
117
- const a = sk.arcs[id];
118
- if (a.kind !== 'wall') continue;
119
- deg[a.a] = (deg[a.a] || 0) + 1; deg[a.b] = (deg[a.b] || 0) + 1;
120
- }
121
- return deg;
122
- }
123
-
124
- /** Wall centerline path, extended at joints by half the thickness so corners close. */
125
- function wallPath(c: C2D, T: Xform, sk: Sketch, l: Line, deg: Record<string, number>, extra?: number): void {
126
- const A = sk.points[l.a], B = sk.points[l.b];
127
- const dx = B.x - A.x, dy = B.y - A.y, L = hyp(dx, dy) || 1e-9, ux = dx / L, uy = dy / L, e = l.thick / 2 + (extra || 0);
128
- const ea = deg[l.a] > 1 ? e : 0, eb = deg[l.b] > 1 ? e : 0;
129
- const [x1, y1] = S(T, A.x - ux * ea, A.y - uy * ea), [x2, y2] = S(T, B.x + ux * eb, B.y + uy * eb);
130
- c.moveTo(x1, y1); c.lineTo(x2, y2);
131
- }
132
-
133
146
  interface CurveWall { id: string; kind: 'circle' | 'arc'; thick: number; constr?: boolean }
134
147
 
135
148
  function curveWalls(sk: Sketch): CurveWall[] {
@@ -151,46 +164,137 @@ function curveWallPath(c: C2D, T: Xform, sk: Sketch, cw: CurveWall): void {
151
164
  }
152
165
  }
153
166
 
167
+ /**
168
+ * Cabinets in the plan: each one's footprint against its wall, from the wall's view (read-only — tap one to open its
169
+ * wall). Drawn as cabinetry, not a shape to edit: base cabinets hatched, with a bold front where the doors face the room
170
+ * and their name when there's room for it; wall (upper) cabinets dashed with a cross, as drafted.
171
+ */
172
+ function drawCabinetFootprints(c: C2D, T: Xform, sk: Sketch): void {
173
+ const fps = cabinetFootprints(sk);
174
+ if (!fps.length) return;
175
+ for (const fp of fps) {
176
+ // pts: along the wall face (0 → 1), then the front, out in the room (2 → 3)
177
+ const q = fp.pts.map(p => S(T, p.x, p.y));
178
+ const path = () => { c.beginPath(); q.forEach(([x, y], i) => (i ? c.lineTo(x, y) : c.moveTo(x, y))); c.closePath(); };
179
+ const hov = ed.hover?.type === 'cabfoot' && ed.hover.id === fp.key + '#' + fp.id;
180
+ c.save();
181
+ if (fp.upper) {
182
+ path(); c.setLineDash([6, 4]); c.strokeStyle = TH.ink2; c.lineWidth = hov ? 2 : 1.2; c.stroke();
183
+ c.beginPath(); c.moveTo(...q[0]); c.lineTo(...q[2]); c.moveTo(...q[1]); c.lineTo(...q[3]); c.lineWidth = 1; c.stroke();
184
+ c.restore();
185
+ continue;
186
+ }
187
+ path();
188
+ c.fillStyle = hov ? 'rgba(128,128,128,.30)' : 'rgba(128,128,128,.16)'; c.fill();
189
+ // hatching, clipped to the footprint
190
+ c.save(); path(); c.clip();
191
+ const xs = q.map(p => p[0]), ys = q.map(p => p[1]), x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys);
192
+ c.strokeStyle = TH.ink2; c.globalAlpha = 0.45; c.lineWidth = 1; c.beginPath();
193
+ for (let k = x0 - (y1 - y0); k < x1; k += 7) { c.moveTo(k, y1); c.lineTo(k + (y1 - y0), y0); }
194
+ c.stroke(); c.restore();
195
+ path(); c.strokeStyle = TH.ink2; c.lineWidth = hov ? 2 : 1.2; c.stroke();
196
+ // the front (where its doors are)
197
+ c.beginPath(); c.moveTo(...q[2]); c.lineTo(...q[3]); c.strokeStyle = TH.ink; c.lineWidth = 2.5; c.stroke();
198
+ const w = Math.hypot(q[1][0] - q[0][0], q[1][1] - q[0][1]), d = Math.hypot(q[2][0] - q[1][0], q[2][1] - q[1][1]);
199
+ if (Math.max(w, d) > 64 && Math.min(w, d) > 18) {
200
+ // along its longer side, reading upright
201
+ const [p0, p1] = w >= d ? [q[0], q[1]] : [q[1], q[2]], cx = (q[0][0] + q[2][0]) / 2, cy = (q[0][1] + q[2][1]) / 2;
202
+ let a = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
203
+ if (a > Math.PI / 2) a -= Math.PI; else if (a <= -Math.PI / 2) a += Math.PI;
204
+ c.translate(cx, cy); c.rotate(a);
205
+ c.font = '600 10px ' + FONT; c.textAlign = 'center'; c.textBaseline = 'middle';
206
+ c.fillStyle = TH.bg; c.globalAlpha = 0.85; const tw = c.measureText('Cabinet').width; c.fillRect(-tw / 2 - 3, -7, tw + 6, 14);
207
+ c.globalAlpha = 1; c.fillStyle = TH.ink; c.fillText('Cabinet', 0, 0.5);
208
+ }
209
+ c.restore();
210
+ }
211
+ }
212
+
213
+ /** A wall piece's outline, added to the current path (`fresh`: start a new path first). */
214
+ function piecePath(c: C2D, T: Xform, pts: XY[], fresh = true): void {
215
+ if (fresh) c.beginPath();
216
+ pts.forEach((p, i) => { const [x, y] = S(T, p.x, p.y); if (i) c.lineTo(x, y); else c.moveTo(x, y); });
217
+ c.closePath();
218
+ }
219
+
220
+ /**
221
+ * Walls as solid bands of their thickness, outlined in the definition colour. Straight walls are filled outlines cut
222
+ * to fit where they meet (model/wallshape.ts), so corners at any angle close cleanly; curved walls are thick strokes.
223
+ * Every outline is drawn first and the fill over all of them, so only the outside of the outlines shows, not the seams.
224
+ */
154
225
  function drawWalls(c: C2D, T: Xform, sk: Sketch, thumb?: boolean): void {
155
- const deg = wallDegree(sk);
156
- const walls = Object.entries(sk.lines).filter(([, l]) => l.kind === 'wall' && !(l.constr && !settings.layers.construction));
157
- if (!walls.length && !curveWalls(sk).length) return;
158
- c.lineCap = 'butt';
226
+ const shown = (w: { constr?: boolean }) => !(w.constr && !settings.layers.construction);
227
+ const pieces = wallPieces(sk, shown), curves = curveWalls(sk);
228
+ if (!pieces.length && !curves.length) return;
159
229
  const ow = thumb ? 1 : 1.6;
160
- // outline pass (definition color), then fill pass
161
- for (const [id, l] of walls) {
162
- const d = lineDefined(ed.ana, l);
163
- c.beginPath(); wallPath(c, T, sk, l, deg, ow / T.s);
164
- c.lineWidth = l.thick * T.s + ow * 2;
165
- c.strokeStyle = ed.highlight.has(id) ? TH.red : inkFor(d && !thumb, l.constr);
166
- c.stroke();
167
- }
168
- // curved walls: same outline-then-fill treatment
169
- const curves = curveWalls(sk);
230
+ const ink = (id: string) => { const l = sk.lines[id]; return ed.highlight.has(id) ? TH.red : inkFor(lineDefined(ed.ana, l) && !thumb, l.constr); };
231
+ c.save();
232
+ c.lineJoin = 'round'; c.lineCap = 'butt';
233
+ for (const p of pieces) { piecePath(c, T, p.pts); c.lineWidth = ow * 2; c.strokeStyle = ink(p.of); c.stroke(); }
170
234
  for (const cw of curves) {
171
235
  curveWallPath(c, T, sk, cw);
172
236
  c.lineWidth = cw.thick * T.s + ow * 2;
173
237
  c.strokeStyle = ed.highlight.has(cw.id) ? TH.red : inkFor(curveDefined(ed.ana, sk, cw.id) && !thumb, cw.constr);
174
238
  c.stroke();
175
239
  }
176
- for (const [, l] of walls) {
177
- c.beginPath(); wallPath(c, T, sk, l, deg, 0);
178
- c.lineWidth = Math.max(0.5, l.thick * T.s); c.strokeStyle = TH.wall; c.stroke();
179
- }
240
+ // one fill for every piece: pieces filled one by one leave a hairline where they meet
241
+ c.fillStyle = TH.wall;
242
+ c.beginPath();
243
+ for (const p of pieces) piecePath(c, T, p.pts, false);
244
+ c.fill();
180
245
  for (const cw of curves) { curveWallPath(c, T, sk, cw); c.lineWidth = Math.max(0.5, cw.thick * T.s); c.strokeStyle = TH.wall; c.stroke(); }
181
- if (thumb) return;
182
- for (const [id, l] of walls) {
183
- if (isSel('line', id) || (ed.hover && ed.hover.type === 'line' && ed.hover.id === id)) {
184
- c.beginPath(); wallPath(c, T, sk, l, deg, 0);
185
- c.lineWidth = Math.max(3, l.thick * T.s);
186
- c.strokeStyle = isSel('line', id) ? 'rgba(255,173,0,.55)' : 'rgba(255,173,0,.28)';
187
- c.stroke();
246
+ if (!thumb) {
247
+ for (const p of pieces) {
248
+ if (!p.id || !(isSel('line', p.id) || (ed.hover && ed.hover.type === 'line' && ed.hover.id === p.id))) continue;
249
+ piecePath(c, T, p.pts);
250
+ c.fillStyle = isSel('line', p.id) ? 'rgba(255,173,0,.55)' : 'rgba(255,173,0,.28)';
251
+ c.fill();
188
252
  }
253
+ drawEdges(c, T, sk);
254
+ }
255
+ c.restore();
256
+ }
257
+
258
+ /**
259
+ * Wall edges picked on their own (and the one under the pointer): along the face, from its corner to its corner — a
260
+ * curved wall's all along it.
261
+ */
262
+ function drawEdges(c: C2D, T: Xform, sk: Sketch): void {
263
+ const ids = ed.sel.filter(s => s.type === 'wface').map(s => s.id);
264
+ const hov = ed.hover?.type === 'wface' && !ids.includes(ed.hover.id) ? ed.hover.id : null;
265
+ if (hov) ids.push(hov);
266
+ for (const id of ids) {
267
+ const [lid, side] = edgeOf(id);
268
+ if (!sk.lines[lid]) { drawCurveEdge(c, T, sk, lid, side, id !== hov); continue; }
269
+ const a = faceCorner(sk, lid, side, 'a'), b = faceCorner(sk, lid, side, 'b');
270
+ if (!a || !b) continue;
271
+ const [ax, ay] = S(T, a.x, a.y), [bx, by] = S(T, b.x, b.y), lit = id !== hov;
272
+ c.lineCap = 'round';
273
+ c.strokeStyle = lit ? TH.yellow : 'rgba(255,173,0,.6)'; c.lineWidth = lit ? 3.5 : 2.5;
274
+ c.beginPath(); c.moveTo(ax, ay); c.lineTo(bx, by); c.stroke();
275
+ // (its corners: where a length on it is measured from and to)
276
+ c.fillStyle = lit ? TH.yellow : 'rgba(255,173,0,.6)';
277
+ for (const [x, y] of [[ax, ay], [bx, by]]) { c.beginPath(); c.arc(x, y, lit ? 4 : 3, 0, Math.PI * 2); c.fill(); }
189
278
  }
190
279
  }
191
280
 
281
+ /** A curved wall's outer (+1) or inner (−1) edge, picked (`lit`) or under the pointer. */
282
+ function drawCurveEdge(c: C2D, T: Xform, sk: Sketch, id: string, side: 1 | -1, lit: boolean): void {
283
+ const w = sk.circles[id] ?? sk.arcs[id], f = curveFrame(sk, id);
284
+ if (!w || !f) return;
285
+ const rr = (f.r + (side * wallThick(w)) / 2) * T.s, [x, y] = S(T, f.o.x, f.o.y);
286
+ if (rr <= 0) return;
287
+ c.lineCap = 'round';
288
+ c.strokeStyle = lit ? TH.yellow : 'rgba(255,173,0,.6)'; c.lineWidth = lit ? 3.5 : 2.5;
289
+ c.beginPath();
290
+ if (f.arc) c.arc(x, y, rr, -f.arc.t0, -f.arc.t1, true); else c.arc(x, y, rr, 0, Math.PI * 2);
291
+ c.stroke();
292
+ }
293
+
192
294
  function drawOpenings(c: C2D, T: Xform, sk: Sketch): void {
193
- for (const id in sk.openings) {
295
+ // the selected or hovered one last: a neighbour drawn after it would knock the wall out over its outline
296
+ const lit = (id: string) => isSel('opening', id) || (ed.hover?.type === 'opening' && ed.hover.id === id);
297
+ for (const id of Object.keys(sk.openings).sort((a, b) => Number(lit(a)) - Number(lit(b)))) {
194
298
  const o = sk.openings[id], g = openingFrame(sk, id);
195
299
  if (!g) continue;
196
300
  const { A, ux, uy, nx, ny, s0, s1, w, t } = g, f = o.flip ? -1 : 1;
@@ -207,10 +311,10 @@ function drawOpenings(c: C2D, T: Xform, sk: Sketch): void {
207
311
  const sel1 = isSel('opening', id);
208
312
  const hov = !sel1 && ed.hover && ed.hover.type === 'opening' && ed.hover.id === id;
209
313
  const ext = OPENING_TICK_EXT / T.s;
210
- // hover / selection band so it's obvious what a click will grab
314
+ // hover / selection band so it's obvious what a click will grab: exactly its footprint in the wall, face to face
315
+ // and end to end (not padded out past them)
211
316
  if (sel1 || hov) {
212
- const pad = OPENING_PAD / T.s;
213
- quad(s0 - pad, s1 + pad, t / 2 + ext + pad);
317
+ quad(s0, s1, t / 2);
214
318
  c.fillStyle = sel1 ? 'rgba(255,173,0,.30)' : 'rgba(255,173,0,.16)'; c.fill();
215
319
  c.lineWidth = sel1 ? 2 : 1.5; c.strokeStyle = sel1 ? TH.yellow : 'rgba(255,173,0,.85)'; c.stroke();
216
320
  }
@@ -255,6 +359,7 @@ function drawLines(c: C2D, T: Xform, sk: Sketch, thumb?: boolean): void {
255
359
  c.beginPath(); c.moveTo(x1, y1); c.lineTo(x2, y2);
256
360
  c.lineWidth = 7; c.strokeStyle = selected ? 'rgba(255,173,0,.6)' : 'rgba(255,173,0,.3)'; c.stroke();
257
361
  }
362
+ if (l.anchor || l.cab) continue; // a wall face's edge from the plan, a cabinet's: the wall view's backdrop draws them
258
363
  c.beginPath(); c.moveTo(x1, y1); c.lineTo(x2, y2);
259
364
  if (l.constr) { c.setLineDash([6, 5]); c.lineWidth = 1.2; } else c.lineWidth = thumb ? 1.2 : 2;
260
365
  c.strokeStyle = ed.highlight.has(id) ? TH.red : inkFor(lineDefined(ed.ana, l) && !thumb, l.constr);
@@ -297,9 +402,15 @@ function drawCurves(c: C2D, T: Xform, sk: Sketch, thumb?: boolean): void {
297
402
  }
298
403
  }
299
404
 
405
+ /** Points on a line that isn't fully defined: drawn in ink, not green (worked out once a frame, not once a point). */
406
+ const looseEnds = (sk: Sketch): Set<string> => frameMemo(sk, 'looseEnds', () => {
407
+ const out = new Set<string>();
408
+ for (const lid in sk.lines) { const l = sk.lines[lid]; if (!lineDefined(ed.ana, l)) { out.add(l.a); out.add(l.b); } }
409
+ return out;
410
+ });
411
+
300
412
  function inkForPoint(sk: Sketch, id: string): string {
301
- for (const lid in sk.lines) { const l = sk.lines[lid]; if ((l.a === id || l.b === id) && !lineDefined(ed.ana, l)) return TH.ink; }
302
- return TH.def;
413
+ return looseEnds(sk).has(id) ? TH.ink : TH.def;
303
414
  }
304
415
 
305
416
  function drawLock(c: C2D, x: number, y: number): void {
@@ -316,6 +427,8 @@ function drawLock(c: C2D, x: number, y: number): void {
316
427
  function drawPoints(c: C2D, T: Xform, sk: Sketch): void {
317
428
  const { W, H } = cvs;
318
429
  const touch = ed.lastPointerTouch;
430
+ // a cabinet's corners aren't grab points: it moves as one, like a door or window
431
+ const cabCorners = new Set(Object.values((sk as WallFace).cabs ?? {}).flatMap(cabPoints));
319
432
  for (const id in sk.points) {
320
433
  const p = sk.points[id];
321
434
  const [x, y] = S(T, p.x, p.y);
@@ -325,13 +438,16 @@ function drawPoints(c: C2D, T: Xform, sk: Sketch): void {
325
438
  c.beginPath(); c.moveTo(x - 9, y); c.lineTo(x + 9, y); c.moveTo(x, y - 9); c.lineTo(x, y + 9); c.stroke();
326
439
  }
327
440
  const selected = isSel('point', id), hov = ed.hover && ed.hover.type === 'point' && ed.hover.id === id;
441
+ // a wall face's corners from the plan, and a cabinet's, show only when picked or under the pointer (like a window's)
442
+ if ((p.anchor || cabCorners.has(id)) && !selected && !hov) continue;
328
443
  const r = (touch ? 5 : 4.2) + (hov ? 1.5 : 0);
444
+ // (a door's or window's corners on a wall face are free until the plan locks it, or the face pins it: as a cabinet's)
329
445
  const det = ed.ana.det.has(id);
330
446
  c.beginPath(); c.arc(x, y, r, 0, Math.PI * 2);
331
447
  if (selected) { c.fillStyle = TH.yellow; c.fill(); c.lineWidth = 1.5; c.strokeStyle = TH.ink; c.stroke(); }
332
448
  else if (det) { c.fillStyle = ed.highlight.has(id) ? TH.red : p.origin ? TH.ink : inkForPoint(sk, id); c.fill(); }
333
449
  else { c.fillStyle = TH.bg; c.fill(); c.lineWidth = 1.8; c.strokeStyle = TH.ink; c.stroke(); }
334
- if (p.fixed && !p.origin) drawLock(c, x + 9, y - 14);
450
+ if (p.fixed && !p.origin && !p.anchor) drawLock(c, x + 9, y - 14);
335
451
  }
336
452
  }
337
453
 
@@ -343,21 +459,23 @@ type DimAnchor =
343
459
  function dimAnchor(sk: Sketch, c: Constraint): DimAnchor | null {
344
460
  const P = (id: string) => sk.points[id];
345
461
  if (c.type === 'dist') {
346
- const a = P(c.a!), b = P(c.b!);
462
+ // (measured on a wall's face: from its corner to corner, the label off the face on that side)
463
+ const e = dimEnds(sk, c);
464
+ if (!e) return null;
465
+ const { a, b } = e;
347
466
  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;
348
467
  return { m: { x: (a.x + b.x) / 2 + nx * off, y: (a.y + b.y) / 2 + ny * off }, a, b, nx, ny, L };
349
468
  }
350
469
  if (c.type === 'pldist') {
351
- const l = sk.lines[c.l!], p = P(c.p!), A = P(l.a), B = P(l.b);
352
- const t = projT(A, B, p);
353
- const f = { x: A.x + (B.x - A.x) * t, y: A.y + (B.y - A.y) * t };
354
- return { m: { x: (p.x + f.x) / 2, y: (p.y + f.y) / 2 }, a: p, b: f };
470
+ // (between walls' faces: from face to face)
471
+ const e = dimEnds(sk, c);
472
+ if (!e) return null;
473
+ return { m: { x: (e.a.x + e.b.x) / 2, y: (e.a.y + e.b.y) / 2 }, a: e.a, b: e.b };
355
474
  }
356
475
  if (c.type === 'diam') {
357
- const ci = curveInfo(sk, c.c)!;
358
- const o = P(ci.o.c);
359
- const r = ci.kind === 'circle' ? ci.o.r : hyp(P(ci.o.a).x - o.x, P(ci.o.a).y - o.y);
360
- return { m: { x: o.x, y: o.y }, o, r };
476
+ // (across a curved wall's face: its arrows on that face)
477
+ const g = dimRadius(sk, c);
478
+ return g ? { m: { x: g.o.x, y: g.o.y }, o: g.o, r: g.r } : null;
361
479
  }
362
480
  if (c.type === 'angle') {
363
481
  const l1 = sk.lines[c.l1!], l2 = sk.lines[c.l2!];
@@ -373,20 +491,38 @@ function dimAnchor(sk: Sketch, c: Constraint): DimAnchor | null {
373
491
  return null;
374
492
  }
375
493
 
376
- /** Driving/reference dimension chips. (Lengths are added from the bottom bar's + / Length, not on-canvas prompts.) */
494
+ /** Whether a dimension, or something it measures (its lines, points or curves), is selected or hovered. */
495
+ function dimLive(sk: Sketch, id: string, d: Constraint): boolean {
496
+ const pts = consPoints(sk, d), lines = new Set([d.line, d.l, d.l1, d.l2].filter(Boolean)), curves = new Set([d.c, d.c1, d.c2].filter(Boolean));
497
+ const hit = (x: { type: string; id: string } | null) => !!x && (
498
+ ((x.type === 'dim' || x.type === 'cons') && x.id === id) || (x.type === 'point' && pts.has(x.id)) || (x.type === 'line' && lines.has(x.id))
499
+ || ((x.type === 'circle' || x.type === 'arc') && curves.has(x.id)));
500
+ return ed.sel.some(hit) || hit(ed.hover);
501
+ }
502
+
503
+ /**
504
+ * Driving/reference dimension chips. (Lengths are added from the bottom bar's + / Length, not on-canvas prompts.) What's
505
+ * been entered (blue, or red when it can't be met) always shows; a reference only while it or what it measures is
506
+ * selected or hovered.
507
+ */
377
508
  function drawDims(c: C2D, T: Xform, sk: Sketch): void {
378
509
  if (!settings.layers.dims) return;
379
510
  for (const id in sk.cons) {
380
511
  const d = sk.cons[id];
381
- if (!isDim(d) || d.layer === 'print') continue;
512
+ if (!isDim(d) || d.layer === 'print' || d.elev) continue; // the wall view's own dimensions are its chips
382
513
  const A = dimAnchor(sk, d);
383
514
  if (!A) continue;
384
515
  const ref = d.driving === false, val = ref ? measure(sk, d) : d.value!;
385
516
  const selected = isSel('dim', id) || isSel('cons', id);
386
517
  const bad = ed.highlight.has(id) || ed.ana.bad.has(id);
518
+ if (ref && !bad && !dimLive(sk, id, d)) continue;
519
+ // a measurement of the job sets it: outlined, with a link mark — red once the job's measurement is gone
520
+ const linked = !!d.measurementRef && !ref && !bad, gone = linked && linkInfoOf(d.measurementRef!)?.exists === false;
387
521
  let bg = ref ? TH.ref : TH.blue, fg = ref ? TH.reftext : '#fff';
388
522
  if (bad) { bg = TH.red; fg = '#fff'; }
389
- const border: [string, number] | null = selected ? [TH.yellow, 2.5] : null;
523
+ if (linked) { bg = TH.bg; fg = gone ? TH.red : d.type === 'angle' ? TH.purple : linkBlue(); }
524
+ const border: [string, number] | null = selected ? [TH.yellow, 2.5] : linked ? [fg, 1.5] : null;
525
+ const mark = linked ? 'link' as const : undefined;
390
526
  const wrap = (s: string) => (ref ? '(' : '') + s + (ref ? ')' : '');
391
527
  if (d.type === 'angle' && 'vtx' in A) {
392
528
  const [vx, vy] = S(T, A.vtx.x, A.vtx.y);
@@ -397,26 +533,29 @@ function drawDims(c: C2D, T: Xform, sk: Sketch): void {
397
533
  while (sweep > Math.PI) sweep -= 2 * Math.PI;
398
534
  while (sweep < -Math.PI) sweep += 2 * Math.PI;
399
535
  c.save(); c.strokeStyle = bad ? TH.red : ref ? TH.chipline : TH.purple; c.lineWidth = 1.2;
536
+ if (ref) c.setLineDash([3, 3]);
400
537
  c.beginPath(); c.arc(vx, vy, 22, t1, t1 + sweep, sweep < 0); c.stroke(); c.restore();
401
538
  }
402
- chip(c, wrap(fmtAng(val)), vx + A.bis[0] * 50, vy - A.bis[1] * 50, { bg: bad ? TH.red : ref ? TH.ref : TH.purple, fg: ref ? TH.reftext : '#fff', border, kind: 'dim', id });
539
+ chip(c, wrap(fmtAng(val)), vx + A.bis[0] * 50, vy - A.bis[1] * 50, linked ? { bg, fg, border, kind: 'dim', id, mark }
540
+ : { bg: bad ? TH.red : ref ? TH.ref : TH.purple, fg: ref ? TH.reftext : '#fff', border, kind: 'dim', id });
403
541
  continue;
404
542
  }
405
543
  if (d.type === 'diam' && 'o' in A) {
406
- drawDiameter(c, T, A.o, A.r, d, 'Ø ' + wrap(fmtLen(val)), bad ? TH.red : ref ? TH.chipline : TH.blue, { bg, fg, border, kind: 'dim', id });
544
+ drawDiameter(c, T, A.o, A.r, d, d.rad ? 'R ' + wrap(fmtLen(val / 2)) : 'Ø ' + wrap(fmtLen(val)), bad ? TH.red : ref ? TH.chipline : TH.blue, { bg, fg, border, kind: 'dim', id, mark }, ref);
407
545
  continue;
408
546
  }
409
547
  if (!('a' in A)) continue;
410
548
  const [mx, my] = S(T, A.m.x, A.m.y);
411
549
  const lineColor = bad ? TH.red : ref ? TH.chipline : TH.blue;
412
550
  if (d.type === 'dist' && A.nx != null && Math.abs((d.off || 0) * T.s) > 10) {
413
- drawDimLeaders(c, T, A.a, A.b, A.nx, A.ny!, d.off!, lineColor);
551
+ drawDimLeaders(c, T, A.a, A.b, A.nx, A.ny!, d.off!, lineColor, ref);
414
552
  } else if (d.type === 'pldist' || !d.line) {
553
+ // (dashed only when it's a reference: a dashed line means the number holds nothing, wherever it's drawn)
415
554
  const [ax, ay] = S(T, A.a.x, A.a.y), [bx, by] = S(T, A.b.x, A.b.y);
416
- c.save(); c.setLineDash([3, 3]); c.strokeStyle = bad ? TH.red : ref ? TH.chipline : TH.blue; c.lineWidth = 1.2;
555
+ c.save(); if (ref) c.setLineDash([3, 3]); c.strokeStyle = bad ? TH.red : ref ? TH.chipline : TH.blue; c.lineWidth = 1.2;
417
556
  c.beginPath(); c.moveTo(ax, ay); c.lineTo(bx, by); c.stroke(); c.restore();
418
557
  }
419
- chip(c, wrap(fmtLen(val)), mx, my, { bg, fg, border, kind: 'dim', id });
558
+ chip(c, wrap(fmtLen(val)), mx, my, { bg, fg, border, kind: 'dim', id, mark });
420
559
  }
421
560
  }
422
561
 
@@ -482,6 +621,8 @@ function drawOpeningDims(c: C2D, T: Xform, sk: Sketch): void {
482
621
  for (const d of o.dims || []) {
483
622
  const r = refS(sk, o, d.ref), rp = refPoint(sk, d.ref);
484
623
  if (r == null || !rp) continue;
624
+ // a reference shows only while its door or window is selected or hovered; what locks it always
625
+ if (!d.driving && !ed.badOpeningDims.has(d.id) && !active) continue;
485
626
  const e = d.edge === 's0' ? g.s0 : g.s1;
486
627
  const k = d.off ?? kRow(place(r, e));
487
628
  const bad = ed.badOpeningDims.has(d.id);
@@ -504,13 +645,15 @@ function drawOpeningDims(c: C2D, T: Xform, sk: Sketch): void {
504
645
  // blue is only for dimensions that lock something
505
646
  if (active && !locked) {
506
647
  const grey = TH.chipline;
507
- if (g.s0 > 0.5) { ext(0, kw, grey); span(0, g.s0, kw, grey); label(0, g.s0, 'a', fmtLen(g.s0), kw, TH.ref, TH.reftext); }
508
- if (L - g.s1 > 0.5) { ext(L, kw, grey); span(g.s1, L, kw, grey); label(g.s1, L, 'b', fmtLen(L - g.s1), kw, TH.ref, TH.reftext); }
648
+ if (g.s0 > 0.5) { ext(0, kw, grey); span(0, g.s0, kw, grey); label(0, g.s0, 'a', '(' + fmtLen(g.s0) + ')', kw, TH.ref, TH.reftext); }
649
+ if (L - g.s1 > 0.5) { ext(L, kw, grey); span(g.s1, L, kw, grey); label(g.s1, L, 'b', '(' + fmtLen(L - g.s1) + ')', kw, TH.ref, TH.reftext); }
509
650
  }
510
- // width: a solid dimension line with extension lines from the jambs whenever it's in view (hovered or selected,
511
- // locked or not) or has been pulled away from its usual spot
512
- if (o.wOff != null || active) { ext(g.s0, kw, TH.blue); ext(g.s1, kw, TH.blue); span(g.s0, g.s1, kw, TH.blue, false); }
513
- label(g.s0, g.s1, 'w', fmtLen(g.w), kw, TH.blue, '#fff', isSel('opening', id) ? [TH.yellow, 2] : null);
651
+ // width: a full dimension, a solid line with extension lines from the jambs; blue once it's been entered (and then
652
+ // always shown), grey while it's the standard size it was placed at (and then only while the opening is selected or hovered)
653
+ if (!widthEntered(o) && !active) continue;
654
+ const wc = widthEntered(o) ? TH.blue : TH.chipline;
655
+ ext(g.s0, kw, wc); ext(g.s1, kw, wc); span(g.s0, g.s1, kw, wc, false);
656
+ label(g.s0, g.s1, 'w', fmtLen(g.w), kw, widthEntered(o) ? TH.blue : TH.ref, widthEntered(o) ? '#fff' : TH.reftext, isSel('opening', id) ? [TH.yellow, 2] : null);
514
657
  }
515
658
  }
516
659
 
@@ -522,7 +665,8 @@ function drawOpeningAnchors(c: C2D, T: Xform, sk: Sketch): void {
522
665
  if (ed.mode !== 'draft') return;
523
666
  for (const id in sk.openings) {
524
667
  const o = sk.openings[id];
525
- const show = ed.tool === 'dim' || isSel('opening', id) || (ed.hover != null && ed.hover.id === id);
668
+ const endPicked = (e: string) => isSel('oanchor', id + '|' + e);
669
+ const show = ed.tool === 'dim' || isSel('opening', id) || endPicked('s0') || endPicked('s1') || (ed.hover != null && ed.hover.id === id);
526
670
  if (!show) continue;
527
671
  const locked = !!drivingDim(o);
528
672
  for (const edge of ['s0', 's1'] as const) {
@@ -531,7 +675,7 @@ function drawOpeningAnchors(c: C2D, T: Xform, sk: Sketch): void {
531
675
  const [x, y] = S(T, p.x, p.y);
532
676
  const hov = ed.hover && ed.hover.type === 'oanchor' && ed.hover.id === id && ed.hover.part === edge;
533
677
  // a body pick hasn't settled on an edge yet, so both light up
534
- const picked = ed.pickedAnchor && ed.pickedAnchor.id === id && (ed.pickedAnchor.body || ed.pickedAnchor.edge === edge);
678
+ const picked = (ed.pickedAnchor && ed.pickedAnchor.id === id && (ed.pickedAnchor.body || ed.pickedAnchor.edge === edge)) || endPicked(edge);
535
679
  const r = (ed.lastPointerTouch ? 5 : 4.2) + (hov ? 1.5 : 0);
536
680
  c.save();
537
681
  c.beginPath(); c.rect(x - r, y - r, r * 2, r * 2);
@@ -614,7 +758,7 @@ export function drawPendingDim(c: C2D, T: Xform, p: { from: XY; to: XY; a: XY; b
614
758
  * A linear dimension pulled off its line: extension lines from both ends out to a dimension line with
615
759
  * arrowheads, so the label always reads as attached to what it measures.
616
760
  */
617
- function drawDimLeaders(c: C2D, T: Xform, a: XY, b: XY, nx: number, ny: number, off: number, color: string): void {
761
+ function drawDimLeaders(c: C2D, T: Xform, a: XY, b: XY, nx: number, ny: number, off: number, color: string, dashed = false): void {
618
762
  const [ax, ay] = S(T, a.x, a.y), [bx, by] = S(T, b.x, b.y);
619
763
  const [qax, qay] = S(T, a.x + nx * off, a.y + ny * off), [qbx, qby] = S(T, b.x + nx * off, b.y + ny * off);
620
764
  const [ex, ey] = norm2(qax - ax, qay - ay); // screen direction from geometry toward the dimension line
@@ -625,7 +769,9 @@ function drawDimLeaders(c: C2D, T: Xform, a: XY, b: XY, nx: number, ny: number,
625
769
  for (const [x0, y0, x1, y1] of [[ax, ay, qax, qay], [bx, by, qbx, qby]]) { c.moveTo(x0 + ex * gap, y0 + ey * gap); c.lineTo(x1 + ex * over, y1 + ey * over); }
626
770
  c.stroke();
627
771
  c.lineWidth = 1.2;
772
+ if (dashed) c.setLineDash([3, 3]);
628
773
  c.beginPath(); c.moveTo(qax, qay); c.lineTo(qbx, qby); c.stroke();
774
+ c.setLineDash([]);
629
775
  const [ux, uy] = norm2(qbx - qax, qby - qay);
630
776
  arrowHead(c, [qbx, qby], ux, uy); arrowHead(c, [qax, qay], -ux, -uy);
631
777
  c.restore();
@@ -635,7 +781,7 @@ function drawDimLeaders(c: C2D, T: Xform, a: XY, b: XY, nx: number, ny: number,
635
781
  * Diameter dimension, CAD style: a line across the circle through its centre with arrowheads on the
636
782
  * circle, a leader out past the edge and a short landing with the label, so the centre stays free to grab.
637
783
  */
638
- function drawDiameter(c: C2D, T: Xform, o: XY, r: number, d: Constraint, text: string, color: string, chipOpts: ChipOpts): void {
784
+ function drawDiameter(c: C2D, T: Xform, o: XY, r: number, d: Constraint, text: string, color: string, chipOpts: ChipOpts, dashed = false): void {
639
785
  const a = d.ang ?? DIAM_DEFAULT_ANG, ux = Math.cos(a), uy = Math.sin(a);
640
786
  const [ox, oy] = S(T, o.x, o.y);
641
787
  const rs = r * T.s, sx = ux, sy = -uy; // screen direction (y down)
@@ -646,11 +792,16 @@ function drawDiameter(c: C2D, T: Xform, o: XY, r: number, d: Constraint, text: s
646
792
  const end = [q[0] + side * land, q[1]];
647
793
  c.save();
648
794
  c.strokeStyle = color; c.fillStyle = color; c.lineWidth = 1.2;
649
- c.beginPath(); c.moveTo(p2[0], p2[1]); c.lineTo(q[0], q[1]); c.lineTo(end[0], end[1]); c.stroke();
650
- arrowHead(c, p1, sx, sy); arrowHead(c, p2, -sx, -sy);
795
+ // a radius (a fillet's) runs from the centre out through the arc; a diameter right across
796
+ const from = d.rad ? [ox, oy] : p2;
797
+ if (dashed) c.setLineDash([3, 3]);
798
+ c.beginPath(); c.moveTo(from[0], from[1]); c.lineTo(q[0], q[1]); c.lineTo(end[0], end[1]); c.stroke();
799
+ c.setLineDash([]);
800
+ arrowHead(c, p1, sx, sy);
801
+ if (!d.rad) arrowHead(c, p2, -sx, -sy);
651
802
  c.restore();
652
803
  c.font = (chipOpts.font || '600 12px') + ' ' + FONT;
653
- const w = c.measureText(text).width + 14;
804
+ const w = c.measureText(text).width + 14 + (chipOpts.mark ? MARK_W : 0);
654
805
  chip(c, text, end[0] + side * (w / 2 + 2), end[1], chipOpts);
655
806
  }
656
807
 
@@ -697,6 +848,21 @@ function drawPrintDims(c: C2D, T: Xform, sk: Sketch): void {
697
848
  }
698
849
  }
699
850
 
851
+ /**
852
+ * A badge's symbol drawn as lines rather than a character: ∥ ⊥ ∘ ◎ aren't in every typeface — on a device the
853
+ * app's own (Inter) lacks them and Skia has nothing to fall back on, so they came out as the font's "missing glyph"
854
+ * box. Letters and = — are in every font and stay text. False for a symbol that isn't one of these.
855
+ */
856
+ function drawSymbol(c: C2D, g: string, x: number, y: number, ink: string): boolean {
857
+ c.strokeStyle = ink; c.lineWidth = 1.6; c.lineCap = 'round';
858
+ c.beginPath();
859
+ if (g === '∥') { c.moveTo(x - 2.5, y - 5); c.lineTo(x - 2.5, y + 5); c.moveTo(x + 2.5, y - 5); c.lineTo(x + 2.5, y + 5); c.stroke(); return true; }
860
+ if (g === '⊥') { c.moveTo(x, y - 5); c.lineTo(x, y + 4.5); c.moveTo(x - 5, y + 4.5); c.lineTo(x + 5, y + 4.5); c.stroke(); return true; }
861
+ if (g === '∘') { c.arc(x, y, 3, 0, Math.PI * 2); c.stroke(); return true; }
862
+ if (g === '◎') { c.lineWidth = 1.3; c.arc(x, y, 4.5, 0, Math.PI * 2); c.stroke(); c.beginPath(); c.arc(x, y, 1.8, 0, Math.PI * 2); c.fill(); return true; }
863
+ return false;
864
+ }
865
+
700
866
  const GLYPH: Partial<Record<Constraint['type'], string>> = { h: 'H', v: 'V', par: '∥', perp: '⊥', col: '—', tan: 'T', eq: '=', eqr: '=', mid: 'M', online: '∘', oncircle: '∘', conc: '◎' };
701
867
 
702
868
  function badgeSpots(T: Xform, sk: Sketch, c: Constraint): [number, number][] {
@@ -712,7 +878,7 @@ function badgeSpots(T: Xform, sk: Sketch, c: Constraint): [number, number][] {
712
878
  out.push([x1 + (x2 - x1) * frac + nx * off * side, y1 + (y2 - y1) * frac + ny * off * side]);
713
879
  };
714
880
  if (c.type === 'h' || c.type === 'v') {
715
- const lid = linesBetween(sk, c.a!, c.b!)[0];
881
+ const lid = lineBetween(sk, c.a!, c.b!);
716
882
  if (lid) onLine(lid, 0.22, -1);
717
883
  else {
718
884
  const a = P(c.a!), b = P(c.b!);
@@ -755,7 +921,7 @@ function drawBadges(c: C2D, T: Xform, sk: Sketch): void {
755
921
  }
756
922
  for (const id in sk.cons) {
757
923
  const k = sk.cons[id];
758
- if (isDim(k)) continue;
924
+ if (isDim(k) || k.elev) continue;
759
925
  const g = GLYPH[k.type];
760
926
  if (!g) continue;
761
927
  const related = selIds.has(id) || [k.l, k.l1, k.l2, k.c, k.c1, k.c2].some(x => x && selIds.has(x)) || [k.a, k.b, k.p].some(x => x && selPts.has(x));
@@ -767,8 +933,9 @@ function drawBadges(c: C2D, T: Xform, sk: Sketch): void {
767
933
  rr(c, x - 9, y - 9, 18, 18, 5);
768
934
  c.fillStyle = bad ? TH.red : selected ? TH.yellow : TH.badge; c.fill();
769
935
  c.lineWidth = 1; c.strokeStyle = bad ? TH.red : TH.badgeline; c.stroke();
770
- c.fillStyle = bad ? '#fff' : selected ? '#181A1C' : TH.badgefg;
771
- c.font = '700 11px ' + FONT; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(g, x, y + 0.5);
936
+ const ink = bad ? '#fff' : selected ? '#181A1C' : TH.badgefg;
937
+ c.fillStyle = ink;
938
+ if (!drawSymbol(c, g, x, y, ink)) { c.font = '700 11px ' + FONT; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(g, x, y + 0.5); }
772
939
  c.restore();
773
940
  addHit({ kind: 'cons', id, x: x - 10, y: y - 10, w: 20, h: 20 });
774
941
  }
@@ -792,74 +959,80 @@ export function cloudPath(c: C2D, x: number, y: number, w: number, h: number): v
792
959
  function drawNotes(c: C2D, T: Xform, sk: Sketch): void {
793
960
  if (!settings.layers.annot) return;
794
961
  const dim = ed.mode === 'draft' ? 0.75 : 1;
962
+ nextThumbFrame();
795
963
  for (const n of sk.notes) {
796
964
  const selected = isSel('note', n.id);
965
+ if (isLink(n)) { drawLink(c, T, n, { selected, alpha: dim, info: linkInfoOf(n.link) }); continue; }
797
966
  c.save();
798
967
  c.globalAlpha = dim; c.strokeStyle = n.color; c.fillStyle = n.color; c.lineCap = 'round'; c.lineJoin = 'round';
799
968
  if (selected) { c.shadowColor = TH.yellow; c.shadowBlur = 0; }
800
- if ((n.type === 'pen' || n.type === 'arrow') && n.pts) {
801
- c.lineWidth = 3; c.beginPath();
802
- n.pts.forEach((p, i) => { const [x, y] = S(T, p[0], p[1]); i ? c.lineTo(x, y) : c.moveTo(x, y); });
803
- if (selected) { c.save(); c.lineWidth = 9; c.strokeStyle = 'rgba(255,173,0,.45)'; c.stroke(); c.restore(); }
804
- c.stroke();
805
- if (n.type === 'arrow' && n.pts.length > 1) {
806
- const p1 = n.pts[n.pts.length - 2], p2 = n.pts[n.pts.length - 1];
807
- const [x1, y1] = S(T, p1[0], p1[1]), [x2, y2] = S(T, p2[0], p2[1]);
808
- const a = Math.atan2(y2 - y1, x2 - x1);
809
- c.beginPath(); c.moveTo(x2, y2);
810
- c.lineTo(x2 - 13 * Math.cos(a - 0.45), y2 - 13 * Math.sin(a - 0.45));
811
- c.lineTo(x2 - 13 * Math.cos(a + 0.45), y2 - 13 * Math.sin(a + 0.45));
812
- c.closePath(); c.fill();
813
- }
969
+ const look = noteLook(n);
970
+ const outline = shapeOutline(n);
971
+ if (outline) {
972
+ strokeOutline(c, outline.map(p => S(T, p[0], p[1])), look, selected ? 'rgba(255,173,0,.45)' : undefined);
973
+ } else if ((n.type === 'pen' || isSegment(n)) && n.pts) {
974
+ strokeMarkup(c, n.pts.map(p => S(T, p[0], p[1])), look, selected ? 'rgba(255,173,0,.45)' : undefined);
814
975
  } else if (n.type === 'text') {
815
- const [x, y] = S(T, n.x!, n.y!);
816
- c.font = '600 14px ' + FONT;
817
- const w = c.measureText(n.text!).width + 16;
818
- rr(c, x, y - 14, w, 28, 7);
976
+ // laid out at the one text size and scaled to the note's size on the drawing (its outline stays the same width)
977
+ const { x, w, mid, k } = noteTextBox(T, n, c), lw = selected ? 2.5 : 1.5;
978
+ c.translate(x, mid); c.scale(k, k);
979
+ rr(c, 0, -14, w / k, 28, 7);
819
980
  c.fillStyle = TH.bg; c.globalAlpha = 0.92 * dim; c.fill(); c.globalAlpha = dim;
820
- c.lineWidth = selected ? 2.5 : 1.5; c.strokeStyle = selected ? TH.yellow : n.color; c.stroke();
821
- c.fillStyle = n.color; c.textBaseline = 'middle'; c.fillText(n.text!, x + 8, y + 0.5);
981
+ c.lineWidth = lw / k; c.strokeStyle = selected ? TH.yellow : n.color;
982
+ c.setLineDash(dashFor(look.style, lw).map(v => v / k)); c.stroke(); c.setLineDash([]);
983
+ c.font = noteFont(NOTE_TEXT_SIZE);
984
+ c.fillStyle = n.color; c.textAlign = 'left'; c.textBaseline = 'middle'; c.fillText(n.text!, 8, 0.5 / k); // (left: not whatever was drawn last)
822
985
  } else if (n.type === 'cloud' && n.pts) {
823
986
  const [x1, y1] = S(T, n.pts[0][0], n.pts[0][1]), [x2, y2] = S(T, n.pts[1][0], n.pts[1][1]);
824
987
  cloudPath(c, Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
825
- c.lineWidth = selected ? 4 : 2.5;
826
- if (selected) c.strokeStyle = TH.yellow;
988
+ if (selected) { c.save(); c.lineWidth = look.width + 6; c.strokeStyle = 'rgba(255,173,0,.45)'; c.stroke(); c.restore(); }
989
+ c.lineWidth = look.width;
990
+ if (look.style === 'dot') c.lineCap = 'round';
991
+ c.setLineDash(dashFor(look.style, look.width));
827
992
  c.stroke();
993
+ c.setLineDash([]);
828
994
  }
829
995
  c.restore();
830
996
  }
831
997
  }
832
998
 
833
- /** Selected image: blue outline with corner (scale) and side (stretch) handles, or a lock badge when locked. */
999
+ /** Selected images: blue outline with corner (scale) and side (stretch) handles — one image alone — or a lock badge when locked. */
834
1000
  function drawUnderlaySelection(c: C2D, T: Xform): void {
835
- const u = page().underlay;
836
- if (!u || !settings.layers.underlay || ed.mode !== 'annotate' || !isSel('underlay', 'u')) return;
837
- const hs = underlayHandles(T, u), nw = hs[0], se = hs[4];
838
- c.save();
839
- c.strokeStyle = TH.blue; c.lineWidth = 1.5;
840
- if (u.locked) c.setLineDash([6, 4]);
841
- c.strokeRect(nw.x, nw.y, se.x - nw.x, se.y - nw.y);
842
- c.setLineDash([]);
843
- if (u.locked) { drawLock(c, nw.x + 12, nw.y + 12); c.restore(); return; }
844
- drawHandleSet(c, hs);
845
- c.restore();
1001
+ if (!settings.layers.underlay || ed.mode !== 'annotate') return;
1002
+ const sel = page().underlays.filter(u => isSel('underlay', u.id));
1003
+ for (const u of sel) {
1004
+ const hs = underlayHandles(T, u), nw = hs[0];
1005
+ c.save();
1006
+ c.strokeStyle = TH.blue; c.lineWidth = 1.5;
1007
+ if (u.locked) c.setLineDash([6, 4]);
1008
+ // its outline, turned with it: through the corners
1009
+ c.beginPath(); [hs[0], hs[2], hs[4], hs[6]].forEach((q, i) => (i ? c.lineTo(q.x, q.y) : c.moveTo(q.x, q.y))); c.closePath(); c.stroke();
1010
+ c.setLineDash([]);
1011
+ if (u.locked) drawLock(c, nw.x + 12, nw.y + 12);
1012
+ else if (sel.length === 1) drawHandleSet(c, hs);
1013
+ c.restore();
1014
+ }
846
1015
  }
847
1016
 
848
- /** Resize handles: round white corners (and arrow ends), blue bars on the sides. */
849
- function drawHandleSet(c: C2D, hs: { part: string; x: number; y: number }[]): void {
1017
+ /** Resize handles: round white corners (and arrow ends), blue bars on the sides — along them, on a turned image (`ang`). */
1018
+ function drawHandleSet(c: C2D, hs: { part: string; x: number; y: number; ang?: number }[]): void {
850
1019
  const r = ed.lastPointerTouch ? 8 : 6;
851
1020
  for (const hd of hs) {
852
1021
  const side = hd.part === 'n' || hd.part === 's' || hd.part === 'e' || hd.part === 'w';
1022
+ c.save();
1023
+ c.translate(hd.x, hd.y);
1024
+ if (hd.ang) c.rotate(hd.ang);
853
1025
  c.beginPath();
854
- if (!side) c.arc(hd.x, hd.y, r, 0, Math.PI * 2);
855
- else if (hd.part === 'e' || hd.part === 'w') rr(c, hd.x - 3.5, hd.y - 15, 7, 30, 3.5);
856
- else rr(c, hd.x - 12, hd.y - 3, 24, 6, 3);
1026
+ if (!side) c.arc(0, 0, r, 0, Math.PI * 2);
1027
+ else if (hd.part === 'e' || hd.part === 'w') rr(c, -3.5, -15, 7, 30, 3.5);
1028
+ else rr(c, -12, -3, 24, 6, 3);
857
1029
  c.fillStyle = side ? TH.blue : '#fff'; c.fill();
858
1030
  c.lineWidth = 1.5; c.strokeStyle = TH.blue; c.stroke();
1031
+ c.restore();
859
1032
  }
860
1033
  }
861
1034
 
862
- /** A single selected arrow, cloud or pen stroke: its adjustment handles (and a box around clouds and strokes). */
1035
+ /** A single selected piece of markup: its adjustment handles (and a box around all but the straight ones and notes). */
863
1036
  function drawNoteSelection(c: C2D, T: Xform): void {
864
1037
  if (ed.mode !== 'annotate' || !settings.layers.annot) return;
865
1038
  const notes = ed.sel.filter(s => s.type === 'note');
@@ -869,7 +1042,8 @@ function drawNoteSelection(c: C2D, T: Xform): void {
869
1042
  const hs = noteHandles(T, n);
870
1043
  if (!hs.length) return;
871
1044
  c.save();
872
- if (n.type !== 'arrow') {
1045
+ // (a note has its own outline, and a link its card)
1046
+ if (!isSegment(n) && n.type !== 'text' && n.type !== 'link') {
873
1047
  const nw = hs[0], se = hs[4];
874
1048
  c.strokeStyle = TH.blue; c.lineWidth = 1.2; c.setLineDash([5, 4]);
875
1049
  c.strokeRect(nw.x, nw.y, se.x - nw.x, se.y - nw.y);
@@ -899,6 +1073,10 @@ function drawSnap(c: C2D, T: Xform): void {
899
1073
  else if (s.kind === 'inter') { c.moveTo(x - 6, y - 6); c.lineTo(x + 6, y + 6); c.moveTo(x + 6, y - 6); c.lineTo(x - 6, y + 6); }
900
1074
  else c.rect(x - 5, y - 5, 10, 10);
901
1075
  c.stroke();
1076
+ } else if (s.kind === 'grid') {
1077
+ // on the grid: the grid point it's on, marked (it moves in steps from one to the next)
1078
+ c.beginPath(); c.arc(x, y, 3.5, 0, Math.PI * 2); c.fill();
1079
+ c.lineWidth = 1.2; c.beginPath(); c.arc(x, y, 7, 0, Math.PI * 2); c.stroke();
902
1080
  }
903
1081
  const lbl = s.label || (s.guides && s.guides.length ? s.guides.map(g => g.label).join(' · ') : '');
904
1082
  if (lbl) {
@@ -913,8 +1091,9 @@ function drawSnap(c: C2D, T: Xform): void {
913
1091
 
914
1092
  /** Paint the browser canvas, keeping the frame's tap targets for hit testing. */
915
1093
  export function draw(): void {
916
- const { W, H, dpr, ctx } = cvs;
917
- if (!W) return;
1094
+ const { cv, W, H, dpr, ctx } = cvs;
1095
+ // (no browser canvas — a unit test — nothing to paint)
1096
+ if (!W || !cv) return;
918
1097
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
919
1098
  ed.hitChips = drawPlan(ctx, W, H);
920
1099
  }
@@ -926,25 +1105,106 @@ export function draw(): void {
926
1105
  export function drawPlan(ctx: C2D, W: number, H: number): HitChip[] {
927
1106
  const out: HitChip[] = [];
928
1107
  hits = out;
929
- try { paintPlan(ctx, W, H); } finally { hits = null; }
1108
+ try { paintPlan(ctx, W, H); paintOverlay(ctx, W, H); } finally { hits = null; }
930
1109
  return out;
931
1110
  }
932
1111
 
933
- function paintPlan(ctx: C2D, W: number, H: number): void {
934
- ctx.fillStyle = TH.bg; ctx.fillRect(0, 0, W, H);
935
- const sk = SK(), T = mainT();
936
- drawGrid(ctx, T); drawUnderlay(ctx, T);
1112
+ /**
1113
+ * How a host that keeps the drawing as a picture (see PlanCanvas.native) wants it drawn. It moves and scales the
1114
+ * picture with the view until it's redrawn, so: `margin` px of background and grid past each edge of the screen (so
1115
+ * a picture zoomed out or panned a little shows no edge), and what stays put on the screen however the view moves —
1116
+ * the wall view's key plan, the conflict border — drawn on `fixed` instead. Without them, everything is drawn on the
1117
+ * one canvas, to the screen's edges (the browser).
1118
+ */
1119
+ /**
1120
+ * `layer`: just the background, grid and photos and plans ('under'), or everything over them ('above') — a host that
1121
+ * keeps them apart redraws the photos alone as one is turned or moved. Without it, all of it.
1122
+ */
1123
+ export interface KeepOpts { margin?: number; fixed?: C2D; layer?: 'under' | 'above' }
1124
+
1125
+ /**
1126
+ * The drawing without its moving parts (see drawPlanOverlay), for a host that keeps it as a picture and redraws only
1127
+ * those parts as the pointer moves. Returns the tap targets, as drawPlan does.
1128
+ */
1129
+ export function drawPlanStatic(ctx: C2D, W: number, H: number, opts: KeepOpts = {}): HitChip[] {
1130
+ const out: HitChip[] = [];
1131
+ hits = out;
1132
+ try { paintPlan(ctx, W, H, opts); } finally { hits = null; }
1133
+ return out;
1134
+ }
1135
+
1136
+ /** The photos and plans under the drawing, with the background and grid (see KeepOpts.layer). */
1137
+ export function drawPlanUnder(ctx: C2D, W: number, H: number, opts: KeepOpts = {}): void {
1138
+ paintPlan(ctx, W, H, { ...opts, layer: 'under' });
1139
+ }
1140
+
1141
+ /** The parts that move with the pointer: a tool's preview, the snap marker, the selection box, the conflict tint. */
1142
+ export function drawPlanOverlay(ctx: C2D, W: number, H: number, opts: KeepOpts = {}): void {
1143
+ paintOverlay(ctx, W, H, opts.fixed ?? ctx);
1144
+ }
1145
+
1146
+ function paintPlan(ctx: C2D, W: number, H: number, opts: KeepOpts = {}): void {
1147
+ const m = opts.margin ?? 0, wall = elevOpen() && !!elev.frame;
1148
+ if (opts.layer !== 'above') { ctx.fillStyle = TH.bg; ctx.fillRect(-m, -m, W + 2 * m, H + 2 * m); }
1149
+ // the photos and plans under the drawing (the wall view has none: its grid is on the wall, drawn with it)
1150
+ if (opts.layer === 'under') { if (!wall) { const T = mainT(); drawGrid(ctx, T, m); drawUnderlay(ctx, T); } return; }
1151
+ const sk = SK();
1152
+ // (the sketch can't change while a frame is drawn: index its lines once for the frame)
1153
+ withLineIndex(sk, () => {
1154
+ if (wall) paintWall(ctx, W, opts.fixed ?? ctx);
1155
+ else paintPlanView(ctx, sk, m, opts.layer !== 'above');
1156
+ });
1157
+ }
1158
+
1159
+ function paintPlanView(ctx: C2D, sk: Sketch, m: number, under = true): void {
1160
+ const T = mainT();
1161
+ if (under) { drawGrid(ctx, T, m); drawUnderlay(ctx, T); }
937
1162
  if (settings.layers.rooms) drawRooms(ctx, T, sk);
1163
+ if (settings.layers.cabinets) drawCabinetFootprints(ctx, T, sk);
938
1164
  drawWalls(ctx, T, sk); drawOpenings(ctx, T, sk); drawLines(ctx, T, sk); drawCurves(ctx, T, sk);
939
1165
  drawPrintDims(ctx, T, sk);
940
1166
  if (ed.mode === 'draft') drawBadges(ctx, T, sk);
941
- drawPoints(ctx, T, sk);
1167
+ // (the drawing's points only in Draft: Annotate can't move them, and shouldn't look as if it could — the user's
1168
+ // call, 2026-10-21)
1169
+ if (ed.mode === 'draft') drawPoints(ctx, T, sk);
942
1170
  drawOpeningAnchors(ctx, T, sk);
943
1171
  drawDims(ctx, T, sk);
944
1172
  drawOpeningDims(ctx, T, sk);
945
1173
  drawNotes(ctx, T, sk);
946
- drawUnderlaySelection(ctx, T);
947
1174
  drawNoteSelection(ctx, T);
1175
+ }
1176
+
1177
+ /**
1178
+ * The wall view: the open wall face, drawn as the plan is (its grid on the wall only, lines, curves, constraints, dimensions, points), over
1179
+ * the wall itself (render/elevation.ts) and under the dimensions its doors, windows and cabinets get, plus a key plan.
1180
+ */
1181
+ function paintWall(ctx: C2D, W: number, fixed: C2D): void {
1182
+ const sk = SK() as WallFace, plan = planSK(), f = elev.frame!, T = mainT();
1183
+ // the grid only on the wall itself: outside it is plain, so it's plain that nothing goes there
1184
+ const [gx0, gy0] = S(T, f.face[0], f.H), [gx1, gy1] = S(T, f.face[1], 0);
1185
+ ctx.save();
1186
+ ctx.beginPath(); ctx.rect(gx0, gy0, gx1 - gx0, gy1 - gy0); ctx.clip();
1187
+ drawGrid(ctx, T);
1188
+ ctx.restore();
1189
+ drawWallBackdrop(ctx, T, plan, f, sk);
1190
+ drawLines(ctx, T, sk); drawCurves(ctx, T, sk);
1191
+ // (over the edges — its own and its neighbours' — so a selected cabinet's outline shows all the way round)
1192
+ drawWallSelection(ctx, T, plan, f, sk);
1193
+ drawPrintDims(ctx, T, sk);
1194
+ drawBadges(ctx, T, sk);
1195
+ if (ed.mode === 'draft') drawPoints(ctx, T, sk);
1196
+ drawDims(ctx, T, sk);
1197
+ const on = (a: ElevAct) => isSel('elev', JSON.stringify(a)) || (a.k === 'fdim' && isSel('dim', a.id));
1198
+ for (const h of drawWallChips(ctx, T, plan, f, sk, on)) addHit({ kind: 'elev', id: JSON.stringify(h.act), part: h.look, x: h.x, y: h.y, w: h.w, h: h.h });
1199
+ // (where you stand: it stays put on the screen as the wall is panned and zoomed)
1200
+ const k = keyPlanSize(W);
1201
+ drawKeyPlan(fixed, W - k - 14, 70, k, plan, f);
1202
+ }
1203
+
1204
+ function paintOverlay(ctx: C2D, W: number, H: number, fixed: C2D = ctx): void {
1205
+ const T = mainT();
1206
+ // a selected image's outline and handles: with the moving parts, as it's turned, moved or sized (over the drawing)
1207
+ drawUnderlaySelection(ctx, T);
948
1208
  if (ed.preview) ed.preview(ctx, T);
949
1209
  drawSnap(ctx, T);
950
1210
  if (ed.boxSel) {
@@ -958,11 +1218,12 @@ function paintPlan(ctx: C2D, W: number, H: number): void {
958
1218
  ctx.strokeRect(Math.min(x0, x1) + 0.5, Math.min(y0, y1) + 0.5, Math.abs(x1 - x0), Math.abs(y1 - y0));
959
1219
  ctx.restore();
960
1220
  }
1221
+ // (the screen's edge, however the view moves)
961
1222
  if (ed.conflictTint) {
962
- ctx.save();
963
- ctx.fillStyle = 'rgba(218,17,0,.07)'; ctx.fillRect(0, 0, W, H);
964
- ctx.strokeStyle = 'rgba(218,17,0,.7)'; ctx.lineWidth = 6; ctx.strokeRect(3, 3, W - 6, H - 6);
965
- ctx.restore();
1223
+ fixed.save();
1224
+ fixed.fillStyle = 'rgba(218,17,0,.07)'; fixed.fillRect(0, 0, W, H);
1225
+ fixed.strokeStyle = 'rgba(218,17,0,.7)'; fixed.lineWidth = 6; fixed.strokeRect(3, 3, W - 6, H - 6);
1226
+ fixed.restore();
966
1227
  }
967
1228
  }
968
1229
 
@@ -991,6 +1252,7 @@ export function paintThumb(c: C2D, w: number, h: number, pg: Page): void {
991
1252
  const save = { ana: ed.ana, sel: ed.sel, hover: ed.hover, W: cvs.W, H: cvs.H };
992
1253
  ed.ana = { dof: 0, det: new Set(), detR: new Set(), bad: new Set(), ok: true };
993
1254
  ed.sel = []; ed.hover = null; cvs.W = w; cvs.H = h;
994
- try { drawRooms(c, T, sk, true); drawWalls(c, T, sk, true); drawLines(c, T, sk, true); drawCurves(c, T, sk, true); }
1255
+ // (the page's lines indexed by their ends while it's drawn, as a frame is: each room's edges looked up, not searched for)
1256
+ try { withLineIndex(sk, () => { drawRooms(c, T, sk, true); drawWalls(c, T, sk, true); drawLines(c, T, sk, true); drawCurves(c, T, sk, true); }); }
995
1257
  finally { cvs.W = save.W; cvs.H = save.H; ed.ana = save.ana; ed.sel = save.sel; ed.hover = save.hover; }
996
1258
  }