@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,52 +1,36 @@
1
- import { ed } from '../app/state';
2
- import { elevLines, elevLineUZ, elevOpenings, toU, type ElevFrame, type ElevOpening } from '../model/elevation';
3
- import { edgeS, openingSpan, pickBetween, refS, sameRef } from '../model/openings';
4
- import type { OpeningDim, OpeningEdge, OpeningRef, Sketch } from '../model/types';
1
+ import { ed, isSel } from '../app/state';
2
+ import { measure } from '../model/constraints';
3
+ import { elevOpenings, toU, type ElevFrame, type ElevOpening } from '../model/elevation';
4
+ import { cabPoints, cornerAnchor, emptyFace, openingAnchor } from '../model/faces';
5
+ import { edgeS, openingSpan, pickBetween, refS, sameRef, widthEntered } from '../model/openings';
6
+ import type { Constraint, OpeningDim, OpeningEdge, OpeningRef, Sketch, WallFace } from '../model/types';
5
7
  import { fmtLen } from '../units';
6
- import { chip } from './draw';
8
+ import { chip, linkBlue, linkMark } from './draw';
7
9
  import { FONT, TH } from './theme';
8
10
  import { S, type Xform } from './view';
9
11
  import type { DrawCtx } from './ctx';
10
12
 
11
13
  /*
12
- Draws a wall elevation (see model/elevation.ts): u along the wall to the right, z up from the floor.
13
- The same Xform/S() as the plan, fed (u, z) instead of (x, y). Dimensions are tappable: drawElevation returns their hit boxes.
14
+ How the wall view draws a wall face (see model/elevation.ts and model/faces.ts): x across the wall as seen, y up
15
+ from the floor, through the plan's Xform. Under what's drawn on the face: the wall itself, the walls cut at its
16
+ corners, its doors, windows and cabinets. Over it: the dimensions doors, windows and cabinets get on their own —
17
+ the strings along the floor, heights beside them, the wall's height and length — all tappable.
14
18
  */
15
19
 
16
20
  type C2D = DrawCtx;
17
21
 
18
- /** Elevation view: centre (u, z) and pixels per inch. */
19
- export interface ElevView { cu: number; cz: number; s: number }
20
-
21
- export const elevT = (v: ElevView, W: number, H: number): Xform => ({ s: v.s, ox: W / 2 - v.cu * v.s, oy: H / 2 + v.cz * v.s });
22
-
23
- /** Screen room taken by dimensions around the wall, pixels: below the floor, right of the face. */
24
- const DIM_BELOW = 92, DIM_RIGHT = 70, PAD = 36;
25
-
26
- /**
27
- * Fit the wall to a W × H view, leaving pixel margins for the dimensions and for anything overlaid
28
- * (`keep`: space taken at the top or the right, e.g. the key plan).
29
- */
30
- export function elevFit(f: ElevFrame, W: number, H: number, keep: { top?: number; right?: number; left?: number; bottom?: number } = {}): ElevView {
31
- const u0 = Math.min(f.face[0], ...f.strips.map(s => s[0])), u1 = Math.max(f.face[1], ...f.strips.map(s => s[1]));
32
- const w = u1 - u0, h = f.H, top = keep.top || 0, right = Math.max(DIM_RIGHT, keep.right || 0), left = keep.left || 0, below = DIM_BELOW + (keep.bottom || 0);
33
- const s = Math.max(0.05, Math.min((W - 2 * PAD - right - left) / (w || 1), (H - 2 * PAD - below - top) / (h || 1)));
34
- // centre the drawing in the space left after the margins
35
- return { s, cu: (u0 + u1) / 2 + (right - left) / 2 / s, cz: h / 2 - (below - top) / 2 / s };
36
- }
37
-
38
- /** What tapping an elevation dimension does. */
22
+ /** What tapping one of the wall view's dimensions does (stored as JSON in the selection and the tap targets). */
39
23
  export type ElevAct =
40
24
  | { k: 'w' | 'sill' | 'h'; op: string }
41
25
  | { k: 'wallH' }
42
26
  | { k: 'len' }
43
- /** an opening itself: tap to select, drag to move */
44
- | { k: 'body'; op: string }
45
- /** a corner of the wall face (its left or right edge as seen): tap to select */
46
- | { k: 'corner'; end: 'a' | 'b' }
47
- /** a line drawn on the face (picked by distance, not by a box) */
48
- | { k: 'eline'; id: string }
49
- | { k: 'pos'; op: string; edge: OpeningEdge; ref: OpeningRef; title: string };
27
+ | { k: 'pos'; op: string; edge: OpeningEdge; ref: OpeningRef; title: string }
28
+ /** a dimension on the face (a constraint of its sketch) */
29
+ | { k: 'fdim'; id: string }
30
+ /** a distance nothing sets yet between two face points (grey): entering it makes it a face dimension */
31
+ | { k: 'gap'; a: string; b: string; kind: 'h' | 'v'; title: string }
32
+ /** a cabinet's depth (out from the wall: it shows in the plan) */
33
+ | { k: 'depth'; cab: string };
50
34
  export interface ElevHit { x: number; y: number; w: number; h: number; act: ElevAct; /** chip colour: drive (blue), ref, free (grey), bad (red) */ look?: string }
51
35
 
52
36
  /**
@@ -58,27 +42,15 @@ const LOOK = (look: Look) => look === 'drive' ? { bg: TH.blue, fg: '#fff', line:
58
42
  : look === 'linked' ? { bg: TH.bg, fg: linkBlue(), line: linkBlue() }
59
43
  : look === 'ref' ? { bg: TH.ref, fg: TH.reftext, line: TH.chipline } : { bg: TH.chip, fg: TH.ink, line: TH.ink2 };
60
44
 
61
- /** Blue for outlined (linked) chips: the lighter blue on a dark canvas, so the text stays readable. */
62
- function linkBlue(): string {
63
- const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i.exec(TH.bg);
64
- const dark = !!m && (parseInt(m[1], 16) * 299 + parseInt(m[2], 16) * 587 + parseInt(m[3], 16) * 114) / 1000 < 128;
65
- return dark ? TH.lblue : TH.blue;
66
- }
67
-
68
- /** A small chain-link mark centred on (x, y). */
69
- function linkMark(c: C2D, x: number, y: number, color: string): void {
70
- c.save();
71
- c.translate(x, y); c.rotate(-Math.PI / 4);
72
- c.strokeStyle = color; c.lineWidth = 1.4;
73
- 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();
74
- c.restore();
75
- }
76
-
77
45
  /**
78
46
  * Linear dimension between screen points p and q, its line offset by (nx, ny) pixels: ticks, extension lines and
79
47
  * the value chip. Returns the chip's box (for taps). `force` draws the chip even for a zero-length dimension.
80
48
  */
