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