@reekon-tools/boldr-cad 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (547) hide show
  1. package/README.md +81 -9
  2. package/dist/app/edit.d.ts +12 -1
  3. package/dist/app/edit.js +96 -11
  4. package/dist/app/edit.js.map +1 -1
  5. package/dist/app/state.d.ts +31 -4
  6. package/dist/app/state.js +31 -2
  7. package/dist/app/state.js.map +1 -1
  8. package/dist/data/memory.d.ts +5 -0
  9. package/dist/data/memory.js +2 -2
  10. package/dist/data/memory.js.map +1 -1
  11. package/dist/data/provider.d.ts +19 -7
  12. package/dist/data/sync.d.ts +7 -2
  13. package/dist/data/sync.js +115 -64
  14. package/dist/data/sync.js.map +1 -1
  15. package/dist/editor/actions.d.ts +1 -2
  16. package/dist/editor/actions.js +391 -113
  17. package/dist/editor/actions.js.map +1 -1
  18. package/dist/editor/confirm.d.ts +23 -0
  19. package/dist/editor/confirm.js +35 -0
  20. package/dist/editor/confirm.js.map +1 -0
  21. package/dist/editor/elevation.d.ts +191 -90
  22. package/dist/editor/elevation.js +812 -616
  23. package/dist/editor/elevation.js.map +1 -1
  24. package/dist/editor/face.d.ts +73 -0
  25. package/dist/editor/face.js +269 -0
  26. package/dist/editor/face.js.map +1 -0
  27. package/dist/editor/files.d.ts +6 -0
  28. package/dist/editor/files.js +50 -4
  29. package/dist/editor/files.js.map +1 -1
  30. package/dist/editor/gestures.d.ts +18 -3
  31. package/dist/editor/gestures.js +145 -30
  32. package/dist/editor/gestures.js.map +1 -1
  33. package/dist/editor/keypad.d.ts +19 -4
  34. package/dist/editor/keypad.js +39 -9
  35. package/dist/editor/keypad.js.map +1 -1
  36. package/dist/editor/keys.js +36 -4
  37. package/dist/editor/keys.js.map +1 -1
  38. package/dist/editor/laser.d.ts +20 -4
  39. package/dist/editor/laser.js +104 -21
  40. package/dist/editor/laser.js.map +1 -1
  41. package/dist/editor/linkActions.d.ts +49 -0
  42. package/dist/editor/linkActions.js +210 -0
  43. package/dist/editor/linkActions.js.map +1 -0
  44. package/dist/editor/linkFollow.d.ts +2 -0
  45. package/dist/editor/linkFollow.js +53 -0
  46. package/dist/editor/linkFollow.js.map +1 -0
  47. package/dist/editor/linkWrite.d.ts +13 -0
  48. package/dist/editor/linkWrite.js +84 -0
  49. package/dist/editor/linkWrite.js.map +1 -0
  50. package/dist/editor/links.d.ts +84 -0
  51. package/dist/editor/links.js +130 -0
  52. package/dist/editor/links.js.map +1 -0
  53. package/dist/editor/markup.d.ts +29 -3
  54. package/dist/editor/markup.js +95 -10
  55. package/dist/editor/markup.js.map +1 -1
  56. package/dist/editor/mode.d.ts +40 -2
  57. package/dist/editor/mode.js +71 -4
  58. package/dist/editor/mode.js.map +1 -1
  59. package/dist/editor/ports.d.ts +36 -6
  60. package/dist/editor/ports.js +10 -1
  61. package/dist/editor/ports.js.map +1 -1
  62. package/dist/editor/project.d.ts +38 -6
  63. package/dist/editor/project.js +167 -27
  64. package/dist/editor/project.js.map +1 -1
  65. package/dist/editor/selection.d.ts +10 -0
  66. package/dist/editor/selection.js +62 -8
  67. package/dist/editor/selection.js.map +1 -1
  68. package/dist/editor/status.js +3 -0
  69. package/dist/editor/status.js.map +1 -1
  70. package/dist/editor/tooltips.d.ts +5 -0
  71. package/dist/editor/tooltips.js +48 -0
  72. package/dist/editor/tooltips.js.map +1 -0
  73. package/dist/editor/view3d.d.ts +17 -0
  74. package/dist/editor/view3d.js +135 -0
  75. package/dist/editor/view3d.js.map +1 -0
  76. package/dist/editor/view3dState.d.ts +39 -0
  77. package/dist/editor/view3dState.js +44 -0
  78. package/dist/editor/view3dState.js.map +1 -0
  79. package/dist/export/dxf.js +1 -1
  80. package/dist/export/dxf.js.map +1 -1
  81. package/dist/export/pdf.js +17 -12
  82. package/dist/export/pdf.js.map +1 -1
  83. package/dist/export/plan.d.ts +23 -6
  84. package/dist/export/plan.js +127 -44
  85. package/dist/export/plan.js.map +1 -1
  86. package/dist/index.d.ts +5 -0
  87. package/dist/index.js +2 -0
  88. package/dist/index.js.map +1 -1
  89. package/dist/model/constraints.js +15 -4
  90. package/dist/model/constraints.js.map +1 -1
  91. package/dist/model/demo.js +3 -3
  92. package/dist/model/demo.js.map +1 -1
  93. package/dist/model/elevation.d.ts +6 -9
  94. package/dist/model/elevation.js +5 -13
  95. package/dist/model/elevation.js.map +1 -1
  96. package/dist/model/faces.d.ts +58 -0
  97. package/dist/model/faces.js +197 -0
  98. package/dist/model/faces.js.map +1 -0
  99. package/dist/model/facesolve.d.ts +72 -0
  100. package/dist/model/facesolve.js +369 -0
  101. package/dist/model/facesolve.js.map +1 -0
  102. package/dist/model/history.d.ts +3 -2
  103. package/dist/model/history.js +17 -11
  104. package/dist/model/history.js.map +1 -1
  105. package/dist/model/images.d.ts +18 -0
  106. package/dist/model/images.js +26 -0
  107. package/dist/model/images.js.map +1 -0
  108. package/dist/model/links.d.ts +46 -0
  109. package/dist/model/links.js +73 -0
  110. package/dist/model/links.js.map +1 -0
  111. package/dist/model/markup.d.ts +46 -0
  112. package/dist/model/markup.js +71 -0
  113. package/dist/model/markup.js.map +1 -0
  114. package/dist/model/measurement.d.ts +11 -0
  115. package/dist/model/measurement.js +24 -0
  116. package/dist/model/measurement.js.map +1 -0
  117. package/dist/model/openings.d.ts +4 -0
  118. package/dist/model/openings.js +4 -0
  119. package/dist/model/openings.js.map +1 -1
  120. package/dist/model/planGeom.d.ts +72 -2
  121. package/dist/model/planGeom.js +184 -10
  122. package/dist/model/planGeom.js.map +1 -1
  123. package/dist/model/project.js +3 -2
  124. package/dist/model/project.js.map +1 -1
  125. package/dist/model/projectfile.js +2 -1
  126. package/dist/model/projectfile.js.map +1 -1
  127. package/dist/model/rooms.js +2 -2
  128. package/dist/model/rooms.js.map +1 -1
  129. package/dist/model/settings.d.ts +6 -0
  130. package/dist/model/settings.js +2 -2
  131. package/dist/model/settings.js.map +1 -1
  132. package/dist/model/sketch.d.ts +14 -0
  133. package/dist/model/sketch.js +143 -24
  134. package/dist/model/sketch.js.map +1 -1
  135. package/dist/model/smooth.d.ts +6 -0
  136. package/dist/model/smooth.js +52 -0
  137. package/dist/model/smooth.js.map +1 -0
  138. package/dist/model/store.d.ts +12 -0
  139. package/dist/model/store.js +35 -3
  140. package/dist/model/store.js.map +1 -1
  141. package/dist/model/types.d.ts +173 -8
  142. package/dist/model/wallfaces.d.ts +32 -0
  143. package/dist/model/wallfaces.js +133 -20
  144. package/dist/model/wallfaces.js.map +1 -1
  145. package/dist/model/wallshape.d.ts +15 -0
  146. package/dist/model/wallshape.js +89 -0
  147. package/dist/model/wallshape.js.map +1 -0
  148. package/dist/react/BottomBar.d.ts +9 -5
  149. package/dist/react/BottomBar.js +213 -24
  150. package/dist/react/BottomBar.js.map +1 -1
  151. package/dist/react/Buttons.d.ts +13 -2
  152. package/dist/react/Buttons.js +37 -5
  153. package/dist/react/Buttons.js.map +1 -1
  154. package/dist/react/CadEditor.d.ts +46 -3
  155. package/dist/react/CadEditor.js +77 -20
  156. package/dist/react/CadEditor.js.map +1 -1
  157. package/dist/react/ConfirmSheet.d.ts +4 -0
  158. package/dist/react/ConfirmSheet.js +31 -0
  159. package/dist/react/ConfirmSheet.js.map +1 -0
  160. package/dist/react/ConflictSheet.d.ts +2 -1
  161. package/dist/react/ConflictSheet.js +16 -8
  162. package/dist/react/ConflictSheet.js.map +1 -1
  163. package/dist/react/Drawer.d.ts +8 -2
  164. package/dist/react/Drawer.js +167 -38
  165. package/dist/react/Drawer.js.map +1 -1
  166. package/dist/react/Duck.d.ts +13 -0
  167. package/dist/react/Duck.js +45 -0
  168. package/dist/react/Duck.js.map +1 -0
  169. package/dist/react/Empty3D.d.ts +2 -0
  170. package/dist/react/Empty3D.js +39 -0
  171. package/dist/react/Empty3D.js.map +1 -0
  172. package/dist/react/FitScroll.d.ts +6 -0
  173. package/dist/react/FitScroll.js +14 -0
  174. package/dist/react/FitScroll.js.map +1 -0
  175. package/dist/react/Icon.native.d.ts +6 -0
  176. package/dist/react/Icon.native.js +4 -0
  177. package/dist/react/Icon.native.js.map +1 -1
  178. package/dist/react/Icon.web.d.ts +6 -0
  179. package/dist/react/Icon.web.js +4 -0
  180. package/dist/react/Icon.web.js.map +1 -1
  181. package/dist/react/KeypadSheet.d.ts +9 -2
  182. package/dist/react/KeypadSheet.js +174 -42
  183. package/dist/react/KeypadSheet.js.map +1 -1
  184. package/dist/react/LaserTray.js +8 -5
  185. package/dist/react/LaserTray.js.map +1 -1
  186. package/dist/react/MarkupPanel.d.ts +8 -0
  187. package/dist/react/MarkupPanel.js +67 -0
  188. package/dist/react/MarkupPanel.js.map +1 -0
  189. package/dist/react/PageThumb.native.d.ts +7 -2
  190. package/dist/react/PageThumb.native.js +19 -7
  191. package/dist/react/PageThumb.native.js.map +1 -1
  192. package/dist/react/PageThumb.web.d.ts +3 -2
  193. package/dist/react/PageThumb.web.js +4 -4
  194. package/dist/react/PageThumb.web.js.map +1 -1
  195. package/dist/react/PlanCanvas.native.d.ts +1 -1
  196. package/dist/react/PlanCanvas.native.js +134 -36
  197. package/dist/react/PlanCanvas.native.js.map +1 -1
  198. package/dist/react/PlanCanvas.web.js +16 -5
  199. package/dist/react/PlanCanvas.web.js.map +1 -1
  200. package/dist/react/Popovers.d.ts +12 -6
  201. package/dist/react/Popovers.js +53 -26
  202. package/dist/react/Popovers.js.map +1 -1
  203. package/dist/react/Rail.d.ts +24 -0
  204. package/dist/react/Rail.js +324 -34
  205. package/dist/react/Rail.js.map +1 -1
  206. package/dist/react/ShapeMenu.d.ts +7 -0
  207. package/dist/react/ShapeMenu.js +58 -0
  208. package/dist/react/ShapeMenu.js.map +1 -0
  209. package/dist/react/Sheet.d.ts +14 -4
  210. package/dist/react/Sheet.js +54 -9
  211. package/dist/react/Sheet.js.map +1 -1
  212. package/dist/react/Slider.d.ts +18 -0
  213. package/dist/react/Slider.js +27 -0
  214. package/dist/react/Slider.js.map +1 -0
  215. package/dist/react/Toast.js +14 -6
  216. package/dist/react/Toast.js.map +1 -1
  217. package/dist/react/ToolTip.d.ts +7 -0
  218. package/dist/react/ToolTip.js +43 -0
  219. package/dist/react/ToolTip.js.map +1 -0
  220. package/dist/react/TopBar.d.ts +6 -3
  221. package/dist/react/TopBar.js +48 -17
  222. package/dist/react/TopBar.js.map +1 -1
  223. package/dist/react/Tween.d.ts +7 -0
  224. package/dist/react/Tween.js +26 -0
  225. package/dist/react/Tween.js.map +1 -0
  226. package/dist/react/Tween.native.d.ts +6 -0
  227. package/dist/react/Tween.native.js +43 -0
  228. package/dist/react/Tween.native.js.map +1 -0
  229. package/dist/react/View3D.native.d.ts +4 -0
  230. package/dist/react/View3D.native.js +103 -0
  231. package/dist/react/View3D.native.js.map +1 -0
  232. package/dist/react/View3D.web.d.ts +4 -0
  233. package/dist/react/View3D.web.js +151 -0
  234. package/dist/react/View3D.web.js.map +1 -0
  235. package/dist/react/View3DHost.d.ts +143 -0
  236. package/dist/react/View3DHost.js +377 -0
  237. package/dist/react/View3DHost.js.map +1 -0
  238. package/dist/react/corners.d.ts +7 -0
  239. package/dist/react/corners.js +51 -0
  240. package/dist/react/corners.js.map +1 -0
  241. package/dist/react/easing.d.ts +15 -0
  242. package/dist/react/easing.js +10 -0
  243. package/dist/react/easing.js.map +1 -0
  244. package/dist/react/host.d.ts +7 -2
  245. package/dist/react/host.js +8 -3
  246. package/dist/react/host.js.map +1 -1
  247. package/dist/react/hostKeypad.d.ts +56 -0
  248. package/dist/react/hostKeypad.js +5 -0
  249. package/dist/react/hostKeypad.js.map +1 -0
  250. package/dist/react/motion.d.ts +77 -0
  251. package/dist/react/motion.js +173 -0
  252. package/dist/react/motion.js.map +1 -0
  253. package/dist/react/ports.js +90 -8
  254. package/dist/react/ports.js.map +1 -1
  255. package/dist/react/saving.d.ts +15 -0
  256. package/dist/react/saving.js +76 -0
  257. package/dist/react/saving.js.map +1 -0
  258. package/dist/react/store.d.ts +38 -1
  259. package/dist/react/store.js +31 -1
  260. package/dist/react/store.js.map +1 -1
  261. package/dist/react/theme.d.ts +10 -0
  262. package/dist/react/theme.js +8 -0
  263. package/dist/react/theme.js.map +1 -1
  264. package/dist/react/topRow.d.ts +22 -0
  265. package/dist/react/topRow.js +61 -0
  266. package/dist/react/topRow.js.map +1 -0
  267. package/dist/react/touch.d.ts +6 -0
  268. package/dist/react/touch.js +9 -0
  269. package/dist/react/touch.js.map +1 -0
  270. package/dist/render/ctx.d.ts +8 -0
  271. package/dist/render/draw.d.ts +33 -1
  272. package/dist/render/draw.js +593 -215
  273. package/dist/render/draw.js.map +1 -1
  274. package/dist/render/elevation.d.ts +65 -43
  275. package/dist/render/elevation.js +414 -140
  276. package/dist/render/elevation.js.map +1 -1
  277. package/dist/render/figmaIcons.d.ts +12 -0
  278. package/dist/render/figmaIcons.js +21 -0
  279. package/dist/render/figmaIcons.js.map +1 -0
  280. package/dist/render/hit.d.ts +22 -2
  281. package/dist/render/hit.js +168 -25
  282. package/dist/render/hit.js.map +1 -1
  283. package/dist/render/icons.js +69 -10
  284. package/dist/render/icons.js.map +1 -1
  285. package/dist/render/linkTile.d.ts +15 -0
  286. package/dist/render/linkTile.js +369 -0
  287. package/dist/render/linkTile.js.map +1 -0
  288. package/dist/render/markup.d.ts +11 -0
  289. package/dist/render/markup.js +112 -0
  290. package/dist/render/markup.js.map +1 -0
  291. package/dist/render/navigate.d.ts +88 -0
  292. package/dist/render/navigate.js +100 -0
  293. package/dist/render/navigate.js.map +1 -0
  294. package/dist/render/snap.d.ts +20 -1
  295. package/dist/render/snap.js +118 -25
  296. package/dist/render/snap.js.map +1 -1
  297. package/dist/render/squircle.d.ts +8 -0
  298. package/dist/render/squircle.js +71 -0
  299. package/dist/render/squircle.js.map +1 -0
  300. package/dist/render/view.d.ts +115 -2
  301. package/dist/render/view.js +244 -19
  302. package/dist/render/view.js.map +1 -1
  303. package/dist/skia/SkiaDrawCtx.d.ts +30 -2
  304. package/dist/skia/SkiaDrawCtx.js +198 -43
  305. package/dist/skia/SkiaDrawCtx.js.map +1 -1
  306. package/dist/skia/host.d.ts +18 -2
  307. package/dist/skia/host.js +46 -1
  308. package/dist/skia/host.js.map +1 -1
  309. package/dist/solver/equations.js +37 -2
  310. package/dist/solver/equations.js.map +1 -1
  311. package/dist/solver/gauss-newton.d.ts +17 -0
  312. package/dist/solver/gauss-newton.js +144 -36
  313. package/dist/solver/gauss-newton.js.map +1 -1
  314. package/dist/solver/linalg.d.ts +45 -2
  315. package/dist/solver/linalg.js +164 -10
  316. package/dist/solver/linalg.js.map +1 -1
  317. package/dist/solver/types.d.ts +5 -0
  318. package/dist/tools/annotate.d.ts +57 -1
  319. package/dist/tools/annotate.js +64 -43
  320. package/dist/tools/annotate.js.map +1 -1
  321. package/dist/tools/chamfer.d.ts +12 -0
  322. package/dist/tools/chamfer.js +114 -0
  323. package/dist/tools/chamfer.js.map +1 -0
  324. package/dist/tools/corner.d.ts +38 -0
  325. package/dist/tools/corner.js +99 -0
  326. package/dist/tools/corner.js.map +1 -0
  327. package/dist/tools/dim.d.ts +86 -9
  328. package/dist/tools/dim.js +581 -96
  329. package/dist/tools/dim.js.map +1 -1
  330. package/dist/tools/elevation.js +1 -1
  331. package/dist/tools/elevation.js.map +1 -1
  332. package/dist/tools/fillet.d.ts +9 -0
  333. package/dist/tools/fillet.js +86 -0
  334. package/dist/tools/fillet.js.map +1 -0
  335. package/dist/tools/freehand.d.ts +3 -2
  336. package/dist/tools/freehand.js +19 -12
  337. package/dist/tools/freehand.js.map +1 -1
  338. package/dist/tools/index.js +14 -4
  339. package/dist/tools/index.js.map +1 -1
  340. package/dist/tools/line.d.ts +8 -3
  341. package/dist/tools/line.js +66 -30
  342. package/dist/tools/line.js.map +1 -1
  343. package/dist/tools/modify.d.ts +13 -0
  344. package/dist/tools/modify.js +105 -25
  345. package/dist/tools/modify.js.map +1 -1
  346. package/dist/tools/openings.js +22 -15
  347. package/dist/tools/openings.js.map +1 -1
  348. package/dist/tools/registry.d.ts +8 -0
  349. package/dist/tools/registry.js +42 -2
  350. package/dist/tools/registry.js.map +1 -1
  351. package/dist/tools/select.d.ts +77 -3
  352. package/dist/tools/select.js +560 -74
  353. package/dist/tools/select.js.map +1 -1
  354. package/dist/tools/shapes.d.ts +14 -0
  355. package/dist/tools/shapes.js +154 -75
  356. package/dist/tools/shapes.js.map +1 -1
  357. package/dist/units/format.d.ts +10 -2
  358. package/dist/units/format.js +18 -3
  359. package/dist/units/format.js.map +1 -1
  360. package/dist/view3d/build.d.ts +35 -0
  361. package/dist/view3d/build.js +630 -0
  362. package/dist/view3d/build.js.map +1 -0
  363. package/dist/view3d/camera.d.ts +60 -0
  364. package/dist/view3d/camera.js +84 -0
  365. package/dist/view3d/camera.js.map +1 -0
  366. package/dist/view3d/fatLines.d.ts +11 -0
  367. package/dist/view3d/fatLines.js +83 -0
  368. package/dist/view3d/fatLines.js.map +1 -0
  369. package/dist/view3d/lines.d.ts +17 -0
  370. package/dist/view3d/lines.js +271 -0
  371. package/dist/view3d/lines.js.map +1 -0
  372. package/dist/view3d/mesh.d.ts +51 -0
  373. package/dist/view3d/mesh.js +101 -0
  374. package/dist/view3d/mesh.js.map +1 -0
  375. package/dist/view3d/poly.d.ts +16 -0
  376. package/dist/view3d/poly.js +109 -0
  377. package/dist/view3d/poly.js.map +1 -0
  378. package/dist/view3d/viewer.d.ts +86 -0
  379. package/dist/view3d/viewer.js +350 -0
  380. package/dist/view3d/viewer.js.map +1 -0
  381. package/package.json +15 -2
  382. package/src/app/edit.ts +83 -12
  383. package/src/app/main.ts +11 -5
  384. package/src/app/state.ts +38 -5
  385. package/src/data/memory.ts +2 -2
  386. package/src/data/provider.ts +15 -7
  387. package/src/data/sync.ts +87 -50
  388. package/src/editor/actions.ts +292 -83
  389. package/src/editor/confirm.ts +58 -0
  390. package/src/editor/elevation.ts +698 -478
  391. package/src/editor/face.ts +227 -0
  392. package/src/editor/files.ts +40 -4
  393. package/src/editor/gestures.ts +147 -31
  394. package/src/editor/keypad.ts +43 -11
  395. package/src/editor/keys.ts +25 -5
  396. package/src/editor/laser.ts +98 -20
  397. package/src/editor/linkActions.ts +194 -0
  398. package/src/editor/linkFollow.ts +43 -0
  399. package/src/editor/linkWrite.ts +77 -0
  400. package/src/editor/links.ts +181 -0
  401. package/src/editor/markup.ts +92 -8
  402. package/src/editor/mode.ts +83 -7
  403. package/src/editor/ports.ts +37 -6
  404. package/src/editor/project.ts +155 -26
  405. package/src/editor/selection.ts +54 -8
  406. package/src/editor/status.ts +2 -0
  407. package/src/editor/tooltips.ts +47 -0
  408. package/src/editor/view3d.ts +119 -0
  409. package/src/editor/view3dState.ts +61 -0
  410. package/src/export/dxf.ts +1 -1
  411. package/src/export/pdf.ts +15 -7
  412. package/src/export/plan.ts +119 -35
  413. package/src/index.ts +5 -0
  414. package/src/input/keyboard.ts +2 -4
  415. package/src/model/constraints.ts +13 -4
  416. package/src/model/demo.ts +3 -3
  417. package/src/model/elevation.ts +6 -16
  418. package/src/model/faces.ts +153 -0
  419. package/src/model/facesolve.ts +322 -0
  420. package/src/model/history.ts +19 -11
  421. package/src/model/images.ts +35 -0
  422. package/src/model/links.ts +77 -0
  423. package/src/model/markup.ts +80 -0
  424. package/src/model/measurement.ts +26 -0
  425. package/src/model/openings.ts +6 -0
  426. package/src/model/planGeom.ts +174 -8
  427. package/src/model/project.ts +3 -2
  428. package/src/model/projectfile.ts +2 -1
  429. package/src/model/rooms.ts +1 -1
  430. package/src/model/settings.ts +8 -2
  431. package/src/model/sketch.ts +101 -14
  432. package/src/model/smooth.ts +51 -0
  433. package/src/model/store.ts +35 -4
  434. package/src/model/types.ts +170 -8
  435. package/src/model/wallfaces.ts +126 -17
  436. package/src/model/wallshape.ts +97 -0
  437. package/src/react/BottomBar.tsx +263 -46
  438. package/src/react/Buttons.tsx +50 -9
  439. package/src/react/CadEditor.tsx +133 -30
  440. package/src/react/ConfirmSheet.tsx +38 -0
  441. package/src/react/ConflictSheet.tsx +19 -12
  442. package/src/react/Drawer.tsx +213 -110
  443. package/src/react/Duck.tsx +50 -0
  444. package/src/react/Empty3D.tsx +32 -0
  445. package/src/react/FitScroll.tsx +19 -0
  446. package/src/react/Icon.native.tsx +5 -0
  447. package/src/react/Icon.web.tsx +5 -0
  448. package/src/react/KeypadSheet.tsx +253 -60
  449. package/src/react/LaserTray.tsx +12 -8
  450. package/src/react/MarkupPanel.tsx +150 -0
  451. package/src/react/PageThumb.native.tsx +18 -7
  452. package/src/react/PageThumb.web.tsx +4 -4
  453. package/src/react/PlanCanvas.native.tsx +151 -34
  454. package/src/react/PlanCanvas.web.tsx +15 -5
  455. package/src/react/Popovers.tsx +116 -51
  456. package/src/react/Rail.tsx +360 -27
  457. package/src/react/ShapeMenu.tsx +66 -0
  458. package/src/react/Sheet.tsx +56 -11
  459. package/src/react/Slider.tsx +48 -0
  460. package/src/react/Toast.tsx +16 -9
  461. package/src/react/ToolTip.tsx +49 -0
  462. package/src/react/TopBar.tsx +97 -39
  463. package/src/react/Tween.native.tsx +36 -0
  464. package/src/react/Tween.tsx +24 -0
  465. package/src/react/View3D.native.tsx +131 -0
  466. package/src/react/View3D.web.tsx +131 -0
  467. package/src/react/View3DHost.ts +367 -0
  468. package/src/react/corners.ts +52 -0
  469. package/src/react/easing.ts +14 -0
  470. package/src/react/host.ts +8 -3
  471. package/src/react/hostKeypad.ts +66 -0
  472. package/src/react/motion.tsx +239 -0
  473. package/src/react/ports.ts +78 -9
  474. package/src/react/saving.ts +80 -0
  475. package/src/react/store.ts +35 -3
  476. package/src/react/theme.ts +8 -0
  477. package/src/react/topRow.ts +71 -0
  478. package/src/react/touch.ts +10 -0
  479. package/src/render/ctx.ts +8 -0
  480. package/src/render/draw.ts +437 -175
  481. package/src/render/elevation.ts +349 -143
  482. package/src/render/figmaIcons.ts +22 -0
  483. package/src/render/hit.ts +140 -21
  484. package/src/render/icons.ts +69 -10
  485. package/src/render/linkTile.ts +297 -0
  486. package/src/render/markup.ts +91 -0
  487. package/src/render/navigate.ts +114 -0
  488. package/src/render/snap.ts +105 -22
  489. package/src/render/squircle.ts +73 -0
  490. package/src/render/view.ts +255 -19
  491. package/src/skia/SkiaDrawCtx.ts +163 -41
  492. package/src/skia/host.ts +46 -1
  493. package/src/solver/equations.ts +37 -2
  494. package/src/solver/gauss-newton.ts +125 -28
  495. package/src/solver/linalg.ts +141 -5
  496. package/src/solver/types.ts +5 -0
  497. package/src/styles.css +91 -42
  498. package/src/tools/annotate.ts +58 -29
  499. package/src/tools/chamfer.ts +103 -0
  500. package/src/tools/corner.ts +99 -0
  501. package/src/tools/dim.ts +524 -92
  502. package/src/tools/elevation.ts +1 -1
  503. package/src/tools/fillet.ts +82 -0
  504. package/src/tools/freehand.ts +20 -16
  505. package/src/tools/index.ts +17 -4
  506. package/src/tools/line.ts +59 -23
  507. package/src/tools/modify.ts +86 -24
  508. package/src/tools/openings.ts +8 -13
  509. package/src/tools/registry.ts +27 -2
  510. package/src/tools/select.ts +424 -75
  511. package/src/tools/shapes.ts +119 -58
  512. package/src/ui/ctx.ts +2 -1
  513. package/src/ui/domPorts.ts +23 -4
  514. package/src/ui/drawer.ts +6 -26
  515. package/src/ui/elevation.ts +10 -101
  516. package/src/ui/keypad.ts +8 -14
  517. package/src/ui/laser.ts +2 -2
  518. package/src/ui/menu.ts +63 -17
  519. package/src/ui/noteinput.ts +3 -3
  520. package/src/ui/opacity.ts +108 -0
  521. package/src/ui/palette.ts +67 -12
  522. package/src/ui/rail.ts +21 -7
  523. package/src/ui/topbar.ts +4 -14
  524. package/src/units/format.ts +18 -3
  525. package/src/view3d/build.ts +607 -0
  526. package/src/view3d/camera.ts +113 -0
  527. package/src/view3d/fatLines.ts +83 -0
  528. package/src/view3d/lines.ts +258 -0
  529. package/src/view3d/mesh.ts +111 -0
  530. package/src/view3d/poly.ts +104 -0
  531. package/src/view3d/viewer.ts +348 -0
  532. package/dist/react/ElevCanvas.native.d.ts +0 -3
  533. package/dist/react/ElevCanvas.native.js +0 -58
  534. package/dist/react/ElevCanvas.native.js.map +0 -1
  535. package/dist/react/ElevCanvas.web.d.ts +0 -4
  536. package/dist/react/ElevCanvas.web.js +0 -44
  537. package/dist/react/ElevCanvas.web.js.map +0 -1
  538. package/dist/react/ElevationSheet.d.ts +0 -2
  539. package/dist/react/ElevationSheet.js +0 -38
  540. package/dist/react/ElevationSheet.js.map +0 -1
  541. package/dist/render/elevationView.d.ts +0 -3
  542. package/dist/render/elevationView.js +0 -100
  543. package/dist/render/elevationView.js.map +0 -1
  544. package/src/react/ElevCanvas.native.tsx +0 -68
  545. package/src/react/ElevCanvas.web.tsx +0 -42
  546. package/src/react/ElevationSheet.tsx +0 -88
  547. package/src/render/elevationView.ts +0 -73
