@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
package/dist/tools/dim.js CHANGED
@@ -1,30 +1,47 @@
1
- import { afterEdit, tryAddConstraint } from '../app/edit';
1
+ import { afterEdit, holdGroups, tryAddConstraint } from '../app/edit';
2
2
  import { ed } from '../app/state';
3
3
  import { measure } from '../model/constraints';
4
4
  import { hyp, norm2, pointInPoly, projT } from '../model/geom';
5
5
  import { dropHistory, pushHistory } from '../model/history';
6
- import { addCons, lengthDimOf, lineLen, restorePos, roomValid, snapshotPos } from '../model/sketch';
6
+ import { addCons, lengthDimOf, lineLen, roomValid } from '../model/sketch';
7
7
  import { SK } from '../model/store';
8
+ import { askLinkedChange, changesValue } from '../editor/linkWrite';
9
+ import { offersJobMeasurements } from '../editor/links';
10
+ import { fromUm, linkInfoOf, linkSnapshot } from '../model/links';
11
+ import { defaultFace, dimEnds, edgeId, edgeLabel, edgeOf, faceName, sideOf, spaceBeside, wallOf } from '../model/planGeom';
12
+ import { faceLength } from '../model/wallfaces';
13
+ import { fitState, overflow, pastWallMsg, restoreFit } from '../editor/elevation';
14
+ import { pushedOut } from '../model/facesolve';
8
15
  import { hitAny } from '../render/hit';
9
16
  import { chooseEdges, edgePoint, nearerEdge, openingOnLineAt } from '../model/openings';
10
17
  import { drawPendingDim, drawRubberBand } from '../render/draw';
11
- import { mainT, requestDraw, s2w, V } from '../render/view';
18
+ import { mainT, requestDraw, requestOverlayDraw, s2w, V } from '../render/view';
12
19
  import { solver } from '../solver';
13
20
  import { fmtAng, fmtLen } from '../units';
14
21
  import { addOpeningDim } from './openings';
22
+ import { keypadFresh, keypadOpen, keypadSetReading } from '../editor/keypad';
23
+ import { cornerLines } from '../editor/selection';
15
24
  import { closeKeypad, kpTyped, openKeypad, showConflict, toast, updateCtx, updateHint } from '../editor/ports';