81
- function dim(c: C2D, p: [number, number], q: [number, number], nx: number, ny: number, label: string, look: Look = 'free', force = false, selected = false): Omit<ElevHit, 'act'> | null {
49
+ /**
50
+ * A dimension: extension lines, the dimension line with 45° ticks, the chip. `dashed`: a number that holds nothing (only
51
+ * measured, or a reference) — its dimension line dashed and its value in parentheses, as in the plan.
52
+ */
53
+ function dim(c: C2D, p: [number, number], q: [number, number], nx: number, ny: number, label: string, look: Look = 'free', force = false, selected = false, dashed = false): Omit<ElevHit, 'act'> | null {
82
54
  const len = Math.hypot(q[0] - p[0], q[1] - p[1]);
83
55
  if (len < 2 && !force) return null;
84
56
  const a = [p[0] + nx, p[1] + ny], b = [q[0] + nx, q[1] + ny], L = Math.hypot(nx, ny) || 1, st = LOOK(look);
@@ -87,7 +59,11 @@ function dim(c: C2D, p: [number, number], q: [number, number], nx: number, ny: n
87
59
  c.beginPath();
88
60
  for (const [o, e] of [[p, a], [q, b]]) { c.moveTo(o[0] + (nx / L) * 4, o[1] + (ny / L) * 4); c.lineTo(e[0] + (nx / L) * 5, e[1] + (ny / L) * 5); }
89
61
  if (len >= 2) {
90
- c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]);
62
+ if (dashed) {
63
+ c.stroke(); c.beginPath(); c.setLineDash([3, 3]);
64
+ c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]);
65
+ c.stroke(); c.beginPath(); c.setLineDash([]);
66
+ } else { c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]); }
91
67
  // 45° architectural ticks
92
68
  const ux = (b[0] - a[0]) / len, uy = (b[1] - a[1]) / len;
93
69
  const tx = (ux + nx / L) * 4, ty = (uy + ny / L) * 4;