@@ -1,23 +1,31 @@
1
- import { afterEdit, solveSketch, tryAddConstraint, undo } from '../app/edit';
1
+ import { afterEdit, holdFaces, solveSketch, tryAddConstraint, undo } from '../app/edit';
2
2
  import { ed, type OpeningDimPart } from '../app/state';
3
- import { CONS_LABEL } from '../model/constraints';
3
+ import { CONS_LABEL, measure } from '../model/constraints';
4
4
  import { dropHistory, pushHistory } from '../model/history';
5
- import { createsCycle, facingEdges, nearerEdge } from '../model/openings';
5
+ import { createsCycle, edgePoint, facingEdges, nearerEdge } from '../model/openings';
6
+ import { cabOfPoint } from '../model/faces';
6
7
  import { settings } from '../model/settings';
7
- import { connectedGeometry, curveInfo, deleteSel, mergePoints } from '../model/sketch';
8
- import { page, saveSoon, SK } from '../model/store';
9
- import type { Constraint, ConsType, Sketch } from '../model/types';
8
+ import { connectedGeometry, curveInfo, deleteSel, joinPoints } from '../model/sketch';
9
+ import { page, planSK, replaceSK, saveSoon, SK } from '../model/store';
10
+ import type { Constraint, ConsType, OpeningEdge, Sketch, Underlay, WallFace } from '../model/types';
10
11
  import { fitView, requestDraw } from '../render/view';
