@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,33 +1,63 @@
1
1
  import { ed, isSel } 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';
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 } from './elevation';
10
16
  import { fmtAng, fmtArea, fmtLen } from '../units';
11
17
  import { FONT, TH } from './theme';
12
- import { cvs, imgCache, mainT, noteHandles, S, sketchBounds, underlayH, underlayHandles } from './view';
18
+ import { cvs, imgCache, keyPlanSize, mainT, nextThumbFrame, noteFont, noteHandles, noteTextBox, S, sketchBounds, underlayH, underlayHandles, underlayMid } from './view';
19
+ import { dashFor, isSegment, NOTE_TEXT_SIZE, noteLook, shapeOutline } from '../model/markup';
20
+ import { strokeMarkup, strokeOutline } from './markup';
21
+ import { drawLink } from './linkTile';
22
+ import { isLink, linkInfoOf } from '../model/links';
23
+ import { smoothRectPath } from './squircle';
13
24
  /** Tap targets registered while drawPlan() paints (null outside it: thumbnails and the elevation register none). */
14
25
  let hits = null;
15
26
  const addHit = (h) => { if (hits)
16
27
  hits.push(h); };
17
- const OPENING_PAD = 4;
28
+ /** A rounded rectangle's path, its corners smoothed as Figma's 60% ("iOS"): render/squircle.ts. */
18
29
  export function rr(c, x, y, w, h, r) {
30
+ // (one shape for each size, moved into place where the engine can: hundreds of these are drawn every frame)
31
+ if (c.shape)
32
+ c.shape('rr ' + w + ' ' + h + ' ' + r, x, y, (cc, X, Y) => smoothRectPath(cc, X, Y, w, h, r));
33
+ else
34
+ smoothRectPath(c, x, y, w, h, r);
35
+ }
36
+ /** Blue for outlined (linked) chips: the lighter blue on a dark canvas, so the text stays readable. */
37
+ export function linkBlue() {
38
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i.exec(TH.bg);
39
+ const dark = !!m && (parseInt(m[1], 16) * 299 + parseInt(m[2], 16) * 587 + parseInt(m[3], 16) * 114) / 1000 < 128;
40
+ return dark ? TH.lblue : TH.blue;
41
+ }
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;
19
49
  c.beginPath();
20
- c.moveTo(x + r, y);
21
- c.arcTo(x + w, y, x + w, y + h, r);
22
- c.arcTo(x + w, y + h, x, y + h, r);
23
- c.arcTo(x, y + h, x, y, r);
24
- c.arcTo(x, y, x + w, y, r);
25
- c.closePath();
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();
26
54
  }
55
+ /** What a link mark takes before a chip's text. */
56
+ const MARK_W = 14;
27
57
  /** Rounded label on the canvas; registers a hit region when `kind` is given. */