16
25
  export const dimTool = {
17
- st: { first: null },
18
- reset() { this.st = { first: null }; ed.hover = null; ed.pickedAnchor = null; },
26
+ /** angle: the first pick is a corner whose angle keypad is open (the next tap closes it) */
27
+ st: { first: null, angle: false },
28
+ reset() { if (this.st.angle && keypadOpen())
29
+ closeKeypad(); this.st = { first: null, angle: false }; ed.hover = null; ed.pickedAnchor = null; },
19
30
  busy() { return !!this.st.first || !!ed.pickedAnchor; },
20
31
  hover(ev) {
32
+ const was = ed.hover;
21
33
  ed.hover = pickForDim(ev);
22
34
  if (ed.hover && ed.hover.type === 'room')
23
35
  ed.hover = null;
24
36
  // after the first pick: a rubber band from it to what's under the pointer
25
37
  const band = this.band(ev);
26
38
  ed.preview = band ? (c, T) => drawRubberBand(c, T, band.from, band.to, band.snapped) : null;
27
- requestDraw();
39
+ // what's under the pointer is highlighted in the drawing itself; otherwise only the band moved
40
+ const same = was?.type === ed.hover?.type && was?.id === ed.hover?.id && was?.part === ed.hover?.part;
41
+ if (same)
42
+ requestOverlayDraw();
43
+ else
44
+ requestDraw();
28
45
  },
29
46
  /** Ends of the rubber band: the first pick (an opening body resolves to the edge facing the target) and the target. */
30
47
  band(ev) {
@@ -69,6 +86,12 @@ export const dimTool = {
69
86
  up(ev) {
70
87
  const h = pickForDim(ev);
71
88
  const print = ed.dimLayer === 'print', st = this.st;
89
+ // a corner's angle keypad is open: the next tap closes it and goes on (a second point measures from the corner)
90
+ if (st.angle) {
91
+ st.angle = false;
92
+ if (keypadOpen())
93
+ closeKeypad();
94
+ }
72
95
  if (!h || h.type === 'room') {
73
96
  st.first = null;
74
97
  ed.pickedAnchor = null;
@@ -83,7 +106,7 @@ export const dimTool = {
83
106
  if (h.type === 'oanchor') {
84
107
  const edge = h.part;
85
108
  if (print) {
86
- toast('Print dimensions can’t attach to doors or windows yet. Switch to Driving in the bar below.');
109
+ toast('Print dimensions can’t attach to doors or windows yet. Switch to Driving in the bar below.', { warn: true });
87
110
  return;
88
111
  }
89
112
  if (st.first) {
@@ -120,13 +143,16 @@ export const dimTool = {
120
143
  updateHint();
121
144
  return;
122
145
  }
123
- toast('Tap a corner, a point, or another door or window edge to measure to.');
146
+ toast('Tap a corner, a point, or another door or window edge to measure to.', { warn: true });
124
147
  return;
125
148
  }
126
149
  if (h.type === 'point') {
127
150
  if (!st.first) {
128
151
  st.first = h.id;
129
152
  ed.sel = [{ type: 'point', id: h.id }];
153
+ // a corner: its angle straight away (close the keypad to measure from the corner to another point instead)
154
+ if (!print)
155
+ st.angle = cornerAngle(h.id, () => { this.st.first = null; this.st.angle = false; ed.sel = []; updateHint(); requestDraw(); }, true);
130
156
  updateHint();
131
157
  requestDraw();
132
158
  return;
@@ -137,7 +163,30 @@ export const dimTool = {
137
163
  updateHint();
138
164
  return;
139
165
  }
166
+ // a wall's edge: its length on that face — or, after a point, the distance from the face to it; a curved wall's,
167
+ // its diameter across that face
168
+ if (h.type === 'wface') {
169
+ const [lid, side] = edgeOf(h.id);
170
+ if (!SK().lines[lid]) {
171
+ dimCurve(lid, side);
172
+ return;
173
+ }
174
+ if (st.first) {
175
+ const p = st.first;
176
+ st.first = null;
177
+ if (print)
178
+ dimPointLine(p, lid);
179
+ else
180
+ dimEdgePoint(lid, side, p);
181
+ updateHint();
182
+ return;
183
+ }
184
+ dimLine(lid, print, side);
185
+ return;
186
+ }
140
187
  if (h.type === 'line') {
188
+ if (h.side)
189
+ ed.wallTap = { id: h.id, side: h.side };
141
190
  if (st.first) {
142
191
  dimPointLine(st.first, h.id);
143
192
  st.first = null;
@@ -147,8 +196,11 @@ export const dimTool = {
147
196
  dimLine(h.id, print);
148
197
  return;
149
198
  }
150
- if (h.type === 'circle' || h.type === 'arc')
199
+ if (h.type === 'circle' || h.type === 'arc') {
200
+ if (h.side)
201
+ ed.wallTap = { id: h.id, side: h.side };
151
202
  dimCurve(h.id);
203
+ }
152
204
  },
153
205
  hint() {
154
206
  if (ed.pickedAnchor)
@@ -163,13 +215,17 @@ export const dimTool = {
163
215
  * counts as its nearer edge, so openings are as easy to dimension as points.
164
216
  */
165
217
  function pickForDim(ev) {
166
- const h = hitAny(ev.sx, ev.sy, { touch: ev.touch, only: 'geom' });
167
- if (!h || h.type !== 'line')
218
+ const h = hitAny(ev.sx, ev.sy, { touch: ev.touch, only: 'geom', faces: true });
219
+ if (!h || (h.type !== 'line' && h.type !== 'wface'))
168
220
  return h;
169
- const sk = SK(), l = sk.lines[h.id], A = sk.points[l.a], B = sk.points[l.b];
221
+ const lid = h.type === 'wface' ? edgeOf(h.id)[0] : h.id;
222
+ const sk = SK(), l = sk.lines[lid];
223
+ if (!l)
224
+ return h; // (a curved wall's edge)
225
+ const A = sk.points[l.a], B = sk.points[l.b];
170
226
  const [wx, wy] = s2w(ev.sx, ev.sy);
171
227
  const s = projT(A, B, { x: wx, y: wy }) * hyp(B.x - A.x, B.y - A.y);
172
- const a = openingOnLineAt(sk, h.id, s, 2 / mainT().s);
228
+ const a = openingOnLineAt(sk, lid, s, 2 / mainT().s);
173
229
  // weak = the body was tapped: the edge gets chosen to face whatever it's measured to
174
230
  return a ? { type: 'oanchor', id: a.id, part: a.edge, weak: true } : h;
175
231
  }
@@ -185,38 +241,272 @@ function sideAwayFromRooms(lid) {
185
241
  }
186
242
  return 1;
187
243
  }
188
- /** Tap on a line (or its + chip): open the keypad for its length, or edit the existing length. */
189
- export function dimLine(lid, print) {
244
+ /**
245
+ * Tap on a line (or its + chip): open the keypad for its length, or edit the existing length. A wall's is measured on
246
+ * `side`, the edge picked — or, picked whole, on the side a space is on (the side it was tapped on, between two), and
247
+ * along its centerline where there's no space; the keypad can switch it.
248
+ */
249
+ export function dimLine(lid, print, side) {
190
250
  const sk = SK(), l = sk.lines[lid];
191
251
  if (!l)
192
252
  return;
253
+ const wall = faced(sk, lid);
193
254
  if (print) {
194
255
  pushHistory();
195
- const off = (36 / V().s) * sideAwayFromRooms(lid);
196
- addCons(sk, { type: 'dist', a: l.a, b: l.b, kind: 'al', value: lineLen(sk, l), line: lid, driving: false, layer: 'print', off });
256
+ // (on a wall's edge: that face's length, labelled on that side)
257
+ const f = wall ? side : undefined;
258
+ const off = (36 / V().s) * (f ?? sideAwayFromRooms(lid));
259
+ addCons(sk, { type: 'dist', a: l.a, b: l.b, kind: 'al', value: f ? faceLength(sk, lid, f) : lineLen(sk, l), line: lid, driving: false, layer: 'print', off, ...(f ? { face: f } : {}) });
197
260
  afterEdit();
198
261
  toast('Print dimension added. Drag it to move it.');
199
262
  return;
200
263
  }
201
264
  const ex = lengthDimOf(sk, lid);
202
265
  if (ex) {
203
- editDim(ex);
266
+ editDim(ex, undefined, false, wall && side ? { face: side } : undefined);
204
267
  return;
205
268
  }
206
- ed.sel = [{ type: 'line', id: lid }];
207
- requestDraw();
208
- openKeypad({
209
- title: 'Length', sub: l.kind === 'wall' ? 'Wall centerline' : '', initial: fmtLen(lineLen(sk, l)), selectAll: true, laser: true,
210
- onDone: v => {
211
- if (v <= 0)
212
- return;
213
- pushHistory();
214
- const r = tryAddConstraint({ type: 'dist', a: l.a, b: l.b, kind: 'al', value: v, line: lid, driving: true, off: 0 });
215
- if (!r.ok)
216
- dropHistory();
217
- afterEdit();
218
- },
219
- });
269
+ let face = wall ? side ?? defaultFace(sk, lid, ed.wallTap?.id === lid ? ed.wallTap.side : undefined) ?? 0 : 0;
270
+ const len = () => (face ? faceLength(sk, lid, face) : lineLen(sk, l));
271
+ // (while it's being entered, what it measures is what's highlighted: the edge, or the wall)
272
+ const was = ed.sel;
273
+ const show = () => { ed.sel = face ? [{ type: 'wface', id: edgeId(lid, face) }] : [{ type: 'line', id: lid }]; requestDraw(); };
274
+ show();
275
+ const spec = {
276
+ title: 'Length', sub: wall ? lengthSub(sk, lid, face) : '', initial: fmtLen(len()), selectAll: true, laser: true,
277
+ extras: wall ? () => lengthChoices(sk, lid).map(ch => ({
278
+ label: ch.label, on: ch.face === face,
279
+ onPress: () => { face = ch.face; spec.sub = lengthSub(sk, lid, face); show(); if (keypadFresh())
280
+ keypadSetReading(len()); },
281
+ })) : undefined,
282
+ onClose: () => { ed.sel = was; updateCtx(); requestDraw(); },
283
+ onDone: (v, capture) => { if (v > 0)
284
+ addLength(v, capture !== undefined ? { capture } : {}); },
285
+ // (or a length of the job's, from its list: the line takes it, linked to it)
286
+ measurements: offersJobMeasurements() ? {
287
+ kind: 'length', pick: t => {
288
+ const v = jobValue(t, 'length');
289
+ if (v == null)
290
+ return;
291
+ closeKeypad();
292
+ addLength(v, { measurementRef: linkSnapshot(t) });
293
+ },
294
+ } : undefined,
295
+ };
296
+ /** The line's length dimension, driving, measured where the keypad says (`extra`: what it's from). */
297
+ function addLength(v, extra) {
298
+ pushHistory();
299
+ const r = tryAddConstraint({ type: 'dist', a: l.a, b: l.b, kind: 'al', value: v, line: lid, driving: true, off: face ? (face * 30) / V().s : 0, ...(face ? { face } : {}), ...extra });
300
+ // a measurement of the job drives a line, or nothing: not one whose length the drawing already sets
301
+ if (r.ok && r.ref && extra.measurementRef) {
302
+ delete SK().cons[r.id];
303
+ dropHistory();
304
+ toast(LENGTH_SET_ALREADY, { warn: true });
305
+ }
306
+ else if (!r.ok)
307
+ dropHistory();
308
+ afterEdit();
309
+ }
310
+ openKeypad(spec);
311
+ }
312
+ /** Why a measurement of the job can't drive a line the drawing already sets the length of. */
313
+ export const LENGTH_SET_ALREADY = 'The drawing already sets that line’s length. Free it first (make one of its dimensions a reference), then pick the measurement.';
314
+ /** Why a measurement of the job can't drive an angle the drawing already sets. */
315
+ export const ANGLE_SET_ALREADY = 'The drawing already sets that angle. Free it first (make one of its dimensions a reference), then pick the measurement.';
316
+ /**
317
+ * A measurement of the job's as a keypad of `kind` takes it — inches for a length, degrees for an angle (what the host
318
+ * says it is now, or what it was when listed); null, said why, if it has none, or isn't of that kind.
319
+ */
320
+ function jobValue(t, kind) {
321
+ if ((t.mkind ?? 'length') !== kind) {
322
+ toast(kind === 'angle' ? 'That’s a length. Pick an angle, or type one in.' : 'That’s an angle. Pick a length, or type one in.', { warn: true });
323
+ return null;
324
+ }
325
+ const v = linkInfoOf(t)?.value ?? t.value;
326
+ if (v == null || !(v > 0)) {
327
+ toast(kind === 'angle' ? 'That measurement has no angle to use. Pick another, or type one in.' : 'That measurement has no length to use. Pick another, or type one in.', { warn: true });
328
+ return null;
329
+ }
330
+ return kind === 'angle' ? v : fromUm(v);
331
+ }
332
+ /* Measuring off a wall's faces (its edges) rather than its centerline. */
333
+ /** A straight wall, whose length can be measured on either face (not a guide). */
334
+ function faced(sk, lid) {
335
+ const l = lid ? sk.lines[lid] : undefined;
336
+ return !!l && l.kind === 'wall' && !l.constr;
337
+ }
338
+ /** What a wall's length (a curved one's diameter) is measured on, as the keypad says it. */
339
+ const lengthSub = (sk, lid, face) => (face ? edgeLabel(sk, lid, face) : 'Wall centerline');
340
+ /** A wall's face as an icon: the one on top, at the bottom, left or right (as the page shows it) — or its centerline. */
341
+ function faceIcon(sk, lid, side) {
342
+ const l = sk.lines[lid], a = sk.points[l.a], b = sk.points[l.b], level = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y);
343
+ if (!side)
344
+ return level ? 'faceMidH' : 'faceMidV';
345
+ // (the face's outward direction: the left of A → B for +1; y is up)
346
+ const nx = -(b.y - a.y) * side, ny = (b.x - a.x) * side;
347
+ return level ? (ny > 0 ? 'faceT' : 'faceB') : nx > 0 ? 'faceR' : 'faceL';
348
+ }
349
+ /**
350
+ * A wall's length: on its inside face (or one space's side, or — named by the way they face — its top or left), its
351
+ * centerline, its other face.
352
+ */
353
+ function lengthChoices(sk, lid) {
354
+ const n = faceName(sk, lid, 1), byWay = ['Top', 'Bottom', 'Left', 'Right'].includes(n);
355
+ const first = byWay ? (n === 'Top' || n === 'Left' ? 1 : -1) : spaceBeside(sk, lid, 1) ? 1 : -1;
356
+ const side = (f) => ({ label: edgeLabel(sk, lid, f), short: faceName(sk, lid, f).replace(/ side$/, ''), icon: faceIcon(sk, lid, f), face: f });
357
+ return [side(first), { label: 'Centerline', short: 'Center', icon: faceIcon(sk, lid, 0), face: 0 }, side(first === 1 ? -1 : 1)];
358
+ }
359
+ /**
360
+ * A curved wall's diameter (or radius): across its inner face, its centerline, its outer face — its outer face first
361
+ * when that's the one a space is against (a round column in a room).
362
+ */
363
+ function curveChoices(sk, id) {
364
+ const first = spaceBeside(sk, id, 1) && !spaceBeside(sk, id, -1) ? 1 : -1;
365
+ const side = (f) => ({ label: edgeLabel(sk, id, f), short: faceName(sk, id, f), icon: f === 1 ? 'ringOut' : 'ringIn', face: f });
366
+ return [side(first), { label: 'Centerline', short: 'Center', icon: 'ringMid', face: 0 }, side(first === 1 ? -1 : 1)];
367
+ }
368
+ /** The faces a dimension on walls can be measured on, as choices; none when it isn't on a wall. */
369
+ export function faceChoices(sk, c) {
370
+ if (c.type === 'dist' && c.line && (!c.kind || c.kind === 'al') && faced(sk, c.line))
371
+ return lengthChoices(sk, c.line);
372
+ if (c.type === 'diam' && c.c && !sk.lines[c.c] && wallOf(sk, c.c))
373
+ return curveChoices(sk, c.c);
374
+ if (c.type === 'pldist' && c.l && sk.lines[c.l] && sk.points[c.p]) {
375
+ const w1 = faced(sk, c.l), w2 = faced(sk, c.l2);
376
+ if (!w1 && !w2)
377
+ return [];
378
+ const k = (v) => [w1 ? v : 0, w2 ? v : 0];
379
+ if (w1 && w2) {
380
+ const l = sk.lines[c.l], a = sk.points[l.a], b = sk.points[l.b], H = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y) ? 'H' : '';
381
+ return [
382
+ { label: 'Inside faces', short: 'Inside', icon: 'spanIn' + H, faces: k(1) },
383
+ { label: 'Centerlines', short: 'Centers', icon: 'spanMid' + H, faces: [0, 0] },
384
+ { label: 'Outside faces', short: 'Outside', icon: 'spanOut' + H, faces: k(-1) },
385
+ ];
386
+ }
387
+ // one wall: its face toward the other end, its centerline, its far face
388
+ const wl = w1 ? c.l : c.l2, other = w1 ? sk.points[c.p] : sk.points[sk.lines[c.l].a], toward = sideOf(sk, wl, other);
389
+ return [
390
+ { label: 'Near face', short: 'Near', icon: faceIcon(sk, wl, toward), faces: k(1) },
391
+ { label: 'Centerline', short: 'Center', icon: faceIcon(sk, wl, 0), faces: [0, 0] },
392
+ { label: 'Far face', short: 'Far', icon: faceIcon(sk, wl, -toward), faces: k(-1) },
393
+ ];
394
+ }
395
+ return [];
396
+ }
397
+ /** Whether a dimension is measured on the faces a choice says. */
398
+ export function onChoice(c, ch) {
399
+ if (ch.face !== undefined)
400
+ return (c.face ?? 0) === ch.face;
401
+ return !!ch.faces && (c.faces?.[0] ?? 0) === ch.faces[0] && (c.faces?.[1] ?? 0) === ch.faces[1];
402
+ }
403
+ /** Measure a dimension on the faces a choice says (its number left as it is). A length's label moves to that side. */
404
+ function applyFace(c, ch) {
405
+ if (ch.face !== undefined) {
406
+ if (ch.face)
407
+ c.face = ch.face;
408
+ else
409
+ delete c.face;
410
+ if (ch.face && c.type === 'dist') {
411
+ const l = SK().lines[c.line], k = l && c.a === l.a ? 1 : -1;
412
+ c.off = (Math.abs(c.off ?? 0) || 30 / V().s) * ch.face * k;
413
+ }
414
+ }
415
+ if (ch.faces) {
416
+ if (ch.l2)
417
+ c.l2 = ch.l2;
418
+ if (ch.faces[0] || ch.faces[1])
419
+ c.faces = [ch.faces[0], ch.faces[1]];
420
+ else
421
+ delete c.faces;
422
+ }
423
+ }
424
+ /** A copy of a dimension, measured on the faces a choice says. */
425
+ function withFace(c, ch) {
426
+ const n = { ...c };
427
+ if (ch)
428
+ applyFace(n, ch);
429
+ return n;
430
+ }
431
+ /** What a dimension's faces are, to put back. */
432
+ const faceState = (c) => ({ face: c.face, faces: c.faces, l2: c.l2, off: c.off });
433
+ function restoreFace(c, f) {
434
+ if (f.face === undefined)
435
+ delete c.face;
436
+ else
437
+ c.face = f.face;
438
+ if (f.faces === undefined)
439
+ delete c.faces;
440
+ else
441
+ c.faces = f.faces;
442
+ if (f.l2 === undefined)
443
+ delete c.l2;
444
+ else
445
+ c.l2 = f.l2;
446
+ if (f.off === undefined)
447
+ delete c.off;
448
+ else
449
+ c.off = f.off;
450
+ }
451
+ /** Measure a dimension on other faces of its walls, the drawing kept as it is: its number is what it measures there now. */
452
+ export function setDimFace(id, ch) {
453
+ const sk = SK(), c = sk.cons[id];
454
+ if (!c || onChoice(c, ch))
455
+ return;
456
+ pushHistory();
457
+ applyFace(c, ch);
458
+ if (c.driving !== false)
459
+ c.value = measure(sk, c);
460
+ afterEdit();
461
+ }
462
+ /**
463
+ * The face of wall `lid` to measure from, to something at `at`: +1 its face toward it, −1 its far face, 0 its centerline.
464
+ * `side` is the edge picked (+1 / −1: the left / right of its A → B; 0 its centerline). Picked whole, to a point: the
465
+ * face toward it where a space is against it. To another wall (`wall`): its room's side, as its length is measured —
466
+ * the side toward the other between two spaces; with no space beside it, its centerline.
467
+ */
468
+ function faceToward(sk, lid, at, side, wall = false) {
469
+ if (!faced(sk, lid))
470
+ return 0;
471
+ const toward = sideOf(sk, lid, at);
472
+ if (side === undefined)
473
+ return spaceBeside(sk, lid, toward) ? 1 : wall && spaceBeside(sk, lid, toward === 1 ? -1 : 1) ? -1 : 0;
474
+ return side === 0 ? 0 : side === toward ? 1 : -1;
475
+ }
476
+ /** From a wall's edge to a point: square to the wall, from that face. */
477
+ export function dimEdgePoint(lid, side, p) {
478
+ const sk = SK(), l = sk.lines[lid];
479
+ // (one of the wall's own ends is no distance from it)
480
+ if (!l || p === l.a || p === l.b) {
481
+ toast('Pick a point off the wall to measure from its edge.', { warn: true });
482
+ return;
483
+ }
484
+ dimPointLine(p, lid, { faces: [faceToward(sk, lid, sk.points[p], side), 0], title: 'Distance from the edge' });
485
+ }
486
+ /**
487
+ * Two things picked, at least one a wall's edge: the distance between them — two edges of one wall, its thickness
488
+ * (`thickness` opens it); of two walls, face to face; an edge and a line, from that face; an edge and a point, from it.
489
+ */
490
+ export function dimEdges(items, thickness) {
491
+ const edges = items.filter(x => x.type === 'wface').map(x => edgeOf(x.id));
492
+ const other = items.find(x => x.type !== 'wface');
493
+ if (edges.length === 2) {
494
+ const [[a, sa], [b, sb]] = edges;
495
+ if (a === b) {
496
+ thickness(a);
497
+ return;
498
+ }
499
+ if (SK().lines[a] && SK().lines[b])
500
+ dimTwoLines(a, b, { [a]: sa, [b]: sb });
501
+ return;
502
+ }
503
+ if (edges.length !== 1 || !other)
504
+ return;
505
+ const [lid, side] = edges[0];
506
+ if (other.type === 'point')
507
+ dimEdgePoint(lid, side, other.id);
508
+ else if (other.type === 'line')
509
+ dimTwoLines(lid, other.id, { [lid]: side });
220
510
  }
221
511
  export function dimPoints(a, b, print) {
222
512
  const sk = SK(), A = sk.points[a], B = sk.points[b];
@@ -234,9 +524,9 @@ export function dimPoints(a, b, print) {
234
524
  openKeypad({
235
525
  onClose: () => { ed.preview = null; requestDraw(); },
236
526
  title: 'Distance', initial: fmtLen(L), selectAll: true, laser: true,
237
- onDone: v => {
527
+ onDone: (v, capture) => {
238
528
  pushHistory();
239
- const r = tryAddConstraint({ type: 'dist', a, b, kind: 'al', value: v, driving: true, off: 0 });
529
+ const r = tryAddConstraint({ type: 'dist', a, b, kind: 'al', value: v, driving: true, off: 0, ...(capture !== undefined ? { capture } : {}) });
240
530
  if (!r.ok)
241
531
  dropHistory();
242
532
  ed.sel = [];
@@ -246,9 +536,11 @@ export function dimPoints(a, b, print) {
246
536
  }
247
537
  /**
248
538
  * Dimension between two lines: their distance if they're parallel (measured square to them, and kept parallel),
249
- * otherwise the angle between them. An existing dimension between them is edited instead of stacking another.
539
+ * otherwise the angle between them. An existing dimension between them is edited instead of stacking another. Between
540
+ * walls it's measured on `sides`, the edges picked (+1 / −1: the face to the left / right of A → B; 0 its centerline) —
541
+ * or, picked whole, each wall's room side (see faceToward).
250
542
  */
251
- export function dimTwoLines(l1, l2) {
543
+ export function dimTwoLines(l1, l2, sides = {}) {
252
544
  const sk = SK(), L1 = sk.lines[l1], L2 = sk.lines[l2];
253
545
  if (!L1 || !L2)
254
546
  return;
@@ -264,8 +556,15 @@ export function dimTwoLines(l1, l2) {
264
556
  return (c.l1 === l1 && c.l2 === l2) || (c.l1 === l2 && c.l2 === l1);
265
557
  return (c.l === l1 && (c.p === L2.a || c.p === L2.b)) || (c.l === l2 && (c.p === L1.a || c.p === L1.b));
266
558
  });
559
+ /** The faces between line `l` (the one measured square to) and the other, from point `p` on the other. */
560
+ const facesFrom = (l, p) => {
561
+ const o = l === l1 ? l2 : l1, ol = sk.lines[o], on = sk.lines[l];
562
+ return { faces: [faceToward(sk, l, sk.points[p], sides[l], true), faceToward(sk, o, sk.points[on.a], sides[o], true)], ...(faced(sk, o) && ol ? { l2: o } : {}) };
563
+ };
267
564
  if (existing) {
268
- editDim(existing);
565
+ const c = sk.cons[existing];
566
+ // (the edges picked: it's measured between them now)
567
+ editDim(existing, undefined, false, c.type === 'pldist' && Object.keys(sides).length ? facesFrom(c.l, c.p) : undefined);
269
568
  return;
270
569
  }
271
570
  if (!parallel) {
@@ -276,23 +575,38 @@ export function dimTwoLines(l1, l2) {
276
575
  const A = sk.points[L1.a], B = sk.points[L1.b], len2 = (B.x - A.x) ** 2 + (B.y - A.y) ** 2 || 1;
277
576
  const t = (pid) => { const P = sk.points[pid]; return ((P.x - A.x) * (B.x - A.x) + (P.y - A.y) * (B.y - A.y)) / len2; };
278
577
  const p = Math.abs(t(L2.a) - 0.5) <= Math.abs(t(L2.b) - 0.5) ? L2.a : L2.b;
279
- dimPointLine(p, l1, { keepParallel: l2, title: 'Distance between lines' });
578
+ const f = facesFrom(l1, p);
579
+ dimPointLine(p, l1, { keepParallel: l2, title: 'Distance between lines', faces: f.faces, l2: f.l2 });
280
580
  }
581
+ /**
582
+ * The distance from point `p` to line `lid`, square to it. On a wall it's measured from `faces[0]`, its face toward
583
+ * the point (+1), its far face (−1) or its centerline (0) — by default the face toward it where a space is against it;
584
+ * `faces[1]` is the same for wall `l2`, which the point is the end of (a distance between two walls). The keypad can
585
+ * switch them.
586
+ */
281
587
  export function dimPointLine(p, lid, opts = {}) {
282
588
  const sk = SK(), l = sk.lines[lid], P = sk.points[p], A = sk.points[l.a], B = sk.points[l.b];
283
589
  const sd = ((B.x - A.x) * (P.y - A.y) - (B.y - A.y) * (P.x - A.x)) / (hyp(B.x - A.x, B.y - A.y) || 1);
284
590
  const sign = sd < 0 ? -1 : 1;
285
- const t = ((P.x - A.x) * (B.x - A.x) + (P.y - A.y) * (B.y - A.y)) / ((B.x - A.x) ** 2 + (B.y - A.y) ** 2 || 1);
286
- const foot = { x: A.x + (B.x - A.x) * t, y: A.y + (B.y - A.y) * t };
591
+ const c = { type: 'pldist', p, l: lid, value: 0, sign, driving: true, ...(opts.l2 ? { l2: opts.l2 } : {}) };
592
+ applyFace(c, { faces: opts.faces ?? [faceToward(sk, lid, P), 0] });
593
+ const choices = faceChoices(sk, c);
287
594
  ed.sel = [];
288
- ed.preview = (c, T) => drawPendingDim(c, T, { from: P, to: foot, a: P, b: foot, nx: 0, ny: 0, off: 0, label: fmtLen(kpTyped() ?? Math.abs(sd)) });
595
+ ed.preview = (cx, T) => { const e = dimEnds(sk, c); if (e)
596
+ drawPendingDim(cx, T, { from: e.a, to: e.b, a: e.a, b: e.b, nx: 0, ny: 0, off: 0, label: fmtLen(kpTyped() ?? measure(sk, c)) }); };
289
597
  requestDraw();
290
- openKeypad({
598
+ const spec = {
291
599
  onClose: () => { ed.preview = null; requestDraw(); },
292
- title: opts.title || 'Distance to line', sub: opts.keepParallel ? 'Measured square to the lines' : '', initial: fmtLen(Math.abs(sd)), selectAll: true,
600
+ // (which faces it's measured between: the choices beside the keys)
601
+ title: opts.title || 'Distance to line', sub: opts.keepParallel ? 'Measured square to the lines' : '', initial: fmtLen(measure(sk, c)), selectAll: true,
602
+ extras: choices.length ? () => choices.map(ch => ({
603
+ label: ch.label, on: onChoice(c, ch),
604
+ onPress: () => { applyFace(c, ch); requestOverlayDraw(); if (keypadFresh())
605
+ keypadSetReading(measure(sk, c)); },
606
+ })) : undefined,
293
607
  onDone: v => {
294
608
  pushHistory();
295
- const r = tryAddConstraint({ type: 'pldist', p, l: lid, value: v, sign, driving: true });
609
+ const r = tryAddConstraint({ ...c, value: v });
296
610
  if (!r.ok)
297
611
  dropHistory();
298
612
  // a distance between two lines only holds if they stay parallel (no-op if something already keeps them so)
@@ -301,49 +615,102 @@ export function dimPointLine(p, lid, opts = {}) {
301
615
  ed.sel = [];
302
616
  afterEdit();
303
617
  },
304
- });
618
+ };
619
+ openKeypad(spec);
305
620
  }
306
- export function dimCurve(id) {
307
- const sk = SK();
621
+ /**
622
+ * A circle's or arc's diameter: its keypad, or its dimension's. A curved wall's is measured across `side`, the edge
623
+ * picked (+1 its outer face, −1 its inner) — or, picked whole, the side a space is on (the side it was tapped on,
624
+ * between two), and its centerline where there's no space; the keypad can switch it.
625
+ */
626
+ export function dimCurve(id, side) {
627
+ const sk = SK(), wall = !!wallOf(sk, id);
308
628
  for (const k in sk.cons) {
309
629
  const c = sk.cons[k];
310
630
  if (c.type === 'diam' && c.c === id) {
311
- editDim(k);
631
+ editDim(k, undefined, false, wall && side ? { face: side } : undefined);
312
632
  return;
313
633
  }
314
634
  }
315
- const cur = measure(sk, { type: 'diam', c: id });
316
- ed.sel = [{ type: sk.circles[id] ? 'circle' : 'arc', id }];
317
- requestDraw();
318
- openKeypad({
319
- title: 'Diameter', initial: fmtLen(cur), selectAll: true,
635
+ let face = wall ? side ?? defaultFace(sk, id, ed.wallTap?.id === id ? ed.wallTap.side : undefined) ?? 0 : 0;
636
+ const now = () => measure(sk, { type: 'diam', c: id, ...(face ? { face } : {}) });
637
+ // (while it's being entered, what it measures across is what's highlighted: the edge, or the wall)
638
+ const was = ed.sel, type = sk.circles[id] ? 'circle' : 'arc';
639
+ const show = () => { ed.sel = face ? [{ type: 'wface', id: edgeId(id, face) }] : [{ type, id }]; requestDraw(); };
640
+ show();
641
+ const spec = {
642
+ title: 'Diameter', sub: wall ? lengthSub(sk, id, face) : '', initial: fmtLen(now()), selectAll: true,
643
+ extras: wall ? () => curveChoices(sk, id).map(ch => ({
644
+ label: ch.label, on: ch.face === face,
645
+ onPress: () => { face = ch.face; spec.sub = lengthSub(sk, id, face); show(); if (keypadFresh())
646
+ keypadSetReading(now()); },
647
+ })) : undefined,
648
+ onClose: () => { ed.sel = was; requestDraw(); },
320
649
  onDone: v => {
321
650
  if (v <= 0)
322
651
  return;
323
652
  pushHistory();
324
- const r = tryAddConstraint({ type: 'diam', c: id, value: v, driving: true });
653
+ const r = tryAddConstraint({ type: 'diam', c: id, value: v, driving: true, ...(face ? { face } : {}) });
325
654
  if (!r.ok)
326
655
  dropHistory();
656
+ // (as a length: the dimension made, nothing left highlighted)
657
+ ed.sel = [];
327
658
  afterEdit();
328
659
  },
329
- });
660
+ };
661
+ openKeypad(spec);
662
+ }
663
+ /** A corner's angle (two lines meeting at `pid`): its angle dimension's keypad, or a new one's. False if it isn't a corner. */
664
+ export function cornerAngle(pid, done, modeless = false) {
665
+ const sk = SK(), pair = cornerLines(sk, pid);
666
+ if (!pair)
667
+ return false;
668
+ // edit the corner's existing angle dimension rather than stacking a second one
669
+ const ex = Object.keys(sk.cons).find(id => { const c = sk.cons[id]; return c.type === 'angle' && c.layer !== 'print' && ((c.l1 === pair[0] && c.l2 === pair[1]) || (c.l1 === pair[1] && c.l2 === pair[0])); });
670
+ if (ex)
671
+ editDim(ex, done, modeless);
672
+ else
673
+ dimAngle(pair[0], pair[1], done, modeless);
674
+ return true;
330
675
  }
331
- export function dimAngle(l1, l2) {
676
+ export function dimAngle(l1, l2, done, modeless = false) {
332
677
  const c = { type: 'angle', l1, l2, value: 0, driving: true };
333
678
  const cur = measure(SK(), c);
679
+ /** The angle dimension, driving, at `v` degrees (`extra`: what it's from). */
680
+ const addAngle = (v, extra) => {
681
+ pushHistory();
682
+ const r = tryAddConstraint({ ...c, value: v, ...extra });
683
+ // a measurement of the job drives an angle, or nothing: not one the drawing already sets
684
+ if (r.ok && r.ref && extra.measurementRef) {
685
+ delete SK().cons[r.id];
686
+ dropHistory();
687
+ toast(ANGLE_SET_ALREADY, { warn: true });
688
+ }
689
+ else if (!r.ok)
690
+ dropHistory();
691
+ afterEdit();
692
+ done?.();
693
+ };
334
694
  openKeypad({
335
- title: 'Angle', sub: 'Degrees', numeric: true, initial: String(Math.round(cur * 10) / 10), selectAll: true,
336
- onDone: v => {
337
- pushHistory();
338
- c.value = v;
339
- const r = tryAddConstraint(c);
340
- if (!r.ok)
341
- dropHistory();
342
- afterEdit();
343
- },
695
+ title: 'Angle', sub: 'Degrees', numeric: true, initial: String(Math.round(cur * 10) / 10), selectAll: true, modeless,
696
+ onDone: v => addAngle(v, {}),
697
+ // (or an angle of the job's, from its list: the corner takes it, linked to it)
698
+ measurements: offersJobMeasurements() ? {
699
+ kind: 'angle', pick: t => {
700
+ const v = jobValue(t, 'angle');
701
+ if (v == null)
702
+ return;
703
+ closeKeypad();
704
+ addAngle(v, { measurementRef: linkSnapshot(t) });
705
+ },
706
+ } : undefined,
344
707
  });
345
708
  }
346
- export function editDim(id) {
709
+ /**
710
+ * The keypad for a dimension. One measured on walls can be switched to their other faces (or centerlines) there — to
711
+ * `pick` straight away — taken with the value on Apply.
712
+ */
713
+ export function editDim(id, done, modeless = false, pick) {
347
714
  const sk = SK(), c = sk.cons[id];
348
715
  if (!c)
349
716
  return;
@@ -351,57 +718,175 @@ export function editDim(id) {
351
718
  requestDraw();
352
719
  updateCtx();
353
720
  const isAng = c.type === 'angle';
354
- const cur = c.driving === false ? measure(sk, c) : c.value;
355
- const title = c.type === 'diam' ? 'Diameter' : isAng ? 'Angle' : c.type === 'pldist' ? 'Offset distance' : c.line ? 'Length' : 'Distance';
356
- openKeypad({
721
+ // a fillet's radius is kept as its diameter: shown and entered as the radius
722
+ const k = c.rad ? 2 : 1;
723
+ let face = pick && !onChoice(c, pick) ? pick : null;
724
+ /** What it measures on the faces chosen (as it stands, for one that doesn't change them). */
725
+ const now = () => (face ? measure(sk, withFace(c, face)) : c.driving === false ? measure(sk, c) : c.value) / k;
726
+ const title = c.type === 'diam' ? (c.rad ? 'Radius' : 'Diameter') : isAng ? 'Angle' : c.type === 'pldist' ? 'Offset distance' : c.line ? 'Length' : 'Distance';
727
+ // (which faces it's measured on: the choices beside the keys)
728
+ const spec = {
357
729
  title,
358
730
  sub: c.driving === false ? (c.layer === 'print' ? 'Print dimension (label only)' : 'Reference — not driving the shape') : 'Driving dimension',
359
- numeric: isAng, initial: isAng ? String(Math.round(cur * 10) / 10) : fmtLen(cur), selectAll: true, laser: !isAng,
360
- extras: () => [
361
- ...(c.layer !== 'print' ? [{ label: c.driving === false ? 'Make driving' : 'Make reference', onPress: () => { closeKeypad(); toggleRef(id); } }] : []),
362
- { label: 'Delete', danger: true, onPress: () => { closeKeypad(); pushHistory(); delete SK().cons[id]; ed.sel = []; afterEdit(); } },
363
- ],
364
- onDone: v => setDimValue(id, v),
365
- });
731
+ numeric: isAng, initial: isAng ? String(Math.round(now() * 10) / 10) : fmtLen(now()), selectAll: true, laser: !isAng, modeless,
732
+ // (just the faces it's measured on: Reference and Delete are the bar's, for a dimension selected)
733
+ extras: () => faceChoices(sk, withFace(c, face)).map(ch => ({
734
+ label: ch.label, on: onChoice(withFace(c, face), ch),
735
+ onPress: () => { face = onChoice(c, ch) ? null : { ...ch, l2: face?.l2 ?? ch.l2 }; if (keypadFresh())
736
+ keypadSetReading(now()); },
737
+ })),
738
+ onDone: (v, capture) => { setDimValue(id, v * k, face ?? undefined, { capture }); done?.(); },
739
+ // a line's length or an angle: or one of the job's, from its list — the dimension takes it, linked to it (a
740
+ // dimension already linked to another, re-linked)
741
+ measurements: ((c.type === 'dist' && c.line) || isAng) && c.layer !== 'print' && offersJobMeasurements() ? {
742
+ kind: isAng ? 'angle' : 'length', pick: t => {
743
+ const v = jobValue(t, isAng ? 'angle' : 'length');
744
+ if (v == null)
745
+ return;
746
+ closeKeypad();
747
+ setDimValue(id, v, face ?? undefined, { link: linkSnapshot(t), asked: true });
748
+ done?.();
749
+ },
750
+ } : undefined,
751
+ };
752
+ openKeypad(spec);
366
753
  }
367
754
  const fmtValue = (c, v) => (c.type === 'angle' ? fmtAng(v) : fmtLen(v));
368
- export function setDimValue(id, v) {
755
+ /**
756
+ * For a cabinet's width or height on a wall face: which of its corners to keep still while the new size is met, in
757
+ * the order to try — its bottom, then its top (height); its left side, then its right (width). Empty otherwise.
758
+ */
759
+ function holdFirst(sk, id) {
760
+ for (const cb of Object.values(sk.cabs ?? {})) {
761
+ if (cb.h === id)
762
+ return [[cb.bl, cb.br], [cb.tl, cb.tr]];
763
+ if (cb.w === id)
764
+ return [[cb.bl, cb.tl], [cb.br, cb.tr]];
765
+ }
766
+ return [];
767
+ }
768
+ /** What a new value brings with it, once it's taken: a link (or none), and the laser reading it's from (or none). */
769
+ function takeLink(c, o) {
770
+ if (o.link) {
771
+ c.measurementRef = o.link;
772
+ delete c.measurementId;
773
+ }
774
+ else if (o.link === null)
775
+ delete c.measurementRef;
776
+ // (a reading's record is kept for saving to the group; any other value isn't a reading's)
777
+ if (o.capture !== undefined && !c.measurementRef)
778
+ c.capture = o.capture;
779
+ else if (!o.quiet)
780
+ delete c.capture;
781
+ }
782
+ /**
783
+ * Give dimension `id` value `v`, solving the drawing to it (or explaining why it can't). Whether it took — false too
784
+ * when the person is asked first, for a dimension linked to a measurement of the job (see editor/linkWrite.ts).
785
+ */
786
+ export function setDimValue(id, v, face, opts = {}) {
369
787
  const sk = SK(), c = sk.cons[id];
370
788
  if (!c)
371
- return;
789
+ return false;
372
790
  if (c.layer === 'print') {
373
- toast('Print dimensions follow the drawing. Change a driving dimension instead.');
374
- return;
791
+ toast('Print dimensions follow the drawing. Change a driving dimension instead.', { warn: true });
792
+ return false;
793
+ }
794
+ if (c.measurementRef && opts.link === undefined && !opts.asked && !opts.quiet && changesValue(c, v)) {
795
+ askLinkedChange(c, v, unlink => setDimValue(id, v, face, { ...opts, asked: true, ...(unlink ? { link: null } : {}) }), opts.capture);
796
+ return false;
375
797
  }
798
+ const push = opts.quiet ? () => { } : pushHistory, drop = opts.quiet ? () => { } : dropHistory;
376
799
  if (c.driving === false) {
377
- pushHistory();
800
+ push();
378
801
  delete sk.cons[id];
379
- const r = tryAddConstraint(Object.assign({}, c, { driving: true, value: v }));
802
+ const n = withFace(Object.assign({}, c, { driving: true, value: v }), face ?? null);
803
+ delete n.auto; // entered: a measurement now
804
+ takeLink(n, opts);
805
+ const r = tryAddConstraint(n);
380
806
  if (!r.ok) {
381
807
  sk.cons[id] = c;
382
- dropHistory();
808
+ drop();
809
+ }
810
+ else if (r.id) {
811
+ // a cabinet's size on a wall face: it's this one now
812
+ for (const cb of Object.values(sk.cabs ?? {})) {
813
+ if (cb.w === id)
814
+ cb.w = r.id;
815
+ if (cb.h === id)
816
+ cb.h = r.id;
817
+ }
383
818
  }
384
819
  afterEdit();
385
- return;
820
+ return r.ok;
386
821
  }
387
- pushHistory();
388
- const old = c.value;
822
+ push();
823
+ const old = c.value, wasAuto = c.auto, was = overflow(sk), oldFace = faceState(c);
389
824
  c.value = v;
390
- const snap = snapshotPos(sk);
391
- const s = solver.solve(sk);
825
+ if (face)
826
+ applyFace(c, face);
827
+ delete c.auto; // entered: a measurement now
828
+ const snap = fitState(sk);
829
+ // a cabinet's own size grows from the side it stands on: its height upward (its foot stays on the floor), its
830
+ // width to the right — and only the other way if that would run past the wall
831
+ const holding = (hold) => {
832
+ // (held exactly: fixed points aren't moved by the solver at all; put back as they were after)
833
+ const pts = hold.map(pid => sk.points[pid]).filter(Boolean), had = pts.map(p => p.fixed);
834
+ pts.forEach(p => { p.fixed = true; });
835
+ const r = solver.solve(sk);
836
+ pts.forEach((p, i) => { if (had[i])
837
+ p.fixed = true;
838
+ else
839
+ delete p.fixed; });
840
+ return r;
841
+ };
842
+ let s = null, fit = false, solved = false;
843
+ // (a cabinet's own size: its sides in order; any other dimension: as it lands, then keeping each thing it ties still)
844
+ const own = holdFirst(sk, id), holds = own.length ? [...own, null] : [null, ...holdGroups(sk, c)];
845
+ for (const hold of holds) {
846
+ if (s)
847
+ restoreFit(sk, snap);
848
+ s = hold ? holding(hold) : solver.solve(sk);
849
+ solved ||= s.ok;
850
+ if (s.ok && !pushedOut(was, overflow(sk))) {
851
+ fit = true;
852
+ break;
853
+ }
854
+ }
855
+ s = s;
856
+ // a value that would push anything past its wall (or make a wall shorter than what's on it) is refused
857
+ if (solved && !fit) {
858
+ restoreFit(sk, snap);
859
+ c.value = old;
860
+ restoreFace(c, oldFace);
861
+ if (wasAuto)
862
+ c.auto = true;
863
+ drop();
864
+ if (!opts.quiet)
865
+ toast(pastWallMsg(), { warn: true });
866
+ afterEdit();
867
+ return false;
868
+ }
392
869
  if (!s.ok) {
393
- restorePos(sk, snap);
870
+ restoreFit(sk, snap);
394
871
  c.value = old;
395
- dropHistory();
872
+ restoreFace(c, oldFace);
873
+ if (wasAuto)
874
+ c.auto = true;
875
+ drop();
396
876
  const bad = s.bad.filter(cid => cid !== id && sk.cons[cid]);
397
- showConflict({
398
- title: fmtValue(c, v) + ' doesn’t fit',
399
- msg: 'The other constraints can’t hold with that value. Remove one of these, or try a different number.',
400
- ids: bad.slice(0, 8),
401
- retry: () => setDimValue(id, v),
402
- });
877
+ if (!opts.quiet)
878
+ showConflict({
879
+ title: fmtValue(c, v) + ' doesn’t fit',
880
+ msg: 'The other constraints can’t hold with that value. Remove one of these, or try a different number.',
881
+ ids: bad.slice(0, 8),
882
+ retry: () => setDimValue(id, v, face, { ...opts, asked: true }),
883
+ });
884
+ afterEdit();
885
+ return false;
403
886
  }
887
+ takeLink(c, opts);
404
888
  afterEdit();
889
+ return true;
405
890
  }
406
891
  export function toggleRef(id) {
407
892
  const sk = SK(), c = sk.cons[id];