11
12
  import { convertNote } from '../tools/annotate';
12
- import { dimAngle, dimCurve, dimLine, dimPointLine, dimPoints, dimTwoLines, editDim, toggleRef } from '../tools/dim';
13
+ import { cornerAngle, dimCurve, dimEdges, dimLine, dimPointLine, dimPoints, dimTwoLines, editDim, faceChoices, onChoice, setDimFace, toggleRef } from '../tools/dim';
14
+ import { edgeLabel, edgeOf } from '../model/planGeom';
13
15
  import type { LineTool } from '../tools/line';
14
16
  import { offsetPrompt, splitLine } from '../tools/modify';
17
+ import { filletable, filletPrompt } from '../tools/fillet';
18
+ import { chamferPrompt } from '../tools/chamfer';
15
19
  import { addOpeningDim, editOpeningDim, toggleOpeningDim } from '../tools/openings';
16
20
  import { setTool, tools } from '../tools/registry';
17
21
  import { fmtLen } from '../units';
18
- import { openElevation } from './elevation';
19
- import { editRoomName, openKeypad, openPalette, toast, updateCtx, updateTopBar } from './ports';
20
- import { cornerLines, selInfo } from './selection';
22
+ import { elevOpen, fitState, OPENING_SIZES, openElevation, openingLabel, overflow, restoreFit, TOO_SHORT, wallAction, wallBar, wallDelete, wallPlus } from './elevation';
23
+ import { pushedOut } from '../model/facesolve';
24
+ import { editRoomName, openKeypad, openPalette, toast, toggleImageOpacity, toggleImageRotation, toggleTileSize, updateCtx, updateTopBar } from './ports';
25
+ import { cornerLines, edgePair, selInfo } from './selection';
26
+ import { restackImage } from './project';
27
+ import { imageById } from '../model/images';
28
+ import { canOpenLink, canSaveDim, linkSizeVisible, openDimLink, openSelectedLink, saveDimToGroup, selectedLink, toggleLeader, unlinkDim } from './linkActions';
21
29
 