@@ -96,7 +72,8 @@ function dim(c: C2D, p: [number, number], q: [number, number], nx: number, ny: n
96
72
  c.stroke();
97
73
  c.restore();
98
74
  const cx = (a[0] + b[0]) / 2 + (len < 2 ? (nx / L) * 4 : 0), cy = (a[1] + b[1]) / 2 - (len < 2 ? 12 : 0), h = 20;
99
- const linked = look === 'linked', text = look === 'ref' ? '(' + label + ')' : linked ? '\u2007\u2007' + label : label; // (figure spaces: room for the link mark)
75
+ // a number that holds nothing (dashed) reads in parentheses, as in the plan
76
+ const linked = look === 'linked', text = dashed ? '(' + label + ')' : linked ? '  ' + label : label; // (figure spaces: room for the link mark)
100
77
  const w = chip(c, text, cx, cy, { bg: st.bg, fg: st.fg, font: '600 11.5px', h, pad: 6, border: selected ? [TH.yellow, 2] : linked ? [st.line, 1.5] : null });
101
78
  if (linked) linkMark(c, cx - w / 2 + 11, cy, st.fg);
102
79
  return { x: cx - w / 2 - 4, y: cy - h / 2 - 4, w: w + 8, h: h + 8 };
@@ -135,113 +112,345 @@ function drawOpening(c: C2D, T: Xform, o: ElevOpening): void {
135
112
  c.restore();
136
113
  }
137
114
 
138
- /** Draw the elevation of frame `f` into a W × H (CSS pixel) canvas context. */
139
- /** `selected`: the selected dimension's action (JSON), drawn with the yellow selection border. */
140
- export function drawElevation(c: C2D, W: number, H: number, v: ElevView, sk: Sketch, f: ElevFrame, selected: string | null = null): ElevHit[] {
141
- const T = elevT(v, W, H);
142
- c.fillStyle = TH.bg; c.fillRect(0, 0, W, H);
143
- const [fx0, fyTop] = S(T, f.face[0], f.H), [fx1, fyBot] = S(T, f.face[1], 0);
115
+ /** A cabinet's tint and panel lines: neutral grey (no yellow — that's the selection's), against a window's blue glass. */
116
+ const WOOD = 'rgba(128,128,128,.14)', WOOD_LINE = 'rgba(128,128,128,.9)';
117
+
118
+ /**
119
+ * A cabinet, as cabinetry is drawn in elevation: a wood-tinted box, a drawer across its top with a pull, and a door
120
+ * below it with a handle — so it reads as a cabinet at a glance, not a window. Its name and size sit on the door.
121
+ */
122
+ function drawCabinet(c: C2D, T: Xform, x: number, y: number, w: number, h: number, name: string): void {
123
+ const pad = Math.min(1.5 * T.s, w / 10, h / 10), drawer = Math.min(6 * T.s, h * 0.24);
124
+ c.save();
125
+ c.fillStyle = TH.bg; c.fillRect(x, y, w, h);
126
+ c.fillStyle = WOOD; c.fillRect(x, y, w, h);
127
+ c.strokeStyle = TH.ink; c.lineWidth = 1.5; c.strokeRect(x, y, w, h);
128
+ // the drawer front and the door, inset in the box
129
+ const ix = x + pad, iw = w - 2 * pad, dy = y + pad, dh = drawer - pad, oy = y + drawer + pad * 0.5, oh = h - drawer - pad * 1.5;
130
+ c.strokeStyle = WOOD_LINE; c.lineWidth = 1;
131
+ if (dh > 3) c.strokeRect(ix, dy, iw, dh);
132
+ if (oh > 6) c.strokeRect(ix, oy, iw, oh);
133
+ // a pull on the drawer, a handle at the top of the door on its right
134
+ c.strokeStyle = TH.ink; c.lineWidth = 2; c.lineCap = 'round';
135
+ c.beginPath();
136
+ if (dh > 6 && iw > 16) { const pw = Math.min(iw * 0.3, 4 * T.s); c.moveTo(ix + iw / 2 - pw / 2, dy + dh / 2); c.lineTo(ix + iw / 2 + pw / 2, dy + dh / 2); }
137
+ if (oh > 24 && iw > 16) { const hx = ix + iw - Math.max(5, pad * 2), hl = Math.min(oh * 0.18, 4 * T.s); c.moveTo(hx, oy + Math.max(6, pad * 1.5)); c.lineTo(hx, oy + Math.max(6, pad * 1.5) + hl); }
138
+ c.stroke();
139
+ // its name, on the door (its sizes are in the bar when it's picked: on the drawing they'd be noise)
140
+ c.fillStyle = TH.ink2; c.textAlign = 'center'; c.textBaseline = 'middle';
141
+ const cx = x + w / 2, cy = oy + oh / 2;
142
+ if (w > 44 && oh > 22) {
143
+ c.font = '600 11px ' + FONT;
144
+ c.fillText(name, cx, cy);
145
+ }
146
+ c.restore();
147
+ }
148
+
149
+ /** A cabinet's extent on the face. */
150
+ export function cabBox(face: WallFace, id: string): { u0: number; u1: number; z0: number; z1: number } | null {
151
+ const cb = face.cabs?.[id], ps = cb ? cabPoints(cb).map(p => face.points[p]) : [];
152
+ if (!cb || ps.some(p => !p)) return null;
153
+ return { u0: Math.min(...ps.map(p => p.x)), u1: Math.max(...ps.map(p => p.x)), z0: Math.min(...ps.map(p => p.y)), z1: Math.max(...ps.map(p => p.y)) };
154
+ }
155
+
156
+ /** A cabinet's left and right bottom corners as seen. */
157
+ const cabSides = (face: WallFace, id: string): [string, string] => {
158
+ const cb = face.cabs[id];
159
+ return face.points[cb.bl].x <= face.points[cb.br].x ? [cb.bl, cb.br] : [cb.br, cb.bl];
160
+ };
144
161
 
145
- // walls cut at the corners, then the face
162
+ /**
163
+ * Under what's drawn on the face: the walls cut at its corners, the face, its doors and windows, its cabinets (a door
164
+ * panel and their name and size; their edges are the face's lines, drawn over this), the face's outline and the floor.
165
+ * (The selected door, window or cabinet's outline: drawWallSelection, over all of it.)
166
+ */
167
+ export function drawWallBackdrop(c: C2D, T: Xform, sk: Sketch, f: ElevFrame, face: WallFace): void {
168
+ const [fx0, fyTop] = S(T, f.face[0], f.H), [fx1, fyBot] = S(T, f.face[1], 0);
146
169
  for (const [a, b] of f.strips) { const [x0] = S(T, a, 0), [x1] = S(T, b, 0); hatch(c, x0, fyTop, x1 - x0, fyBot - fyTop); }
147
170
  c.fillStyle = TH.room; c.fillRect(fx0, fyTop, fx1 - fx0, fyBot - fyTop);
148
- const ops = elevOpenings(sk, f);
149
- const hits: ElevHit[] = [];
150
- // the face's end edges, as tap targets for the corners (first, so openings and chips on top win)
151
- for (const [u, end] of [[f.face[0], f.leftEnd], [f.face[1], f.leftEnd === 'a' ? 'b' : 'a']] as const) {
152
- const [x, yTop] = S(T, u, f.H), [, yBot] = S(T, u, 0);
153
- hits.push({ x: x - 10, y: yTop, w: 20, h: yBot - yTop, act: { k: 'corner', end } });
171
+ for (const o of elevOpenings(sk, f)) drawOpening(c, T, o);
172
+ for (const [id, cb] of Object.entries(face.cabs ?? {})) {
173
+ const b = cabBox(face, id);
174
+ if (!b) continue;
175
+ const [x0, y0] = S(T, b.u0, b.z1), [x1, y1] = S(T, b.u1, b.z0);
176
+ drawCabinet(c, T, x0, y0, x1 - x0, y1 - y0, cb.label || 'Cabinet');
154
177
  }
155
- for (const o of ops) {
156
- drawOpening(c, T, o);
157
- const [x0, y0] = S(T, o.u0, o.head), [x1, y1] = S(T, o.u1, o.sill);
158
- hits.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0, act: { k: 'body', op: o.id } }); // first, so chips on top win taps
159
- }
160
- // lines drawn on this face: solid, or dashed guides
161
- c.save(); c.lineCap = 'round';
162
- for (const ln of elevLines(sk, f)) {
163
- const [a, b] = elevLineUZ(f, ln), p = S(T, a[0], a[1]), q = S(T, b[0], b[1]);
164
- c.strokeStyle = ln.guide ? TH.constr : TH.ink; c.lineWidth = ln.guide ? 1.5 : 2;
165
- c.setLineDash(ln.guide ? [6, 5] : []);
166
- c.beginPath(); c.moveTo(p[0], p[1]); c.lineTo(q[0], q[1]); c.stroke();
167
- }
168
- c.restore();
178
+ c.save();
169
179
  c.strokeStyle = TH.ink; c.lineWidth = 2; c.strokeRect(fx0, fyTop, fx1 - fx0, fyBot - fyTop);
170
- // floor line across everything
180
+ // the floor line, across the cut walls too
171
181
  const u0 = Math.min(f.face[0], ...f.strips.map(s => s[0])), u1 = Math.max(f.face[1], ...f.strips.map(s => s[1]));
172
182
  const [gx0] = S(T, u0, 0), [gx1] = S(T, u1, 0);
173
183
  c.lineWidth = 3; c.beginPath(); c.moveTo(gx0 - 14, fyBot); c.lineTo(gx1 + 14, fyBot); c.stroke();
184
+ c.restore();
185
+ }
174
186
 
175
- // dimensions (all tappable but the overall length): a chain along the floor (corner → openings → corner),
176
- // the overall face, the wall height, and each opening's sill and height
177
- /** a dimension chip with its tap action; the selected one gets the plan's yellow border */
178
- const tag = (p: [number, number], q: [number, number], nx: number, ny: number, label: string, look: Look, act: ElevAct, force = false) => {
179
- const box = dim(c, p, q, nx, ny, label, look, force, selected === JSON.stringify(act));
180
- if (box) hits.push({ ...box, act, look });
187
+ /**
188
+ * The selected door, window or cabinet (and the one under the pointer, fainter): a yellow outline right on its edges,
189
+ * drawn over everything else on the wall — its own edges and its neighbours' — so the whole of it shows, even where
190
+ * it shares a side with another cabinet, or lines up with a window.
191
+ */
192
+ export function drawWallSelection(c: C2D, T: Xform, sk: Sketch, f: ElevFrame, face: WallFace): void {
193
+ const lit = (type: 'opening' | 'cab', id: string) => isSel(type, id) || (ed.hover?.type === type && ed.hover.id === id);
194
+ const ring = (u0: number, z1: number, u1: number, z0: number, selected: boolean) => {
195
+ const [x0, y0] = S(T, u0, z1), [x1, y1] = S(T, u1, z0);
196
+ c.save(); c.strokeStyle = TH.yellow; c.globalAlpha = selected ? 1 : 0.5; c.lineWidth = 3; c.lineJoin = 'miter';
197
+ c.strokeRect(x0, y0, x1 - x0, y1 - y0);
198
+ c.restore();
181
199
  };
182
- const { segs } = chain(sk, f), lookOf = (d: OpeningDim | undefined): Look => (!d ? 'free' : ed.badOpeningDims.has(d.id) ? 'bad' : d.driving ? 'drive' : 'ref');
183
- for (const g of segs) {
184
- const p = S(T, g.u0, 0), q = S(T, g.u1, 0), text = fmtLen(g.u1 - g.u0);
185
- if (g.act.k === 'w') tag(p, q, 0, ROW0, text, 'drive', g.act);
186
- else tag(p, q, 0, ROW0, text, lookOf(g.dim), g.act);
200
+ for (const o of elevOpenings(sk, f)) if (lit('opening', o.id)) ring(o.u0, o.head, o.u1, o.sill, isSel('opening', o.id));
201
+ for (const id of Object.keys(face.cabs ?? {})) {
202
+ if (!lit('cab', id)) continue;
203
+ const b = cabBox(face, id);
204
+ if (b) ring(b.u0, b.z1, b.u1, b.z0, isSel('cab', id));
187
205
  }
188
- // saved dimensions that aren't one step of the string (e.g. window → door across another window, or from a
189
- // plan wall point): each on its own row below it, so it can be seen, selected and deleted
190
- const extra = extraDims(sk, f);
191
- for (const x of extra) tag(S(T, x.u0, 0), S(T, x.u1, 0), 0, ROW0 + ROW * (x.row + 1), fmtLen(x.u1 - x.u0), lookOf(x.d), x.act);
192
- const rows = extra.reduce((m, x) => Math.max(m, x.row + 1), 0);
193
- // the wall's length comes from the floor plan: linked (outlined blue) when a dimension locks it there, grey when free
194
- const planLocked = Object.values(sk.cons).some(cn => cn.type === 'dist' && cn.line === f.lid && cn.driving !== false && cn.layer !== 'print');
195
- tag(S(T, f.face[0], 0), S(T, f.face[1], 0), 0, segs.length || rows ? ROW0 + ROW * (rows + 1) : ROW0, fmtLen(f.face[1] - f.face[0]), planLocked ? 'linked' : 'free', { k: 'len' });
196
- // heights: blue once entered (the sill: locked), grey while they're defaults / free to drag
197
- tag(S(T, f.face[1], 0), S(T, f.face[1], f.H), 34, 0, fmtLen(f.H), sk.lines[f.lid].height != null ? 'drive' : 'free', { k: 'wallH' });
198
- for (const o of ops) {
199
- const off = 24;
200
- const op = sk.openings[o.id];
201
- if (o.sill > 0.01 || o.type === 'window') tag(S(T, o.u1, 0), S(T, o.u1, o.sill), off, 0, fmtLen(o.sill), op.sillLocked ? 'drive' : 'free', { k: 'sill', op: o.id }, true);
202
- tag(S(T, o.u1, o.sill), S(T, o.u1, o.head), off, 0, fmtLen(o.head - o.sill), op.h != null ? 'drive' : 'free', { k: 'h', op: o.id });
203
- }
204
- return hits;
205
206
  }
206
207
 
207
- /** Rows of dimensions below the floor, pixels: the string, then extra rows, then the overall length. */
208
+ /** Rows of dimensions below the floor, pixels: the strings, then extra rows, then the overall length. */
208
209
  const ROW0 = 30, ROW = 34;
209
210
 
210
211
  type PosAct = Extract<ElevAct, { k: 'pos' }>;
211
- interface Seg { u0: number; u1: number; act: PosAct | { k: 'w'; op: string }; dim?: OpeningDim }
212
+ /** Something in a string along the floor: a door or window, or a cabinet (its left and right points as seen). */
213
+ type Item = { u0: number; u1: number; op?: ElevOpening; cab?: { id: string; l: string; r: string } };
214
+ interface Seg { u0: number; u1: number; act: ElevAct; look: Look; /** an object's own width: always shown */ width?: true }
215
+
216
+ /** One rule for every chip: blue once someone entered it; grey while it's a standard size (a new cabinet's) or only measured. */
217
+ const faceLook = (c: Constraint, id: string): Look => (ed.highlight.has(id) || ed.ana.bad.has(id) || ed.badFaceCons.has(id) ? 'bad' : c.driving === false ? 'ref' : c.auto ? 'free' : 'drive');
218
+
219
+ /** The face dimension across the wall between two points, if there is one. */
220
+ function dimAcross(face: WallFace, a: string, b: string): string | null {
221
+ for (const [id, c] of Object.entries(face.cons)) if (c.type === 'dist' && c.kind === 'h' && ((c.a === a && c.b === b) || (c.a === b && c.b === a))) return id;
222
+ return null;
223
+ }
224
+
225
+ /** The face dimension of a cabinet's height above the floor, if there is one. */
226
+ export function cabLiftDim(face: WallFace, id: string): string | null {
227
+ const cb = face.cabs[id];
228
+ for (const [cid, k] of Object.entries(face.cons)) {
229
+ const ends = [k.a, k.b];
230
+ if (k.type === 'dist' && k.kind === 'v' && ends.some(v => !!v && !!face.points[v]?.anchor && face.points[v].y === 0) && ends.some(v => v === cb.bl || v === cb.br)) return cid;
231
+ }
232
+ return null;
233
+ }
212
234
 
213
- /** The dimension string along the floor: corner → each opening's edges → corner, left to right. */
214
- function chain(sk: Sketch, f: ElevFrame): { segs: Seg[] } {
215
- const ops = elevOpenings(sk, f), other: 'a' | 'b' = f.leftEnd === 'a' ? 'b' : 'a';
216
- type Node = { u: number; end?: 'a' | 'b'; op?: ElevOpening; edge?: OpeningEdge };
217
- const nodes: Node[] = [{ u: f.face[0], end: f.leftEnd }];
218
- for (const o of ops) {
219
- if (o.u0 < nodes[nodes.length - 1].u - 0.01 || o.u1 > f.face[1] + 0.01) continue; // overlapping or past the corner: left out of the string
220
- nodes.push({ u: o.u0, op: o, edge: o.e0 }, { u: o.u1, op: o, edge: o.e1 });
235
+ /**
236
+ * The strings along the floor, corner to corner, left to right: the doors and windows and the cabinets that fit
237
+ * between them in the first; cabinets that overlap those (hung above a counter, say) in the next ones.
238
+ */
239
+ function strings(sk: Sketch, f: ElevFrame, face: WallFace): Item[][] {
240
+ const ops: Item[] = elevOpenings(sk, f).map(o => ({ u0: o.u0, u1: o.u1, op: o }));
241
+ const cabs: Item[] = Object.keys(face.cabs ?? {}).flatMap(id => {
242
+ const b = cabBox(face, id);
243
+ if (!b) return [];
244
+ const [l, r] = cabSides(face, id);
245
+ return [{ u0: b.u0, u1: b.u1, cab: { id, l, r } }];
246
+ });
247
+ const rows: Item[][] = [];
248
+ const inside = (it: Item) => it.u0 >= f.face[0] - 0.01 && it.u1 <= f.face[1] + 0.01;
249
+ const fits = (row: Item[], it: Item) => inside(it) && !row.some(x => it.u0 < x.u1 - 0.01 && it.u1 > x.u0 + 0.01);
250
+ // doors and windows first, then cabinets: each in the first string it fits, overlapping ones in strings of their own
251
+ for (const it of [...ops.sort((p, q) => p.u0 - q.u0), ...cabs.sort((p, q) => p.u0 - q.u0)]) {
252
+ if (!inside(it)) continue;
253
+ const row = rows.find(r => fits(r, it));
254
+ if (row) row.push(it); else rows.push([it]);
221
255
  }
222
- nodes.push({ u: f.face[1], end: other });
223
- if (nodes.length <= 2) return { segs: [] };
224
- const face = (end: 'a' | 'b'): OpeningRef => ({ face: end, line: f.lid, side: f.side as 1 | -1 });
256
+ return rows.map(r => r.sort((p, q) => p.u0 - q.u0));
257
+ }
258
+
259
+ /** One string's dimensions: each gap between corners, doors, windows and cabinets, and each one's width. */
260
+ function stringSegs(sk: Sketch, f: ElevFrame, face: WallFace, row: Item[]): Seg[] {
261
+ const other: 'a' | 'b' = f.leftEnd === 'a' ? 'b' : 'a';
262
+ type Node = { u: number; pt: string; end?: 'a' | 'b'; it?: Item; edge?: OpeningEdge };
263
+ const nodes: Node[] = [{ u: f.face[0], pt: cornerAnchor(f.leftEnd), end: f.leftEnd }];
264
+ for (const it of row) {
265
+ if (it.op) nodes.push({ u: it.u0, pt: openingAnchor(it.op.id, it.op.e0), it, edge: it.op.e0 }, { u: it.u1, pt: openingAnchor(it.op.id, it.op.e1), it, edge: it.op.e1 });
266
+ else nodes.push({ u: it.u0, pt: it.cab!.l, it }, { u: it.u1, pt: it.cab!.r, it });
267
+ }
268
+ nodes.push({ u: f.face[1], pt: cornerAnchor(other), end: other });
269
+ if (nodes.length <= 2) return [];
270
+ const faceRef = (end: 'a' | 'b'): OpeningRef => ({ face: end, line: f.lid, side: f.side as 1 | -1 });
225
271
  const segs: Seg[] = [];
226
272
  for (let i = 1; i < nodes.length; i++) {
227
273
  const n1 = nodes[i - 1], n2 = nodes[i];
228
- if (n1.op && n2.op && n1.op === n2.op) { segs.push({ u0: n1.u, u1: n2.u, act: { k: 'w', op: n1.op.id } }); continue; }
229
- let act: PosAct;
230
- if (!n1.op) act = { k: 'pos', op: n2.op!.id, edge: n2.edge!, ref: face(n1.end!), title: 'Distance from the corner' };
231
- else if (!n2.op) act = { k: 'pos', op: n1.op.id, edge: n1.edge!, ref: face(n2.end!), title: 'Distance from the corner' };
232
- // between two openings: one of them is locked to the other (pickBetween says which)
233
- else act = { k: 'pos', ...pickBetween(sk, n1.op.id, n1.edge!, n2.op.id, n2.edge!), title: 'Space between' };
234
- const dim = (sk.openings[act.op].dims || []).find(d => d.edge === act.edge && sameRef(d.ref, act.ref));
235
- segs.push({ u0: n1.u, u1: n2.u, act, dim });
274
+ if (n1.it && n1.it === n2.it) {
275
+ // a door's or window's width (its own), or a cabinet's (a face dimension)
276
+ if (n1.it.op) segs.push({ u0: n1.u, u1: n2.u, act: { k: 'w', op: n1.it.op.id }, look: widthEntered(sk.openings[n1.it.op.id]) ? 'drive' : 'free', width: true });
277
+ else {
278
+ const id = face.cabs[n1.it.cab!.id].w;
279
+ segs.push(id && face.cons[id]
280
+ ? { u0: n1.u, u1: n2.u, act: { k: 'fdim', id }, look: faceLook(face.cons[id], id), width: true }
281
+ : { u0: n1.u, u1: n2.u, act: { k: 'gap', a: n1.pt, b: n2.pt, kind: 'h', title: 'Cabinet width' }, look: 'free', width: true });
282
+ }
283
+ continue;
284
+ }
285
+ if (!n1.it?.cab && !n2.it?.cab) {
286
+ if (!n1.it && !n2.it) continue;
287
+ // between corners, doors and windows: the plan's door/window dimensions
288
+ let act: PosAct;
289
+ if (!n1.it) act = { k: 'pos', op: n2.it!.op!.id, edge: n2.edge!, ref: faceRef(n1.end!), title: 'Distance from the corner' };
290
+ else if (!n2.it) act = { k: 'pos', op: n1.it.op!.id, edge: n1.edge!, ref: faceRef(n2.end!), title: 'Distance from the corner' };
291
+ else act = { k: 'pos', ...pickBetween(sk, n1.it.op!.id, n1.edge!, n2.it.op!.id, n2.edge!), title: 'Space between' };
292
+ const d = (sk.openings[act.op].dims || []).find(x => x.edge === act.edge && sameRef(x.ref, act.ref));
293
+ segs.push({ u0: n1.u, u1: n2.u, act, look: !d ? 'free' : ed.badOpeningDims.has(d.id) ? 'bad' : d.driving ? 'drive' : 'ref' });
294
+ continue;
295
+ }
296
+ // to or from a cabinet: a face dimension once entered
297
+ const id = dimAcross(face, n1.pt, n2.pt);
298
+ const title = n1.end || n2.end ? 'Distance from the corner' : 'Space between';
299
+ segs.push(id ? { u0: n1.u, u1: n2.u, act: { k: 'fdim', id }, look: faceLook(face.cons[id], id) } : { u0: n1.u, u1: n2.u, act: { k: 'gap', a: n1.pt, b: n2.pt, kind: 'h', title }, look: 'free' });
236
300
  }
237
- return { segs };
301
+ return segs;
302
+ }
303
+
304
+ /** The doors, windows and cabinets a wall-view dimension measures from or to (by the points it runs between). */
305
+ function owners(face: WallFace, act: ElevAct): string[] {
306
+ const out: string[] = [];
307
+ const of = (pid: string | undefined) => {
308
+ if (!pid) return;
309
+ const m = /^@o:([^:]+):/.exec(pid);
310
+ if (m) { out.push(m[1]); return; }
311
+ for (const [id, cb] of Object.entries(face.cabs ?? {})) if ([cb.bl, cb.br, cb.tr, cb.tl].includes(pid)) out.push(id);
312
+ };
313
+ if ('op' in act) out.push(act.op);
314
+ if (act.k === 'pos' && 'op' in act.ref) out.push(act.ref.op);
315
+ if (act.k === 'depth') out.push(act.cab);
316
+ if (act.k === 'gap') { of(act.a); of(act.b); }
317
+ if (act.k === 'fdim') { const c = face.cons[act.id]; of(c?.a); of(c?.b); }
318
+ // the wall's own length and height: the wall's
319
+ if (act.k === 'len' || act.k === 'wallH') out.push(WALL);
320
+ return out;
321
+ }
322
+
323
+ /**
324
+ * The doors, windows and cabinets whose reference dimensions show (as the plan shows a door's or window's distances
325
+ * only while it's selected): the selected and hovered ones, and the one a selected dimension belongs to.
326
+ */
327
+ /** The wall itself, among what a wall view's dimensions belong to: live while one of its edges or corners is picked. */
328
+ const WALL = '@wall';
329
+ /** One of the wall's own edges or corners (the face's anchors that aren't a door's or window's, nor a shape's). */
330
+ const wallPart = (id: string): boolean => id.startsWith('@') && !id.startsWith('@o:') && !id.startsWith('@k:');
331
+
332
+ function liveObjects(face: WallFace): Set<string> {
333
+ const live = new Set<string>();
334
+ const add = (type: string, id: string) => {
335
+ if (type === 'opening' || type === 'cab') live.add(id);
336
+ else if ((type === 'line' || type === 'point') && wallPart(id)) live.add(WALL);
337
+ else if (type === 'elev') { try { for (const o of owners(face, JSON.parse(id) as ElevAct)) live.add(o); } catch { /* not a chip */ } }
338
+ else if (type === 'dim' && face.cons[id]) for (const o of owners(face, { k: 'fdim', id })) live.add(o);
339
+ };
340
+ for (const s of ed.sel) add(s.type, s.id);
341
+ if (ed.hover) add(ed.hover.type, ed.hover.id);
342
+ return live;
343
+ }
344
+
345
+ /**
346
+ * The wall view's dimensions, over the face: the strings along the floor, door/window dimensions that aren't a step of
347
+ * one, face dimensions entered from a chip that no longer are, the wall's length (set in the plan) and height, and
348
+ * beside each door, window and cabinet its height off the floor and its own height. `on`: whether a chip is selected.
349
+ * As in the plan, what's been entered (blue) always shows; the rest (grey: just measured, a standard size or a
350
+ * reference) only while what it belongs to — a door, window or cabinet, or the wall itself (an edge or corner of it) —
351
+ * is selected or hovered. Returns the chips, to tap.
352
+ */
353
+ export function drawWallChips(c: C2D, T: Xform, sk: Sketch, f: ElevFrame, face: WallFace, on: (act: ElevAct) => boolean): ElevHit[] {
354
+ const hits: ElevHit[] = [], shown = new Set<string>(), live = liveObjects(face);
355
+ /** entered by someone, so it shows in blue (or red, when it can't be met) — not just measured, a standard size or a reference */
356
+ const entered = (act: ElevAct, look: Look) => (look === 'drive' || look === 'bad' || look === 'linked') && !(act.k === 'fdim' && face.cons[act.id]?.auto);
357
+ /** an object's own size (a door's, window's or cabinet's width or height; the wall's length or height), not a distance */
358
+ const ownSize = (act: ElevAct): boolean => act.k === 'w' || act.k === 'h' || act.k === 'len' || act.k === 'wallH'
359
+ || (act.k === 'gap' && (act.title === 'Cabinet width' || act.title === 'Cabinet height'))
360
+ || (act.k === 'fdim' && Object.values(face.cabs ?? {}).some(cb => cb.w === act.id || cb.h === act.id));
361
+ // a dimension shows once it's entered, and otherwise only while what it belongs to is selected or hovered
362
+ const tag = (p: [number, number], q: [number, number], nx: number, ny: number, label: string, look: Look, act: ElevAct, force = false) => {
363
+ if (!entered(act, look) && !on(act) && !owners(face, act).some(o => live.has(o))) return;
364
+ // as in the plan, a number that holds nothing is dashed and in parentheses: a reference, or one only measured —
365
+ // a distance, or the wall's length when the plan doesn't lock it (a standard size, which holds, stays solid)
366
+ const dashed = look === 'ref' || (look === 'free' && !(ownSize(act) && act.k !== 'len'));
367
+ const box = dim(c, p, q, nx, ny, label, look, force, on(act), dashed);
368
+ if (box) hits.push({ ...box, act, look });
369
+ if (act.k === 'fdim') shown.add(act.id);
370
+ };
371
+ /** the chip's value: a driving face dimension's own, otherwise what's measured */
372
+ const val = (act: ElevAct, measured: number) => (act.k === 'fdim' && face.cons[act.id]?.driving !== false ? face.cons[act.id].value! : measured);
373
+ const rows = strings(sk, f, face);
374
+ rows.forEach((row, r) => {
375
+ for (const g of stringSegs(sk, f, face, row)) tag(S(T, g.u0, 0), S(T, g.u1, 0), 0, ROW0 + ROW * r, fmtLen(val(g.act, g.u1 - g.u0)), g.look, g.act);
376
+ });
377
+ let next = Math.max(rows.length, 1);
378
+ // door/window dimensions that aren't a step of a string, packed into rows of their own
379
+ const extra = extraDims(sk, f, rows);
380
+ for (const x of extra) tag(S(T, x.u0, 0), S(T, x.u1, 0), 0, ROW0 + ROW * (next + x.row), fmtLen(x.u1 - x.u0), ed.badOpeningDims.has(x.d.id) ? 'bad' : x.d.driving ? 'drive' : 'ref', x.act);
381
+ next += extra.reduce((m, x) => Math.max(m, x.row + 1), 0);
382
+ // heights beside doors, windows and cabinets
383
+ for (const o of elevOpenings(sk, f)) {
384
+ const op = sk.openings[o.id];
385
+ if (o.sill > 0.01 || o.type === 'window') tag(S(T, o.u1, 0), S(T, o.u1, o.sill), 24, 0, fmtLen(o.sill), op.sillLocked ? 'drive' : 'free', { k: 'sill', op: o.id }, true);
386
+ tag(S(T, o.u1, o.sill), S(T, o.u1, o.head), 24, 0, fmtLen(o.head - o.sill), op.h != null ? 'drive' : 'free', { k: 'h', op: o.id });
387
+ }
388
+ // beside a cabinet: on its right like a window's, unless a cabinet or opening is right there (a run of cabinets),
389
+ // then on its left, else just inside it
390
+ const boxes = [...Object.keys(face.cabs ?? {}).map(id => ({ id, b: cabBox(face, id) })).filter(x => x.b).map(x => ({ id: x.id, ...x.b! })),
391
+ ...elevOpenings(sk, f).map(o => ({ id: o.id, u0: o.u0, u1: o.u1, z0: o.sill, z1: o.head }))];
392
+ const room = 64 / T.s;
393
+ const blocked = (id: string, b: { z0: number; z1: number }, lo: number, hi: number) =>
394
+ boxes.some(o => o.id !== id && o.u1 > lo + 0.01 && o.u0 < hi - 0.01 && o.z1 > b.z0 - 0.01 && o.z0 < b.z1 + 0.01);
395
+ for (const [id, cb] of Object.entries(face.cabs ?? {})) {
396
+ const b = cabBox(face, id);
397
+ if (!b) continue;
398
+ const [l, r] = cabSides(face, id), zid = cabLiftDim(face, id);
399
+ const right = !blocked(id, b, b.u1, b.u1 + room), left = !right && !blocked(id, b, b.u0 - room, b.u0);
400
+ const u = right ? b.u1 : b.u0, off = right ? 24 : left ? -24 : 24, at = right || left ? u : b.u1 - 48 / T.s;
401
+ // its height off the floor: when it's off the floor, set, or selected (a cabinet on the floor needs no 0")
402
+ if (b.z0 > 0.01 || zid || live.has(id)) {
403
+ const zAct: ElevAct = zid ? { k: 'fdim', id: zid } : { k: 'gap', a: cornerAnchor(f.leftEnd), b: l, kind: 'v', title: 'Cabinet off the floor' };
404
+ tag(S(T, at, 0), S(T, at, b.z0), off, 0, fmtLen(val(zAct, b.z0)), zid ? faceLook(face.cons[zid], zid) : 'free', zAct, true);
405
+ }
406
+ // its depth, on its door near the bottom: a chip like the others (grey until it's entered, and then only while
407
+ // the cabinet is selected or hovered), on the cabinet itself
408
+ if (cb.depthSet || live.has(id) || on({ k: 'depth', cab: id })) {
409
+ const dAct: ElevAct = { k: 'depth', cab: id }, look: Look = cb.depthSet ? 'drive' : 'free', st = LOOK(look);
410
+ const [cx] = S(T, (b.u0 + b.u1) / 2, 0), [, by] = S(T, 0, b.z0), [, ty] = S(T, 0, b.z1);
411
+ if (by - ty > 40) {
412
+ const y = by - Math.min(16, (by - ty) * 0.15), text = 'D ' + fmtLen(cb.depth);
413
+ const w = chip(c, text, cx, y, { bg: st.bg, fg: st.fg, font: '600 11.5px', h: 20, pad: 6, border: on(dAct) ? [TH.yellow, 2] : null });
414
+ hits.push({ x: cx - w / 2 - 4, y: y - 14, w: w + 8, h: 28, act: dAct, look });
415
+ }
416
+ }
417
+ const top = r === cb.br ? cb.tr : cb.tl;
418
+ const hAct: ElevAct = cb.h && face.cons[cb.h] ? { k: 'fdim', id: cb.h } : { k: 'gap', a: r, b: top, kind: 'v', title: 'Cabinet height' };
419
+ tag(S(T, at, b.z0), S(T, at, b.z1), off, 0, fmtLen(val(hAct, b.z1 - b.z0)), hAct.k === 'fdim' ? faceLook(face.cons[hAct.id], hAct.id) : 'free', hAct);
420
+ }
421
+ // face dimensions entered from a chip that isn't drawn now (what they measure has moved): across in rows of their own, up beside their point
422
+ const lostRows: [number, number][][] = [];
423
+ for (const [id, k] of Object.entries(face.cons)) {
424
+ if (!k.elev || k.anchor || k.type !== 'dist' || shown.has(id) || !k.a || !k.b || !face.points[k.a] || !face.points[k.b]) continue;
425
+ if (Object.values(face.cabs ?? {}).some(cb => cb.w === id || cb.h === id)) continue;
426
+ const A = face.points[k.a], B = face.points[k.b], act: ElevAct = { k: 'fdim', id }, text = fmtLen(k.driving === false ? measure(face, k) : k.value!);
427
+ if (k.kind === 'v') { tag(S(T, B.x, A.y), S(T, B.x, B.y), -24, 0, text, faceLook(k, id), act, true); continue; }
428
+ const u0 = Math.min(A.x, B.x), u1 = Math.max(A.x, B.x);
429
+ let row = 0;
430
+ while ((lostRows[row] || []).some(([x, y]) => u0 < y + 6 && u1 > x - 6)) row++;
431
+ (lostRows[row] = lostRows[row] || []).push([u0, u1]);
432
+ tag(S(T, A.x, 0), S(T, B.x, 0), 0, ROW0 + ROW * (next + row), text, faceLook(k, id), act);
433
+ }
434
+ next += lostRows.length;
435
+ // the wall's length comes from the floor plan: linked (outlined blue) when a dimension locks it there, grey when free
436
+ const planLocked = Object.values(sk.cons).some(cn => cn.type === 'dist' && cn.line === f.lid && cn.driving !== false && cn.layer !== 'print');
437
+ const anyRow = rows.some(r => r.length) || next > 1;
438
+ tag(S(T, f.face[0], 0), S(T, f.face[1], 0), 0, anyRow ? ROW0 + ROW * next : ROW0, fmtLen(f.face[1] - f.face[0]), planLocked ? 'linked' : 'free', { k: 'len' });
439
+ // the wall's height: blue once entered, grey while it's the default
440
+ tag(S(T, f.face[1], 0), S(T, f.face[1], f.H), 34, 0, fmtLen(f.H), sk.lines[f.lid].height != null ? 'drive' : 'free', { k: 'wallH' });
441
+ return hits;
238
442
  }
239
443
 
240
444
  export interface ExtraDim { u0: number; u1: number; d: OpeningDim; act: PosAct; row: number }
241
445
 
242
- /** Saved door/window dimensions on this wall that the string doesn't show, packed into rows (u intervals don't overlap). */
243
- export function extraDims(sk: Sketch, f: ElevFrame): ExtraDim[] {
244
- const shown = new Set(chain(sk, f).segs.map(g => g.dim?.id).filter(Boolean));
446
+ /** Saved door/window dimensions on this wall that the strings (`rows`) don't show, packed into rows. */
447
+ export function extraDims(sk: Sketch, f: ElevFrame, strs?: Item[][]): ExtraDim[] {
448
+ const shown = new Set<string>();
449
+ for (const row of strs ?? strings(sk, f, emptyFace())) for (const g of stringSegs(sk, f, emptyFace(), row.filter(it => it.op))) {
450
+ if (g.act.k !== 'pos') continue;
451
+ const a = g.act, d = (sk.openings[a.op].dims || []).find(x => x.edge === a.edge && sameRef(x.ref, a.ref));
452
+ if (d) shown.add(d.id);
453
+ }
245
454
  const out: ExtraDim[] = [], rows: [number, number][][] = [];
246
455
  for (const o of elevOpenings(sk, f)) {
247
456
  const op = sk.openings[o.id], g = openingSpan(sk, op);
@@ -251,20 +460,17 @@ export function extraDims(sk: Sketch, f: ElevFrame): ExtraDim[] {
251
460
  const r = refS(sk, op, d.ref);
252
461
  if (r == null) continue;
253
462
  const a = toU(f, r), b = toU(f, edgeS(op, g.w, d.edge)), u0 = Math.min(a, b), u1 = Math.max(a, b);
254
- let row = 0;
255
- while ((rows[row] || []).some(([x, y]) => u0 < y + 6 && u1 > x - 6)) row++;
256
- (rows[row] = rows[row] || []).push([u0, u1]);
463
+ let k = 0;
464
+ while ((rows[k] || []).some(([x, y]) => u0 < y + 6 && u1 > x - 6)) k++;
465
+ (rows[k] = rows[k] || []).push([u0, u1]);
257
466
  const title = 'op' in d.ref ? 'Space between' : 'face' in d.ref ? 'Distance from the corner' : 'Distance from the wall end';
258
- out.push({ u0, u1, d, row, act: { k: 'pos', op: o.id, edge: d.edge, ref: d.ref, title } });
467
+ out.push({ u0, u1, d, row: k, act: { k: 'pos', op: o.id, edge: d.edge, ref: d.ref, title } });
259
468
  }
260
469
  }
261
470
  return out;
262
471
  }
263
472
 
264
- /** Extra pixels below the floor that the extra dimension rows take (for fitting the view). */
265
- export const extraRowsPx = (sk: Sketch, f: ElevFrame): number => ROW * extraDims(sk, f).reduce((m, x) => Math.max(m, x.row + 1), 0);
266
-
267
- /** A chip for previews (line length while drawing). */
473
+ /** A chip for previews (a ghost's size, a height while placing). */
268
474
  export const previewChip = (c: C2D, text: string, x: number, y: number) => chip(c, text, x, y, { bg: TH.chip, fg: TH.ink, font: '600 11.5px', h: 20, pad: 6, border: [TH.purple, 1.5] });
269
475
 
270
476
  /** A small plan of the page in the corner: the wall in yellow and an arrow showing where you're looking from. */
@@ -0,0 +1,22 @@
1
+ /*
2
+ Icons drawn in a Figma frame, kept exactly as Figma exports them (their own size, viewBox and colours) rather than
3
+ redrawn on the rail's 24 grid: the Pages panel's Delete and + Page (Figma 2548:101989).
4
+ */
5
+
6
+ /** Delete page: a trash can, white, for the red pill. */
7
+ export const TRASH_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="22.7032" height="25.25" viewBox="0 0 22.7032 25.25" fill="none" xmlns="http://www.w3.org/2000/svg">
8
+ <g id="Group 1707480533">
9
+ <path id="Vector" d="M8.79826 11.3535V19.0063" stroke="white" stroke-width="2.29583" stroke-linecap="round" stroke-linejoin="round"/>
10
+ <path id="Vector_2" d="M13.9033 11.3535V19.0063" stroke="white" stroke-width="2.29583" stroke-linecap="round" stroke-linejoin="round"/>
11
+ <path id="Vector_3" d="M1.14792 6.24557H21.5553" stroke="white" stroke-width="2.29583" stroke-linecap="round" stroke-linejoin="round"/>
12
+ <path id="Vector_4" d="M3.69674 6.24557H11.3495H19.0023V20.2757C19.0023 22.389 17.2892 24.1021 15.1759 24.1021H7.52313C5.40988 24.1021 3.69674 22.389 3.69674 20.2757V6.24557Z" stroke="white" stroke-width="2.29583" stroke-linecap="round" stroke-linejoin="round"/>
13
+ <path id="Vector_5" d="M7.52578 3.69884C7.52578 2.29 8.66787 1.14792 10.0767 1.14792H12.6276C14.0365 1.14792 15.1786 2.29 15.1786 3.69884V6.24977H7.52578V3.69884Z" stroke="white" stroke-width="2.29583" stroke-linecap="round" stroke-linejoin="round"/>
14
+ </g>
15
+ </svg>`;
16
+ export const TRASH_SIZE = { width: 22.7032, height: 25.25 };
17
+
18
+ /** + Page: a plus, dark grey, for the yellow pill. */
19
+ export const PLUS_SVG = `<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="18.5" height="18.5" viewBox="0 0 18.5 18.5" fill="none" xmlns="http://www.w3.org/2000/svg">
20
+ <path id="Vector" d="M9.25 0.125C9.58152 0.125 9.89937 0.256791 10.1338 0.491211C10.3682 0.725631 10.5 1.04348 10.5 1.375V8H17.125C17.4565 8 17.7744 8.13179 18.0088 8.36621C18.2432 8.60063 18.375 8.91848 18.375 9.25C18.375 9.58152 18.2432 9.89937 18.0088 10.1338C17.7744 10.3682 17.4565 10.5 17.125 10.5H10.5V17.125C10.5 17.4565 10.3682 17.7744 10.1338 18.0088C9.89937 18.2432 9.58152 18.375 9.25 18.375C8.91848 18.375 8.60063 18.2432 8.36621 18.0088C8.13179 17.7744 8 17.4565 8 17.125V10.5H1.375C1.04348 10.5 0.725631 10.3682 0.491211 10.1338C0.256791 9.89937 0.125 9.58152 0.125 9.25C0.125 8.91848 0.256791 8.60063 0.491211 8.36621C0.725631 8.13179 1.04348 8 1.375 8H8V1.375C8 1.04348 8.13179 0.725631 8.36621 0.491211C8.60063 0.256791 8.91848 0.125 9.25 0.125Z" fill="#181A1C" stroke="#181A1C" stroke-width="0.25"/>
21
+ </svg>`;
22
+ export const PLUS_SIZE = { width: 18.5, height: 18.5 };