@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,475 +1,427 @@
1
- import { afterEdit, redo, resolveOpeningsNow, undo } from '../app/edit';
1
+ import { afterEdit, analyze, resolveOpeningsNow, undo } from '../app/edit';
2
2
  import { ed } from '../app/state';
3
- import { addElevLine, elevFrame, elevKey, elevLines, elevLineUZ, elevOpenings, openingHeights, roomSideOf, toU, wallHeight, WINDOW_H } from '../model/elevation';
3
+ import { DOOR_H, elevFrame, elevOpenings, openingHeights, roomSideOf, toU, wallHeight, WINDOW_H } from '../model/elevation';
4
+ import { faceEdit, faceKey, faceKeyParts } from '../model/faces';
5
+ import { wallFace } from '../model/wallfaces';
6
+ import { faceOverflow, forgetSynced, openFace, placedMarks, planOverflow, pushedOut, setPlacedMarks, syncFace, syncFaces } from '../model/facesolve';
4
7
  import { clamp } from '../model/geom';
5
8
  import { pushHistory } from '../model/history';
6
9
  import { uid } from '../model/ids';
7
- import { applySlide, openingSpan, pickBetween, sameRef, slideGroup } from '../model/openings';
8
- import { onSessionReset, sessionState } from '../model/session';
10
+ import { applySlide, edgeS, openingSpan, pickBetween, resolveOpenings, sameRef, slideGroup, STANDARD_OPENING_W } from '../model/openings';
11
+ import { sessionState } from '../model/session';
9
12
  import { settings } from '../model/settings';
10
- import { deleteSel, lineLen } from '../model/sketch';
11
- import { SK } from '../model/store';
12
- import { elevT } from '../render/elevation';
13
- import { requestDraw, S } from '../render/view';
13
+ import { deleteSel, gc, lineLen, restorePos, snapshotPos } from '../model/sketch';
14
+ import { planSK, SK } from '../model/store';
15
+ import { cabBox, previewChip } from '../render/elevation';
16
+ import { TH } from '../render/theme';
17
+ import { fitView, mainT, requestDraw, requestOverlayDraw, S, setWallBox } from '../render/view';
14
18
  import { editElevationPosition, editOpeningHeight, editSill, editWallHeight } from '../tools/elevation';
15
19
  import { editOpeningDim } from '../tools/openings';
20
+ import { cancelTool } from '../tools/registry';
16
21
  import { fmtLen } from '../units';
17
- import { openKeypad, renderElevation, setCursor, toast, updateCtx } from './ports';
22
+ import { addCabinet, addHeightLine, CAB, cabinetSpot, deleteCabinets, dimensionFace, editCabinet, editCabinetDepth, snapObjectMove } from './face';
23
+ import { openKeypad, refreshModes, renderElevation, toast, updateCtx, updateHint, updateRail, updateTopBar } from './ports';
18
24
  /*
19
- The wall elevation: a full-screen view of one straight wall, straight on, whatever draws it.
20
- It shows the live drawing (nothing is copied), so the plan and the elevation can't disagree. Drag a door or
21
- window to move it (unless a dimension locks it); tap a dimension to select it, then + changes it; draw lines and
22
- guides on the wall face; place doors and windows. This module holds the view's state, tools, snapping, selection,
23
- the bar's decisions, delete, and pointer routing; the interface draws it (render/elevationView.ts) and shows it.
25
+ The wall view: one straight wall seen straight on, edited with the plan's own tools. Opening a wall makes the
26
+ editor work on that wall face's sketch (model/faces.ts: SK() is the face) — its grid, lines, circles and arcs,
27
+ constraints, dimensions, selection and dragging are the plan's — with the plan's corners, floor, ceiling, doors and
28
+ windows as fixed anchors to snap and constrain to. On top of that, the wall view's own: doors and windows placed
29
+ and dragged on the wall (they're the plan's), cabinets, height lines, and the dimension chips doors, windows and
30
+ cabinets get (render/elevation.ts), each tapped to select and changed with the yellow +.
24
31
  */
25
- /** The open wall view's state (the interface reads it to draw). */
32
+ /** The open wall: which wall, from which side, and its frame (from the live plan). */
26
33
  export const elev = sessionState(() => ({
27
34
  lid: null,
28
35
  flip: false,
29
- view: null,
30
36
  frame: null,
31
- W: 0, H: 0,
32
- hits: [],
33
- /** the selected dimension (ringed in yellow; the bar's + opens its keypad) */
34
- active: null,
35
- /** the opening being dragged (outlined in yellow) */
36
- dragOp: null,
37
- /** selected openings, wall corners and drawn lines (tap; Shift/⌘-tap adds) — the bar's + dimensions them */
38
- objs: [],
39
- /** the active tool (the toolbar on the left) */
40
- tool: 'select',
41
- /** line / guide tool: the first point, [u, z], once placed */
42
- lineStart: null,
43
- /** where the pointer is (snapped), for previews while a drawing tool is active */
44
- cursor: null,
45
37
  }));
46
- const objKey = (o) => (o.k === 'corner' ? 'corner:' + o.end : o.k + ':' + o.id);
47
38
  /** Size of a new door/window placed in the wall view (as the plan's tool), inches. */
48
- export const NEW_W = 36;
39
+ export const NEW_W = STANDARD_OPENING_W;
49
40
  export const elevOpen = () => elev.lid != null;