28
- export function chip(c, text, cx, cy, { bg, fg, border, font = '600 12px', pad = 7, h = 22, minW = 0, kind, id, rot = 0 }) {
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 }) {
29
59
  c.font = font + ' ' + FONT;
30
- const w = Math.max(minW, c.measureText(text).width + pad * 2);
60
+ const mw = mark ? MARK_W : 0, w = Math.max(minW, c.measureText(text).width + pad * 2 + mw);
31
61
  const x = cx - w / 2, y = cy - h / 2;
32
62
  c.save();
33
63
  if (rot) {
@@ -46,7 +76,9 @@ export function chip(c, text, cx, cy, { bg, fg, border, font = '600 12px', pad =
46
76
  c.fillStyle = fg;
47
77
  c.textAlign = 'center';
48
78
  c.textBaseline = 'middle';
49
- c.fillText(text, cx, cy + 0.5);
79
+ c.fillText(text, cx + mw / 2, cy + 0.5);
80
+ if (mark)
81
+ linkMark(c, x + pad + 4.5, cy, fg);
50
82
  c.restore();
51
83
  if (kind) {
52
84
  const ext = rot ? Math.max(w, h) / 2 : 0;
@@ -54,44 +86,51 @@ export function chip(c, text, cx, cy, { bg, fg, border, font = '600 12px', pad =
54
86
  }
55
87
  return w;
56
88
  }
57
- function drawGrid(c, T) {
89
+ /** The grid over the screen, and `m` px past each edge (see KeepOpts). */
90
+ function drawGrid(c, T, m = 0) {
58
91
  const { W, H } = cvs;
59
92
  const { minor, major } = gridSteps(T.s);
60
- 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;
93
+ 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;
61
94
  const lines = (step, color, w) => {
62
- if ((x1 - x0) / step > 600)
95
+ // (too dense to draw: judged on the screen's width, whatever is drawn past it)
96
+ if (W / T.s / step > 600)
63
97
  return;
64
98
  c.beginPath();
65
99
  c.strokeStyle = color;
66
100
  c.lineWidth = w;
67
101
  for (let x = Math.floor(x0 / step) * step; x <= x1; x += step) {
68
102
  const sx = Math.round(x * T.s + T.ox) + 0.5;
69
- c.moveTo(sx, 0);
70
- c.lineTo(sx, H);
103
+ c.moveTo(sx, -m);
104
+ c.lineTo(sx, H + m);
71
105
  }
72
106
  for (let y = Math.floor(y0 / step) * step; y <= y1; y += step) {
73
107
  const sy = Math.round(-y * T.s + T.oy) + 0.5;
74
- c.moveTo(0, sy);
75
- c.lineTo(W, sy);
108
+ c.moveTo(-m, sy);
109
+ c.lineTo(W + m, sy);
76
110
  }
77
111
  c.stroke();
78
112
  };
79
113
  lines(minor, TH.grid, 1);
80
114
  lines(major, TH.grid2, 1);
81
115
  }
116
+ /** The page's photos and plans, in order (the last on top). */
82
117
  function drawUnderlay(c, T) {
83
- const u = page().underlay;
84
- if (!u || !settings.layers.underlay)
118
+ if (!settings.layers.underlay)
85
119
  return;
86
- const im = imgCache(u.src);
87
- if (!im || !im.complete || !im.naturalWidth)
88
- return;
89
- const h = underlayH(u);
90
- const [x, y] = S(T, u.x, u.y);
91
- c.save();
92
- c.globalAlpha = u.opacity ?? 0.6;
93
- c.drawImage(im, x, y, u.w * T.s, h * T.s);
94
- c.restore();
120
+ for (const u of page().underlays) {
121
+ const im = imgCache(u.src);
122
+ if (!im || !im.complete || !im.naturalWidth)
123
+ continue;
124
+ // drawn about its middle, turned as it's set (clockwise as seen: the canvas turns that way)
125
+ const h = underlayH(u), m = underlayMid(u), [x, y] = S(T, m.x, m.y), w = u.w * T.s, hh = h * T.s;
126
+ c.save();
127
+ c.globalAlpha = u.opacity ?? 0.6;
128
+ c.translate(x, y);
129
+ if (u.rot)
130
+ c.rotate((u.rot * Math.PI) / 180);
131
+ c.drawImage(im, -w / 2, -hh / 2, w, hh);
132
+ c.restore();
133
+ }
95
134
  }
96
135
  const inkFor = (defined, constr) => (constr ? TH.constr : defined ? TH.def : TH.ink);
97
136
  function drawRooms(c, T, sk, thumb) {
@@ -122,33 +161,6 @@ function drawRooms(c, T, sk, thumb) {
122
161
  addHit({ kind: 'room', id, x: cx - w / 2, y: cy - 20, w, h: 40 });
123
162
  }
124
163
  }
125
- function wallDegree(sk) {
126
- const deg = {};
127
- for (const id in sk.lines) {
128
- const l = sk.lines[id];
129
- if (l.kind !== 'wall')
130
- continue;
131
- deg[l.a] = (deg[l.a] || 0) + 1;
132
- deg[l.b] = (deg[l.b] || 0) + 1;
133
- }
134
- for (const id in sk.arcs) {
135
- const a = sk.arcs[id];
136
- if (a.kind !== 'wall')
137
- continue;
138
- deg[a.a] = (deg[a.a] || 0) + 1;
139
- deg[a.b] = (deg[a.b] || 0) + 1;
140
- }
141
- return deg;
142
- }
143
- /** Wall centerline path, extended at joints by half the thickness so corners close. */
144
- function wallPath(c, T, sk, l, deg, extra) {
145
- const A = sk.points[l.a], B = sk.points[l.b];
146
- 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);
147
- const ea = deg[l.a] > 1 ? e : 0, eb = deg[l.b] > 1 ? e : 0;
148
- 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);
149
- c.moveTo(x1, y1);
150
- c.lineTo(x2, y2);
151
- }
152
164
  function curveWalls(sk) {
153
165
  const out = [];
154
166
  const show = (constr) => !(constr && !settings.layers.construction);
@@ -175,57 +187,209 @@ function curveWallPath(c, T, sk, cw) {
175
187
  c.arc(x, y, Math.max(0.5, g.r * T.s), -g.t0, -g.t1, true);
176
188
  }
177
189
  }
190
+ /**
191
+ * Cabinets in the plan: each one's footprint against its wall, from the wall's view (read-only — tap one to open its
192
+ * wall). Drawn as cabinetry, not a shape to edit: base cabinets hatched, with a bold front where the doors face the room
193
+ * and their name when there's room for it; wall (upper) cabinets dashed with a cross, as drafted.
194
+ */
195
+ function drawCabinetFootprints(c, T, sk) {
196
+ const fps = cabinetFootprints(sk);
197
+ if (!fps.length)
198
+ return;
199
+ for (const fp of fps) {
200
+ // pts: along the wall face (0 → 1), then the front, out in the room (2 → 3)
201
+ const q = fp.pts.map(p => S(T, p.x, p.y));
202
+ const path = () => { c.beginPath(); q.forEach(([x, y], i) => (i ? c.lineTo(x, y) : c.moveTo(x, y))); c.closePath(); };
203
+ const hov = ed.hover?.type === 'cabfoot' && ed.hover.id === fp.key + '#' + fp.id;
204
+ c.save();
205
+ if (fp.upper) {
206
+ path();
207
+ c.setLineDash([6, 4]);
208
+ c.strokeStyle = TH.ink2;
209
+ c.lineWidth = hov ? 2 : 1.2;
210
+ c.stroke();
211
+ c.beginPath();
212
+ c.moveTo(...q[0]);
213
+ c.lineTo(...q[2]);
214
+ c.moveTo(...q[1]);
215
+ c.lineTo(...q[3]);
216
+ c.lineWidth = 1;
217
+ c.stroke();
218
+ c.restore();
219
+ continue;
220
+ }
221
+ path();
222
+ c.fillStyle = hov ? 'rgba(128,128,128,.30)' : 'rgba(128,128,128,.16)';
223
+ c.fill();
224
+ // hatching, clipped to the footprint
225
+ c.save();
226
+ path();
227
+ c.clip();
228
+ 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);
229
+ c.strokeStyle = TH.ink2;
230
+ c.globalAlpha = 0.45;
231
+ c.lineWidth = 1;
232
+ c.beginPath();
233
+ for (let k = x0 - (y1 - y0); k < x1; k += 7) {
234
+ c.moveTo(k, y1);
235
+ c.lineTo(k + (y1 - y0), y0);
236
+ }
237
+ c.stroke();
238
+ c.restore();
239
+ path();
240
+ c.strokeStyle = TH.ink2;
241
+ c.lineWidth = hov ? 2 : 1.2;
242
+ c.stroke();
243
+ // the front (where its doors are)
244
+ c.beginPath();
245
+ c.moveTo(...q[2]);
246
+ c.lineTo(...q[3]);
247
+ c.strokeStyle = TH.ink;
248
+ c.lineWidth = 2.5;
249
+ c.stroke();
250
+ 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]);
251
+ if (Math.max(w, d) > 64 && Math.min(w, d) > 18) {
252
+ // along its longer side, reading upright
253
+ 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;
254
+ let a = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
255
+ if (a > Math.PI / 2)
256
+ a -= Math.PI;
257
+ else if (a <= -Math.PI / 2)
258
+ a += Math.PI;
259
+ c.translate(cx, cy);
260
+ c.rotate(a);
261
+ c.font = '600 10px ' + FONT;
262
+ c.textAlign = 'center';
263
+ c.textBaseline = 'middle';
264
+ c.fillStyle = TH.bg;
265
+ c.globalAlpha = 0.85;
266
+ const tw = c.measureText('Cabinet').width;
267
+ c.fillRect(-tw / 2 - 3, -7, tw + 6, 14);
268
+ c.globalAlpha = 1;
269
+ c.fillStyle = TH.ink;
270
+ c.fillText('Cabinet', 0, 0.5);
271
+ }
272
+ c.restore();
273
+ }
274
+ }
275
+ /** A wall piece's outline, added to the current path (`fresh`: start a new path first). */
276
+ function piecePath(c, T, pts, fresh = true) {
277
+ if (fresh)
278
+ c.beginPath();
279
+ pts.forEach((p, i) => { const [x, y] = S(T, p.x, p.y); if (i)
280
+ c.lineTo(x, y);
281
+ else
282
+ c.moveTo(x, y); });
283
+ c.closePath();
284
+ }
285
+ /**
286
+ * Walls as solid bands of their thickness, outlined in the definition colour. Straight walls are filled outlines cut
287
+ * to fit where they meet (model/wallshape.ts), so corners at any angle close cleanly; curved walls are thick strokes.
288
+ * Every outline is drawn first and the fill over all of them, so only the outside of the outlines shows, not the seams.
289
+ */
178
290
  function drawWalls(c, T, sk, thumb) {
179
- const deg = wallDegree(sk);
180
- const walls = Object.entries(sk.lines).filter(([, l]) => l.kind === 'wall' && !(l.constr && !settings.layers.construction));
181
- if (!walls.length && !curveWalls(sk).length)
291
+ const shown = (w) => !(w.constr && !settings.layers.construction);
292
+ const pieces = wallPieces(sk, shown), curves = curveWalls(sk);
293
+ if (!pieces.length && !curves.length)
182
294
  return;
183
- c.lineCap = 'butt';
184
295
  const ow = thumb ? 1 : 1.6;
185
- // outline pass (definition color), then fill pass
186
- for (const [id, l] of walls) {
187
- const d = lineDefined(ed.ana, l);
188
- c.beginPath();
189
- wallPath(c, T, sk, l, deg, ow / T.s);
190
- c.lineWidth = l.thick * T.s + ow * 2;
191
- c.strokeStyle = ed.highlight.has(id) ? TH.red : inkFor(d && !thumb, l.constr);
296
+ const ink = (id) => { const l = sk.lines[id]; return ed.highlight.has(id) ? TH.red : inkFor(lineDefined(ed.ana, l) && !thumb, l.constr); };
297
+ c.save();
298
+ c.lineJoin = 'round';
299
+ c.lineCap = 'butt';
300
+ for (const p of pieces) {
301
+ piecePath(c, T, p.pts);
302
+ c.lineWidth = ow * 2;
303
+ c.strokeStyle = ink(p.of);
192
304
  c.stroke();
193
305
  }
194
- // curved walls: same outline-then-fill treatment
195
- const curves = curveWalls(sk);
196
306
  for (const cw of curves) {
197
307
  curveWallPath(c, T, sk, cw);
198
308
  c.lineWidth = cw.thick * T.s + ow * 2;
199
309
  c.strokeStyle = ed.highlight.has(cw.id) ? TH.red : inkFor(curveDefined(ed.ana, sk, cw.id) && !thumb, cw.constr);
200
310
  c.stroke();
201
311
  }
202
- for (const [, l] of walls) {
203
- c.beginPath();
204
- wallPath(c, T, sk, l, deg, 0);
205
- c.lineWidth = Math.max(0.5, l.thick * T.s);
206
- c.strokeStyle = TH.wall;
207
- c.stroke();
208
- }
312
+ // one fill for every piece: pieces filled one by one leave a hairline where they meet
313
+ c.fillStyle = TH.wall;
314
+ c.beginPath();
315
+ for (const p of pieces)
316
+ piecePath(c, T, p.pts, false);
317
+ c.fill();
209
318
  for (const cw of curves) {
210
319
  curveWallPath(c, T, sk, cw);
211
320
  c.lineWidth = Math.max(0.5, cw.thick * T.s);
212
321
  c.strokeStyle = TH.wall;
213
322
  c.stroke();
214
323
  }
215
- if (thumb)
216
- return;
217
- for (const [id, l] of walls) {
218
- if (isSel('line', id) || (ed.hover && ed.hover.type === 'line' && ed.hover.id === id)) {
324
+ if (!thumb) {
325
+ for (const p of pieces) {
326
+ if (!p.id || !(isSel('line', p.id) || (ed.hover && ed.hover.type === 'line' && ed.hover.id === p.id)))
327
+ continue;
328
+ piecePath(c, T, p.pts);
329
+ c.fillStyle = isSel('line', p.id) ? 'rgba(255,173,0,.55)' : 'rgba(255,173,0,.28)';
330
+ c.fill();
331
+ }
332
+ drawEdges(c, T, sk);
333
+ }
334
+ c.restore();
335
+ }
336
+ /**
337
+ * Wall edges picked on their own (and the one under the pointer): along the face, from its corner to its corner — a
338
+ * curved wall's all along it.
339
+ */
340
+ function drawEdges(c, T, sk) {
341
+ const ids = ed.sel.filter(s => s.type === 'wface').map(s => s.id);
342
+ const hov = ed.hover?.type === 'wface' && !ids.includes(ed.hover.id) ? ed.hover.id : null;
343
+ if (hov)
344
+ ids.push(hov);
345
+ for (const id of ids) {
346
+ const [lid, side] = edgeOf(id);
347
+ if (!sk.lines[lid]) {
348
+ drawCurveEdge(c, T, sk, lid, side, id !== hov);
349
+ continue;
350
+ }
351
+ const a = faceCorner(sk, lid, side, 'a'), b = faceCorner(sk, lid, side, 'b');
352
+ if (!a || !b)
353
+ continue;
354
+ const [ax, ay] = S(T, a.x, a.y), [bx, by] = S(T, b.x, b.y), lit = id !== hov;
355
+ c.lineCap = 'round';
356
+ c.strokeStyle = lit ? TH.yellow : 'rgba(255,173,0,.6)';
357
+ c.lineWidth = lit ? 3.5 : 2.5;
358
+ c.beginPath();
359
+ c.moveTo(ax, ay);
360
+ c.lineTo(bx, by);
361
+ c.stroke();
362
+ // (its corners: where a length on it is measured from and to)
363
+ c.fillStyle = lit ? TH.yellow : 'rgba(255,173,0,.6)';
364
+ for (const [x, y] of [[ax, ay], [bx, by]]) {
219
365
  c.beginPath();
220
- wallPath(c, T, sk, l, deg, 0);
221
- c.lineWidth = Math.max(3, l.thick * T.s);
222
- c.strokeStyle = isSel('line', id) ? 'rgba(255,173,0,.55)' : 'rgba(255,173,0,.28)';
223
- c.stroke();
366
+ c.arc(x, y, lit ? 4 : 3, 0, Math.PI * 2);
367
+ c.fill();
224
368
  }
225
369
  }
226
370
  }
371
+ /** A curved wall's outer (+1) or inner (−1) edge, picked (`lit`) or under the pointer. */
372
+ function drawCurveEdge(c, T, sk, id, side, lit) {
373
+ const w = sk.circles[id] ?? sk.arcs[id], f = curveFrame(sk, id);
374
+ if (!w || !f)
375
+ return;
376
+ const rr = (f.r + (side * wallThick(w)) / 2) * T.s, [x, y] = S(T, f.o.x, f.o.y);
377
+ if (rr <= 0)
378
+ return;
379
+ c.lineCap = 'round';
380
+ c.strokeStyle = lit ? TH.yellow : 'rgba(255,173,0,.6)';
381
+ c.lineWidth = lit ? 3.5 : 2.5;
382
+ c.beginPath();
383
+ if (f.arc)
384
+ c.arc(x, y, rr, -f.arc.t0, -f.arc.t1, true);
385
+ else
386
+ c.arc(x, y, rr, 0, Math.PI * 2);
387
+ c.stroke();
388
+ }
227
389
  function drawOpenings(c, T, sk) {
228
- for (const id in sk.openings) {
390
+ // the selected or hovered one last: a neighbour drawn after it would knock the wall out over its outline
391
+ const lit = (id) => isSel('opening', id) || (ed.hover?.type === 'opening' && ed.hover.id === id);
392
+ for (const id of Object.keys(sk.openings).sort((a, b) => Number(lit(a)) - Number(lit(b)))) {
229
393
  const o = sk.openings[id], g = openingFrame(sk, id);
230
394
  if (!g)
231
395
  continue;
@@ -244,10 +408,10 @@ function drawOpenings(c, T, sk) {
244
408
  const sel1 = isSel('opening', id);
245
409
  const hov = !sel1 && ed.hover && ed.hover.type === 'opening' && ed.hover.id === id;
246
410
  const ext = OPENING_TICK_EXT / T.s;
247
- // hover / selection band so it's obvious what a click will grab
411
+ // hover / selection band so it's obvious what a click will grab: exactly its footprint in the wall, face to face
412
+ // and end to end (not padded out past them)
248
413
  if (sel1 || hov) {
249
- const pad = OPENING_PAD / T.s;
250
- quad(s0 - pad, s1 + pad, t / 2 + ext + pad);
414
+ quad(s0, s1, t / 2);
251
415
  c.fillStyle = sel1 ? 'rgba(255,173,0,.30)' : 'rgba(255,173,0,.16)';
252
416
  c.fill();
253
417
  c.lineWidth = sel1 ? 2 : 1.5;
@@ -317,6 +481,8 @@ function drawLines(c, T, sk, thumb) {
317
481
  c.strokeStyle = selected ? 'rgba(255,173,0,.6)' : 'rgba(255,173,0,.3)';
318
482
  c.stroke();
319
483
  }
484
+ if (l.anchor || l.cab)
485
+ continue; // a wall face's edge from the plan, a cabinet's: the wall view's backdrop draws them
320
486
  c.beginPath();
321
487
  c.moveTo(x1, y1);
322
488
  c.lineTo(x2, y2);
@@ -380,13 +546,20 @@ function drawCurves(c, T, sk, thumb) {
380
546
  c.setLineDash([]);
381
547
  }
382
548
  }
383
- function inkForPoint(sk, id) {
549
+ /** Points on a line that isn't fully defined: drawn in ink, not green (worked out once a frame, not once a point). */
550
+ const looseEnds = (sk) => frameMemo(sk, 'looseEnds', () => {
551
+ const out = new Set();
384
552
  for (const lid in sk.lines) {
385
553
  const l = sk.lines[lid];
386
- if ((l.a === id || l.b === id) && !lineDefined(ed.ana, l))
387
- return TH.ink;
554
+ if (!lineDefined(ed.ana, l)) {
555
+ out.add(l.a);
556
+ out.add(l.b);
557
+ }
388
558
  }
389
- return TH.def;
559
+ return out;
560
+ });
561
+ function inkForPoint(sk, id) {
562
+ return looseEnds(sk).has(id) ? TH.ink : TH.def;
390
563
  }
391
564
  function drawLock(c, x, y) {
392
565
  c.save();
@@ -409,6 +582,8 @@ function drawLock(c, x, y) {
409
582
  function drawPoints(c, T, sk) {
410
583
  const { W, H } = cvs;
411
584
  const touch = ed.lastPointerTouch;
585
+ // a cabinet's corners aren't grab points: it moves as one, like a door or window
586
+ const cabCorners = new Set(Object.values(sk.cabs ?? {}).flatMap(cabPoints));
412
587
  for (const id in sk.points) {
413
588
  const p = sk.points[id];
414
589
  const [x, y] = S(T, p.x, p.y);
@@ -425,7 +600,11 @@ function drawPoints(c, T, sk) {
425
600
  c.stroke();
426
601
  }
427
602
  const selected = isSel('point', id), hov = ed.hover && ed.hover.type === 'point' && ed.hover.id === id;
603
+ // a wall face's corners from the plan, and a cabinet's, show only when picked or under the pointer (like a window's)
604
+ if ((p.anchor || cabCorners.has(id)) && !selected && !hov)
605
+ continue;
428
606
  const r = (touch ? 5 : 4.2) + (hov ? 1.5 : 0);
607
+ // (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)
429
608
  const det = ed.ana.det.has(id);
430
609
  c.beginPath();
431
610
  c.arc(x, y, r, 0, Math.PI * 2);
@@ -447,28 +626,32 @@ function drawPoints(c, T, sk) {
447
626
  c.strokeStyle = TH.ink;
448
627
  c.stroke();
449
628
  }
450
- if (p.fixed && !p.origin)
629
+ if (p.fixed && !p.origin && !p.anchor)
451
630
  drawLock(c, x + 9, y - 14);
452
631
  }
453
632
  }
454
633
  function dimAnchor(sk, c) {
455
634
  const P = (id) => sk.points[id];
456
635
  if (c.type === 'dist') {
457
- const a = P(c.a), b = P(c.b);
636
+ // (measured on a wall's face: from its corner to corner, the label off the face on that side)
637
+ const e = dimEnds(sk, c);
638
+ if (!e)
639
+ return null;
640
+ const { a, b } = e;
458
641
  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;
459
642
  return { m: { x: (a.x + b.x) / 2 + nx * off, y: (a.y + b.y) / 2 + ny * off }, a, b, nx, ny, L };
460
643
  }
461
644
  if (c.type === 'pldist') {
462
- const l = sk.lines[c.l], p = P(c.p), A = P(l.a), B = P(l.b);
463
- const t = projT(A, B, p);
464
- const f = { x: A.x + (B.x - A.x) * t, y: A.y + (B.y - A.y) * t };
465
- return { m: { x: (p.x + f.x) / 2, y: (p.y + f.y) / 2 }, a: p, b: f };
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 };
466
650
  }
467
651
  if (c.type === 'diam') {
468
- const ci = curveInfo(sk, c.c);
469
- const o = P(ci.o.c);
470
- const r = ci.kind === 'circle' ? ci.o.r : hyp(P(ci.o.a).x - o.x, P(ci.o.a).y - o.y);
471
- return { m: { x: o.x, y: o.y }, o, r };
652
+ // (across a curved wall's face: its arrows on that face)
653
+ const g = dimRadius(sk, c);
654
+ return g ? { m: { x: g.o.x, y: g.o.y }, o: g.o, r: g.r } : null;
472
655
  }
473
656
  if (c.type === 'angle') {
474
657
  const l1 = sk.lines[c.l1], l2 = sk.lines[c.l2];
@@ -483,26 +666,46 @@ function dimAnchor(sk, c) {
483
666
  }
484
667
  return null;
485
668
  }
486
- /** Driving/reference dimension chips. (Lengths are added from the bottom bar's + / Length, not on-canvas prompts.) */
669
+ /** Whether a dimension, or something it measures (its lines, points or curves), is selected or hovered. */
670
+ function dimLive(sk, id, d) {
671
+ 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));
672
+ const hit = (x) => !!x && (((x.type === 'dim' || x.type === 'cons') && x.id === id) || (x.type === 'point' && pts.has(x.id)) || (x.type === 'line' && lines.has(x.id))
673
+ || ((x.type === 'circle' || x.type === 'arc') && curves.has(x.id)));
674
+ return ed.sel.some(hit) || hit(ed.hover);
675
+ }
676
+ /**
677
+ * Driving/reference dimension chips. (Lengths are added from the bottom bar's + / Length, not on-canvas prompts.) What's
678
+ * been entered (blue, or red when it can't be met) always shows; a reference only while it or what it measures is
679
+ * selected or hovered.
680
+ */
487
681
  function drawDims(c, T, sk) {
488
682
  if (!settings.layers.dims)
489
683
  return;
490
684
  for (const id in sk.cons) {
491
685
  const d = sk.cons[id];
492
- if (!isDim(d) || d.layer === 'print')
493
- continue;
686
+ if (!isDim(d) || d.layer === 'print' || d.elev)
687
+ continue; // the wall view's own dimensions are its chips
494
688
  const A = dimAnchor(sk, d);
495
689
  if (!A)
496
690
  continue;
497
691
  const ref = d.driving === false, val = ref ? measure(sk, d) : d.value;
498
692
  const selected = isSel('dim', id) || isSel('cons', id);
499
693
  const bad = ed.highlight.has(id) || ed.ana.bad.has(id);
694
+ if (ref && !bad && !dimLive(sk, id, d))
695
+ continue;
696
+ // a measurement of the job sets it: outlined, with a link mark — red once the job's measurement is gone
697
+ const linked = !!d.measurementRef && !ref && !bad, gone = linked && linkInfoOf(d.measurementRef)?.exists === false;
500
698
  let bg = ref ? TH.ref : TH.blue, fg = ref ? TH.reftext : '#fff';
501
699
  if (bad) {
502
700
  bg = TH.red;
503
701
  fg = '#fff';
504
702
  }
505
- const border = selected ? [TH.yellow, 2.5] : null;
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;
708
+ const mark = linked ? 'link' : undefined;
506
709
  const wrap = (s) => (ref ? '(' : '') + s + (ref ? ')' : '');
507
710
  if (d.type === 'angle' && 'vtx' in A) {
508
711
  const [vx, vy] = S(T, A.vtx.x, A.vtx.y);
@@ -517,16 +720,19 @@ function drawDims(c, T, sk) {
517
720
  c.save();
518
721
  c.strokeStyle = bad ? TH.red : ref ? TH.chipline : TH.purple;
519
722
  c.lineWidth = 1.2;
723
+ if (ref)
724
+ c.setLineDash([3, 3]);
520
725
  c.beginPath();
521
726
  c.arc(vx, vy, 22, t1, t1 + sweep, sweep < 0);
522
727
  c.stroke();
523
728
  c.restore();
524
729
  }
525
- 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 });
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 });
526
732
  continue;
527
733
  }
528
734
  if (d.type === 'diam' && 'o' in A) {
529
- 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 });
735
+ 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);
530
736
  continue;
531
737
  }
532
738
  if (!('a' in A))
@@ -534,12 +740,14 @@ function drawDims(c, T, sk) {
534
740
  const [mx, my] = S(T, A.m.x, A.m.y);
535
741
  const lineColor = bad ? TH.red : ref ? TH.chipline : TH.blue;
536
742
  if (d.type === 'dist' && A.nx != null && Math.abs((d.off || 0) * T.s) > 10) {
537
- drawDimLeaders(c, T, A.a, A.b, A.nx, A.ny, d.off, lineColor);
743
+ drawDimLeaders(c, T, A.a, A.b, A.nx, A.ny, d.off, lineColor, ref);
538
744
  }
539
745
  else if (d.type === 'pldist' || !d.line) {
746
+ // (dashed only when it's a reference: a dashed line means the number holds nothing, wherever it's drawn)
540
747
  const [ax, ay] = S(T, A.a.x, A.a.y), [bx, by] = S(T, A.b.x, A.b.y);
541
748
  c.save();
542
- c.setLineDash([3, 3]);
749
+ if (ref)
750
+ c.setLineDash([3, 3]);
543
751
  c.strokeStyle = bad ? TH.red : ref ? TH.chipline : TH.blue;
544
752
  c.lineWidth = 1.2;
545
753
  c.beginPath();
@@ -548,7 +756,7 @@ function drawDims(c, T, sk) {
548
756
  c.stroke();
549
757
  c.restore();
550
758
  }
551
- chip(c, wrap(fmtLen(val)), mx, my, { bg, fg, border, kind: 'dim', id });
759
+ chip(c, wrap(fmtLen(val)), mx, my, { bg, fg, border, kind: 'dim', id, mark });
552
760
  }
553
761
  }
554
762
  /**
@@ -639,6 +847,9 @@ function drawOpeningDims(c, T, sk) {
639
847
  const r = refS(sk, o, d.ref), rp = refPoint(sk, d.ref);
640
848
  if (r == null || !rp)
641
849
  continue;
850
+ // a reference shows only while its door or window is selected or hovered; what locks it always
851
+ if (!d.driving && !ed.badOpeningDims.has(d.id) && !active)
852
+ continue;
642
853
  const e = d.edge === 's0' ? g.s0 : g.s1;
643
854
  const k = d.off ?? kRow(place(r, e));
644
855
  const bad = ed.badOpeningDims.has(d.id);
@@ -673,22 +884,23 @@ function drawOpeningDims(c, T, sk) {
673
884
  if (g.s0 > 0.5) {
674
885
  ext(0, kw, grey);
675
886
  span(0, g.s0, kw, grey);
676
- label(0, g.s0, 'a', fmtLen(g.s0), kw, TH.ref, TH.reftext);
887
+ label(0, g.s0, 'a', '(' + fmtLen(g.s0) + ')', kw, TH.ref, TH.reftext);
677
888
  }
678
889
  if (L - g.s1 > 0.5) {
679
890
  ext(L, kw, grey);
680
891
  span(g.s1, L, kw, grey);
681
- label(g.s1, L, 'b', fmtLen(L - g.s1), kw, TH.ref, TH.reftext);
892
+ label(g.s1, L, 'b', '(' + fmtLen(L - g.s1) + ')', kw, TH.ref, TH.reftext);
682
893
  }
683
894
  }
684
- // width: a solid dimension line with extension lines from the jambs whenever it's in view (hovered or selected,
685
- // locked or not) or has been pulled away from its usual spot
686
- if (o.wOff != null || active) {
687
- ext(g.s0, kw, TH.blue);
688
- ext(g.s1, kw, TH.blue);
689
- span(g.s0, g.s1, kw, TH.blue, false);
690
- }
691
- label(g.s0, g.s1, 'w', fmtLen(g.w), kw, TH.blue, '#fff', isSel('opening', id) ? [TH.yellow, 2] : null);
895
+ // width: a full dimension, a solid line with extension lines from the jambs; blue once it's been entered (and then
896
+ // always shown), grey while it's the standard size it was placed at (and then only while the opening is selected or hovered)
897
+ if (!widthEntered(o) && !active)
898
+ continue;
899
+ const wc = widthEntered(o) ? TH.blue : TH.chipline;
900
+ ext(g.s0, kw, wc);
901
+ ext(g.s1, kw, wc);
902
+ span(g.s0, g.s1, kw, wc, false);
903
+ 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);
692
904
  }
693
905
  }
694
906
  /**
@@ -700,7 +912,8 @@ function drawOpeningAnchors(c, T, sk) {
700
912
  return;
701
913
  for (const id in sk.openings) {
702
914
  const o = sk.openings[id];
703
- const show = ed.tool === 'dim' || isSel('opening', id) || (ed.hover != null && ed.hover.id === id);
915
+ const endPicked = (e) => isSel('oanchor', id + '|' + e);
916
+ const show = ed.tool === 'dim' || isSel('opening', id) || endPicked('s0') || endPicked('s1') || (ed.hover != null && ed.hover.id === id);
704
917
  if (!show)
705
918
  continue;
706
919
  const locked = !!drivingDim(o);
@@ -711,7 +924,7 @@ function drawOpeningAnchors(c, T, sk) {
711
924
  const [x, y] = S(T, p.x, p.y);
712
925
  const hov = ed.hover && ed.hover.type === 'oanchor' && ed.hover.id === id && ed.hover.part === edge;
713
926
  // a body pick hasn't settled on an edge yet, so both light up
714
- const picked = ed.pickedAnchor && ed.pickedAnchor.id === id && (ed.pickedAnchor.body || ed.pickedAnchor.edge === edge);
927
+ const picked = (ed.pickedAnchor && ed.pickedAnchor.id === id && (ed.pickedAnchor.body || ed.pickedAnchor.edge === edge)) || endPicked(edge);
715
928
  const r = (ed.lastPointerTouch ? 5 : 4.2) + (hov ? 1.5 : 0);
716
929
  c.save();
717
930
  c.beginPath();
@@ -840,7 +1053,7 @@ export function drawPendingDim(c, T, p) {
840
1053
  * A linear dimension pulled off its line: extension lines from both ends out to a dimension line with
841
1054
  * arrowheads, so the label always reads as attached to what it measures.
842
1055
  */
843
- function drawDimLeaders(c, T, a, b, nx, ny, off, color) {
1056
+ function drawDimLeaders(c, T, a, b, nx, ny, off, color, dashed = false) {
844
1057
  const [ax, ay] = S(T, a.x, a.y), [bx, by] = S(T, b.x, b.y);
845
1058
  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);
846
1059
  const [ex, ey] = norm2(qax - ax, qay - ay); // screen direction from geometry toward the dimension line
@@ -856,10 +1069,13 @@ function drawDimLeaders(c, T, a, b, nx, ny, off, color) {
856
1069
  }
857
1070
  c.stroke();
858
1071
  c.lineWidth = 1.2;
1072
+ if (dashed)
1073
+ c.setLineDash([3, 3]);
859
1074
  c.beginPath();
860
1075
  c.moveTo(qax, qay);
861
1076
  c.lineTo(qbx, qby);
862
1077
  c.stroke();
1078
+ c.setLineDash([]);
863
1079
  const [ux, uy] = norm2(qbx - qax, qby - qay);
864
1080
  arrowHead(c, [qbx, qby], ux, uy);
865
1081
  arrowHead(c, [qax, qay], -ux, -uy);
@@ -869,7 +1085,7 @@ function drawDimLeaders(c, T, a, b, nx, ny, off, color) {
869
1085
  * Diameter dimension, CAD style: a line across the circle through its centre with arrowheads on the
870
1086
  * circle, a leader out past the edge and a short landing with the label, so the centre stays free to grab.
871
1087
  */
872
- function drawDiameter(c, T, o, r, d, text, color, chipOpts) {
1088
+ function drawDiameter(c, T, o, r, d, text, color, chipOpts, dashed = false) {
873
1089
  const a = d.ang ?? DIAM_DEFAULT_ANG, ux = Math.cos(a), uy = Math.sin(a);
874
1090
  const [ox, oy] = S(T, o.x, o.y);
875
1091
  const rs = r * T.s, sx = ux, sy = -uy; // screen direction (y down)
@@ -882,16 +1098,22 @@ function drawDiameter(c, T, o, r, d, text, color, chipOpts) {
882
1098
  c.strokeStyle = color;
883
1099
  c.fillStyle = color;
884
1100
  c.lineWidth = 1.2;
1101
+ // a radius (a fillet's) runs from the centre out through the arc; a diameter right across
1102
+ const from = d.rad ? [ox, oy] : p2;
1103
+ if (dashed)
1104
+ c.setLineDash([3, 3]);
885
1105
  c.beginPath();
886
- c.moveTo(p2[0], p2[1]);
1106
+ c.moveTo(from[0], from[1]);
887
1107
  c.lineTo(q[0], q[1]);
888
1108
  c.lineTo(end[0], end[1]);
889
1109
  c.stroke();
1110
+ c.setLineDash([]);
890
1111
  arrowHead(c, p1, sx, sy);
891
- arrowHead(c, p2, -sx, -sy);
1112
+ if (!d.rad)
1113
+ arrowHead(c, p2, -sx, -sy);
892
1114
  c.restore();
893
1115
  c.font = (chipOpts.font || '600 12px') + ' ' + FONT;
894
- const w = c.measureText(text).width + 14;
1116
+ const w = c.measureText(text).width + 14 + (chipOpts.mark ? MARK_W : 0);
895
1117
  chip(c, text, end[0] + side * (w / 2 + 2), end[1], chipOpts);
896
1118
  }
897
1119
  /** Architectural dimension strings on the Print dimensions layer (reference-only). */
@@ -954,6 +1176,48 @@ function drawPrintDims(c, T, sk) {
954
1176
  addHit({ kind: 'pdim', id, x: mx - w / 2 - 4, y: my - 18, w: w + 8, h: 24 });
955
1177
  }
956
1178
  }
1179
+ /**
1180
+ * A badge's symbol drawn as lines rather than a character: ∥ ⊥ ∘ ◎ aren't in every typeface — on a device the
1181
+ * app's own (Inter) lacks them and Skia has nothing to fall back on, so they came out as the font's "missing glyph"
1182
+ * box. Letters and = — are in every font and stay text. False for a symbol that isn't one of these.
1183
+ */
1184
+ function drawSymbol(c, g, x, y, ink) {
1185
+ c.strokeStyle = ink;
1186
+ c.lineWidth = 1.6;
1187
+ c.lineCap = 'round';
1188
+ c.beginPath();
1189
+ if (g === '∥') {
1190
+ c.moveTo(x - 2.5, y - 5);
1191
+ c.lineTo(x - 2.5, y + 5);
1192
+ c.moveTo(x + 2.5, y - 5);
1193
+ c.lineTo(x + 2.5, y + 5);
1194
+ c.stroke();
1195
+ return true;
1196
+ }
1197
+ if (g === '⊥') {
1198
+ c.moveTo(x, y - 5);
1199
+ c.lineTo(x, y + 4.5);
1200
+ c.moveTo(x - 5, y + 4.5);
1201
+ c.lineTo(x + 5, y + 4.5);
1202
+ c.stroke();
1203
+ return true;
1204
+ }
1205
+ if (g === '∘') {
1206
+ c.arc(x, y, 3, 0, Math.PI * 2);
1207
+ c.stroke();
1208
+ return true;
1209
+ }
1210
+ if (g === '◎') {
1211
+ c.lineWidth = 1.3;
1212
+ c.arc(x, y, 4.5, 0, Math.PI * 2);
1213
+ c.stroke();
1214
+ c.beginPath();
1215
+ c.arc(x, y, 1.8, 0, Math.PI * 2);
1216
+ c.fill();
1217
+ return true;
1218
+ }
1219
+ return false;
1220
+ }
957
1221
  const GLYPH = { h: 'H', v: 'V', par: '∥', perp: '⊥', col: '—', tan: 'T', eq: '=', eqr: '=', mid: 'M', online: '∘', oncircle: '∘', conc: '◎' };
958
1222
  function badgeSpots(T, sk, c) {
959
1223
  const P = (id) => sk.points[id];
@@ -969,7 +1233,7 @@ function badgeSpots(T, sk, c) {
969
1233
  out.push([x1 + (x2 - x1) * frac + nx * off * side, y1 + (y2 - y1) * frac + ny * off * side]);
970
1234
  };
971
1235
  if (c.type === 'h' || c.type === 'v') {
972
- const lid = linesBetween(sk, c.a, c.b)[0];
1236
+ const lid = lineBetween(sk, c.a, c.b);
973
1237
  if (lid)
974
1238
  onLine(lid, 0.22, -1);
975
1239
  else {
@@ -1025,7 +1289,7 @@ function drawBadges(c, T, sk) {
1025
1289
  }
1026
1290
  for (const id in sk.cons) {
1027
1291
  const k = sk.cons[id];
1028
- if (isDim(k))
1292
+ if (isDim(k) || k.elev)
1029
1293
  continue;
1030
1294
  const g = GLYPH[k.type];
1031
1295
  if (!g)
@@ -1044,11 +1308,14 @@ function drawBadges(c, T, sk) {
1044
1308
  c.lineWidth = 1;
1045
1309
  c.strokeStyle = bad ? TH.red : TH.badgeline;
1046
1310
  c.stroke();
1047
- c.fillStyle = bad ? '#fff' : selected ? '#181A1C' : TH.badgefg;
1048
- c.font = '700 11px ' + FONT;
1049
- c.textAlign = 'center';
1050
- c.textBaseline = 'middle';
1051
- c.fillText(g, x, y + 0.5);
1311
+ const ink = bad ? '#fff' : selected ? '#181A1C' : TH.badgefg;
1312
+ c.fillStyle = ink;
1313
+ if (!drawSymbol(c, g, x, y, ink)) {
1314
+ c.font = '700 11px ' + FONT;
1315
+ c.textAlign = 'center';
1316
+ c.textBaseline = 'middle';
1317
+ c.fillText(g, x, y + 0.5);
1318
+ }
1052
1319
  c.restore();
1053
1320
  addHit({ kind: 'cons', id, x: x - 10, y: y - 10, w: 20, h: 20 });
1054
1321
  }
@@ -1074,8 +1341,13 @@ function drawNotes(c, T, sk) {
1074
1341
  if (!settings.layers.annot)
1075
1342
  return;
1076
1343
  const dim = ed.mode === 'draft' ? 0.75 : 1;
1344
+ nextThumbFrame();
1077
1345
  for (const n of sk.notes) {
1078
1346
  const selected = isSel('note', n.id);
1347
+ if (isLink(n)) {
1348
+ drawLink(c, T, n, { selected, alpha: dim, info: linkInfoOf(n.link) });
1349
+ continue;
1350
+ }
1079
1351
  c.save();
1080
1352
  c.globalAlpha = dim;
1081
1353
  c.strokeStyle = n.color;
@@ -1086,98 +1358,105 @@ function drawNotes(c, T, sk) {
1086
1358
  c.shadowColor = TH.yellow;
1087
1359
  c.shadowBlur = 0;
1088
1360
  }
1089
- if ((n.type === 'pen' || n.type === 'arrow') && n.pts) {
1090
- c.lineWidth = 3;
1091
- c.beginPath();
1092
- n.pts.forEach((p, i) => { const [x, y] = S(T, p[0], p[1]); i ? c.lineTo(x, y) : c.moveTo(x, y); });
1093
- if (selected) {
1094
- c.save();
1095
- c.lineWidth = 9;
1096
- c.strokeStyle = 'rgba(255,173,0,.45)';
1097
- c.stroke();
1098
- c.restore();
1099
- }
1100
- c.stroke();
1101
- if (n.type === 'arrow' && n.pts.length > 1) {
1102
- const p1 = n.pts[n.pts.length - 2], p2 = n.pts[n.pts.length - 1];
1103
- const [x1, y1] = S(T, p1[0], p1[1]), [x2, y2] = S(T, p2[0], p2[1]);
1104
- const a = Math.atan2(y2 - y1, x2 - x1);
1105
- c.beginPath();
1106
- c.moveTo(x2, y2);
1107
- c.lineTo(x2 - 13 * Math.cos(a - 0.45), y2 - 13 * Math.sin(a - 0.45));
1108
- c.lineTo(x2 - 13 * Math.cos(a + 0.45), y2 - 13 * Math.sin(a + 0.45));
1109
- c.closePath();
1110
- c.fill();
1111
- }
1361
+ const look = noteLook(n);
1362
+ const outline = shapeOutline(n);
1363
+ if (outline) {
1364
+ strokeOutline(c, outline.map(p => S(T, p[0], p[1])), look, selected ? 'rgba(255,173,0,.45)' : undefined);
1365
+ }
1366
+ else if ((n.type === 'pen' || isSegment(n)) && n.pts) {
1367
+ strokeMarkup(c, n.pts.map(p => S(T, p[0], p[1])), look, selected ? 'rgba(255,173,0,.45)' : undefined);
1112
1368
  }
1113
1369
  else if (n.type === 'text') {
1114
- const [x, y] = S(T, n.x, n.y);
1115
- c.font = '600 14px ' + FONT;
1116
- const w = c.measureText(n.text).width + 16;
1117
- rr(c, x, y - 14, w, 28, 7);
1370
+ // laid out at the one text size and scaled to the note's size on the drawing (its outline stays the same width)
1371
+ const { x, w, mid, k } = noteTextBox(T, n, c), lw = selected ? 2.5 : 1.5;
1372
+ c.translate(x, mid);
1373
+ c.scale(k, k);
1374
+ rr(c, 0, -14, w / k, 28, 7);
1118
1375
  c.fillStyle = TH.bg;
1119
1376
  c.globalAlpha = 0.92 * dim;
1120
1377
  c.fill();
1121
1378
  c.globalAlpha = dim;
1122
- c.lineWidth = selected ? 2.5 : 1.5;
1379
+ c.lineWidth = lw / k;
1123
1380
  c.strokeStyle = selected ? TH.yellow : n.color;
1381
+ c.setLineDash(dashFor(look.style, lw).map(v => v / k));
1124
1382
  c.stroke();
1383
+ c.setLineDash([]);
1384
+ c.font = noteFont(NOTE_TEXT_SIZE);
1125
1385
  c.fillStyle = n.color;
1386
+ c.textAlign = 'left';
1126
1387
  c.textBaseline = 'middle';
1127
- c.fillText(n.text, x + 8, y + 0.5);
1388
+ c.fillText(n.text, 8, 0.5 / k); // (left: not whatever was drawn last)
1128
1389
  }
1129
1390
  else if (n.type === 'cloud' && n.pts) {
1130
1391
  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]);
1131
1392
  cloudPath(c, Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
1132
- c.lineWidth = selected ? 4 : 2.5;
1133
- if (selected)
1134
- c.strokeStyle = TH.yellow;
1393
+ if (selected) {
1394
+ c.save();
1395
+ c.lineWidth = look.width + 6;
1396
+ c.strokeStyle = 'rgba(255,173,0,.45)';
1397
+ c.stroke();
1398
+ c.restore();
1399
+ }
1400
+ c.lineWidth = look.width;
1401
+ if (look.style === 'dot')
1402
+ c.lineCap = 'round';
1403
+ c.setLineDash(dashFor(look.style, look.width));
1135
1404
  c.stroke();
1405
+ c.setLineDash([]);
1136
1406
  }
1137
1407
  c.restore();
1138
1408
  }
1139
1409
  }
1140
- /** Selected image: blue outline with corner (scale) and side (stretch) handles, or a lock badge when locked. */
1410
+ /** Selected images: blue outline with corner (scale) and side (stretch) handles — one image alone — or a lock badge when locked. */
1141
1411
  function drawUnderlaySelection(c, T) {
1142
- const u = page().underlay;
1143
- if (!u || !settings.layers.underlay || ed.mode !== 'annotate' || !isSel('underlay', 'u'))
1412
+ if (!settings.layers.underlay || ed.mode !== 'annotate')
1144
1413
  return;
1145
- const hs = underlayHandles(T, u), nw = hs[0], se = hs[4];
1146
- c.save();
1147
- c.strokeStyle = TH.blue;
1148
- c.lineWidth = 1.5;
1149
- if (u.locked)
1150
- c.setLineDash([6, 4]);
1151
- c.strokeRect(nw.x, nw.y, se.x - nw.x, se.y - nw.y);
1152
- c.setLineDash([]);
1153
- if (u.locked) {
1154
- drawLock(c, nw.x + 12, nw.y + 12);
1414
+ const sel = page().underlays.filter(u => isSel('underlay', u.id));
1415
+ for (const u of sel) {
1416
+ const hs = underlayHandles(T, u), nw = hs[0];
1417
+ c.save();
1418
+ c.strokeStyle = TH.blue;
1419
+ c.lineWidth = 1.5;
1420
+ if (u.locked)
1421
+ c.setLineDash([6, 4]);
1422
+ // its outline, turned with it: through the corners
1423
+ c.beginPath();
1424
+ [hs[0], hs[2], hs[4], hs[6]].forEach((q, i) => (i ? c.lineTo(q.x, q.y) : c.moveTo(q.x, q.y)));
1425
+ c.closePath();
1426
+ c.stroke();
1427
+ c.setLineDash([]);
1428
+ if (u.locked)
1429
+ drawLock(c, nw.x + 12, nw.y + 12);
1430
+ else if (sel.length === 1)
1431
+ drawHandleSet(c, hs);
1155
1432
  c.restore();
1156
- return;
1157
1433
  }
1158
- drawHandleSet(c, hs);
1159
- c.restore();
1160
1434
  }
1161
- /** Resize handles: round white corners (and arrow ends), blue bars on the sides. */
1435
+ /** Resize handles: round white corners (and arrow ends), blue bars on the sides — along them, on a turned image (`ang`). */
1162
1436
  function drawHandleSet(c, hs) {
1163
1437
  const r = ed.lastPointerTouch ? 8 : 6;
1164
1438
  for (const hd of hs) {
1165
1439
  const side = hd.part === 'n' || hd.part === 's' || hd.part === 'e' || hd.part === 'w';
1440
+ c.save();
1441
+ c.translate(hd.x, hd.y);
1442
+ if (hd.ang)
1443
+ c.rotate(hd.ang);
1166
1444
  c.beginPath();
1167
1445
  if (!side)
1168
- c.arc(hd.x, hd.y, r, 0, Math.PI * 2);
1446
+ c.arc(0, 0, r, 0, Math.PI * 2);
1169
1447
  else if (hd.part === 'e' || hd.part === 'w')
1170
- rr(c, hd.x - 3.5, hd.y - 15, 7, 30, 3.5);
1448
+ rr(c, -3.5, -15, 7, 30, 3.5);
1171
1449
  else
1172
- rr(c, hd.x - 12, hd.y - 3, 24, 6, 3);
1450
+ rr(c, -12, -3, 24, 6, 3);
1173
1451
  c.fillStyle = side ? TH.blue : '#fff';
1174
1452
  c.fill();
1175
1453
  c.lineWidth = 1.5;
1176
1454
  c.strokeStyle = TH.blue;
1177
1455
  c.stroke();
1456
+ c.restore();
1178
1457
  }
1179
1458
  }
1180
- /** A single selected arrow, cloud or pen stroke: its adjustment handles (and a box around clouds and strokes). */
1459
+ /** A single selected piece of markup: its adjustment handles (and a box around all but the straight ones and notes). */
1181
1460
  function drawNoteSelection(c, T) {
1182
1461
  if (ed.mode !== 'annotate' || !settings.layers.annot)
1183
1462
  return;
@@ -1191,7 +1470,8 @@ function drawNoteSelection(c, T) {
1191
1470
  if (!hs.length)
1192
1471
  return;
1193
1472
  c.save();
1194
- if (n.type !== 'arrow') {
1473
+ // (a note has its own outline, and a link its card)
1474
+ if (!isSegment(n) && n.type !== 'text' && n.type !== 'link') {
1195
1475
  const nw = hs[0], se = hs[4];
1196
1476
  c.strokeStyle = TH.blue;
1197
1477
  c.lineWidth = 1.2;
@@ -1243,6 +1523,16 @@ function drawSnap(c, T) {
1243
1523
  c.rect(x - 5, y - 5, 10, 10);
1244
1524
  c.stroke();
1245
1525
  }
1526
+ else if (s.kind === 'grid') {
1527
+ // on the grid: the grid point it's on, marked (it moves in steps from one to the next)
1528
+ c.beginPath();
1529
+ c.arc(x, y, 3.5, 0, Math.PI * 2);
1530
+ c.fill();
1531
+ c.lineWidth = 1.2;
1532
+ c.beginPath();
1533
+ c.arc(x, y, 7, 0, Math.PI * 2);
1534
+ c.stroke();
1535
+ }
1246
1536
  const lbl = s.label || (s.guides && s.guides.length ? s.guides.map(g => g.label).join(' · ') : '');
1247
1537
  if (lbl) {
1248
1538
  c.font = '600 11px ' + FONT;
@@ -1259,8 +1549,9 @@ function drawSnap(c, T) {
1259
1549
  }
1260
1550
  /** Paint the browser canvas, keeping the frame's tap targets for hit testing. */
1261
1551
  export function draw() {
1262
- const { W, H, dpr, ctx } = cvs;
1263
- if (!W)
1552
+ const { cv, W, H, dpr, ctx } = cvs;
1553
+ // (no browser canvas — a unit test — nothing to paint)
1554
+ if (!W || !cv)
1264
1555
  return;
1265
1556
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1266
1557
  ed.hitChips = drawPlan(ctx, W, H);
@@ -1274,20 +1565,70 @@ export function drawPlan(ctx, W, H) {
1274
1565
  hits = out;
1275
1566
  try {
1276
1567
  paintPlan(ctx, W, H);
1568
+ paintOverlay(ctx, W, H);
1277
1569
  }
1278
1570
  finally {
1279
1571
  hits = null;
1280
1572
  }
1281
1573
  return out;
1282
1574
  }
1283
- function paintPlan(ctx, W, H) {
1284
- ctx.fillStyle = TH.bg;
1285
- ctx.fillRect(0, 0, W, H);
1286
- const sk = SK(), T = mainT();
1287
- drawGrid(ctx, T);
1288
- drawUnderlay(ctx, T);
1575
+ /**
1576
+ * The drawing without its moving parts (see drawPlanOverlay), for a host that keeps it as a picture and redraws only
1577
+ * those parts as the pointer moves. Returns the tap targets, as drawPlan does.
1578
+ */
1579
+ export function drawPlanStatic(ctx, W, H, opts = {}) {
1580
+ const out = [];
1581
+ hits = out;
1582
+ try {
1583
+ paintPlan(ctx, W, H, opts);
1584
+ }
1585
+ finally {
1586
+ hits = null;
1587
+ }
1588
+ return out;
1589
+ }
1590
+ /** The photos and plans under the drawing, with the background and grid (see KeepOpts.layer). */
1591
+ export function drawPlanUnder(ctx, W, H, opts = {}) {
1592
+ paintPlan(ctx, W, H, { ...opts, layer: 'under' });
1593
+ }
1594
+ /** The parts that move with the pointer: a tool's preview, the snap marker, the selection box, the conflict tint. */
1595
+ export function drawPlanOverlay(ctx, W, H, opts = {}) {
1596
+ paintOverlay(ctx, W, H, opts.fixed ?? ctx);
1597
+ }
1598
+ function paintPlan(ctx, W, H, opts = {}) {
1599
+ const m = opts.margin ?? 0, wall = elevOpen() && !!elev.frame;
1600
+ if (opts.layer !== 'above') {
1601
+ ctx.fillStyle = TH.bg;
1602
+ ctx.fillRect(-m, -m, W + 2 * m, H + 2 * m);
1603
+ }
1604
+ // the photos and plans under the drawing (the wall view has none: its grid is on the wall, drawn with it)
1605
+ if (opts.layer === 'under') {
1606
+ if (!wall) {
1607
+ const T = mainT();
1608
+ drawGrid(ctx, T, m);
1609
+ drawUnderlay(ctx, T);
1610
+ }
1611
+ return;
1612
+ }
1613
+ const sk = SK();
1614
+ // (the sketch can't change while a frame is drawn: index its lines once for the frame)
1615
+ withLineIndex(sk, () => {
1616
+ if (wall)
1617
+ paintWall(ctx, W, opts.fixed ?? ctx);
1618
+ else
1619
+ paintPlanView(ctx, sk, m, opts.layer !== 'above');
1620
+ });
1621
+ }
1622
+ function paintPlanView(ctx, sk, m, under = true) {
1623
+ const T = mainT();
1624
+ if (under) {
1625
+ drawGrid(ctx, T, m);
1626
+ drawUnderlay(ctx, T);
1627
+ }
1289
1628
  if (settings.layers.rooms)
1290
1629
  drawRooms(ctx, T, sk);
1630
+ if (settings.layers.cabinets)
1631
+ drawCabinetFootprints(ctx, T, sk);
1291
1632
  drawWalls(ctx, T, sk);
1292
1633
  drawOpenings(ctx, T, sk);
1293
1634
  drawLines(ctx, T, sk);
@@ -1295,13 +1636,51 @@ function paintPlan(ctx, W, H) {
1295
1636
  drawPrintDims(ctx, T, sk);
1296
1637
  if (ed.mode === 'draft')
1297
1638
  drawBadges(ctx, T, sk);
1298
- drawPoints(ctx, T, sk);
1639
+ // (the drawing's points only in Draft: Annotate can't move them, and shouldn't look as if it could — the user's
1640
+ // call, 2026-10-21)
1641
+ if (ed.mode === 'draft')
1642
+ drawPoints(ctx, T, sk);
1299
1643
  drawOpeningAnchors(ctx, T, sk);
1300
1644
  drawDims(ctx, T, sk);
1301
1645
  drawOpeningDims(ctx, T, sk);
1302
1646
  drawNotes(ctx, T, sk);
1303
- drawUnderlaySelection(ctx, T);
1304
1647
  drawNoteSelection(ctx, T);
1648
+ }
1649
+ /**
1650
+ * The wall view: the open wall face, drawn as the plan is (its grid on the wall only, lines, curves, constraints, dimensions, points), over
1651
+ * the wall itself (render/elevation.ts) and under the dimensions its doors, windows and cabinets get, plus a key plan.
1652
+ */
1653
+ function paintWall(ctx, W, fixed) {
1654
+ const sk = SK(), plan = planSK(), f = elev.frame, T = mainT();
1655
+ // the grid only on the wall itself: outside it is plain, so it's plain that nothing goes there
1656
+ const [gx0, gy0] = S(T, f.face[0], f.H), [gx1, gy1] = S(T, f.face[1], 0);
1657
+ ctx.save();
1658
+ ctx.beginPath();
1659
+ ctx.rect(gx0, gy0, gx1 - gx0, gy1 - gy0);
1660
+ ctx.clip();
1661
+ drawGrid(ctx, T);
1662
+ ctx.restore();
1663
+ drawWallBackdrop(ctx, T, plan, f, sk);
1664
+ drawLines(ctx, T, sk);
1665
+ drawCurves(ctx, T, sk);
1666
+ // (over the edges — its own and its neighbours' — so a selected cabinet's outline shows all the way round)
1667
+ drawWallSelection(ctx, T, plan, f, sk);
1668
+ drawPrintDims(ctx, T, sk);
1669
+ drawBadges(ctx, T, sk);
1670
+ if (ed.mode === 'draft')
1671
+ drawPoints(ctx, T, sk);
1672
+ drawDims(ctx, T, sk);
1673
+ const on = (a) => isSel('elev', JSON.stringify(a)) || (a.k === 'fdim' && isSel('dim', a.id));
1674
+ for (const h of drawWallChips(ctx, T, plan, f, sk, on))
1675
+ addHit({ kind: 'elev', id: JSON.stringify(h.act), part: h.look, x: h.x, y: h.y, w: h.w, h: h.h });
1676
+ // (where you stand: it stays put on the screen as the wall is panned and zoomed)
1677
+ const k = keyPlanSize(W);
1678
+ drawKeyPlan(fixed, W - k - 14, 70, k, plan, f);
1679
+ }
1680
+ function paintOverlay(ctx, W, H, fixed = ctx) {
1681
+ const T = mainT();
1682
+ // a selected image's outline and handles: with the moving parts, as it's turned, moved or sized (over the drawing)
1683
+ drawUnderlaySelection(ctx, T);
1305
1684
  if (ed.preview)
1306
1685
  ed.preview(ctx, T);
1307
1686
  drawSnap(ctx, T);
@@ -1318,14 +1697,15 @@ function paintPlan(ctx, W, H) {
1318
1697
  ctx.strokeRect(Math.min(x0, x1) + 0.5, Math.min(y0, y1) + 0.5, Math.abs(x1 - x0), Math.abs(y1 - y0));
1319
1698
  ctx.restore();
1320
1699
  }
1700
+ // (the screen's edge, however the view moves)
1321
1701
  if (ed.conflictTint) {
1322
- ctx.save();
1323
- ctx.fillStyle = 'rgba(218,17,0,.07)';
1324
- ctx.fillRect(0, 0, W, H);
1325
- ctx.strokeStyle = 'rgba(218,17,0,.7)';
1326
- ctx.lineWidth = 6;
1327
- ctx.strokeRect(3, 3, W - 6, H - 6);
1328
- ctx.restore();
1702
+ fixed.save();
1703
+ fixed.fillStyle = 'rgba(218,17,0,.07)';
1704
+ fixed.fillRect(0, 0, W, H);
1705
+ fixed.strokeStyle = 'rgba(218,17,0,.7)';
1706
+ fixed.lineWidth = 6;
1707
+ fixed.strokeRect(3, 3, W - 6, H - 6);
1708
+ fixed.restore();
1329
1709
  }
1330
1710
  }
1331
1711
  /** Page thumbnail for the drawer (no chips, no definition colors). */
@@ -1360,11 +1740,9 @@ export function paintThumb(c, w, h, pg) {
1360
1740
  ed.hover = null;
1361
1741
  cvs.W = w;
1362
1742
  cvs.H = h;
1743
+ // (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)
1363
1744
  try {
1364
- drawRooms(c, T, sk, true);
1365
- drawWalls(c, T, sk, true);
1366
- drawLines(c, T, sk, true);
1367
- drawCurves(c, T, sk, true);
1745
+ withLineIndex(sk, () => { drawRooms(c, T, sk, true); drawWalls(c, T, sk, true); drawLines(c, T, sk, true); drawCurves(c, T, sk, true); });
1368
1746
  }
1369
1747
  finally {
1370
1748
  cvs.W = save.W;