22
30
  /*
23
31
  The contextual bottom bar's decisions: which actions fit the tool and the selection, what the yellow + measures,
@@ -32,7 +40,7 @@ function lockTargets(): string[] {
32
40
  const sk = SK(), s = selInfo(), ids = new Set(s.points);
33
41
  s.lines.forEach(id => { ids.add(sk.lines[id].a); ids.add(sk.lines[id].b); });
34
42
  s.curves.forEach(id => { const ci = curveInfo(sk, id); if (ci) ids.add(ci.o.c); });
35
- return [...ids].filter(id => sk.points[id] && !sk.points[id].origin);
43
+ return [...ids].filter(id => sk.points[id] && !sk.points[id].origin && !sk.points[id].anchor);
36
44
  }
37
45
 
38
46
  /**
@@ -44,7 +52,7 @@ function arrangeLock(items: CtxEntry[]): CtxEntry[] {
44
52
  if (i < 0) return items;
45
53
  const it = items[i] as CtxItem;
46
54
  const sk = SK();
47
- const locked = it.act === 'imgLock' ? !!page().underlay?.locked : (() => { const l = lockTargets(); return l.length > 0 && l.every(id => sk.points[id].fixed); })();
55
+ const locked = it.act === 'imgLock' ? selImages().length > 0 && selImages().every(u => u.locked) : (() => { const l = lockTargets(); return l.length > 0 && l.every(id => sk.points[id].fixed); })();
48
56
  Object.assign(it, { ic: locked ? 'lock' : 'unlock', label: locked ? 'Locked' : 'Lock', on: undefined, locked });
49
57
  const rest = items.filter((_, k) => k !== i);
50
58
  const out: CtxEntry[] = [it];
@@ -54,52 +62,120 @@ function arrangeLock(items: CtxEntry[]): CtxEntry[] {
54
62
  while (out[out.length - 1] === 'sep') out.pop();
55
63
  return out;
56
64
  }
65
+ /** What the bar calls one end of a door or window. */
66
+ function anchorLabel(id: string): string {
67
+ return (planSK().openings[id.split('|')[0]]?.type === 'door' ? 'Door' : 'Window') + ' edge';
68
+ }
69
+
70
+ /** The selected images. */
71
+ const selImages = (): Underlay[] => ed.sel.filter(x => x.type === 'underlay').map(x => imageById(x.id)).filter((u): u is Underlay => !!u);
72
+
57
73
  export type CtxEntry = CtxItem | 'sep' | { lbl: string };
58
74
 