50
- /** Tappable dimension chips of the open elevation (canvas pixels), for tests. */
51
- export const elevHits = () => elev.hits;
52
- /** Open the elevation of a straight wall, seen from the room side. */
53
- export function openElevation(lid) {
54
- const sk = SK();
55
- if (!elevFrame(sk, lid)) {
56
- toast('Elevations work on straight walls. Turn the line into a wall first.');
41
+ /** The open wall, for the wall view to fit: along it from corner to corner, and from the floor to its height. */
42
+ const wallBox = () => (elev.frame ? { x0: elev.frame.face[0], y0: 0, x1: elev.frame.face[1], y1: elev.frame.H } : null);
43
+ /** Open the wall view of a straight wall, seen from the room side. */
44
+ /** Open a wall's view: from the side a room is on — or `side`, the face to the left (+1) or right (−1) of its A → B. */
45
+ export function openElevation(lid, side) {
46
+ if (!elevFrame(planSK(), lid)) {
47
+ toast('Elevations work on straight walls. Turn the line into a wall first.', { warn: true });
57
48
  return;
58
49
  }
50
+ cancelTool();
59
51
  elev.lid = lid;
60
- elev.flip = false;
61
- elev.view = null;
62
- elev.active = null;
63
- elev.objs = [];
64
- elev.lineStart = null;
65
- elev.cursor = null;
66
- setElevTool('select', true); // renders: the interface shows the sheet first, so the view fits its real size
67
- }
68
- /** Back to the plan (the interface hides the sheet). */
52
+ elev.flip = side != null && side !== (roomSideOf(planSK(), lid) || 1);
53
+ enterFace();
54
+ }
55
+ /** Start editing the open wall's face (from the side `elev.flip` says). */
56
+ function enterFace() {
57
+ const f = elevFrame(planSK(), elev.lid, elev.flip);
58
+ elev.frame = f;
59
+ // (handed over here, not as this module loads: the view module is still loading then)
60
+ setWallBox(wallBox);
61
+ faceEdit.key = faceKey(f.lid, f.side);
62
+ openFace(planSK(), f.lid, f.side);
63
+ ed.mode = 'draft';
64
+ ed.tool = 'select';
65
+ ed.sel = [];
66
+ ed.hover = null;
67
+ ed.snapState = null;
68
+ ed.preview = null;
69
+ ed.boxSel = null;
70
+ analyze();
71
+ fitView();
72
+ refreshModes();
73
+ updateRail();
74
+ updateCtx();
75
+ updateHint();
76
+ updateTopBar();
77
+ renderElevation();
78
+ requestDraw();
79
+ }
80
+ /** Stop editing the face (a face with nothing on it isn't kept). */
81
+ function leaveFace() {
82
+ cancelTool();
83
+ faceEdit.key = null;
84
+ gc(planSK());
85
+ }
86
+ /** Open the wall a cabinet is on, from its side, with the cabinet selected (a tap on its footprint in the plan). */
87
+ export function openCabinet(key, cabId) {
88
+ const { lid, side } = faceKeyParts(key);
89
+ openElevation(lid);
90
+ if (!elevOpen())
91
+ return;
92
+ if (elev.frame && elev.frame.side !== side)
93
+ flipElevation();
94
+ if (SK().cabs?.[cabId]) {
95
+ ed.sel = [{ type: 'cab', id: cabId }];
96
+ updateCtx();
97
+ updateHint();
98
+ requestDraw();
99
+ }
100
+ toast('This cabinet’s wall — Back returns to the plan.');
101
+ }
102
+ /** Back to the plan, with the wall selected. */
69
103
  export function closeElevation() {
70
104
  if (!elevOpen())
71
105
  return;
106
+ const lid = elev.lid;
107
+ leaveFace();
72
108
  elev.lid = null;
73
109
  elev.frame = null;
74
- elev.active = null;
75
- elev.objs = [];
76
- elev.lineStart = null;
77
- elev.cursor = null;
110
+ ed.tool = 'select';
111
+ ed.hover = null;
112
+ ed.preview = null;
113
+ ed.sel = planSK().lines[lid] ? [{ type: 'line', id: lid }] : [];
114
+ analyze();
115
+ refreshModes();
116
+ updateRail();
117
+ updateCtx();
118
+ updateHint();
119
+ updateTopBar();
78
120
  renderElevation();
79
121
  requestDraw();
80
122
  }
123
+ /** Look at the wall from its other side (each side has its own drawing). */
81
124
  export function flipElevation() {
82
125
  if (!elevOpen())
83
126
  return;
127
+ leaveFace();
84
128
  elev.flip = !elev.flip;
85
- elev.view = null;
86
- elev.active = null;
87
- elev.objs = [];
88
- elev.lineStart = null;
89
- renderElevation();
129
+ enterFace();
90
130
  }
91
131
  /** Fit the wall to the view again. */
92
- export function fitElevation() { elev.view = null; renderElevation(); }
93
- /** The open wall's frame, from the live drawing. Closes the view if the wall is gone. */
132
+ export function fitElevation() { fitView(); }
133
+ /** The open wall's frame, from the live plan. Closes the view if the wall is gone (or isn't a straight wall now). */
94
134
  export function elevSyncFrame() {
95
135
  if (!elevOpen())
96
136
  return null;
97
- const f = elevFrame(SK(), elev.lid, elev.flip);
137
+ const f = elevFrame(planSK(), elev.lid, elev.flip);
98
138
  if (!f) {
99
139
  closeElevation();
100
140
  return null;
101
141
  }
102
142
  elev.frame = f;
143
+ // a room appeared or went on one side: the side "inside" is the other face now
144
+ const key = faceKey(f.lid, f.side);
145
+ if (faceEdit.key !== key) {
146
+ faceEdit.key = key;
147
+ openFace(planSK(), f.lid, f.side);
148
+ ed.sel = [];
149
+ analyze();
150
+ }
103
151
  return f;
104
152
  }
105
- /**
106
- * After drawing: keep the tappable chips, drop a selected dimension that no longer shows and selected things that
107
- * are gone. Returns the selected dimension's chip, if any.
108
- */
109
- export function elevAfterRender(hits) {
110
- elev.hits = hits;
111
- let on = elev.active ? hits.find(h => JSON.stringify(h.act) === elev.active) : undefined;
112
- if (elev.active && !on) {
113
- elev.active = null;
114
- on = undefined;
115
- }
116
- const sk = SK(), lines = elev.frame ? elevLines(sk, elev.frame) : [];
117
- elev.objs = elev.objs.filter(o => o.k === 'corner' || (o.k === 'op' ? !!sk.openings[o.id] : lines.some(l => l.id === o.id)));
118
- return on;
119
- }
120
- /** Whether the trash has something to do: free or remove the selected dimension, or delete selected things. */
121
- export const elevTrashEnabled = (on) => on ? !!storedDim(on.act) || !!lockedSill(on.act) : elev.objs.some(o => o.k !== 'corner');
122
153
  /** The view's title: the wall's length, and which side it's seen from. */
123
154
  export function elevTitle() {
124
- const sk = SK(), f = elev.frame, hasRoom = roomSideOf(sk, elev.lid) !== 0;
155
+ const sk = planSK(), f = elev.frame, hasRoom = roomSideOf(sk, elev.lid) !== 0;
125
156
  return {
126
157
  title: 'Wall ' + fmtLen(lineLen(sk, sk.lines[elev.lid])),
127
158
  sub: hasRoom ? (f.inside ? 'Seen from inside' : 'Seen from outside') : 'Side ' + (elev.flip ? 2 : 1) + ' of 2',
128
159
  };
129
160
  }
130
- const hitAt = (x, y) => [...elev.hits].reverse().find(h => x >= h.x && x <= h.x + h.w && y >= h.y && y <= h.y + h.h);
131
- const nameOf = (op) => (SK().openings[op].type === 'door' ? 'door' : 'window');
132
- /** What dragging an opening will do, given its locks. */
133
- function dragHint(op) {
134
- const sk = SK(), o = sk.openings[op], locked = !slideGroup(sk, op), n = nameOf(op);
135
- if (o.type === 'door')
136
- return locked ? 'This door is locked by a dimension. Change the dimension, or make it a reference, to move it.' : 'Drag the door to slide it along the floor.';
137
- if (locked && o.sillLocked)
138
- return 'This window is locked along the wall and in height. Select a blue dimension and press Delete to free it.';
139
- if (o.sillLocked)
140
- return 'This window’s height above the floor is locked — it only slides along the wall. Delete the blue sill dimension to free it.';
141
- return locked ? 'This window is locked along the wall by a dimension — drag it to raise or lower it.' : 'Drag the ' + n + ' to move it along the wall or up and down.';
161
+ // ---- keeping what's drawn inside the wall ----
162
+ /** The open wall face's edges: across from corner to corner, up from the floor to the ceiling. Null outside the wall view. */
163
+ export function wallBounds() {
164
+ const f = elevOpen() ? elev.frame : null;
165
+ return f ? { x0: f.face[0], x1: f.face[1], y0: 0, y1: f.H } : null;
142
166
  }
143
- function startDrag(op, cx, cy) {
144
- const sk = SK(), o = sk.openings[op];
145
- if (!o)
146
- return null;
147
- const group = slideGroup(sk, op);
148
- return { op, group, sill: openingHeights(o).sill, lockedH: !group, started: false, cx, cy };
167
+ /** A point brought inside the wall (as it is, outside the wall view). */
168
+ export function intoWall(x, y) {
169
+ const b = wallBounds();
170
+ return b ? [Math.min(Math.max(x, b.x0), b.x1), Math.min(Math.max(y, b.y0), b.y1)] : [x, y];
149
171
  }
150
- function moveDrag(d, du, dz) {
151
- const sk = SK(), o = sk.openings[d.op], f = elev.frame, g = o && openingSpan(sk, o), l = o && sk.lines[o.line];
152
- if (!o || !f || !g || !l)
153
- return;
154
- if (!d.started) {
155
- const stuck = d.lockedH && (o.type === 'door' || !!o.sillLocked);
156
- if (stuck) {
157
- toast(dragHint(d.op));
158
- elev.dragOp = null;
159
- return;
160
- }
161
- if (d.lockedH || o.sillLocked)
162
- toast(dragHint(d.op));
163
- d.started = true;
164
- pushHistory();
165
- elev.dragOp = d.op;
166
- elev.active = null;
167
- if (!elev.objs.some(x => x.k === 'op' && x.id === d.op))
168
- elev.objs = [{ k: 'op', id: d.op }];
169
- }
170
- if (d.group)
171
- applySlide(sk, d.group, f.mirror ? -du : du);
172
- if (o.type === 'window' && !o.sillLocked) {
173
- const { sill, head } = openingHeights(o), q = 1 / settings.denom;
174
- o.sill = clamp(Math.round((d.sill + dz) / q) * q, 0, Math.max(0, wallHeight(l) - (head - sill)));
175
- }
176
- resolveOpeningsNow();
177
- requestDraw();
178
- renderElevation();
179
- }
180
- // ---- drawing tools ----
181
- /** The wall view's toolbar: [tool, label, shortcut, icon]. */
182
- export const ELEV_RAIL = [
183
- ['select', 'Select', 'V', 'select'], ['door', 'Door', 'B', 'door'], ['window', 'Window', 'N', 'window'],
184
- ['line', 'Line', 'L', 'line'], ['guide', 'Guide line', 'X', 'constr'], '|',
185
- ];
186
- export const TOOL_HINT = {
187
- select: '',
188
- door: 'Tap the wall to place a 3\'-0" door. It sits on the floor where you tap.',
189
- window: 'Tap the wall to place a 3\'-0" window, centered where you tap.',
190
- line: 'Tap two points, or drag, to draw a line. It snaps to corners, openings, the floor and ceiling, and level/plumb.',
191
- guide: 'Tap two points, or drag, to draw a dashed guide line.',
192
- };
193
- /** Switch the wall view's tool. */
194
- export function setElevTool(t, quiet = false) {
195
- elev.tool = t;
196
- elev.lineStart = null;
197
- elev.cursor = null;
198
- if (t !== 'select') {
199
- elev.active = null;
200
- }
201
- if (!quiet && TOOL_HINT[t])
202
- toast(TOOL_HINT[t]);
203
- renderElevation();
204
- }
205
- /** Esc in the wall view: cancel a line in progress, then leave the tool, then close the view. */
206
- export function elevEscape() {
207
- if (elev.lineStart) {
208
- elev.lineStart = null;
209
- renderElevation();
210
- return;
211
- }
212
- if (elev.tool !== 'select') {
213
- setElevTool('select', true);
214
- return;
215
- }
216
- if (elev.objs.length || elev.active) {
217
- elev.objs = [];
218
- elev.active = null;
219
- renderElevation();
220
- return;
221
- }
222
- closeElevation();
223
- }
224
- /** Keyboard shortcuts for the wall view's tools. Returns true if the key was one. */
225
- export function elevToolKey(k) {
226
- const t = { v: 'select', b: 'door', n: 'window', l: 'line', x: 'guide' }[k.toLowerCase()];
227
- if (!t)
228
- return false;
229
- setElevTool(t);
230
- return true;
231
- }
232
- const toWorld = (x, y) => { const T = elevT(elev.view, elev.W, elev.H); return [(x - T.ox) / T.s, (T.oy - y) / T.s]; };
233
172
  /**
234
- * Snap a screen point for drawing: to a feature point (wall corners, opening corners, line ends and middles),
235
- * else each axis to a nearby feature line (floor, ceiling, wall ends, opening edges and sill/head heights, line ends),
236
- * else level/plumb from the line's start, else to the display precision.
173
+ * How far what's drawn on the face reaches past the wall, inches (0: it's all inside): each point, and each circle's
174
+ * and arc's reach. Edits that make this grow are refused, so nothing is pushed out (a wall made shorter in the plan can
175
+ * leave something outside: it can still be edited, just not pushed further out).
237
176
  */
238
- function snapUZ(x, y, from) {
239
- const sk = SK(), f = elev.frame, T = elevT(elev.view, elev.W, elev.H), R = 12 / T.s;
240
- const [u, z] = toWorld(x, y);
241
- const ops = elevOpenings(sk, f), lines = elevLines(sk, f).map(l => elevLineUZ(f, l));
242
- const pts = [[f.face[0], 0], [f.face[0], f.H], [f.face[1], 0], [f.face[1], f.H]];
243
- for (const o of ops)
244
- pts.push([o.u0, o.sill], [o.u0, o.head], [o.u1, o.sill], [o.u1, o.head]);
245
- for (const [a, b] of lines)
246
- pts.push(a, b, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]);
247
- let best = null, bd = R;
248
- for (const p of pts) {
249
- const d = Math.hypot(p[0] - u, p[1] - z);
250
- if (d < bd) {
251
- bd = d;
252
- best = p;
253
- }
254
- }
255
- if (best)
256
- return { p: [best[0], best[1]], kind: 'pt' };
257
- const us = [f.face[0], f.face[1], ...ops.flatMap(o => [o.u0, o.u1]), ...lines.flatMap(([a, b]) => [a[0], b[0]])];
258
- const zs = [0, f.H, ...ops.flatMap(o => [o.sill, o.head]), ...lines.flatMap(([a, b]) => [a[1], b[1]])];
259
- const near = (v, list) => list.reduce((m, x) => (Math.abs(x - v) < R && (m == null || Math.abs(x - v) < Math.abs(m - v)) ? x : m), null);
260
- const q = 1 / settings.denom, su = near(u, us), sz = near(z, zs);
261
- let pu = su ?? Math.round(u / q) * q, pz = sz ?? Math.round(z / q) * q, kind = su != null || sz != null ? 'edge' : null;
262
- if (from) {
263
- const du = Math.abs(pu - from[0]), dz = Math.abs(pz - from[1]), tan = Math.tan((6 * Math.PI) / 180);
264
- if (sz == null && dz < du * tan) {
265
- pz = from[1];
266
- kind = 'hv';
267
- }
268
- else if (su == null && du < dz * tan) {
269
- pu = from[0];
270
- kind = 'hv';
271
- }
272
- }
273
- return { p: [pu, pz], kind };
274
- }
275
- /** Where a new door/window would go for a pointer at (u, z): centre s along the wall, and the window's sill. */
276
- export function openingAt(type, u, z) {
277
- const f = elev.frame, w = NEW_W;
278
- if (f.L < w + 2)
279
- return null;
280
- const at = clamp(toU(f, u), w / 2, f.L - w / 2);
281
- const q = 1 / settings.denom, sill = type === 'door' ? 0 : clamp(Math.round((z - WINDOW_H / 2) / q) * q, 0, Math.max(0, f.H - WINDOW_H));
282
- return { at, sill };
177
+ export function wallOverflow(sk = SK()) {
178
+ const b = wallBounds();
179
+ return b ? faceOverflow(sk, b) : {};
283
180
  }
284
- function placeOpening(type, u, z) {
285
- const sk = SK(), f = elev.frame, spot = openingAt(type, u, z);
286
- if (!spot) {
287
- toast('That wall is too short for a ' + fmtLen(NEW_W) + ' ' + type + '.', { warn: true });
181
+ /**
182
+ * How far things run past their walls, once an edit has been followed through: in the wall view, what's drawn on it;
183
+ * in the plan, the doors and windows (re-placed from their dimensions) and what's drawn on each wall face (brought up
184
+ * to date with the plan). Edits that make this grow are refused — a wall can't be made shorter than what's on it.
185
+ */
186
+ export function overflow(sk = SK()) {
187
+ if (elevOpen())
188
+ return wallOverflow(sk);
189
+ if (sk !== planSK())
190
+ return {};
191
+ resolveOpenings(sk);
192
+ syncFaces(sk, true);
193
+ return planOverflow(sk);
194
+ }
195
+ export function fitState(sk = SK()) {
196
+ if (elevOpen() || sk !== planSK())
197
+ return { pos: snapshotPos(sk) };
198
+ return planFitState(sk);
199
+ }
200
+ /** Everything following a change to the plan through can change (see planFit), whichever view is open. */
201
+ export function planFitState(sk = planSK()) {
202
+ const pos = snapshotPos(sk);
203
+ const ops = {}, faces = {};
204
+ Object.assign(ops, snapOpenings(sk));
205
+ // (copied by hand: React Native has no structuredClone)
206
+ for (const [key, face] of Object.entries(sk.faces ?? {}))
207
+ faces[key] = { pos: snapshotPos(face), wall: face.wall && { u: [...face.wall.u], ends: [[...face.wall.ends[0]], [...face.wall.ends[1]]] } };
208
+ return { pos, ops, faces, marks: placedMarks() };
209
+ }
210
+ /** Each door and window as it is — its place, sill, width, dimensions: whatever an edit to it may change. */
211
+ function snapOpenings(sk) {
212
+ const out = {};
213
+ for (const [id, o] of Object.entries(sk.openings))
214
+ out[id] = JSON.stringify(o);
215
+ return out;
216
+ }
217
+ /** Put them back as they were — in place: what holds on to a door or window (a drag, a keypad) keeps the same one. */
218
+ function putOpenings(sk, snap) {
219
+ for (const [id, v] of Object.entries(snap)) {
220
+ const o = sk.openings[id];
221
+ if (!o)
222
+ continue;
223
+ const was = JSON.parse(v);
224
+ for (const k of Object.keys(o))
225
+ if (!(k in was))
226
+ delete o[k];
227
+ Object.assign(o, was);
228
+ }
229
+ }
230
+ export function restoreFit(sk, s) {
231
+ restorePos(sk, s.pos);
232
+ if (!s.ops)
288
233
  return;
289
- }
290
- pushHistory();
291
- const id = uid('o');
292
- // a door swings toward the side you're looking from
293
- sk.openings[id] = { line: f.lid, at: spot.at, w: NEW_W, type, flip: f.side !== 1, ...(type === 'window' ? { sill: spot.sill } : {}) };
294
- elev.objs = [{ k: 'op', id }];
295
- afterEdit();
234
+ putOpenings(sk, s.ops);
235
+ for (const [key, v] of Object.entries(s.faces ?? {})) {
236
+ const face = sk.faces?.[key];
237
+ if (!face)
238
+ continue;
239
+ restorePos(face, v.pos);
240
+ if (v.wall)
241
+ face.wall = v.wall;
242
+ else
243
+ delete face.wall;
244
+ }
245
+ setPlacedMarks(s.marks);
246
+ forgetSynced();
247
+ }
248
+ /** Limit a move of points (from where they started) so they all stay inside the wall, circles centred on them whole. */
249
+ export function keepInWall(sk, ids, start, dx, dy) {
250
+ const b = wallBounds();
251
+ if (!b || !ids.length)
252
+ return [dx, dy];
253
+ let lo = -Infinity, hi = Infinity, dlo = -Infinity, dhi = Infinity;
254
+ ids.forEach((id, i) => {
255
+ const r = Math.max(0, ...Object.values(sk.circles).filter(c => c.c === id).map(c => c.r)), p = start[i];
256
+ lo = Math.max(lo, b.x0 - (p.x - r));
257
+ hi = Math.min(hi, b.x1 - (p.x + r));
258
+ dlo = Math.max(dlo, b.y0 - (p.y - r));
259
+ dhi = Math.min(dhi, b.y1 - (p.y + r));
260
+ });
261
+ return [lo <= hi ? Math.min(Math.max(dx, lo), hi) : 0, dlo <= dhi ? Math.min(Math.max(dy, dlo), dhi) : 0];
296
262
  }
297
- function finishLine(b) {
298
- const a = elev.lineStart, f = elev.frame;
299
- if (!a || !f)
300
- return;
301
- if (Math.hypot(b[0] - a[0], b[1] - a[1]) < 0.5)
302
- return; // too short: keep waiting for the second point
303
- pushHistory();
304
- const id = uid('el');
305
- addElevLine(SK(), f, a, b, elev.tool === 'guide', id);
306
- elev.lineStart = null;
307
- elev.objs = [{ k: 'line', id }];
308
- afterEdit();
263
+ export const PAST_WALL = 'That would run past the edge of the wall.';
264
+ /** A door or window moved or resized in a way that would shove what's tied to it (a cabinet) off its wall. */
265
+ export const PUSHES_OFF = 'That would push what’s tied to it past the edge of the wall.';
266
+ /**
267
+ * How far things run past their walls in the plan — doors and windows past their walls' ends, what's on each wall
268
+ * face past its corners, floor and ceiling — once a change to the plan has been followed through (the doors and
269
+ * windows re-placed from their dimensions, the faces brought up to date). Whichever view is open: a door or window
270
+ * changed from the wall view (its width, its distance from a corner) is the plan's.
271
+ */
272
+ export function planFit(sk = planSK()) {
273
+ resolveOpenings(sk);
274
+ syncFaces(sk, true);
275
+ return planOverflow(sk);
309
276
  }
310
- /** The drawn line nearest a screen point (within 9 px). */
311
- function lineAt(x, y) {
312
- const sk = SK(), f = elev.frame, T = elev.view && elevT(elev.view, elev.W, elev.H);
313
- if (!f || !T)
314
- return null;
315
- let best = null, bd = 9;
316
- for (const ln of elevLines(sk, f)) {
317
- const [a, b] = elevLineUZ(f, ln), p = S(T, a[0], a[1]), q = S(T, b[0], b[1]);
318
- const dx = q[0] - p[0], dy = q[1] - p[1], L2 = dx * dx + dy * dy || 1, t = clamp(((x - p[0]) * dx + (y - p[1]) * dy) / L2, 0, 1);
319
- const d = Math.hypot(x - (p[0] + dx * t), y - (p[1] + dy * t));
320
- if (d < bd) {
321
- bd = d;
322
- best = ln.id;
323
- }
324
- }
325
- return best;
326
- }
327
- /** What's under a screen point: chips first, then drawn lines, then openings and corners. */
328
- function pickAt(x, y) {
329
- const h = hitAt(x, y);
330
- if (h && h.act.k !== 'body' && h.act.k !== 'corner')
331
- return h.act;
332
- const l = lineAt(x, y);
333
- if (l)
334
- return { k: 'eline', id: l };
335
- return h ? h.act : null;
336
- }
337
- /** Set a drawn line's length (its first end stays put). */
338
- function editLineLength(id) {
339
- const sk = SK(), f = elev.frame, ln = f && elevLines(sk, f).find(l => l.id === id);
340
- if (!ln)
341
- return;
342
- const cur = Math.hypot(ln.b[0] - ln.a[0], ln.b[1] - ln.a[1]);
343
- openKeypad({
344
- title: ln.guide ? 'Guide line length' : 'Line length', initial: fmtLen(cur), selectAll: true, laser: true,
345
- onDone: v => {
346
- if (v <= 0)
347
- return;
348
- pushHistory();
349
- const k = v / (cur || 1);
350
- ln.b = [ln.a[0] + (ln.b[0] - ln.a[0]) * k, ln.a[1] + (ln.b[1] - ln.a[1]) * k];
351
- afterEdit();
352
- },
353
- });
277
+ /**
278
+ * Make a change to the plan's doors and windows — a width, a distance entered — only if it pushes nothing on a wall
279
+ * past its edges (a cabinet tied to a window would otherwise be shoved off the wall). Taken back, and said, if it
280
+ * would. Returns whether it was made.
281
+ */
282
+ export function withinWalls(change) {
283
+ const sk = planSK(), was = planFit(sk), held = planFitState(sk);
284
+ change();
285
+ if (!pushedOut(was, planFit(sk)))
286
+ return true;
287
+ restoreFit(sk, held);
288
+ planFit(sk);
289
+ toast(PUSHES_OFF, { warn: true });
290
+ return false;
354
291
  }
355
292
  /**
356
- * A tap. On a door/window or a wall corner: select it (`add`: add or remove it — Shift/⌘/Ctrl with a mouse, any tap by touch).
357
- * On a dimension chip: select that dimension. The keypad only opens from the bar's yellow +.
293
+ * Slide a chain of doors and windows along their wall in the plan to `delta` from where it started, no further than
294
+ * what's drawn on the wall's faces can follow without being pushed off them (`base`: what ran past its wall when the
295
+ * slide began; `from`: the last slide that pushed nothing further out). The faces follow as it goes. Returns the
296
+ * slide made.
358
297
  */
359
- function tap(a, add) {
360
- if (a && (a.k === 'body' || a.k === 'corner' || a.k === 'eline')) {
361
- const o = a.k === 'body' ? { k: 'op', id: a.op } : a.k === 'eline' ? { k: 'line', id: a.id } : { k: 'corner', end: a.end }, key = objKey(o);
362
- elev.active = null;
363
- if (add)
364
- elev.objs = elev.objs.some(x => objKey(x) === key) ? elev.objs.filter(x => objKey(x) !== key) : [...elev.objs, o];
298
+ export function slideWithinWalls(sk, group, delta, from, base) {
299
+ const held = planFitState(sk);
300
+ const tryAt = (v) => { applySlide(sk, group, v); return !pushedOut(base, planFit(sk)); };
301
+ if (tryAt(delta))
302
+ return delta;
303
+ let lo = 0, hi = 1;
304
+ for (let i = 0; i < 8; i++) {
305
+ const m = (lo + hi) / 2;
306
+ restoreFit(sk, held);
307
+ if (tryAt(from + (delta - from) * m))
308
+ lo = m;
365
309
  else
366
- elev.objs = [o];
367
- renderElevation();
368
- return;
369
- }
370
- elev.objs = [];
371
- selectDim(a);
372
- }
373
- /** Select an elevation dimension (null clears): yellow border and the bar with its name and the yellow +. */
374
- function selectDim(a) {
375
- elev.active = a ? JSON.stringify(a) : null;
376
- renderElevation();
310
+ hi = m;
311
+ }
312
+ // none of the way: back exactly as it was (solved again from there, the faces can land a hair apart — past the wall)
313
+ restoreFit(sk, held);
314
+ if (lo === 0)
315
+ return from;
316
+ const to = from + (delta - from) * lo;
317
+ tryAt(to);
318
+ return to;
319
+ }
320
+ /** In the plan: a wall made shorter than what's on it. */
321
+ export const TOO_SHORT = 'The wall can’t be shorter than what’s on it. Move or remove things on it first.';
322
+ /** The right message for an edit refused by overflow(). */
323
+ export const pastWallMsg = () => (elevOpen() ? PAST_WALL : TOO_SHORT);
324
+ // ---- the dimension chips ----
325
+ /**
326
+ * What can be tapped in the wall view, in canvas pixels, for tests: the doors' and windows' bodies, cabinets, the
327
+ * wall's corners (its end edges), and the dimension chips as last drawn (with their look).
328
+ */
329
+ export function elevHits() {
330
+ const f = elev.frame;
331
+ if (!elevOpen() || !f)
332
+ return [];
333
+ const T = mainT(), out = [];
334
+ for (const o of elevOpenings(planSK(), f)) {
335
+ const [x0, y0] = S(T, o.u0, o.head), [x1, y1] = S(T, o.u1, o.sill);
336
+ out.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0, act: { k: 'body', op: o.id } });
337
+ }
338
+ for (const id of Object.keys(SK().cabs ?? {})) {
339
+ const b = cabBox(SK(), id);
340
+ if (!b)
341
+ continue;
342
+ const [x0, y0] = S(T, b.u0, b.z1), [x1, y1] = S(T, b.u1, b.z0);
343
+ out.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0, act: { k: 'cab', id } });
344
+ }
345
+ for (const [u, end] of [[f.face[0], f.leftEnd], [f.face[1], f.leftEnd === 'a' ? 'b' : 'a']]) {
346
+ const [x, yTop] = S(T, u, f.H), [, yBot] = S(T, u, 0);
347
+ out.push({ x: x - 10, y: yTop, w: 20, h: yBot - yTop, act: { k: 'corner', end } });
348
+ }
349
+ for (const h of ed.hitChips)
350
+ if (h.kind === 'elev')
351
+ out.push({ x: h.x, y: h.y, w: h.w, h: h.h, act: JSON.parse(h.id), look: h.part });
352
+ return out;
353
+ }
354
+ export const elevAct = (id) => JSON.parse(id);
355
+ /** A chip's name, as the bar shows it. */
356
+ export function elevChipName(a) {
357
+ if (a.k === 'gap')
358
+ return a.title;
359
+ if (a.k === 'len')
360
+ return 'Wall length · set in the floor plan';
361
+ if (a.k === 'wallH')
362
+ return 'Wall height';
363
+ if (a.k === 'pos')
364
+ return a.title;
365
+ if (a.k === 'fdim')
366
+ return 'Dimension';
367
+ if (a.k === 'depth')
368
+ return 'Cabinet depth · out from the wall';
369
+ const o = planSK().openings[a.op], n = o ? (o.type === 'door' ? 'Door' : 'Window') : '';
370
+ return a.k === 'w' ? n + ' width' : a.k === 'sill' ? n + ' sill height' : n + ' height';
371
+ }
372
+ /** The yellow + on a selected chip: its keypad. */
373
+ export function editElevChip(a) {
374
+ if (a.k === 'w')
375
+ editOpeningDim(a.op, 'w');
376
+ else if (a.k === 'sill')
377
+ editSill(a.op);
378
+ else if (a.k === 'h')
379
+ editOpeningHeight(a.op);
380
+ else if (a.k === 'wallH')
381
+ editWallHeight(elev.lid);
382
+ else if (a.k === 'pos')
383
+ editElevationPosition(a.op, a.edge, a.ref, a.title);
384
+ else if (a.k === 'gap')
385
+ dimensionFace(a.kind, a.a, a.b, a.title);
386
+ else if (a.k === 'depth')
387
+ editCabinetDepth(a.cab);
388
+ else if (a.k === 'len')
389
+ goToPlan();
377
390
  }
378
- /** What the + does for the selected objects, with the bar's name for it (null: nothing to dimension). */
379
- function objPlan() {
380
- const sk = SK(), f = elev.frame, objs = elev.objs;
381
- if (!f || !objs.length)
382
- return null;
383
- const ops = elevOpenings(sk, f), seen = (id) => ops.find(o => o.id === id);
384
- const label = (id) => (sk.openings[id].type === 'door' ? 'Door' : 'Window');
385
- const opsSel = objs.filter((o) => o.k === 'op').map(o => seen(o.id)).filter((o) => !!o);
386
- const corners = objs.filter((o) => o.k === 'corner');
387
- const lines = objs.filter((o) => o.k === 'line');
388
- if (objs.length === 1 && lines.length === 1) {
389
- const ln = elevLines(sk, f).find(l => l.id === lines[0].id);
390
- if (!ln)
391
- return null;
392
- return { name: (ln.guide ? 'Guide line ' : 'Line ') + fmtLen(Math.hypot(ln.b[0] - ln.a[0], ln.b[1] - ln.a[1])), run: () => editLineLength(ln.id) };
393
- }
394
- if (lines.length)
395
- return { name: objs.length + ' selected', run: () => toast('Select one line to set its length.') };
396
- if (objs.length === 1 && opsSel.length === 1)
397
- return { name: label(opsSel[0].id), run: () => editOpeningDim(opsSel[0].id, 'w') };
398
- if (objs.length === 1 && corners.length === 1)
399
- return { name: 'Corner', run: () => toast('Select a door or window too (Shift-tap), then + for the distance between them.') };
400
- if (objs.length === 2 && opsSel.length === 2) {
401
- const [l, r] = [...opsSel].sort((p, q) => p.u0 - q.u0), t = pickBetween(sk, l.id, l.e1, r.id, r.e0);
402
- return { name: label(l.id) + ' + ' + label(r.id).toLowerCase(), run: () => editElevationPosition(t.op, t.edge, t.ref, 'Space between') };
403
- }
404
- if (objs.length === 2 && opsSel.length === 1 && corners.length === 1) {
405
- const o = opsSel[0], end = corners[0].end, leftCorner = end === f.leftEnd;
406
- const ref = { face: end, line: f.lid, side: f.side };
407
- return { name: label(o.id) + ' + corner', run: () => editElevationPosition(o.id, leftCorner ? o.e0 : o.e1, ref, 'Distance from the corner') };
408
- }
409
- if (corners.length === 2 && objs.length === 2)
410
- return { name: 'Both corners', run: () => toast('The wall’s length is set in the plan. Go back and use the wall’s +.') };
411
- return { name: objs.length + ' selected', run: () => toast('Select two things — two openings, or an opening and a corner — to dimension between them.') };
412
- }
413
- /** The bar: the selected dimension, or the selected objects (with Height / Sill for a single opening). */
414
- export function elevBar(dimAct) {
415
- const plan = dimAct ? null : objPlan();
416
- if (!dimAct && !plan)
417
- return null;
418
- if (dimAct && dimAct.k === 'len')
419
- return { label: 'Wall length · set in the floor plan', plus: false, actions: [{ icon: 'back', label: 'Go to plan', run: goToPlan }] };
420
- if (dimAct)
421
- return { label: dimName(dimAct), plus: true, actions: [] };
422
- const actions = [];
423
- const one = elev.objs.length === 1 && elev.objs[0].k === 'op' ? SK().openings[elev.objs[0].id] : null;
424
- if (one) {
425
- const id = elev.objs[0].id;
426
- actions.push({ icon: 'v', label: 'Height', run: () => editOpeningHeight(id) });
427
- if (one.type === 'window')
428
- actions.push({ icon: 'h', label: 'Sill', run: () => editSill(id) });
429
- }
430
- return { label: plan.name, plus: true, actions };
431
- }
432
- const dimName = (a) => {
433
- const sk = SK(), o = 'op' in a ? sk.openings[a.op] : null, n = o ? (o.type === 'door' ? 'Door' : 'Window') : '';
434
- return a.k === 'w' ? n + ' width' : a.k === 'sill' ? n + ' sill height' : a.k === 'h' ? n + ' height' : a.k === 'wallH' ? 'Wall height'
435
- : a.k === 'pos' ? a.title : '';
436
- };
437
- /** The window whose locked sill height a selected chip shows, if any. */
391
+ /** The window whose locked sill height a chip shows, if any. */
438
392
  function lockedSill(a) {
439
- if (!a || a.k !== 'sill')
393
+ if (a.k !== 'sill')
440
394
  return null;
441
- const o = SK().openings[a.op];
395
+ const o = planSK().openings[a.op];
442
396
  return o && o.sillLocked ? o : null;
443
397
  }
444
- /** The stored door/window dimension a selected position chip shows, if any. */
398
+ /** The stored door/window dimension a position chip shows, if any. */
445
399
  function storedDim(a) {
446
- if (!a || a.k !== 'pos')
400
+ if (a.k !== 'pos')
447
401
  return null;
448
- const o = SK().openings[a.op], d = o && (o.dims || []).find(x => x.edge === a.edge && sameRef(x.ref, a.ref));
402
+ const o = planSK().openings[a.op], d = o && (o.dims || []).find(x => x.edge === a.edge && sameRef(x.ref, a.ref));
449
403
  return d ? { op: a.op, d } : null;
450
404
  }
451
- /**
452
- * Delete (trash or the Delete key). Selected doors/windows are deleted (with Undo), along with dimensions that
453
- * measure from them. A selected dimension: a locked one is freed — it stays as a reference — and a reference is removed.
454
- */
455
- export function deleteSelectedElevDim() {
456
- const doomed = elev.objs.filter((o) => o.k === 'op').map(o => o.id);
457
- const doomedLines = elev.objs.filter((o) => o.k === 'line').map(o => o.id);
458
- if (!elev.active && (doomed.length || doomedLines.length)) {
459
- const sk = SK(), n = doomed.length + doomedLines.length, one = doomed.length === 1 && !doomedLines.length ? sk.openings[doomed[0]] : null;
405
+ /** Whether the trash does something for a selected chip: frees a locked one, or removes a reference. */
406
+ export const elevChipDeletable = (a) => !!storedDim(a) || !!lockedSill(a);
407
+ /** The trash on a selected chip: a locked door/window dimension is freed (it stays as a reference), a reference is removed, a locked sill is freed. */
408
+ export function deleteElevChip(a) {
409
+ // a cabinet's entered depth: back to the standard one (grey), as an entered size is wiped
410
+ if (a.k === 'depth') {
411
+ const cb = SK().cabs?.[a.cab];
412
+ if (!cb)
413
+ return;
414
+ if (!cb.depthSet) {
415
+ toast('That’s the standard depth. Tap + to enter one.', { warn: true });
416
+ return;
417
+ }
460
418
  pushHistory();
461
- deleteSel(sk, doomed.map(id => ({ type: 'opening', id })));
462
- const key = elev.frame && elevKey(elev.frame.lid, elev.frame.side);
463
- if (key && sk.elev?.[key])
464
- sk.elev[key] = sk.elev[key].filter(l => !doomedLines.includes(l.id));
465
- elev.objs = elev.objs.filter(o => o.k === 'corner');
466
- ed.sel = ed.sel.filter(x => !(x.type === 'opening' && doomed.includes(x.id)));
419
+ cb.depth = CAB.d;
420
+ cb.depthSet = false;
467
421
  afterEdit();
468
- const what = one ? (one.type === 'door' ? 'Door' : 'Window') : n === 1 ? 'Line' : n + ' items';
469
- toast(what + ' deleted', { action: { label: 'Undo', fn: () => undo() } });
422
+ toast('Depth cleared — back to the standard ' + fmtLen(CAB.d) + '.', { action: { label: 'Undo', fn: () => undo() } });
470
423
  return;
471
424
  }
472
- const a = elev.active ? JSON.parse(elev.active) : null, s = storedDim(a);
473
425
  const ls = lockedSill(a);
474
426
  if (ls) {
475
427
  pushHistory();
@@ -478,12 +430,12 @@ export function deleteSelectedElevDim() {
478
430
  toast('Sill height freed — drag the window to raise or lower it.');
479
431
  return;
480
432
  }
433
+ const s = storedDim(a);
481
434
  if (!s) {
482
- if (a)
483
- toast('Only locked dimensions and door/window position dimensions can be deleted here.');
435
+ toast('Only locked dimensions and door/window position dimensions can be deleted here.', { warn: true });
484
436
  return;
485
437
  }
486
- const o = SK().openings[s.op], name = o.type === 'door' ? 'door' : 'window';
438
+ const o = planSK().openings[s.op], name = o.type === 'door' ? 'door' : 'window';
487
439
  pushHistory();
488
440
  if (s.d.driving) {
489
441
  s.d.driving = false;
@@ -492,258 +444,502 @@ export function deleteSelectedElevDim() {
492
444
  }
493
445
  else {
494
446
  o.dims = (o.dims || []).filter(x => x !== s.d);
447
+ ed.sel = [];
495
448
  afterEdit();
496
449
  toast('Dimension removed');
497
450
  }
498
451
  }
499
452
  /** Back to the floor plan with this wall selected, ready for its + (where its length is set). */
500
453
  function goToPlan() {
501
- const lid = elev.lid;
502
454
  closeElevation();
503
- if (!lid)
504
- return;
505
- ed.sel = [{ type: 'line', id: lid }];
506
- updateCtx();
507
- requestDraw();
508
455
  toast('Tap + to change the wall’s length.');
509
456
  }
510
- /** The yellow +: the keypad for the selected dimension (or what the selected things measure). */
511
- export function editSelected() {
512
- const a = elev.active ? JSON.parse(elev.active) : null;
513
- if (!a) {
514
- objPlan()?.run();
457
+ // ---- doors and windows on the wall ----
458
+ /** Where a new door/window would go for a pointer at (u, z): centre s along the wall, and the window's sill. */
459
+ export function openingAt(type, u, z, tol = 0) {
460
+ const f = elev.frame, w = NEW_W;
461
+ if (f.L < w + 2)
462
+ return null;
463
+ // snapped (Object snapping) as a door or window dragged on the wall is: its sides to the corners and the other
464
+ // doors', windows' and cabinets' sides; a window's sill and head to the floor, the ceiling, cabinet tops and other
465
+ // windows' (a door sits on the floor)
466
+ const raw = type === 'door' ? 0 : z - WINDOW_H / 2, h = type === 'door' ? DOOR_H : WINDOW_H;
467
+ const [du, dz] = tol > 0 ? snapObjectMove({}, { u0: u - w / 2, u1: u + w / 2, z0: raw, z1: raw + h }, 0, 0, tol) : [0, 0];
468
+ const at = clamp(toU(f, u + du), w / 2, f.L - w / 2);
469
+ const q = 1 / settings.denom, sill = type === 'door' ? 0 : clamp(Math.round((raw + dz) / q) * q, 0, Math.max(0, f.H - WINDOW_H));
470
+ return { at, sill };
471
+ }
472
+ function placeOpening(type, u, z, tol = 0) {
473
+ const sk = planSK(), f = elev.frame, spot = openingAt(type, u, z, tol);
474
+ if (!spot) {
475
+ toast('That wall is too short for a ' + fmtLen(NEW_W) + ' ' + type + '.', { warn: true });
515
476
  return;
516
477
  }
517
- if (a.k === 'w')
518
- editOpeningDim(a.op, 'w');
519
- else if (a.k === 'sill')
520
- editSill(a.op);
521
- else if (a.k === 'h')
522
- editOpeningHeight(a.op);
523
- else if (a.k === 'wallH')
524
- editWallHeight(elev.lid);
525
- else if (a.k === 'pos')
526
- editElevationPosition(a.op, a.edge, a.ref, a.title);
527
- else if (a.k === 'len')
528
- goToPlan();
478
+ pushHistory();
479
+ const id = uid('o');
480
+ // a door swings toward the side you're looking from
481
+ sk.openings[id] = { line: f.lid, at: spot.at, w: NEW_W, wSet: false, type, flip: f.side !== 1, ...(type === 'window' ? { sill: spot.sill } : {}) };
482
+ // (placed, not selected: tap it to size it)
483
+ ed.sel = [];
484
+ afterEdit();
529
485
  }
530
- // ---- pointers: one finger or the mouse pans, two fingers pinch, the wheel zooms; tap selects; drag moves a door/window ----
531
- const gp = new Map();
532
- let pinch = null;
533
- let down = null, drag = null;
534
- /** middle mouse button: always pans; a press that started a line (drag to draw) */
535
- let panBtn = false, pressStartedLine = false, mid = null;
536
- onSessionReset(() => { gp.clear(); pinch = null; down = null; drag = null; panBtn = false; pressStartedLine = false; mid = null; });
537
- function zoomElevAt(sx, sy, f) {
538
- const v = elev.view;
539
- const u = v.cu + (sx - elev.W / 2) / v.s, z = v.cz - (sy - elev.H / 2) / v.s;
540
- v.s = clamp(v.s * f, 0.05, 60);
541
- v.cu = u - (sx - elev.W / 2) / v.s;
542
- v.cz = z + (sy - elev.H / 2) / v.s;
543
- renderElevation();
486
+ const nameOf = (op) => (planSK().openings[op].type === 'door' ? 'door' : 'window');
487
+ /** What dragging an opening will do, given its locks. */
488
+ function dragHint(op) {
489
+ const sk = planSK(), o = sk.openings[op], locked = !slideGroup(sk, op), n = nameOf(op);
490
+ if (o.type === 'door')
491
+ return locked ? 'This door is locked by a dimension. Change the dimension, or make it a reference, to move it.' : 'Drag the door to slide it along the floor.';
492
+ if (locked && o.sillLocked)
493
+ return 'This window is locked along the wall and in height. Select a blue dimension and press Delete to free it.';
494
+ if (o.sillLocked)
495
+ return 'This window’s height above the floor is locked — it only slides along the wall. Delete the blue sill dimension to free it.';
496
+ return locked ? 'This window is locked along the wall by a dimension — drag it to raise or lower it.' : 'Drag the ' + n + ' to move it along the wall or up and down.';
544
497
  }
545
- const drawing = () => elev.tool === 'line' || elev.tool === 'guide';
546
- const endDrag = () => { if (elev.dragOp) {
547
- elev.dragOp = null;
548
- afterEdit();
549
- } };
550
- /** Pointers on the elevation canvas, in canvas pixels; drags measure from `cx`/`cy` (window pixels) when given. */
551
- export function elevPointerDown(e) {
552
- const p = { x: e.x, y: e.y };
553
- gp.set(e.id, p);
554
- pinch = null;
555
- mid = null;
556
- down = gp.size === 1 ? { ...p, moved: false } : null;
557
- panBtn = e.button === 1;
558
- // a second finger while a press is drawing a line: it's a pinch, not a line
559
- if (gp.size > 1 && pressStartedLine) {
560
- elev.lineStart = null;
561
- elev.cursor = null;
562
- renderElevation();
563
- }
564
- pressStartedLine = false;
565
- drag = null;
566
- if (!down || panBtn || !elev.view)
567
- return;
568
- if (drawing()) {
569
- // first point now (drag to draw), or the second point of a tap-tap line
570
- const sp = snapUZ(p.x, p.y, elev.lineStart);
571
- elev.cursor = sp;
572
- if (!elev.lineStart) {
573
- elev.lineStart = sp.p;
574
- pressStartedLine = true;
575
- renderElevation();
576
- }
577
- else
578
- finishLine(sp.p);
579
- return;
580
- }
581
- if (elev.tool !== 'select')
582
- return; // door/window: placed on release (a drag pans instead)
583
- const h = hitAt(p.x, p.y);
584
- drag = h && h.act.k === 'body' ? startDrag(h.act.op, e.cx ?? e.x, e.cy ?? e.y) : null;
498
+ export function startOpeningDrag(op) {
499
+ const sk = planSK(), o = sk.openings[op];
500
+ if (!o)
501
+ return null;
502
+ const group = slideGroup(sk, op), e = elev.frame ? elevOpenings(sk, elev.frame).find(x => x.id === op) : null;
503
+ return { op, group, sill: openingHeights(o).sill, lockedH: !group, started: false, box: e ? { u0: e.u0, u1: e.u1, z0: e.sill, z1: e.head } : null };
585
504
  }
586
- export function elevPointerMove(e) {
587
- const prev = gp.get(e.id), p = { x: e.x, y: e.y };
588
- if (!prev) {
589
- // hover (mouse): cursor, and previews for the drawing tools
590
- if (e.kind !== 'mouse' || !elev.view)
591
- return;
592
- if (elev.tool === 'select') {
593
- const h = pickAt(p.x, p.y);
594
- setCursor(!h ? '' : h.k === 'body' ? 'grab' : 'pointer', 'elevation');
595
- return;
596
- }
597
- elev.cursor = snapUZ(p.x, p.y, elev.lineStart);
598
- renderElevation();
599
- return;
600
- }
601
- if (!elev.view)
602
- return;
603
- if (down && !down.moved) {
604
- if (Math.hypot(p.x - down.x, p.y - down.y) < 6) {
605
- if (drawing()) {
606
- elev.cursor = snapUZ(p.x, p.y, elev.lineStart);
607
- renderElevation();
608
- }
609
- return;
505
+ /**
506
+ * Move a drag to (du, dz) inches from where it started, snapping to corners and the other objects (within `tol`
507
+ * inches). Returns false if it can't move at all (a locked door).
508
+ */
509
+ export function moveOpeningDrag(d, du, dz, tol = 0) {
510
+ if (d.box && tol > 0)
511
+ [du, dz] = snapObjectMove({ op: d.op }, d.box, du, dz, tol);
512
+ const sk = planSK(), o = sk.openings[d.op], f = elev.frame, g = o && openingSpan(sk, o), l = o && sk.lines[o.line];
513
+ if (!o || !f || !g || !l)
514
+ return false;
515
+ if (!d.started) {
516
+ if (d.lockedH && (o.type === 'door' || !!o.sillLocked)) {
517
+ toast(dragHint(d.op), { warn: true });
518
+ return false;
610
519
  }
611
- down.moved = true;
612
- }
613
- gp.set(e.id, p);
614
- if (drawing() && gp.size === 1 && !panBtn) {
615
- elev.cursor = snapUZ(p.x, p.y, elev.lineStart);
616
- renderElevation();
617
- return;
520
+ if (d.lockedH || o.sillLocked)
521
+ toast(dragHint(d.op), { warn: true });
522
+ d.started = true;
618
523
  }
619
- if (gp.size === 2) {
620
- // two fingers: pinch to zoom and move to pan
621
- const [a, b] = [...gp.values()], m = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
622
- if (mid) {
623
- elev.view.cu -= (m.x - mid.x) / elev.view.s;
624
- elev.view.cz += (m.y - mid.y) / elev.view.s;
524
+ const face = faceEdit.key ? SK() : null;
525
+ /** The opening at (u, z) from where it started, and what's drawn on the face following it (cabinets tied to it). */
526
+ const place = (u, z) => {
527
+ // (a slide is measured from where the chain started)
528
+ if (d.group)
529
+ applySlide(sk, d.group, f.mirror ? -u : u);
530
+ if (o.type === 'window' && !o.sillLocked) {
531
+ // kept between the floor and the ceiling — but one already past them (pushed there before) isn't yanked back
532
+ // at the first touch: that dragged whatever's tied to it along, through the floor
533
+ const { sill, head } = openingHeights(o), q = 1 / settings.denom;
534
+ o.sill = clamp(Math.round((d.sill + z) / q) * q, Math.min(0, d.sill), Math.max(0, wallHeight(l) - (head - sill), d.sill));
625
535
  }
626
- mid = m;
627
- }
628
- if (drag && gp.size === 1 && down) {
629
- moveDrag(drag, ((e.cx ?? e.x) - drag.cx) / elev.view.s, -((e.cy ?? e.y) - drag.cy) / elev.view.s);
630
- if (!elev.dragOp)
631
- drag = null; // refused (locked door)
632
- return;
633
- }
634
- if (drag) {
635
- endDrag();
636
- drag = null;
637
- } // a second finger: stop dragging, pinch instead
638
- if (gp.size === 1) {
639
- elev.view.cu -= (p.x - prev.x) / elev.view.s;
640
- elev.view.cz += (p.y - prev.y) / elev.view.s;
641
- renderElevation();
642
- return;
643
- }
644
- const [a, b] = [...gp.values()], d = Math.hypot(a.x - b.x, a.y - b.y);
645
- if (!pinch) {
646
- pinch = { d, s: elev.view.s };
647
- return;
648
- }
649
- zoomElevAt((a.x + b.x) / 2, (a.y + b.y) / 2, (pinch.s * (d / pinch.d)) / elev.view.s);
650
- }
651
- export function elevPointerUp(e) {
652
- const tapped = down && !down.moved && gp.size === 1;
653
- gp.delete(e.id);
654
- pinch = null;
655
- if (panBtn) {
656
- panBtn = false;
657
- down = null;
658
- return;
659
- }
660
- if (drawing()) {
661
- // a drag from the first point draws the line on release; a tap waits for the second tap
662
- if (pressStartedLine && down?.moved && elev.lineStart)
663
- finishLine(snapUZ(e.x, e.y, elev.lineStart).p);
664
- pressStartedLine = false;
665
- down = null;
666
- return;
667
- }
668
- if ((elev.tool === 'door' || elev.tool === 'window') && tapped && elev.view) {
669
- const [u, z] = toWorld(down.x, down.y);
670
- if (z < -2 || z > (elev.frame?.H ?? 0) + 2)
671
- toast('Tap on the wall to place it.');
672
- else
673
- placeOpening(elev.tool, u, z);
674
- down = null;
675
- return;
536
+ resolveOpeningsNow();
537
+ // what's drawn on the face follows the opening's corners as it moves
538
+ if (faceEdit.key)
539
+ syncFace(sk, faceEdit.key);
540
+ };
541
+ if (!face) {
542
+ place(du, dz);
543
+ requestDraw();
544
+ return true;
676
545
  }
677
- if (drag) {
678
- const was = drag.started;
679
- endDrag();
680
- drag = null;
681
- if (was) {
682
- down = null;
683
- return;
546
+ // what's tied to it (a cabinet a set distance below a window) can't be pushed off the wall — past its floor,
547
+ // ceiling or ends: the opening goes as far as that allows and stops there, as a cabinet dragged to the edge does
548
+ d.base ??= wallOverflow(face);
549
+ const from = d.last ?? { du: 0, dz: 0 }, held = holdFace(sk, face);
550
+ const at = (t) => ({ du: from.du + (du - from.du) * t, dz: from.dz + (dz - from.dz) * t });
551
+ const fits = () => !pushedOut(d.base, wallOverflow(face));
552
+ place(du, dz);
553
+ let to = { du, dz };
554
+ if (!fits()) {
555
+ let lo = 0, hi = 1;
556
+ for (let i = 0; i < 8; i++) {
557
+ const m = (lo + hi) / 2;
558
+ putBack(sk, face, held);
559
+ place(at(m).du, at(m).dz);
560
+ if (fits())
561
+ lo = m;
562
+ else
563
+ hi = m;
684
564
  }
565
+ putBack(sk, face, held);
566
+ // (none of the way: left exactly as it was — solved again, it can land a hair apart)
567
+ to = at(lo);
568
+ if (lo > 0)
569
+ place(to.du, to.dz);
685
570
  }
686
- // mouse: Shift/⌘/Ctrl adds to the selection; touch and pencil have no Shift, so their taps always add (tap empty space to clear)
687
- if (tapped)
688
- tap(pickAt(down.x, down.y), !!e.shift || !!e.mod || e.kind !== 'mouse');
689
- down = null;
690
- }
691
- export function elevPointerCancel(e) {
692
- gp.delete(e.id);
693
- pinch = null;
694
- if (drag) {
695
- endDrag();
696
- drag = null;
697
- }
698
- if (pressStartedLine) {
699
- elev.lineStart = null;
700
- pressStartedLine = false;
701
- }
571
+ d.last = to;
572
+ requestDraw();
573
+ return true;
702
574
  }
703
- /** A mouse left the canvas: drop the drawing tools' preview. */
704
- export function elevPointerLeave(kind) {
705
- if (kind === 'mouse' && elev.cursor && !gp.size) {
706
- elev.cursor = null;
707
- renderElevation();
708
- }
575
+ /** The doors' and windows' places in the plan and what's drawn on the face, to put back if a move is taken back. */
576
+ function holdFace(sk, face) {
577
+ return { ops: snapOpenings(sk), pos: snapshotPos(face), marks: placedMarks() };
709
578
  }
710
- /** The wheel zooms around the pointer. */
711
- export function elevWheel(x, y, dy) {
712
- if (!elev.view)
713
- return;
714
- zoomElevAt(x, y, Math.exp(-dy * 0.0015));
579
+ function putBack(sk, face, h) {
580
+ putOpenings(sk, h.ops);
581
+ restorePos(face, h.pos);
582
+ setPlacedMarks(h.marks);
583
+ forgetSynced();
715
584
  }
716
585
  /**
717
- * Keyboard shortcuts while the wall view is open: Esc steps back, ⌘Z / ⇧⌘Z / ⌘Y undo and redo, F fits, tool letters,
718
- * Delete removes. Returns true when the key was used (the host should stop its default action).
586
+ * Where a door or window sliding along its wall in the plan can snap its edges: the wall's ends, its corners on both
587
+ * sides, and the edges of the other doors and windows on it — not those sliding with it (locked to it in a chain).
588
+ * Empty when Object snapping is off.
589
+ */
590
+ /**
591
+ * Where a door or window placed on wall `lid` in the plan snaps its edges (Object snapping): the wall's ends, its
592
+ * corners on both sides, and the edges of the doors and windows already on it — as one slid along it does.
593
+ */
594
+ export function placeTargets(sk, lid) {
595
+ const l = sk.lines[lid];
596
+ if (!l || !settings.layers.objSnap)
597
+ return [];
598
+ const A = sk.points[l.a], B = sk.points[l.b], out = [0, Math.hypot(B.x - A.x, B.y - A.y)];
599
+ for (const side of [1, -1]) {
600
+ const wf = wallFace(sk, lid, side);
601
+ if (wf)
602
+ out.push(wf.s0, wf.s1);
603
+ }
604
+ for (const x of Object.values(sk.openings)) {
605
+ if (x.line !== lid)
606
+ continue;
607
+ const g = openingSpan(sk, x);
608
+ if (g)
609
+ out.push(edgeS(x, g.w, 's0'), edgeS(x, g.w, 's1'));
610
+ }
611
+ return out;
612
+ }
613
+ export function slideTargets(sk, op, group) {
614
+ const o = sk.openings[op], l = o && sk.lines[o.line];
615
+ if (!o || !l || !settings.layers.objSnap)
616
+ return [];
617
+ // which move with it: slide the chain a little and see
618
+ const before = Object.fromEntries(Object.entries(sk.openings).map(([id, x]) => [id, x.at]));
619
+ applySlide(sk, group, group.hi > 0.25 ? 0.25 : group.lo < -0.25 ? -0.25 : (group.hi || group.lo));
620
+ resolveOpeningsNow();
621
+ const moving = new Set(Object.keys(sk.openings).filter(id => Math.abs(sk.openings[id].at - before[id]) > 1e-9));
622
+ applySlide(sk, group, 0);
623
+ resolveOpeningsNow();
624
+ const A = sk.points[l.a], B = sk.points[l.b], out = [0, Math.hypot(B.x - A.x, B.y - A.y)];
625
+ for (const side of [1, -1]) {
626
+ const wf = wallFace(sk, o.line, side);
627
+ if (wf)
628
+ out.push(wf.s0, wf.s1);
629
+ }
630
+ for (const [id, x] of Object.entries(sk.openings)) {
631
+ if (x.line !== o.line || moving.has(id) || id === op)
632
+ continue;
633
+ const g = openingSpan(sk, x);
634
+ if (g)
635
+ out.push(edgeS(x, g.w, 's0'), edgeS(x, g.w, 's1'));
636
+ }
637
+ return out;
638
+ }
639
+ /** A slide `delta` of a door or window (centre `at0`, width `w`, when the drag began) pulled so an edge meets the nearest target within `tol`. */
640
+ export function snapSlide(at0, w, delta, targets, tol) {
641
+ let best = null;
642
+ for (const e of [at0 + delta - w / 2, at0 + delta + w / 2])
643
+ for (const t of targets)
644
+ if (Math.abs(t - e) < tol && (best == null || Math.abs(t - e) < Math.abs(best)))
645
+ best = t - e;
646
+ return delta + (best ?? 0);
647
+ }
648
+ // ---- the wall tools ----
649
+ /** A ghost of what a placing tool would put at the pointer. */
650
+ function ghost(c, T, u0, z0, w, h, label) {
651
+ const [x0, y0] = S(T, u0, z0 + h), [x1, y1] = S(T, u0 + w, z0);
652
+ c.save();
653
+ c.fillStyle = 'rgba(0,102,255,.08)';
654
+ c.fillRect(x0, y0, x1 - x0, y1 - y0);
655
+ c.strokeStyle = TH.blue;
656
+ c.lineWidth = 1.5;
657
+ c.setLineDash([6, 4]);
658
+ c.strokeRect(x0, y0, x1 - x0, y1 - y0);
659
+ c.restore();
660
+ if (label)
661
+ previewChip(c, label, (x0 + x1) / 2, y0 - 16);
662
+ }
663
+ /** On the wall (with a little slack), or not. */
664
+ const onWall = (u, z) => { const f = elev.frame; return !!f && z >= -2 && z <= f.H + 2 && u >= f.face[0] - 2 && u <= f.face[1] + 2; };
665
+ /** A tool that does something where the wall is tapped: a ghost follows the pointer, a tap acts (a drag doesn't). */
666
+ /**
667
+ * A tool that places something where the wall is tapped, a ghost of it following the pointer first. `tol`: how near
668
+ * (inches) it snaps to what's on the wall — a finger's reach or a mouse's, as a drag snaps (Object snapping).
719
669
  */
720
- export function elevKeyDown(e) {
721
- const k = e.key.toLowerCase();
722
- if (e.key === 'Escape') {
723
- elevEscape();
670
+ function placingTool(o) {
671
+ const tolOf = (ev) => (ev.touch ? 16 : 10) / mainT().s;
672
+ return {
673
+ st: {},
674
+ reset() { this.st = {}; ed.preview = null; },
675
+ hover(ev) {
676
+ const tol = tolOf(ev);
677
+ ed.preview = onWall(ev.wx, ev.wy) ? (c, T) => o.preview(c, T, ev.wx, ev.wy, tol) : null;
678
+ requestOverlayDraw();
679
+ },
680
+ down(ev) { this.st = { down: ev, far: false }; this.hover(ev); },
681
+ move(ev) {
682
+ const st = this.st;
683
+ if (st.down && Math.hypot(ev.sx - st.down.sx, ev.sy - st.down.sy) > (ev.touch ? 12 : 6))
684
+ st.far = true;
685
+ this.hover(ev);
686
+ },
687
+ up(ev) {
688
+ const st = this.st;
689
+ this.st = {};
690
+ if (ev.touch)
691
+ ed.preview = null;
692
+ requestOverlayDraw();
693
+ if (!st.down || st.far)
694
+ return;
695
+ if (!onWall(ev.wx, ev.wy)) {
696
+ toast('Tap on the wall to place it.', { warn: true });
697
+ return;
698
+ }
699
+ o.place(ev.wx, ev.wy, tolOf(ev));
700
+ },
701
+ hint: () => o.hint,
702
+ };
703
+ }
704
+ const openingTool = (type) => placingTool({
705
+ preview(c, T, u, z, tol) {
706
+ const spot = openingAt(type, u, z, tol), f = elev.frame;
707
+ if (!spot || !f)
708
+ return;
709
+ const a = toU(f, spot.at - NEW_W / 2), b = toU(f, spot.at + NEW_W / 2);
710
+ ghost(c, T, Math.min(a, b), spot.sill, NEW_W, type === 'door' ? DOOR_H : WINDOW_H);
711
+ },
712
+ place: (u, z, tol) => placeOpening(type, u, z, tol),
713
+ hint: type === 'door' ? 'Tap the wall to place a 3\'-0" door. It sits on the floor where you tap.' : 'Tap the wall to place a 3\'-0" window, centered where you tap.',
714
+ });
715
+ export const wallDoorTool = openingTool('door');
716
+ export const wallWindowTool = openingTool('window');
717
+ export const cabinetTool = placingTool({
718
+ preview(c, T, u, z) { const spot = cabinetSpot(u, z); if (spot)
719
+ ghost(c, T, spot.u0, spot.z0, CAB.w, CAB.h, fmtLen(CAB.w) + ' × ' + fmtLen(CAB.h)); },
720
+ place: (u, z) => { addCabinet(u, z); },
721
+ hint: 'Tap the wall to place a cabinet (24" wide, 34½" high, 24" deep) — anywhere, like a window. Tap it, then one of its dimensions, to set it.',
722
+ });
723
+ export const heightTool = placingTool({
724
+ preview(c, T, _u, z) {
725
+ const f = elev.frame;
726
+ if (!f)
727
+ return;
728
+ const q = 1 / settings.denom, h = clamp(Math.round(z / q) * q, 0, f.H), [x0, y] = S(T, f.face[0], h), [x1] = S(T, f.face[1], h);
729
+ c.save();
730
+ c.strokeStyle = TH.blue;
731
+ c.lineWidth = 1.5;
732
+ c.setLineDash([6, 4]);
733
+ c.beginPath();
734
+ c.moveTo(x0, y);
735
+ c.lineTo(x1, y);
736
+ c.stroke();
737
+ c.restore();
738
+ previewChip(c, fmtLen(h), x1 - 40, y - 16);
739
+ },
740
+ place: (_u, z) => {
741
+ const q = 1 / settings.denom, start = clamp(Math.round(z / q) * q, 0, elev.frame.H);
742
+ openKeypad({ title: 'Height line', sub: 'Floor to the line — it runs across the whole wall', initial: fmtLen(start), selectAll: true, laser: true, onDone: v => { addHeightLine(v); } });
743
+ },
744
+ hint: 'Tap the wall at a height, then enter it: a level line across the wall (a counter, a rail).',
745
+ });
746
+ /** The wall view's toolbar: the plan's drawing tools, then what goes on a wall. (The height line tool is kept off it for now.) */
747
+ export const WALL_RAIL = [
748
+ ['select', 'Select', 'V'], ['line', 'Line', 'L'], ['freehand', 'Draw to shape', 'S'], ['rect', 'Rectangle', 'R'], ['circle', 'Circle', 'C'], ['arc', 'Arc', 'A'], '|',
749
+ ['dim', 'Dimension', 'D'], ['wdoor', 'Door', 'B'], ['wwindow', 'Window', 'N'], ['cabinet', 'Cabinet', 'K'], '|', ['constr', 'Construction lines', 'X', 'toggle'], '|',
750
+ ['undo', 'Undo', '⌘Z', 'act'], ['redo', 'Redo', '⇧⌘Z', 'act'],
751
+ ];
752
+ /** Tool letters in the wall view. */
753
+ export const WALL_KEYS = { v: 'select', l: 'line', s: 'freehand', r: 'rect', c: 'circle', a: 'arc', d: 'dim', t: 'trim', o: 'offset', b: 'wdoor', n: 'wwindow', k: 'cabinet' };
754
+ /** The icon for a wall tool whose id isn't an icon's name. */
755
+ export const WALL_ICON = { wdoor: 'door', wwindow: 'window', height: 'hline' };
756
+ // ---- what's under the pointer, beyond the face's own geometry ----
757
+ /** A cabinet (on it, or within `slack` inches of its outline) or a door/window body under a world point on the face (cabinets first). */
758
+ export function wallBodyAt(wx, wy, slack = 0) {
759
+ const face = SK(), f = elev.frame;
760
+ if (!f)
761
+ return null;
762
+ // (inside it: right on its edge picks the edge, as a window's)
763
+ for (const id of Object.keys(face.cabs ?? {}).reverse()) {
764
+ const b = cabBox(face, id);
765
+ if (b && wx > b.u0 && wx < b.u1 && wy > b.z0 && wy < b.z1)
766
+ return { type: 'cab', id };
767
+ }
768
+ for (const o of elevOpenings(planSK(), f))
769
+ if (wx > o.u0 && wx < o.u1 && wy > o.sill && wy < o.head)
770
+ return { type: 'opening', id: o.id };
771
+ return null;
772
+ }
773
+ /** Two doors/windows, or one and a wall end: the + sets the distance between them (the plan's door/window dimension). */
774
+ function wallPair() {
775
+ if (ed.sel.length !== 2)
776
+ return null;
777
+ const ops = ed.sel.filter(x => x.type === 'opening').map(x => x.id);
778
+ const ends = ed.sel.filter(x => x.type === 'line' && (x.id === '@ea' || x.id === '@eb')).map(x => x.id.slice(2));
779
+ if (ops.length === 2)
780
+ return { op: ops[0], other: { op: ops[1] } };
781
+ if (ops.length === 1 && ends.length === 1)
782
+ return { op: ops[0], other: { end: ends[0] } };
783
+ return null;
784
+ }
785
+ /** The bar for what's selected on the wall that the plan doesn't know (a door or window, a cabinet, a chip). Null: the plan's bar. */
786
+ export function wallBar() {
787
+ if (!elevOpen())
788
+ return null;
789
+ const pair = wallPair();
790
+ if (pair) {
791
+ const name = (id) => (planSK().openings[id]?.type === 'door' ? 'Door' : 'Window');
792
+ const label = name(pair.op) + ' + ' + ('op' in pair.other ? name(pair.other.op).toLowerCase() : 'corner');
793
+ return { label, items: [{ ic: 'dim', label: 'Distance', act: 'wallDist' }], plus: true };
794
+ }
795
+ if (ed.sel.length !== 1)
796
+ return null;
797
+ const s = ed.sel[0], sk = planSK();
798
+ if (s.type === 'elev') {
799
+ const a = elevAct(s.id);
800
+ return { label: elevChipName(a), items: a.k === 'len' ? [{ ic: 'back', label: 'Go to plan', act: 'wallGoPlan' }] : [], plus: a.k !== 'len' };
801
+ }
802
+ // a door, window or cabinet picked as a whole: its sizes to set, by name (no + — it isn't one dimension)
803
+ if (s.type === 'opening') {
804
+ const o = sk.openings[s.id];
805
+ if (!o)
806
+ return null;
807
+ return { label: openingLabel(o), items: OPENING_SIZES, plus: false };
808
+ }
809
+ if (s.type === 'cab') {
810
+ const face = SK(), b = cabBox(face, s.id), cb = face.cabs[s.id];
811
+ return {
812
+ label: 'Cabinet' + (b ? ' ' + fmtLen(b.u1 - b.u0) + ' L × ' + fmtLen(b.z1 - b.z0) + ' H × ' + fmtLen(cb.depth) + ' D' : ''),
813
+ items: [{ ic: 'v', label: 'Height', act: 'cabH' }, { ic: 'h', label: 'Length', act: 'cabW' }, { ic: 'thick', label: 'Depth', act: 'cabD' }], plus: false,
814
+ };
815
+ }
816
+ const face = SK();
817
+ if (s.type === 'line' && face.lines[s.id]?.anchor)
818
+ return { label: anchorName(s.id), items: [], plus: false };
819
+ if (s.type === 'line' && face.lines[s.id]?.cab)
820
+ return { label: 'Cabinet edge', items: [], plus: false };
821
+ if (s.type === 'point' && face.points[s.id]?.anchor)
822
+ return { label: 'Corner · set by the plan', items: [], plus: false };
823
+ return null;
824
+ }
825
+ /** What one of the plan's edges on the face is. */
826
+ function anchorName(id) {
827
+ if (id === '@floor')
828
+ return 'Floor';
829
+ if (id === '@ceil')
830
+ return 'Ceiling';
831
+ if (id === '@ea' || id === '@eb')
832
+ return 'Wall corner';
833
+ const op = planSK().openings[id.split(':')[1]];
834
+ return (op?.type === 'door' ? 'Door' : 'Window') + ' edge';
835
+ }
836
+ /** A door's or window's sizes to set, in the bar (the wall view's and the plan's): its height and its length. */
837
+ export const OPENING_SIZES = [{ ic: 'v', label: 'Height', act: 'opHeight' }, { ic: 'h', label: 'Length', act: 'opLength' }];
838
+ /** What the bar calls a door or window: its kind and sizes, length by height ("Door 3'-0" L × 6'-8" H"). */
839
+ export function openingLabel(o) {
840
+ const { sill, head } = openingHeights(o);
841
+ return (o.type === 'door' ? 'Door ' : 'Window ') + fmtLen(o.w) + ' L × ' + fmtLen(head - sill) + ' H';
842
+ }
843
+ /** Run a bar action for the wall view's things (a door's or window's sizes work in the plan too). Returns false if it isn't one. */
844
+ export function wallAction(act) {
845
+ const s = ed.sel[0];
846
+ switch (act) {
847
+ case 'opHeight':
848
+ editOpeningHeight(s.id);
849
+ return true;
850
+ case 'opLength':
851
+ editOpeningDim(s.id, 'w');
852
+ return true;
853
+ case 'cabW':
854
+ editCabinet(s.id, 'w');
855
+ return true;
856
+ case 'opSill':
857
+ editSill(s.id);
858
+ return true;
859
+ case 'cabH':
860
+ editCabinet(s.id, 'h');
861
+ return true;
862
+ case 'cabD':
863
+ editCabinetDepth(s.id);
864
+ return true;
865
+ case 'cabZ':
866
+ editCabinet(s.id, 'z');
867
+ return true;
868
+ case 'wallGoPlan':
869
+ goToPlan();
870
+ return true;
871
+ case 'wallDist':
872
+ wallPlus();
873
+ return true;
874
+ }
875
+ return false;
876
+ }
877
+ /** The yellow + for wall things. Returns false if the plan's + should handle the selection. */
878
+ export function wallPlus() {
879
+ if (!elevOpen())
880
+ return false;
881
+ const pair = wallPair(), f = elev.frame;
882
+ if (pair) {
883
+ const ops = elevOpenings(planSK(), f), a = ops.find(o => o.id === pair.op);
884
+ if (!a)
885
+ return true;
886
+ if ('op' in pair.other) {
887
+ const otherId = pair.other.op, b = ops.find(o => o.id === otherId);
888
+ if (!b)
889
+ return true;
890
+ const [l, r] = a.u0 <= b.u0 ? [a, b] : [b, a], t = pickBetween(planSK(), l.id, l.e1, r.id, r.e0);
891
+ editElevationPosition(t.op, t.edge, t.ref, 'Space between');
892
+ }
893
+ else {
894
+ const end = pair.other.end, ref = { face: end, line: f.lid, side: f.side };
895
+ editElevationPosition(a.id, end === f.leftEnd ? a.e0 : a.e1, ref, 'Distance from the corner');
896
+ }
724
897
  return true;
725
898
  }
726
- if (e.mod && k === 'z') {
727
- if (e.shift)
728
- redo();
729
- else
730
- undo();
899
+ if (ed.sel.length !== 1)
900
+ return false;
901
+ const s = ed.sel[0];
902
+ if (s.type === 'elev') {
903
+ editElevChip(elevAct(s.id));
731
904
  return true;
732
905
  }
733
- if (e.mod && k === 'y') {
734
- redo();
906
+ if (s.type === 'opening') {
907
+ editOpeningDim(s.id, 'w');
735
908
  return true;
736
909
  }
737
- if (k === 'f' && !e.mod) {
738
- fitElevation();
910
+ if (s.type === 'cab') {
911
+ editCabinet(s.id, 'w');
739
912
  return true;
740
913
  }
741
- if (!e.mod && !e.alt && elevToolKey(e.key))
914
+ return false;
915
+ }
916
+ /**
917
+ * Delete in the wall view: a selected chip's dimension is freed or removed; doors and windows go from the plan,
918
+ * cabinets from the face, the rest as in the plan. The plan's anchors stay. Returns false outside the wall view.
919
+ */
920
+ export function wallDelete() {
921
+ if (!elevOpen())
922
+ return false;
923
+ if (ed.sel.length === 1 && ed.sel[0].type === 'elev') {
924
+ deleteElevChip(elevAct(ed.sel[0].id));
742
925
  return true;
743
- if (e.key === 'Delete' || e.key === 'Backspace') {
744
- deleteSelectedElevDim();
926
+ }
927
+ const face = SK();
928
+ const anchor = (x) => (x.type === 'line' && !!face.lines[x.id]?.anchor) || (x.type === 'point' && !!face.points[x.id]?.anchor);
929
+ const items = ed.sel.filter(x => !anchor(x) && x.type !== 'elev');
930
+ if (!items.length) {
931
+ toast('The wall’s edges and corners come from the plan — they can’t be deleted here.', { warn: true });
745
932
  return true;
746
933
  }
747
- return false;
934
+ const one = items.length === 1 ? items[0] : null, op = one?.type === 'opening' ? planSK().openings[one.id] : null;
935
+ const what = one?.type === 'cab' ? 'Cabinet deleted' : op ? (op.type === 'door' ? 'Door' : 'Window') + ' deleted' : 'Deleted';
936
+ pushHistory();
937
+ deleteSel(planSK(), items.filter(x => x.type === 'opening'));
938
+ deleteCabinets(items.filter(x => x.type === 'cab').map(x => x.id));
939
+ deleteSel(face, items.filter(x => x.type !== 'opening' && x.type !== 'cab'));
940
+ ed.sel = [];
941
+ afterEdit();
942
+ toast(what, { action: { label: 'Undo', fn: () => undo() } });
943
+ return true;
748
944
  }
749
945
  //# sourceMappingURL=elevation.js.map