75
+ /** The tools that place a door, window or cabinet (in the plan or the wall view), which select what they've placed. */
76
+ const PLACING = new Set(['door', 'window', 'wdoor', 'wwindow', 'cabinet']);
77
+
59
78
  function ctxItems(): CtxEntry[] {
60
79
  const items: CtxEntry[] = [], sk = SK(), s = selInfo();
61
80
  const add = (ic: string, label: string, act: string, o: Partial<CtxItem> = {}) => items.push(Object.assign({ ic, label, act }, o));
62
81
  /** To wall (all the selected lines and curves) — or To line + Thickness when they're all walls already. */
63
82
  const wallItems = () => {
83
+ if (elevOpen()) return;
64
84
  const kinds = [...s.lines.map(id => sk.lines[id].kind), ...s.curves.map(id => curveInfo(sk, id)?.o.kind)];
65
85
  if (!kinds.length) return;
66
86
  if (kinds.every(k => k === 'wall')) { add('thick', 'Thickness', 'thick'); add('line', 'To line', 'kind'); }
67
87
  else add('wall', 'To wall', 'kind');
68
88
  };
69
89
  if (ed.mode === 'annotate') {
70
- if (s.n === 1 && s.image) {
71
- const locked = !!page().underlay?.locked;
90
+ const ims = selImages();
91
+ if (ims.length && ims.length === s.n) {
92
+ // an image (or several): lock, its own scale, and which goes in front where they overlap
93
+ // (showing and hiding them all is the Layers menu's)
72
94
  add('lock', 'Lock', 'imgLock');
73
- if (!locked) add('calib', 'Set scale', 'calibtool');
74
- add('eye', 'Hide image', 'toggleUnderlay');
95
+ if (ims.length === 1 && !ims[0].locked) add('calib', 'Set scale', 'calibtool');
96
+ if (ims.length === 1) add('opacity', 'Opacity', 'imgOpacity');
97
+ // (turning moves it: not while it's locked, as it can't be moved or sized then)
98
+ if (ims.length === 1 && !ims[0].locked) add('rotate', 'Rotate', 'imgRotate');
99
+ if (ims.length === 1 && page().underlays.length > 1) {
100
+ const at = page().underlays.indexOf(ims[0]), last = page().underlays.length - 1;
101
+ if (at < last) add('tofront', 'To front', 'imgFront');
102
+ if (at > 0) add('toback', 'To back', 'imgBack');
103
+ }
104
+ return items;
105
+ }
106
+ const lk = selectedLink();
107
+ if (lk) {
108
+ // a link's tile: opening it, its leader, its size (its name is on the tile, not here: the user's call, 2026-10-21)
109
+ if (canOpenLink(lk.link)) add('open', 'Open', 'linkOpen');
110
+ add('leader', 'Leader', 'linkLeader', { on: !!lk.leader });
111
+ // (its leader's colour, as all markup's, is the rail's colour dot's)
112
+ // (every tile on the page: a slider above the bar)
113
+ add('tilesize', 'Size', 'linkSize');
75
114
  return items;
76
115
  }
77
116
  if (s.notes.length) {
117
+ // (no Color: the rail's colour dot styles what's selected)
78
118
  const n = sk.notes.find(n => n.id === s.notes[0]);
79
- add('color', 'Color', 'palette');
80
119
  if (n && n.type === 'pen' && s.notes.length === 1) add('convert', 'To lines', 'convert');
81
- } else {
82
- add('color', 'Color', 'palette');
83
- if (page().underlay) { add('calib', 'Scale', 'calibtool'); add('eye', settings.layers.underlay ? 'Hide image' : 'Show image', 'toggleUnderlay'); }
120
+ if (linkSizeVisible()) add('tilesize', 'Size', 'linkSize');
84
121
  }
122
+ // nothing selected: no bar (the markup colour is the rail's colour dot, for what's selected too; the image's own
123
+ // options come with selecting it)
85
124
  return items;
86
125
  }
87
- if (ed.tool === 'line' || ed.tool === 'wall') {
126
+ if (ed.tool === 'line') {
88
127
  const st = (tools[ed.tool] as LineTool).st;
89
- if (st.start) {
90
- add('typelen', 'Type length', 'typelen');
91
- if (st.chain.length >= 3) add('closeShape', 'Close', 'closeShape');
92
- add('finish', 'Finish', 'finish');
93
- return items;
94
- }
128
+ // drawing: no bar (tap the last point again to finish, the first to close the shape; Esc or Enter on a keyboard)
129
+ if (st.start) return items;
95
130
  }
96
131
  if (ed.tool === 'dim') { add('dim', 'Driving', 'dimDrive', { on: ed.dimLayer === 'drive' }); add('ref', 'Print', 'dimPrint', { on: ed.dimLayer === 'print' }); return items; }
97
- // nothing selected: the bar stays empty (Fit and Icons live in the top bar; grid snap and auto-constrain in Settings)
98
- if (!s.n || ed.tool !== 'select') return items;
132
+ // nothing selected: the bar stays empty (Fit and Icons live in the top bar; grid snap and auto-constrain in Settings).
133
+ // Just placed a door, window or cabinet (its tool still on): its bar, for its sizes
134
+ if (!s.n || (ed.tool !== 'select' && !PLACING.has(ed.tool))) return items;
135
+ // the wall view's own things: a door or window, a cabinet, one of its dimension chips, the plan's edges
136
+ const wb = wallBar();
137
+ if (wb) {
138
+ if (wb.label) items.push({ lbl: wb.label });
139
+ for (const it of wb.items) add(it.ic, it.label, it.act);
140
+ return items;
141
+ }
142
+ // on a wall face, walls don't exist: no To wall / Thickness
143
+ const wall = elevOpen();
99
144
  const P = s.points.length, L = s.lines.length, C = s.curves.length;
145
+ // Guide (dashed) is for lines: a wall is always solid (one that's a guide from before can still go back to Solid)
146
+ const allWalls = L + C > 0 && s.lines.every(id => sk.lines[id].kind === 'wall' && !sk.lines[id].constr) && s.curves.every(id => { const o = curveInfo(sk, id)?.o; return o?.kind === 'wall' && !o.constr; });
147
+ const guide = () => { if (!allWalls) add('constr', 'Guide', 'constr'); };
148
+ // a wall's edge: named, with its wall view; with another edge, a line or a point, the distance between them (two
149
+ // edges of one wall: its thickness)
150
+ if (s.wfaces.length) {
151
+ // (a curved wall has no wall view: just its name)
152
+ if (s.n === 1) {
153
+ const [id, side] = edgeOf(s.wfaces[0]);
154
+ items.push({ lbl: edgeLabel(sk, id, side) });
155
+ if (sk.lines[id]) add('elev', 'Elevation', 'elev');
156
+ }
157
+ else if (edgePair(s)) {
158
+ const one = s.wfaces.length === 2 && edgeOf(s.wfaces[0])[0] === edgeOf(s.wfaces[1])[0];
159
+ add(one ? 'thick' : 'dim', one ? 'Thickness' : 'Distance', 'edgeDist');
160
+ }
161
+ return items;
162
+ }
100
163
  if (s.n === 1 && s.dims.length) {
101
164
  const c = sk.cons[s.dims[0]];
102
- if (c.layer !== 'print') add('ref', c.driving === false ? 'Drive' : 'Reference', 'toggleref');
165
+ // linked to a measurement of the job: opening it, letting go of it — no text saying what it's linked to (the user's
166
+ // call, 2026-10-21; its chip is drawn linked), and no Reference (a measurement drives its line, or it isn't linked)
167
+ if (c.measurementRef) {
168
+ const t = c.measurementRef;
169
+ if (canOpenLink(t)) add('open', 'Open', 'dimLinkOpen');
170
+ add('unlink', 'Unlink', 'dimUnlink');
171
+ } else if (canSaveDim(s.dims[0])) add('savegroup', 'Save to group', 'dimSave');
172
+ // Reference frees what a dimension holds — a distance, say; not a cabinet's own width or height, which it keeps
173
+ const cabSize = Object.values((sk as WallFace).cabs ?? {}).some(cb => cb.w === s.dims[0] || cb.h === s.dims[0]);
174
+ if (c.layer !== 'print' && !cabSize && !c.measurementRef) add('ref', c.driving === false ? 'Drive' : 'Reference', 'toggleref');
175
+ // measured on walls: which of their faces (the drawing stays as it is; the number becomes what it measures there)
176
+ const fc = faceChoices(sk, c);
177
+ if (fc.length && items.length) items.push('sep');
178
+ fc.forEach((ch, i) => add(ch.icon!, ch.short!, 'dimFace' + i, { on: onChoice(c, ch) }));
103
179
  return items;
104
180
  }
105
181
  if (s.n === 1 && s.odims.length) {
@@ -110,26 +186,44 @@ function ctxItems(): CtxEntry[] {
110
186
  // a constraint badge: nothing to offer but delete (the trash), so the bar stays empty
111
187
  if (s.n === 1 && s.cons.length) return items;
112
188
  if (s.n === 1 && s.rooms.length) { add('rename', 'Rename', 'renameRoom'); return items; }
189
+ // one end of a door or window: named; with another point, end or door/window, the distance between them
190
+ if (s.oanchors.length && s.n === 1) { items.push({ lbl: anchorLabel(s.oanchors[0]) }); return items; }
191
+ if (s.oanchors.length && s.n === 2 && s.oanchors.length + s.points.length + s.openings.length === 2) { add('dim', 'Distance', 'anchorDist'); return items; }
113
192
  // two doors/windows, or one and a point: the distance between them (locks the second one picked)
114
193
  if (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1))) { add('dim', 'Distance', 'opDist'); return items; }
115
194
  if (s.n === 1 && s.openings.length) {
116
- // the yellow + sets the width; the type is fixed once placed (Door and Window are separate tools)
117
- if (sk.openings[s.openings[0]].type === 'door') add('flip', 'Flip', 'opFlip');
195
+ // its sizes by name, as in the wall view (no +: it isn't one dimension); a door's swing too. (The type is fixed
196
+ // once placed: Door and Window are separate tools.)
197
+ const o = sk.openings[s.openings[0]];
198
+ items.push({ lbl: openingLabel(o) });
199
+ for (const it of OPENING_SIZES) add(it.ic, it.label, it.act);
200
+ if (o.type === 'door') { items.push('sep'); add('flip', 'Flip', 'opFlip'); }
118
201
  return items;
119
202
  }
120
203
  if (L === 1 && s.n === 1) {
121
204
  const l = sk.lines[s.lines[0]];
122
- add('h', 'Horizontal', 'h', { key: '⇧H' }); add('v', 'Vertical', 'v', { key: '⇧V' }); add('dim', 'Length', 'len');
205
+ add('h', 'Horizontal', 'h', { key: '⇧H' }); add('v', 'Vertical', 'v', { key: '⇧V' }); // (its length: the yellow +)
123
206
  items.push('sep');
124
- add('lock', 'Lock', 'lock', { key: '⇧F' }); add('split', 'Split', 'split'); add('offset', 'Offset', 'offset'); add('chain', 'Connected', 'chain'); add('constr', l.constr ? 'Solid' : 'Guide', 'constr');
125
- if (l.kind === 'wall') { add('thick', 'Thickness', 'thick'); add('line', 'To line', 'kind'); } else add('wall', 'To wall', 'kind');
126
- if (l.kind === 'wall' && !l.constr) { items.push('sep'); add('elev', 'Elevation', 'elev'); }
207
+ add('lock', 'Lock', 'lock', { key: '⇧F' }); add('split', 'Split', 'split'); add('offset', 'Offset', 'offset'); if (l.constr) add('constr', 'Solid', 'constr'); else guide();
208
+ if (!wall) {
209
+ if (l.kind === 'wall') { add('thick', 'Thickness', 'thick'); add('line', 'To line', 'kind'); } else add('wall', 'To wall', 'kind');
210
+ if (l.kind === 'wall' && !l.constr) { items.push('sep'); add('elev', 'Elevation', 'elev'); }
211
+ }
127
212
  return items;
128
213
  }
129
- if (L === 2 && s.n === 2) { add('par', 'Parallel', 'par', { key: '⇧P' }); add('perp', 'Perpendicular', 'perp', { key: '⇧R' }); add('col', 'Collinear', 'col', { key: '⇧L' }); add('eq', 'Equal', 'eq', { key: '⇧E' }); add('angle', 'Angle', 'angle'); wallItems(); return items; }
214
+ if (L === 2 && s.n === 2) {
215
+ // (no Angle here: the yellow + is the angle between them — or, parallel, the distance)
216
+ add('par', 'Parallel', 'par', { key: '⇧P' }); add('perp', 'Perpendicular', 'perp', { key: '⇧R' }); add('col', 'Collinear', 'col', { key: '⇧L' }); add('eq', 'Equal', 'eq', { key: '⇧E' });
217
+ // two lines that meet: round off (or cut off) the corner between them
218
+ if (filletCorner2()) { add('fillet', 'Fillet', 'fillet'); add('chamfer', 'Chamfer', 'chamfer'); }
219
+ wallItems();
220
+ return items;
221
+ }
222
+ // a cabinet's corner: nothing of its own (pick another point or line with it to dimension or constrain)
223
+ if (P === 1 && s.n === 1 && cabOfPoint(sk, s.points[0])) return items;
130
224
  if (P === 1 && s.n === 1) {
131
- // a corner where exactly two lines meet: offer its angle
132
- if (cornerLines(sk, s.points[0])) add('angle', 'Angle', 'cornerAngle');
225
+ // a corner where exactly two lines meet: rounding it off or cutting it off (its angle is the yellow +)
226
+ if (filletable(sk, s.points[0])) { add('fillet', 'Fillet', 'fillet'); add('chamfer', 'Chamfer', 'chamfer'); }
133
227
  add('lock', 'Lock', 'lock', { key: '⇧F' });
134
228
  return items;
135
229
  }
@@ -140,7 +234,8 @@ function ctxItems(): CtxEntry[] {
140
234
  // a circle or arc: the yellow + sets its diameter
141
235
  if (C === 1 && s.n === 1) {
142
236
  const ci = curveInfo(sk, s.curves[0]);
143
- add('constr', 'Guide', 'constr');
237
+ guide();
238
+ if (wall) return items;
144
239
  if (ci && ci.o.kind === 'wall') { add('thick', 'Thickness', 'thick'); add('arc', 'To line', 'kind'); } else add('wall', 'To wall', 'kind');
145
240
  return items;
146
241
  }
@@ -150,33 +245,87 @@ function ctxItems(): CtxEntry[] {
150
245
  add('h', 'Horizontal', 'h', { key: '⇧H' }); add('v', 'Vertical', 'v', { key: '⇧V' });
151
246
  add('par', 'Parallel', 'par', { key: '⇧P' }); add('col', 'Collinear', 'col', { key: '⇧L' }); add('eq', 'Equal', 'eq', { key: '⇧E' });
152
247
  items.push('sep');
153
- add('chain', 'Connected', 'chain'); add('lock', 'Lock', 'lock'); add('constr', 'Guide', 'constr'); wallItems();
248
+ add('lock', 'Lock', 'lock'); guide(); wallItems();
154
249
  return items;
155
250
  }
156
- if (L >= 2) { add('eq', 'Equal', 'eqall'); add('chain', 'Connected', 'chain'); add('lock', 'Lock', 'lock'); add('constr', 'Guide', 'constr'); wallItems(); return items; }
251
+ if (L >= 2) { add('eq', 'Equal', 'eqall'); add('lock', 'Lock', 'lock'); guide(); wallItems(); return items; }
157
252
  add('lock', 'Lock', 'lock');
158
253
  return items;
159
254
  }
160
255
 
256
+ /** The keypad for walls' thickness, finished face to face. */
257
+ function thicknessPrompt(walls: { thick?: number }[]): void {
258
+ const sk = SK();
259
+ if (!walls.length) return;
260
+ openKeypad({
261
+ title: 'Wall thickness', sub: 'Finished face to face', initial: fmtLen(walls[0].thick || 4.5), selectAll: true,
262
+ onDone: v => {
263
+ if (v <= 0) return;
264
+ // a thicker wall's faces are shorter (its corners come in): not shorter than what's on them — and what's measured
265
+ // on walls' faces stays as measured, the centrelines moving to keep it
266
+ const was = overflow(sk), held = fitState(sk), had = walls.map(w => w.thick);
267
+ pushHistory();
268
+ walls.forEach(w => (w.thick = v));
269
+ const faces = holdFaces();
270
+ if (!faces || pushedOut(was, overflow(sk))) {
271
+ walls.forEach((w, i) => (w.thick = had[i])); restoreFit(sk, held); dropHistory();
272
+ toast(faces ? TOO_SHORT : 'The dimensions measured on walls’ faces can’t hold at that thickness.', { warn: true });
273
+ }
274
+ afterEdit();
275
+ },
276
+ });
277
+ }
278
+
279
+ /** Two selected lines that meet at a corner of just those two: that corner (to round it off). */
280
+ function filletCorner2(): string | null {
281
+ const sk = SK(), s = selInfo();
282
+ if (s.lines.length !== 2 || s.n !== 2) return null;
283
+ const [a, b] = s.lines.map(id => sk.lines[id]);
284
+ const p = [a.a, a.b].find(x => x === b.a || x === b.b);
285
+ return p && filletable(sk, p) ? p : null;
286
+ }
287
+
161
288
  /** Buttons for the bottom bar: they adapt to the tool and the selection (Lock first, when offered). */
162
289
  export const barItems = (): CtxEntry[] => arrangeLock(ctxItems());
163
290
 
164
291
  /** The yellow + only appears for something it can measure: not for constraint badges or spaces alone. */
292
+ const DRAW_TOOLS = new Set(['line', 'freehand', 'rect', 'circle', 'arc']);
293
+
165
294
  export function plusVisible(): boolean {
295
+ // markup isn't measured: Annotate has no + (notes are the rail's Text tool)
296
+ if (ed.mode === 'annotate') return false;
297
+ const wb = wallBar();
298
+ if (wb) return wb.plus;
299
+ // a shape just drawn (Rectangle, Circle, Arc keep their tool, the shape selected) gets no + of its own;
300
+ // a door or window just placed does (its width)
301
+ if (ed.mode === 'draft' && DRAW_TOOLS.has(ed.tool)) return false;
302
+ // one end of a door or window, alone: nothing to measure yet
303
+ if (ed.sel.length === 1 && ed.sel[0].type === 'oanchor') return false;
304
+ // one door or window picked as a whole: its sizes are in the bar, by name — no single dimension for a +
305
+ // (two, or one and a point: the + is the distance between them)
306
+ if (ed.sel.length === 1 && ed.sel[0].type === 'opening') return false;
166
307
  return ed.sel.length > 0 && !ed.sel.every(x => x.type === 'cons') && !ed.sel.every(x => x.type === 'room');
167
308
  }
168
309
 
169
- export const plusLabel = (): string => (ed.mode === 'annotate' ? 'Add a note' : 'Add a measurement');
310
+ export const plusLabel = (): string => 'Add a measurement';
170
311
 
171
- /** The yellow +: a note in Annotate; otherwise the measurement that fits the selection (opens the keypad). */
312
+ /** The yellow +: the measurement that fits the selection (opens the keypad). */
172
313
  export function plusAction(): void {
173
- if (ed.mode === 'annotate') { setTool('text'); return; }
314
+ if (ed.mode === 'annotate') return;
315
+ if (wallPlus()) return;
174
316
  const s = selInfo(), sk = SK();
175
- if (s.dims.length === 1 && s.n === 1) { if (sk.cons[s.dims[0]]?.layer === 'print') toast('Print dimensions are labels only — drag to move them.'); else editDim(s.dims[0]); }
317
+ if (s.dims.length === 1 && s.n === 1) { if (sk.cons[s.dims[0]]?.layer === 'print') toast('Print dimensions are labels only — drag to move them.', { warn: true }); else editDim(s.dims[0]); }
176
318
  else if (s.odims.length === 1 && s.n === 1) { const [op, part] = s.odims[0].split('|'); editOpeningDim(op, part as OpeningDimPart); }
319
+ // a wall's edge: its length on that face; with another edge, a line or a point, the distance from it
320
+ // (a curved wall's: its diameter across that face)
321
+ else if (s.wfaces.length === 1 && s.n === 1) { const [id, side] = edgeOf(s.wfaces[0]); if (sk.lines[id]) dimLine(id, false, side); else dimCurve(id, side); }
322
+ else if (edgePair(s)) applyCtxAction('edgeDist');
177
323
  else if (s.lines.length === 1 && s.n === 1) dimLine(s.lines[0], false);
178
324
  else if (s.curves.length === 1 && s.n === 1) dimCurve(s.curves[0]);
179
325
  else if (s.openings.length === 1 && s.n === 1) editOpeningDim(s.openings[0], 'w');
326
+ // a corner where two lines meet: its angle
327
+ else if (s.points.length === 1 && s.n === 1 && cornerLines(sk, s.points[0])) applyCtxAction('cornerAngle');
328
+ else if (s.n === 2 && s.oanchors.length) applyCtxAction('anchorDist');
180
329
  else if (s.n === 2 && (s.openings.length === 2 || (s.openings.length === 1 && s.points.length === 1))) applyCtxAction('opDist');
181
330
  // two things selected: the dimension between them
182
331
  else if (s.n === 2 && s.lines.length === 2) dimTwoLines(s.lines[0], s.lines[1]);
@@ -187,18 +336,20 @@ export function plusAction(): void {
187
336
 
188
337
  export function deleteSelection(): void {
189
338
  if (!ed.sel.length) return;
190
- const items = ed.sel.slice(), sk = SK();
191
- if (items.length === 1 && items.some(s => s.type === 'point' && sk.points[s.id] && sk.points[s.id].origin)) { toast('The origin can’t be deleted.'); return; }
192
- // the image underlay isn't part of the sketch history, so its removal gets its own undo
193
- const removed = items.some(s => s.type === 'underlay') ? page().underlay : null;
339
+ if (wallDelete()) return;
340
+ // (a wall's edge picked: the wall)
341
+ const sk = SK(), wallType = (id: string) => (sk.lines[id] ? 'line' as const : sk.circles[id] ? 'circle' as const : 'arc' as const);
342
+ const items = ed.sel.map(x => (x.type === 'wface' ? { type: wallType(edgeOf(x.id)[0]), id: edgeOf(x.id)[0] } : x)).filter((x, i, a) => a.findIndex(y => y.type === x.type && y.id === x.id) === i);
343
+ if (items.length === 1 && items.some(s => s.type === 'point' && sk.points[s.id] && sk.points[s.id].origin)) { toast('The origin can’t be deleted.', { warn: true }); return; }
344
+ // images are part of the page's undo history, so they go in the same step as the rest
345
+ const imgIds = new Set(items.filter(s => s.type === 'underlay').map(s => s.id));
194
346
  const rest = items.filter(s => s.type !== 'underlay');
195
- if (removed) page().underlay = null;
196
- if (rest.length) { pushHistory(); deleteSel(sk, rest); }
347
+ pushHistory();
348
+ if (imgIds.size) page().underlays = page().underlays.filter(u => !imgIds.has(u.id));
349
+ if (rest.length) deleteSel(sk, rest);
197
350
  ed.sel = [];
198
351
  afterEdit();
199
- toast(removed && !rest.length ? 'Image removed' : 'Deleted', {
200
- action: { label: 'Undo', fn: () => { if (rest.length) undo(); if (removed && !page().underlay) { page().underlay = removed; afterEdit(); } } },
201
- });
352
+ toast(imgIds.size && !rest.length ? (imgIds.size > 1 ? 'Images removed' : 'Image removed') : 'Deleted', { action: { label: 'Undo', fn: () => undo() } });
202
353
  }
203
354
 
204
355
  function consFromSel(type: ConsType): Constraint | null {
@@ -255,7 +406,7 @@ function applyMulti(type: ConsType, list: Constraint[]): void {
255
406
  if (r.ok) added++;
256
407
  else if (r.conflict) failed.push(c);
257
408
  }
258
- if (!added && !failed.length) { dropHistory(); toast('They’re already ' + ADJ[type] + '.'); afterEdit(); return; }
409
+ if (!added && !failed.length) { dropHistory(); toast('They’re already ' + ADJ[type] + '.', { warn: true }); afterEdit(); return; }
259
410
  if (!added) dropHistory();
260
411
  // every selected line that now satisfies it (some may already have, through other constraints)
261
412
  const lines = (type === 'h' || type === 'v' ? list.length : list.length + 1) - failed.length;
@@ -272,7 +423,7 @@ function applyCons(type: ConsType): void {
272
423
  const multi = multiLineCons(type);
273
424
  if (multi && (multi.length > 1 || selInfo().lines.length > 2)) { applyMulti(type, multi); return; }
274
425
  const c = consFromSel(type);
275
- if (!c) { toast('Select ' + (NEEDS[type] || 'geometry') + ' first.'); return; }
426
+ if (!c) { toast('Select ' + (NEEDS[type] || 'geometry') + ' first.', { warn: true }); return; }
276
427
  pushHistory();
277
428
  const r = tryAddConstraint(c, { onResolved: () => afterEdit() });
278
429
  if (!r.ok) dropHistory();
@@ -282,10 +433,21 @@ function applyCons(type: ConsType): void {
282
433
 
283
434
  /** Run a bottom-bar action. `anchor` is the button pressed, for actions that open a popover next to it. */
284
435
  export function applyCtxAction(act: string, anchor?: unknown): void {
436
+ if (wallAction(act)) return;
285
437
  const sk = SK(), s = selInfo();
286
438
  switch (act) {
287
439
  case 'fit': fitView(); return;
288
- case 'elev': if (s.lines.length === 1) openElevation(s.lines[0]); return;
440
+ case 'elev':
441
+ if (s.lines.length === 1) openElevation(s.lines[0]);
442
+ // (an edge: seen from that face)
443
+ else if (s.wfaces.length === 1 && sk.lines[edgeOf(s.wfaces[0])[0]]) openElevation(...edgeOf(s.wfaces[0]));
444
+ return;
445
+ case 'edgeDist': dimEdges(ed.sel, id => thicknessPrompt([sk.lines[id] ?? sk.circles[id] ?? sk.arcs[id]])); return;
446
+ case 'dimFace0': case 'dimFace1': case 'dimFace2': {
447
+ const ch = s.dims.length === 1 && sk.cons[s.dims[0]] ? faceChoices(sk, sk.cons[s.dims[0]])[+act.slice(7)] : undefined;
448
+ if (ch) setDimFace(s.dims[0], ch);
449
+ return;
450
+ }
289
451
  case 'grid': settings.snapGrid = !settings.snapGrid; break;
290
452
  case 'autocons':
291
453
  settings.autoCons = !settings.autoCons;
@@ -299,25 +461,25 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
299
461
  case 'dimPrint': ed.dimLayer = 'print'; break;
300
462
  case 'h': case 'v': case 'par': case 'perp': case 'col': case 'eq': case 'mid': case 'online': case 'oncircle': case 'tan': case 'eqr': case 'conc':
301
463
  applyCons(act); return;
302
- case 'len': if (s.lines.length === 1) dimLine(s.lines[0], false); return;
464
+ case 'len':
465
+ if (s.lines.length === 1) dimLine(s.lines[0], false);
466
+ else if (s.wfaces.length === 1 && sk.lines[edgeOf(s.wfaces[0])[0]]) { const [lid, side] = edgeOf(s.wfaces[0]); dimLine(lid, false, side); }
467
+ return;
303
468
  case 'pdist': if (s.points.length === 2) dimPoints(s.points[0], s.points[1], false); return;
304
469
  case 'pldist': if (s.points.length === 1 && s.lines.length === 1) dimPointLine(s.points[0], s.lines[0]); return;
305
470
  case 'diam': if (s.curves.length === 1) dimCurve(s.curves[0]); return;
306
- case 'angle': if (s.lines.length === 2) dimAngle(s.lines[0], s.lines[1]); return;
307
- case 'cornerAngle': {
308
- const pair = cornerLines(sk, s.points[0]);
309
- if (!pair) return;
310
- // edit the corner's existing angle dimension rather than stacking a second one
311
- 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])); });
312
- if (ex) editDim(ex); else dimAngle(pair[0], pair[1]);
471
+ case 'fillet': case 'chamfer': {
472
+ const p = s.points.length === 1 ? s.points[0] : filletCorner2();
473
+ if (p) (act === 'fillet' ? filletPrompt : chamferPrompt)(p);
313
474
  return;
314
475
  }
476
+ case 'cornerAngle': cornerAngle(s.points[0]); return;
315
477
  case 'merge': {
316
478
  pushHistory();
317
479
  const snap = JSON.stringify(sk);
318
- if (mergePoints(sk, s.points[0], s.points[1])) {
480
+ if (joinPoints(sk, s.points[0], s.points[1])) {
319
481
  const r = solveSketch();
320
- if (!r.ok) { page().sk = JSON.parse(snap) as Sketch; dropHistory(); toast('Can’t join those points — it conflicts with other constraints.', { warn: true }); }
482
+ if (!r.ok) { replaceSK(JSON.parse(snap) as Sketch); dropHistory(); toast('Can’t join those points — it conflicts with other constraints.', { warn: true }); }
321
483
  else { ed.sel = [{ type: 'point', id: s.points[0] }]; toast('Points joined'); }
322
484
  }
323
485
  afterEdit();
@@ -343,7 +505,7 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
343
505
  }
344
506
  case 'constr':
345
507
  pushHistory();
346
- s.lines.forEach(id => (sk.lines[id].constr = !sk.lines[id].constr));
508
+ s.lines.forEach(id => { if (!sk.lines[id].anchor) sk.lines[id].constr = !sk.lines[id].constr; });
347
509
  s.curves.forEach(id => { const ci = curveInfo(sk, id); if (ci) ci.o.constr = !ci.o.constr; });
348
510
  afterEdit();
349
511
  return;
@@ -353,24 +515,34 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
353
515
  if (!targets.length) return;
354
516
  const toWall = !targets.every(t => t.kind === 'wall');
355
517
  pushHistory();
356
- targets.forEach(t => { t.kind = toWall ? 'wall' : 'line'; t.thick = t.thick || 4.5; });
518
+ targets.forEach(t => { t.kind = toWall ? 'wall' : 'line'; t.thick = t.thick || 4.5; if (toWall && (t as { constr?: boolean }).constr) (t as { constr?: boolean }).constr = false; });
519
+ // made lines: what was measured on their faces is measured as they are now (nothing moves); the walls beside
520
+ // them keep theirs
521
+ if (!toWall) {
522
+ const ids = new Set([...s.lines, ...s.curves]);
523
+ for (const c of Object.values(sk.cons)) {
524
+ if (c.type === 'dist' && c.face && c.line && ids.has(c.line)) { delete c.face; if (c.driving !== false) c.value = measure(sk, c); }
525
+ if (c.type === 'diam' && c.face && c.c && ids.has(c.c)) { delete c.face; if (c.driving !== false) c.value = measure(sk, c); }
526
+ if (c.type === 'pldist' && c.faces && ((c.l && ids.has(c.l)) || (c.l2 && ids.has(c.l2)))) {
527
+ if (c.driving !== false) c.value = measure(sk, c);
528
+ if (c.faces[0] && c.l && ids.has(c.l)) c.faces = [0, c.faces[1]];
529
+ if (c.faces[1] && c.l2 && ids.has(c.l2)) c.faces = [c.faces[0], 0];
530
+ if (!c.faces[0] && !c.faces[1]) delete c.faces;
531
+ }
532
+ }
533
+ }
534
+ holdFaces();
357
535
  afterEdit();
358
536
  toast(targets.length > 1 ? (toWall ? targets.length + ' items are now walls' : targets.length + ' items are now lines') : toWall ? 'Now a wall' : 'Now a line');
359
537
  return;
360
538
  }
361
- case 'thick': {
539
+ case 'thick':
362
540
  // straight and curved walls in the selection
363
- const walls: { thick?: number }[] = [
541
+ thicknessPrompt([
364
542
  ...s.lines.map(id => sk.lines[id]).filter(l => l.kind === 'wall'),
365
543
  ...s.curves.map(id => curveInfo(sk, id)?.o).filter((o): o is NonNullable<typeof o> => !!o && o.kind === 'wall'),
366
- ];
367
- if (!walls.length) return;
368
- openKeypad({
369
- title: 'Wall thickness', sub: 'Finished face to face', initial: fmtLen(walls[0].thick || 4.5), selectAll: true,
370
- onDone: v => { if (v <= 0) return; pushHistory(); walls.forEach(w => (w.thick = v)); afterEdit(); },
371
- });
544
+ ]);
372
545
  return;
373
- }
374
546
  case 'eqall': {
375
547
  // lines mixed with other things: equal across just the lines
376
548
  if (s.lines.length < 2) return;
@@ -395,20 +567,57 @@ export function applyCtxAction(act: string, anchor?: unknown): void {
395
567
  }
396
568
  return;
397
569
  }
570
+ case 'anchorDist': {
571
+ // from one end of a door or window to a point, another end, or another door or window (its nearer end): the
572
+ // second one picked is placed from the first (the other way round if that would tie them in a loop)
573
+ const end = (id: string) => { const [op, edge] = id.split('|'); return { op, edge: edge as OpeningEdge }; };
574
+ const [first, second] = ed.sel;
575
+ if (!first || !second) return;
576
+ if (first.type === 'point' || second.type === 'point') {
577
+ const e = end((first.type === 'oanchor' ? first : second).id), pt = (first.type === 'point' ? first : second).id;
578
+ addOpeningDim(e.op, e.edge, { pt });
579
+ return;
580
+ }
581
+ const nearer = (op: string, from: { op: string; edge: OpeningEdge }): OpeningEdge => {
582
+ const p = edgePoint(sk, from.op, from.edge), a = edgePoint(sk, op, 's0'), b = edgePoint(sk, op, 's1');
583
+ return !p || !a || !b || Math.hypot(a.x - p.x, a.y - p.y) <= Math.hypot(b.x - p.x, b.y - p.y) ? 's0' : 's1';
584
+ };
585
+ let a = first.type === 'oanchor' ? end(first.id) : null, b = second.type === 'oanchor' ? end(second.id) : null;
586
+ if (!a) a = { op: first.id, edge: nearer(first.id, b!) };
587
+ if (!b) b = { op: second.id, edge: nearer(second.id, a) };
588
+ if (a.op !== b.op && createsCycle(sk, b.op, { op: a.op, edge: a.edge })) [a, b] = [b, a];
589
+ addOpeningDim(b.op, b.edge, { op: a.op, edge: a.edge });
590
+ return;
591
+ }
398
592
  case 'palette': openPalette(anchor); return;
399
593
  case 'convert': convertNote(s.notes[0]); return;
594
+ case 'linkOpen': openSelectedLink(); return;
595
+ case 'linkSize': toggleTileSize(anchor); return;
596
+ case 'dimLinkOpen': if (s.dims.length === 1) openDimLink(s.dims[0]); return;
597
+ case 'dimUnlink': if (s.dims.length === 1) unlinkDim(s.dims[0]); return;
598
+ case 'dimSave': if (s.dims.length === 1) void saveDimToGroup(s.dims[0]); return;
599
+ case 'linkLeader': if (s.notes.length === 1) toggleLeader(s.notes[0]); return;
400
600
  case 'calibtool': setTool('calib'); return;
401
601
  case 'toggleUnderlay':
402
602
  settings.layers.underlay = !settings.layers.underlay;
403
603
  if (!settings.layers.underlay) ed.sel = ed.sel.filter(x => x.type !== 'underlay');
404
604
  break;
405
605
  case 'imgLock': {
406
- const u = page().underlay;
407
- if (!u) return;
606
+ const ims = selImages();
607
+ if (!ims.length) return;
608
+ const lock = !ims.every(u => u.locked), many = ims.length > 1;
408
609
  pushHistory();
409
- u.locked = !u.locked;
610
+ ims.forEach(u => { u.locked = lock; });
410
611
  afterEdit();
411
- toast(u.locked ? 'Image locked. It won’t move while you trace over it.' : 'Image unlocked. Drag it, or drag its handles to size it.');
612
+ toast(lock ? (many ? 'Images locked. They won’t move while you trace over them.' : 'Image locked. It won’t move while you trace over it.')
613
+ : many ? 'Images unlocked.' : 'Image unlocked. Drag it, or drag its handles to size it.');
614
+ return;
615
+ }
616
+ case 'imgOpacity': toggleImageOpacity(anchor); return;
617
+ case 'imgRotate': toggleImageRotation(anchor); return;
618
+ case 'imgFront': case 'imgBack': {
619
+ const ims = selImages();
620
+ if (ims.length === 1) restackImage(ims[0].id, act === 'imgFront' ? 'front' : 'back');
412
621
  return;
413
622
  }
414
623
  }