@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,16 +1,24 @@
1
+ import { dimEnds, dimRadius, edgeOf, gridSteps, openingFrame } from '../model/planGeom';
2
+ import { imageById } from '../model/images';
3
+ import { editNoteText } from '../editor/markup';
1
4
  import { afterEdit, resolveOpeningsNow, solveSketch, tryAddConstraint } from '../app/edit';
2
5
  import { ed, isSel, type BoxSel, type Hit, type Snap } from '../app/state';
3
- import { hyp, norm2, projT, segInter } from '../model/geom';
6
+ import { clamp, hyp, norm2, projT, segInter } from '../model/geom';
4
7
  import { dropHistory, pushHistory } from '../model/history';
5
8
  import { deep } from '../model/ids';
6
9
  import { settings } from '../model/settings';
7
- import { applySlide, openingSpan, slideGroup, type SlideGroup } from '../model/openings';
8
- import { connectedGeometry, curveInfo, mergePoints, restorePos, snapshotPos } from '../model/sketch';
9
- import { page, SK } from '../model/store';
10
- import type { Note, SelItem, Sketch } from '../model/types';
10
+ import { openingSpan, slideGroup, type SlideGroup } from '../model/openings';
11
+ import { connectedGeometry, joinPoints, mergePoints, restorePos, snapshotPos } from '../model/sketch';
12
+ import { planSK, replaceSK, SK } from '../model/store';
13
+ import type { Note, SelItem, Sketch, Underlay, WallFace } from '../model/types';
14
+ import { cabOfPoint, cabPoints, faceEdit, openingOfPoint } from '../model/faces';
15
+ import { pushedOut, syncFace, type Overflow } from '../model/facesolve';
16
+ import { snapCabinetMove } from '../editor/face';
17
+ import { openCabinet, slideTargets, snapSlide } from '../editor/elevation';
18
+ import { elevAct, elevOpen, fitState, keepInWall, moveOpeningDrag, overflow, planFit, restoreFit, slideWithinWalls, startOpeningDrag, wallBounds, type OpeningDrag } from '../editor/elevation';
11
19
  import { hitAny } from '../render/hit';
12
- import { snapAt } from '../render/snap';
13
- import { mainT, noteBox, requestDraw, S, s2w, underlayH, type HandlePart } from '../render/view';
20
+ import { onGrid, snap45, snapAt } from '../render/snap';
21
+ import { linkExtent, mainT, NOTE_TEXT_IN, NOTE_TEXT_MAX_IN, NOTE_TEXT_MIN_IN, noteBox, noteTextBox, noteTextIn, paintPending, requestDraw, requestImageDraw, requestOverlayDraw, S, s2w, type HandlePart, underlayAt, underlayH, underlayLocal } from '../render/view';
14
22
  import { solver } from '../solver';
15
23
  import type { PEv } from './registry';
16
24
  import { onSessionReset } from '../model/session';
@@ -19,25 +27,70 @@ import { selectionHint } from '../editor/selection';
19
27
 
20
28
  type Drag =
21
29
  | { kind: 'point'; id: string; start: { x: number; y: number }; grab: { x: number; y: number }; snap: Snap | null }
22
- | { kind: 'multi'; ids: string[]; start: { x: number; y: number }[]; grab: { x: number; y: number } }
23
- | { kind: 'radius'; id: string; start: number }
30
+ /** cab: dragging a cabinet (its box where it started): it snaps to corners, the floor, other cabinets */
31
+ | { kind: 'multi'; ids: string[]; start: { x: number; y: number }[]; grab: { x: number; y: number }; cab?: { id: string; box: { u0: number; u1: number; z0: number; z1: number } }; via?: string; gst?: number; gx?: number; gy?: number }
32
+ /** size a circle by its edge; grab: how far out from its centreline it was picked up (a wall's outer face, say) */
33
+ | { kind: 'radius'; id: string; start: number; grab: number }
24
34
  | { kind: 'dimoff'; id: string }
25
35
  /** swing a diameter's line around its circle and move the label in/out */
26
36
  | { kind: 'diamlabel'; id: string }
27
37
  /** slide a door/window dimension (width or a placed one) toward or away from its wall */
28
38
  | { kind: 'odimoff'; id: string; part: string }
29
39
  /** slide a door/window (or the free chain of openings it's locked into) along its wall; grab: s where it was picked up */
30
- | { kind: 'opening'; id: string; grab: number; group: SlideGroup }
31
- | { kind: 'note'; id: string; start: Note; grab: { x: number; y: number } }
32
- | { kind: 'umove'; start: { x: number; y: number }; grab: { x: number; y: number } }
33
- | { kind: 'uresize'; part: HandlePart; start: { x: number; y: number; w: number; h: number } }
40
+ | { kind: 'opening'; id: string; grab: number; group: SlideGroup; snap: { at0: number; w: number; targets: number[] }; base?: Overflow; last?: number }
41
+ /** the wall view: a door or window dragged on the wall (along it, and windows up and down); grab: where it was picked up */
42
+ | { kind: 'wopening'; drag: OpeningDrag; grab: { x: number; y: number }; stuck?: boolean; via?: string }
43
+ /** both: a link's tile and its leader's tip together (grabbed along its leader) */
44
+ | { kind: 'note'; id: string; start: Note; grab: { x: number; y: number }; both?: boolean }
45
+ /** several pieces of markup (and photos) selected, one of them dragged: they all move by as much */
46
+ | { kind: 'group'; notes: { id: string; start: Note }[]; images: { id: string; start: { x: number; y: number } }[]; grab: { x: number; y: number } }
47
+ | { kind: 'umove'; id: string; start: { x: number; y: number }; grab: { x: number; y: number } }
48
+ | { kind: 'uresize'; id: string; part: HandlePart; start: { x: number; y: number; w: number; h: number } }
34
49
  /** reshape markup: move an arrow end, or resize the box of a cloud / pen stroke */
35
50
  | { kind: 'nresize'; id: string; part: string; start: Note };
36
51
 
37
52
  const RESIZE_CURSOR: Record<HandlePart, string> = { nw: 'nwse-resize', se: 'nwse-resize', ne: 'nesw-resize', sw: 'nesw-resize', n: 'ns-resize', s: 'ns-resize', e: 'ew-resize', w: 'ew-resize' };
38
53
 
39
54
  /** `again`: the press landed on what was already the whole selection (a finger's second tap expands it). */
40
- interface SelectState { down?: PEv; hit?: Hit | null; dragging?: boolean; drag?: Drag | null; again?: boolean }
55
+ /** `added`: a finger or pencil press added what it landed on to the selection (it goes again if the press drags it). */
56
+ interface SelectState { down?: PEv; hit?: Hit | null; dragging?: boolean; drag?: Drag | null; again?: boolean; added?: boolean }
57
+
58
+ /**
59
+ * A drag that keeps up with the pointer. Each move is solved as it comes — until a solve takes a good part of a frame
60
+ * (a big, well-constrained plan on a device): then moves that arrive meanwhile wait for the next frame and only the
61
+ * latest is solved. Solving each in turn queued them up behind the solve, and what's constrained to the dragged thing
62
+ * trailed further and further behind the finger. A lift solves any move still waiting first.
63
+ */
64
+ export const dragTiming = {
65
+ /** a solve this slow (ms) and moves wait for the next frame; tests pin it, so a busy machine doesn't change what they see */
66
+ slowMs: 4,
67
+ };
68
+ const dragMove = { ev: null as PEv | null, drag: null as Drag | null, frame: 0, cost: 0 };
69
+ function solveDrag(d: Drag, ev: PEv): void {
70
+ const t0 = performance.now();
71
+ dragTo(d, ev);
72
+ dragMove.cost = performance.now() - t0;
73
+ }
74
+ function moveDrag(d: Drag, ev: PEv): void {
75
+ if (dragMove.cost < dragTiming.slowMs && !dragMove.frame) { solveDrag(d, ev); return; }
76
+ dragMove.ev = ev; dragMove.drag = d;
77
+ if (!dragMove.frame) dragMove.frame = requestAnimationFrame(flushDrag);
78
+ }
79
+ function flushDrag(): void {
80
+ const { ev, drag } = dragMove;
81
+ dropDrag();
82
+ if (!ev || !drag) return;
83
+ solveDrag(drag, ev);
84
+ // (shown in this frame: asking for one from here — itself a frame's callback — would show it a frame later)
85
+ paintPending();
86
+ }
87
+ function dropDrag(): void {
88
+ if (dragMove.frame) cancelAnimationFrame(dragMove.frame);
89
+ dragMove.ev = null; dragMove.drag = null; dragMove.frame = 0;
90
+ }
91
+
92
+ /** What a finger or pencil can pick several of, one tap at a time. */
93
+ const ADDABLE = new Set(['point', 'line', 'circle', 'arc', 'opening', 'oanchor', 'cab', 'wface']);
41
94
 
42
95
  /**
43
96
  * The last tap, for double taps: two taps close together in time and place, with no edit between, act on what the
@@ -58,25 +111,38 @@ function doubleTapOf(at: PEv, h: Hit, now: number): Hit | null {
58
111
 
59
112
  export const selectTool = {
60
113
  st: {} as SelectState,
61
- reset() { this.st = {}; ed.boxSel = null; },
114
+ reset() { dropDrag(); this.st = {}; ed.boxSel = null; },
62
115
  hover(ev: PEv) {
63
116
  if (ev.touch) return;
64
- const h = hitAny(ev.sx, ev.sy, { touch: false });
117
+ const h = hitAny(ev.sx, ev.sy, { touch: false, faces: true }), was = ed.hover;
65
118
  ed.hover = h && !h.weak ? h : null;
66
119
  setCursor(!h ? 'default'
67
120
  : h.type === 'uhandle' ? RESIZE_CURSOR[h.part as HandlePart]
68
121
  : h.type === 'nhandle' ? RESIZE_CURSOR[h.part as HandlePart] ?? 'crosshair'
69
- : h.type === 'underlay' && page().underlay?.locked ? 'default'
70
- : h.type === 'dim' || h.type === 'odim' ? 'pointer' : 'move');
71
- requestDraw();
122
+ : h.type === 'note' && h.part === 'tip' ? 'crosshair'
123
+ : h.type === 'underlay' && imageById(h.id)?.locked ? 'default'
124
+ : h.type === 'dim' || h.type === 'odim' || h.type === 'elev' || h.type === 'cabfoot' ? 'pointer' : 'move');
125
+ // the highlight is part of the drawing: redraw it only when what's under the pointer changed
126
+ const same = was?.type === ed.hover?.type && was?.id === ed.hover?.id && was?.part === ed.hover?.part;
127
+ if (!same) requestDraw();
72
128
  },
73
129
  down(ev: PEv) {
74
- const h = hitAny(ev.sx, ev.sy, { touch: ev.touch });
130
+ const raw = hitAny(ev.sx, ev.sy, { touch: ev.touch, faces: true });
131
+ // (a wall picked whole: the side it was picked on, which side of it a length is measured on between two spaces)
132
+ if (raw?.side && (raw.type === 'line' || raw.type === 'circle' || raw.type === 'arc')) ed.wallTap = { id: raw.id, side: raw.side };
133
+ // (a door's or window's end, as selected: "<opening>|<edge>")
134
+ const h = raw && raw.type === 'oanchor' ? { ...raw, id: raw.id + '|' + raw.part } : raw;
75
135
  const again = !!h && ed.sel.length === 1 && isSel(h.type, h.id);
76
136
  this.st = { down: ev, hit: h, dragging: false, again };
77
- if (h && h.type !== 'dim' && h.type !== 'odim' && h.type !== 'uhandle' && h.type !== 'nhandle') {
137
+ if (h && h.type !== 'dim' && h.type !== 'odim' && h.type !== 'elev' && h.type !== 'cabfoot' && h.type !== 'uhandle' && h.type !== 'nhandle') {
138
+ // (a wall and its edge are the same wall picked two ways: picking one, added to the selection, stands in for the other)
139
+ if ((ev.shift || ev.touch) && !isSel(h.type, h.id)) ed.sel = ed.sel.filter(s => !sameWall(s, h));
78
140
  if (ev.shift) toggleSel(h);
79
- else if (!isSel(h.type, h.id)) ed.sel = [{ type: h.type as SelItem['type'], id: h.id }];
141
+ else if (!isSel(h.type, h.id)) {
142
+ // a finger or pencil has no Shift: tapping something else adds it (tap empty space to clear)
143
+ if (ev.touch && ADDABLE.has(h.type) && ed.sel.length && ed.sel.every(s => ADDABLE.has(s.type))) { ed.sel.push({ type: h.type as SelItem['type'], id: h.id }); this.st.added = true; }
144
+ else ed.sel = [{ type: h.type as SelItem['type'], id: h.id }];
145
+ }
80
146
  updateCtx(); requestDraw();
81
147
  }
82
148
  },
@@ -85,35 +151,44 @@ export const selectTool = {
85
151
  if (!st.down) { this.hover(ev); return; }
86
152
  const d = hyp(ev.sx - st.down.sx, ev.sy - st.down.sy);
87
153
  if (!st.dragging && d < (ev.touch ? 10 : 5)) return;
88
- if (st.hit && st.hit.type !== 'room') {
154
+ if (st.hit && st.hit.type !== 'room' && st.hit.type !== 'cabfoot') {
89
155
  if (!st.dragging) {
90
156
  st.dragging = true;
157
+ // pressed to drag, not to add: just this one moves
158
+ if (st.added) { ed.sel = [{ type: st.hit.type as SelItem['type'], id: st.hit.id }]; updateCtx(); }
91
159
  pushHistory();
92
160
  st.drag = beginDrag(st.hit, st.down);
93
161
  if (!st.drag) { dropHistory(); st.hit = null; return; }
162
+ // a cabinet's, door's or window's corner pressed to drag: it's the object that moves (and is selected)
163
+ const cab = st.hit.type === 'point' ? cabOfPoint(SK(), st.hit.id) : st.hit.type === 'line' ? SK().lines[st.hit.id]?.cab ?? null : null;
164
+ const op = st.hit.type === 'point' && elevOpen() ? openingOfPoint(st.hit.id) : null;
165
+ if ((cab || op) && ed.sel.length === 1 && isSel(st.hit.type, st.hit.id)) { ed.sel = [cab ? { type: 'cab', id: cab } : { type: 'opening', id: op! }]; updateCtx(); }
166
+ dropDrag(); dragMove.cost = 0;
94
167
  }
95
- dragTo(st.drag!, ev);
168
+ moveDrag(st.drag!, ev);
96
169
  } else {
97
170
  st.dragging = true;
98
171
  ed.boxSel = { x0: st.down.sx, y0: st.down.sy, x1: ev.sx, y1: ev.sy };
99
- requestDraw();
172
+ requestOverlayDraw();
100
173
  }
101
174
  },
102
175
  up(ev: PEv) {
103
176
  const st = this.st;
104
177
  if (!st.down) return;
105
- if (st.dragging && st.drag) endDrag(st.drag);
178
+ if (st.dragging && st.drag) { flushDrag(); endDrag(st.drag); }
106
179
  else if (st.dragging && ed.boxSel) { boxSelect(ed.boxSel, ev.shift); ed.boxSel = null; }
107
180
  else if (st.hit) {
108
181
  const h = st.hit, now = performance.now();
182
+ // a cabinet's footprint in the plan: open its wall, the cabinet selected
183
+ if (h.type === 'cabfoot') { const [key, cab] = h.id.split('#'); this.st = {}; openCabinet(key, cab); return; }
109
184
  // with a finger or pencil, tapping the selected line again selects everything joined to it (as a double tap does)
110
- const first = ev.shift || h.type === 'dim' || h.type === 'odim' ? null
185
+ const first = ev.shift || h.type === 'dim' || h.type === 'odim' || h.type === 'elev' ? null
111
186
  : st.down.touch && st.again ? h
112
187
  : doubleTapOf(st.down, h, now);
113
188
  const sk = SK();
114
189
  // a corner point stands for the lines meeting there
115
- const from = first?.type === 'point' ? Object.keys(sk.lines).filter(id => sk.lines[id].a === first.id || sk.lines[id].b === first.id) : [];
116
- if (first && (first.type === 'line' || first.type === 'arc' || from.length)) {
190
+ const from = first?.type === 'point' ? Object.keys(sk.lines).filter(id => !sk.lines[id].anchor && (sk.lines[id].a === first.id || sk.lines[id].b === first.id)) : [];
191
+ if (first && ((first.type === 'line' && !sk.lines[first.id]?.anchor) || first.type === 'arc' || from.length)) {
117
192
  // double tap a line or arc (or a point on one): select everything connected to it
118
193
  const g = connectedGeometry(sk, first.type === 'line' ? [first.id] : from, first.type === 'arc' ? [first.id] : []);
119
194
  ed.sel = [...g.lines.map(id => ({ type: 'line' as const, id })), ...g.arcs.map(id => ({ type: 'arc' as const, id }))];
@@ -122,10 +197,24 @@ export const selectTool = {
122
197
  updateCtx(); updateHint(); requestDraw();
123
198
  return;
124
199
  }
200
+ // a finger or pencil tapping one of several selected things takes it out of the selection
201
+ if (st.down.touch && !st.added && !ev.shift && ed.sel.length > 1 && ADDABLE.has(h.type) && isSel(h.type, h.id)) ed.sel = ed.sel.filter(s => !(s.type === h.type && s.id === h.id));
125
202
  // a tap on a dimension selects it; the keypad only opens from the yellow + (or Length etc.)
126
- if (h.type === 'odim') ed.sel = !h.part || h.part === 'w' ? [{ type: 'opening', id: h.id }] : [{ type: 'odim', id: h.id + '|' + h.part }];
203
+ // (its width chip too: a dimension picked, so the + sets it — the door or window picked whole has no +)
204
+ else if (h.type === 'odim') ed.sel = !h.part ? [{ type: 'opening', id: h.id }] : [{ type: 'odim', id: h.id + '|' + h.part }];
205
+ // a wall view chip: a face dimension is selected as one; the others as the chip
206
+ else if (h.type === 'elev') { const a = elevAct(h.id); ed.sel = a.k === 'fdim' ? [{ type: 'dim', id: a.id }] : [{ type: 'elev', id: h.id }]; }
127
207
  else if (h.type === 'dim') ed.sel = [{ type: 'dim', id: h.id }];
128
208
  else if (h.type === 'room' && first?.type === 'room' && first.id === h.id) openDrawer('rooms');
209
+ // double tap a note (or, with a finger, tap it again): edit its text
210
+ else if (h.type === 'note' && first?.type === 'note' && first.id === h.id && SK().notes.find(n => n.id === h.id)?.type === 'text') {
211
+ ed.sel = [{ type: 'note', id: h.id }];
212
+ lastTap = { t: 0, x: 0, y: 0, hit: null, seq: -1 };
213
+ this.st = {};
214
+ updateCtx(); requestDraw();
215
+ editNoteText(h.id);
216
+ return;
217
+ }
129
218
  lastTap = { t: now, x: st.down.sx, y: st.down.sy, hit: h, seq: ed.editSeq };
130
219
  } else if (!ev.shift) ed.sel = [];
131
220
  this.st = {};
@@ -139,6 +228,17 @@ export const selectTool = {
139
228
  },
140
229
  };
141
230
 
231
+ /** The wall a wall edge's id is on, as it's picked whole: a line, a circle or an arc. */
232
+ function edgeWall(sk: Sketch, id: string): { type: 'line' | 'circle' | 'arc'; id: string } {
233
+ return { type: sk.lines[id] ? 'line' : sk.circles[id] ? 'circle' : 'arc', id };
234
+ }
235
+
236
+ /** Whether `s` is the wall `h` is an edge of, or an edge of the wall `h` is. */
237
+ function sameWall(s: SelItem, h: Hit): boolean {
238
+ if (h.type === 'wface') return s.type !== 'wface' && s.id === edgeOf(h.id)[0];
239
+ return (h.type === 'line' || h.type === 'circle' || h.type === 'arc') && s.type === 'wface' && edgeOf(s.id)[0] === h.id;
240
+ }
241
+
142
242
  function toggleSel(h: Hit): void {
143
243
  if (isSel(h.type, h.id)) ed.sel = ed.sel.filter(s => !(s.type === h.type && s.id === h.id));
144
244
  else ed.sel.push({ type: h.type as SelItem['type'], id: h.id });
@@ -146,12 +246,15 @@ function toggleSel(h: Hit): void {
146
246
 
147
247
  /** Window select (drag right) takes fully enclosed lines; crossing select (drag left) also takes touched ones. */
148
248
  function boxSelect(b: BoxSel, add: boolean): void {
249
+ // each mode selects only its own things: Annotate its markup, Draft the drawing
250
+ if (ed.mode === 'annotate') { boxSelectMarkup(b, add); return; }
149
251
  const sk = SK(), T = mainT(), crossing = b.x1 < b.x0;
150
252
  const x0 = Math.min(b.x0, b.x1), x1 = Math.max(b.x0, b.x1), y0 = Math.min(b.y0, b.y1), y1 = Math.max(b.y0, b.y1);
151
253
  const inside = (id: string) => { const p = sk.points[id]; const [x, y] = S(T, p.x, p.y); return x >= x0 && x <= x1 && y >= y0 && y <= y1; };
152
254
  const out: SelItem[] = add ? ed.sel.slice() : [];
153
255
  for (const id in sk.lines) {
154
256
  const l = sk.lines[id];
257
+ if (l.anchor || l.cab) continue;
155
258
  const ia = inside(l.a), ib = inside(l.b);
156
259
  let hit = ia && ib;
157
260
  if (!hit && crossing) {
@@ -165,43 +268,133 @@ function boxSelect(b: BoxSel, add: boolean): void {
165
268
  }
166
269
  if (hit && !out.some(s => s.id === id)) out.push({ type: 'line', id });
167
270
  }
271
+ // a cabinet: taken whole when it's inside (or, crossing, when any of it is)
272
+ for (const [id, cb] of Object.entries((sk as WallFace).cabs ?? {})) {
273
+ const ins = cabPoints(cb).map(inside);
274
+ if (crossing ? ins.some(Boolean) : ins.every(Boolean)) out.push({ type: 'cab', id });
275
+ }
276
+ // a door or window, the same way: by its corners on a wall face, or its two ends along the wall in the plan
277
+ const atScreen = (x: number, y: number) => { const [sx, sy] = S(T, x, y); return sx >= x0 && sx <= x1 && sy >= y0 && sy <= y1; };
278
+ const opCorners = new Map<string, boolean[]>();
279
+ if (elevOpen()) {
280
+ for (const pid in sk.points) {
281
+ const op = openingOfPoint(pid);
282
+ if (op) opCorners.set(op, [...(opCorners.get(op) ?? []), inside(pid)]);
283
+ }
284
+ } else {
285
+ for (const id in sk.openings) {
286
+ const g = openingFrame(sk, id);
287
+ if (g) opCorners.set(id, [g.s0, g.s1].map(s => atScreen(g.A.x + g.ux * s, g.A.y + g.uy * s)));
288
+ }
289
+ }
290
+ for (const [id, ins] of opCorners) if (crossing ? ins.some(Boolean) : ins.every(Boolean)) out.push({ type: 'opening', id });
168
291
  for (const id in sk.circles) if (inside(sk.circles[id].c)) out.push({ type: 'circle', id });
169
292
  for (const id in sk.arcs) if (inside(sk.arcs[id].c)) out.push({ type: 'arc', id });
170
- if (!out.length) for (const id in sk.points) if (inside(id) && !sk.points[id].origin) out.push({ type: 'point', id });
293
+ const cabCorners = new Set(Object.values((sk as WallFace).cabs ?? {}).flatMap(cabPoints));
294
+ if (!out.length) for (const id in sk.points) if (inside(id) && !sk.points[id].origin && !sk.points[id].anchor && !cabCorners.has(id)) out.push({ type: 'point', id });
295
+ ed.sel = out;
296
+ }
297
+
298
+ /** Annotate's box: the markup in it (window: wholly inside; crossing: any of it inside) — never the drawing or the images. */
299
+ function boxSelectMarkup(b: BoxSel, add: boolean): void {
300
+ const T = mainT(), crossing = b.x1 < b.x0;
301
+ const x0 = Math.min(b.x0, b.x1), x1 = Math.max(b.x0, b.x1), y0 = Math.min(b.y0, b.y1), y1 = Math.max(b.y0, b.y1);
302
+ const out: SelItem[] = add ? ed.sel.filter(s => s.type === 'note') : [];
303
+ if (settings.layers.annot) {
304
+ for (const n of SK().notes) {
305
+ // the note's box on screen
306
+ let l: number, r: number, t: number, bt: number;
307
+ if (n.type === 'text') { const tb = noteTextBox(T, n); l = tb.x; r = tb.x + tb.w; t = tb.y; bt = tb.y + tb.h; }
308
+ else if (n.type === 'link') [l, t, r, bt] = linkExtent(T, n);
309
+ else {
310
+ const nb = noteBox(n);
311
+ if (!nb) continue;
312
+ const [ax, ay] = S(T, nb.left, nb.top), [bx, by] = S(T, nb.right, nb.bottom);
313
+ l = Math.min(ax, bx); r = Math.max(ax, bx); t = Math.min(ay, by); bt = Math.max(ay, by);
314
+ }
315
+ const hit = crossing ? l <= x1 && r >= x0 && t <= y1 && bt >= y0 : l >= x0 && r <= x1 && t >= y0 && bt <= y1;
316
+ if (hit && !out.some(s => s.id === n.id)) out.push({ type: 'note', id: n.id });
317
+ }
318
+ }
171
319
  ed.sel = out;
172
320
  }
173
321
 
174
322
  function beginDrag(h: Hit, down: PEv): Drag | null {
175
323
  const sk = SK();
324
+ // a door's or window's end: it moves, whole, as by its body
325
+ if (h.type === 'oanchor') h = { type: 'opening', id: h.id.split('|')[0] };
326
+ // a wall's edge: the wall moves, as by its body (with the rest of the selection, when the edge is part of it) — a
327
+ // circle's is sized, as by its edge
328
+ const edgeSel = h.type === 'wface' && isSel('wface', h.id);
329
+ if (h.type === 'wface') h = edgeWall(sk, edgeOf(h.id)[0]);
176
330
  const [wx, wy] = s2w(down.sx, down.sy);
177
- if (h.type === 'point') {
178
- const p = sk.points[h.id];
179
- if (p.fixed) { toast(p.origin ? 'The origin stays put. Lock or unlock other points from the bar below.' : 'This point is locked. Unlock it to move it.'); return null; }
180
- return { kind: 'point', id: h.id, start: { x: p.x, y: p.y }, grab: { x: wx, y: wy }, snap: null };
181
- }
331
+ // pressed on one of several things selected: they all move together, by as much as it's dragged
332
+ const picked = edgeSel || isSel(h.type, h.id), several = picked && ed.sel.length > 1;
333
+ // (Annotate's: the selected markup, and photos)
334
+ if (several && (h.type === 'note' || h.type === 'underlay')) return groupDrag(sk, h, wx, wy);
335
+ // a cabinet's, door's or window's corner: it moves, whole (it keeps its size)
336
+ const cabOf = h.type === 'point' ? cabOfPoint(sk, h.id) : h.type === 'line' ? sk.lines[h.id]?.cab ?? null : null, opOf = h.type === 'point' && elevOpen() ? openingOfPoint(h.id) : null;
337
+ // (`via`: the corner it was picked up by — dropped on a point, they join)
338
+ const via = h.type === 'point' && (cabOf || (opOf && sk.points[h.id]?.anchor)) ? h.id : undefined;
339
+ if (cabOf) h = { type: 'cab', id: cabOf };
340
+ else if (opOf && sk.points[h.id]?.anchor) h = { type: 'opening', id: opOf };
341
+ /** The points that move: the whole selection's, when what's pressed is part of it (walls, lines, arcs, circles, points,
342
+ cabinets and spaces — doors and windows go with their walls), otherwise just its own. */
182
343
  const ptsOf = () => {
183
344
  const s = new Set<string>();
184
345
  const addL = (id: string) => { const l = sk.lines[id]; if (l) { s.add(l.a); s.add(l.b); } };
185
- const addArc = (id: string) => { const a = sk.arcs[id]; s.add(a.c); s.add(a.a); s.add(a.b); };
186
- if (isSel(h.type, h.id)) {
346
+ const addArc = (id: string) => { const a = sk.arcs[id]; if (a) { s.add(a.c); s.add(a.a); s.add(a.b); } };
347
+ if (picked) {
187
348
  for (const it of ed.sel) {
349
+ if (it.type === 'cab') { const cb = (sk as WallFace).cabs?.[it.id]; if (cb) cabPoints(cb).forEach(p => s.add(p)); }
188
350
  if (it.type === 'line') addL(it.id);
351
+ if (it.type === 'wface') {
352
+ const w = edgeWall(sk, edgeOf(it.id)[0]);
353
+ if (w.type === 'line') addL(w.id); else if (w.type === 'arc') addArc(w.id); else if (sk.circles[w.id]) s.add(sk.circles[w.id].c);
354
+ }
189
355
  if (it.type === 'point') s.add(it.id);
190
356
  if (it.type === 'arc') addArc(it.id);
191
- if (it.type === 'circle') s.add(sk.circles[it.id].c);
357
+ if (it.type === 'circle') { const ci = sk.circles[it.id]; if (ci) s.add(ci.c); }
358
+ if (it.type === 'room') { const r = sk.rooms[it.id]; if (r) r.loop.forEach(p => s.add(p)); }
192
359
  }
193
360
  } else {
194
361
  if (h.type === 'line') addL(h.id);
195
362
  if (h.type === 'arc') addArc(h.id);
196
363
  }
197
- return [...s].filter(id => !sk.points[id].fixed);
364
+ return [...s].filter(id => sk.points[id] && !sk.points[id].fixed);
198
365
  };
199
- if (h.type === 'line' || h.type === 'arc') {
200
- const ids = ptsOf();
201
- if (!ids.length) { toast('Those points are locked.'); return null; }
366
+ /** Those points moved together, by as much as the pointer moves. */
367
+ const multi = (ids: string[]): Drag | null => {
368
+ if (!ids.length) { toast('Those points are locked.', { warn: true }); return null; }
202
369
  return { kind: 'multi', ids, start: ids.map(id => ({ x: sk.points[id].x, y: sk.points[id].y })), grab: { x: wx, y: wy } };
370
+ };
371
+ if (h.type === 'point') {
372
+ const p = sk.points[h.id];
373
+ if (p.fixed) { toast(p.origin ? 'The origin stays put. Lock or unlock other points from the bar below.' : p.anchor ? 'This corner is set by the plan — move the wall, door or window there.' : 'This point is locked. Unlock it to move it.', { warn: true }); return null; }
374
+ // (one of several selected: the selection moves, as by one of its lines)
375
+ if (several) return multi(ptsOf());
376
+ return { kind: 'point', id: h.id, start: { x: p.x, y: p.y }, grab: { x: wx, y: wy }, snap: null };
377
+ }
378
+ if (h.type === 'cab') {
379
+ // a cabinet moves as a whole (and the rest of the selection with it, when it's part of it)
380
+ const cb = (sk as WallFace).cabs?.[h.id];
381
+ if (!cb) return null;
382
+ const ids = [...new Set([...cabPoints(cb), ...(isSel('cab', h.id) ? ptsOf() : [])])].filter(id => !sk.points[id].fixed);
383
+ const cs = cabPoints(cb).map(p => sk.points[p]), box = { u0: Math.min(...cs.map(p => p.x)), u1: Math.max(...cs.map(p => p.x)), z0: Math.min(...cs.map(p => p.y)), z1: Math.max(...cs.map(p => p.y)) };
384
+ return { kind: 'multi', ids, start: ids.map(id => ({ x: sk.points[id].x, y: sk.points[id].y })), grab: { x: wx, y: wy }, cab: { id: h.id, box }, via };
385
+ }
386
+ if (h.type === 'opening' && elevOpen()) {
387
+ const drag = startOpeningDrag(h.id);
388
+ return drag ? { kind: 'wopening', drag, grab: { x: wx, y: wy }, via } : null;
389
+ }
390
+ if (h.type === 'line' && sk.lines[h.id]?.anchor) { toast('The wall’s edges are set by the plan.', { warn: true }); return null; }
391
+ if (h.type === 'line' || h.type === 'arc') return multi(ptsOf());
392
+ // a circle alone is sized by its edge; one of several selected moves with the rest
393
+ if (h.type === 'circle') {
394
+ if (several) return multi(ptsOf());
395
+ const ci = sk.circles[h.id], o = sk.points[ci.c];
396
+ return { kind: 'radius', id: h.id, start: ci.r, grab: hyp(wx - o.x, wy - o.y) - ci.r };
203
397
  }
204
- if (h.type === 'circle') return { kind: 'radius', id: h.id, start: sk.circles[h.id].r };
205
398
  if (h.type === 'odim') {
206
399
  // width and placed dimensions move; the hover-only wall-end distances don't (tap them to lock instead)
207
400
  if (h.part === 'w' || (h.part || '').startsWith('d:')) return { kind: 'odimoff', id: h.id, part: h.part! };
@@ -214,40 +407,114 @@ function beginDrag(h: Hit, down: PEv): Drag | null {
214
407
  return { kind: 'dimoff', id: h.id };
215
408
  }
216
409
  if (h.type === 'opening') {
410
+ // selected with walls or lines: the selection moves (a door or window goes with its wall), not the door along it
411
+ if (several && ed.sel.some(s => MOVE_WITH.has(s.type))) return multi(ptsOf());
217
412
  const o = sk.openings[h.id];
218
413
  const group = slideGroup(sk, h.id), l = sk.lines[o.line];
219
- if (!group || !l) { toast('This ' + (o.type === 'door' ? 'door' : 'window') + ' is locked by a dimension. Change the dimension, or make it a reference, to move it.'); return null; }
414
+ if (!group || !l) { toast('This ' + (o.type === 'door' ? 'door' : 'window') + ' is locked by a dimension. Change the dimension, or make it a reference, to move it.', { warn: true }); return null; }
220
415
  const A = sk.points[l.a], B = sk.points[l.b];
221
- return { kind: 'opening', id: h.id, grab: projT(A, B, { x: wx, y: wy }) * hyp(B.x - A.x, B.y - A.y), group };
416
+ const g = openingSpan(sk, o);
417
+ return { kind: 'opening', id: h.id, grab: projT(A, B, { x: wx, y: wy }) * hyp(B.x - A.x, B.y - A.y), group, snap: { at0: o.at, w: g ? g.w : o.w, targets: slideTargets(sk, h.id, group) } };
418
+ }
419
+ if (h.type === 'note') {
420
+ const n = sk.notes.find(n => n.id === h.id)!;
421
+ // a link's tile: grabbed by its card, the card moves; by its leader's tip, that end; along its leader, both ends
422
+ // together, as a line moves
423
+ if (h.part === 'tip') return { kind: 'nresize', id: h.id, part: 'tip', start: deep(n) };
424
+ return { kind: 'note', id: h.id, start: deep(n), grab: { x: wx, y: wy }, ...(h.part === 'leader' ? { both: true } : {}) };
222
425
  }
223
- if (h.type === 'note') { const n = sk.notes.find(n => n.id === h.id)!; return { kind: 'note', id: h.id, start: deep(n), grab: { x: wx, y: wy } }; }
224
426
  if (h.type === 'nhandle') { const n = sk.notes.find(x => x.id === h.id); return n ? { kind: 'nresize', id: h.id, part: h.part!, start: deep(n) } : null; }
225
427
  if (h.type === 'underlay' || h.type === 'uhandle') {
226
- const u = page().underlay;
428
+ const u = imageById(h.id);
227
429
  if (!u) return null;
228
- if (u.locked) { toast('This image is locked. Tap Unlock in the bar below to move or resize it.'); return null; }
229
- if (h.type === 'underlay') return { kind: 'umove', start: { x: u.x, y: u.y }, grab: { x: wx, y: wy } };
230
- return { kind: 'uresize', part: h.part as HandlePart, start: { x: u.x, y: u.y, w: u.w, h: underlayH(u) } };
430
+ if (u.locked) { toast('This image is locked. Tap Unlock in the bar below to move or resize it.', { warn: true }); return null; }
431
+ if (h.type === 'underlay') return { kind: 'umove', id: u.id, start: { x: u.x, y: u.y }, grab: { x: wx, y: wy } };
432
+ return { kind: 'uresize', id: u.id, part: h.part as HandlePart, start: { x: u.x, y: u.y, w: u.w, h: underlayH(u) } };
231
433
  }
232
434
  return null;
233
435
  }
234
436
 
437
+ /** What's moved by its points: dragging a door or window selected with any of these moves them all. */
438
+ const MOVE_WITH = new Set(['line', 'arc', 'circle', 'point', 'cab', 'room']);
439
+
440
+ /** Several pieces of markup (and photos) selected, one of them pressed: they all move by as much as it's dragged. */
441
+ function groupDrag(sk: Sketch, h: Hit, wx: number, wy: number): Drag | null {
442
+ if (h.type === 'underlay' && imageById(h.id)?.locked) { toast('This image is locked. Tap Unlock in the bar below to move or resize it.', { warn: true }); return null; }
443
+ const notes = ed.sel.filter(s => s.type === 'note').map(s => sk.notes.find(n => n.id === s.id)).filter((n): n is Note => !!n);
444
+ // (a locked photo stays where it is)
445
+ const images = ed.sel.filter(s => s.type === 'underlay').map(s => imageById(s.id)).filter((u): u is Underlay => !!u && !u.locked);
446
+ return { kind: 'group', notes: notes.map(n => ({ id: n.id, start: deep(n) })), images: images.map(u => ({ id: u.id, start: { x: u.x, y: u.y } })), grab: { x: wx, y: wy } };
447
+ }
448
+
449
+ /** How far things ran past their walls when each drag began: it may not go further. */
450
+ const dragBase = new WeakMap<Drag, Overflow>();
451
+
235
452
  function dragTo(d: Drag, ev: PEv): void {
236
453
  const sk = SK();
237
454
  const [wx, wy] = s2w(ev.sx, ev.sy);
455
+ // a drag that would push anything past its wall (in the plan: make a wall shorter than what's on it) goes as far as
456
+ // it can and stops there (measured against how things were when the drag began)
457
+ const shapes = d.kind === 'point' || d.kind === 'multi' || d.kind === 'radius';
458
+ if (shapes && !dragBase.has(d)) dragBase.set(d, overflow(sk));
459
+ const was = dragBase.get(d) ?? {}, held = shapes ? fitState(sk) : null;
460
+ const fits = () => !held || !pushedOut(was, overflow(sk));
461
+ const keep = () => { if (!fits()) restoreFit(sk, held!); };
462
+ /** Drag points toward where they're going; a move that would run past (a fast one, a long way) stops at the limit, not short of it. */
463
+ const go = (ids: string[], to: { x: number; y: number }[]): boolean => {
464
+ const ok = solver.drag(sk, ids, to);
465
+ if (fits()) return ok;
466
+ const from = ids.map(id => ({ x: held!.pos.p[id][0], y: held!.pos.p[id][1] }));
467
+ const at = (t: number) => from.map((f, i) => ({ x: f.x + (to[i].x - f.x) * t, y: f.y + (to[i].y - f.y) * t }));
468
+ let lo = 0, hi = 1;
469
+ for (let i = 0; i < 6; i++) {
470
+ const m = (lo + hi) / 2;
471
+ restoreFit(sk, held!);
472
+ solver.drag(sk, ids, at(m));
473
+ if (fits()) lo = m; else hi = m;
474
+ }
475
+ restoreFit(sk, held!);
476
+ if (lo > 0) { solver.drag(sk, ids, at(lo)); overflow(sk); }
477
+ return ok;
478
+ };
238
479
  if (d.kind === 'point') {
480
+ // Shift: the end of a line (or wall) goes level, plumb or 45° from the line's other end — at a corner of two, from
481
+ // whichever of their other ends that's nearest to where the pointer is
482
+ // (not a line already held level or plumb: its direction can't change)
483
+ const squared = (l: { a: string; b: string }) => Object.values(sk.cons).some(c => (c.type === 'h' || c.type === 'v') && ((c.a === l.a && c.b === l.b) || (c.a === l.b && c.b === l.a)));
484
+ const ends = ev.shift ? Object.values(sk.lines).filter(l => !l.anchor && (l.a === d.id || l.b === d.id) && !squared(l)).map(l => sk.points[l.a === d.id ? l.b : l.a]) : [];
485
+ if (ends.length) {
486
+ const q = ends.map(o => snap45(o, { x: wx, y: wy })).reduce((m, c) => (hyp(c.x - wx, c.y - wy) < hyp(m.x - wx, m.y - wy) ? c : m));
487
+ // where the drawing can't take it there (other constraints), it's an ordinary drag — never stuck
488
+ if (go([d.id], [{ x: q.x, y: q.y }])) { d.snap = null; ed.snapState = null; return; }
489
+ restoreFit(sk, held!);
490
+ }
239
491
  const s = snapAt(ev.sx, ev.sy, { exclude: new Set([d.id]), touch: ev.touch });
240
492
  d.snap = s.kind === 'point' || s.kind === 'mid' || s.kind === 'online' || s.kind === 'oncircle' ? s : null;
241
493
  ed.snapState = d.snap ? s : null;
242
- solver.drag(sk, [d.id], [{ x: s.x, y: s.y }]);
494
+ go([d.id], [{ x: s.x, y: s.y }]);
243
495
  } else if (d.kind === 'multi') {
244
- const dx = wx - d.grab.x, dy = wy - d.grab.y;
245
- solver.drag(sk, d.ids, d.start.map(p => ({ x: p.x + dx, y: p.y + dy })));
496
+ let mx = wx - d.grab.x, my = wy - d.grab.y;
497
+ // Shift: moved level, plumb or 45° only
498
+ if (ev.shift) ({ x: mx, y: my } = snap45({ x: 0, y: 0 }, { x: mx, y: my }));
499
+ if (d.cab && !ev.shift) [mx, my] = snapCabinetMove(d.cab.id, d.cab.box, mx, my, (ev.touch ? 16 : 10) / mainT().s);
500
+ // with Snap to grid, moved by whole steps of it: what's on the grid stays on it (held steady as snaps are)
501
+ else if (settings.snapGrid && !d.cab) {
502
+ const st = gridSteps(mainT().s).minor;
503
+ d.gx = mx = onGrid(mx, st, d.gst === st ? d.gx : undefined);
504
+ d.gy = my = onGrid(my, st, d.gst === st ? d.gy : undefined);
505
+ d.gst = st;
506
+ }
507
+ const [dx, dy] = keepInWall(sk, d.ids, d.start, mx, my);
508
+ go(d.ids, d.start.map(p => ({ x: p.x + dx, y: p.y + dy })));
246
509
  } else if (d.kind === 'radius') {
247
510
  const ci = sk.circles[d.id], o = sk.points[ci.c];
248
511
  const snap = snapshotPos(sk);
249
- ci.r = Math.max(0.1, hyp(wx - o.x, wy - o.y));
512
+ // (where it was picked up stays under the pointer: a thick wall by its face doesn't jump to the centreline)
513
+ ci.r = Math.max(0.1, hyp(wx - o.x, wy - o.y) - d.grab);
514
+ const b = wallBounds();
515
+ if (b) ci.r = Math.max(0.1, Math.min(ci.r, o.x - b.x0, b.x1 - o.x, o.y - b.y0, b.y1 - o.y));
250
516
  if (!solveSketch(['r:' + d.id]).ok) restorePos(sk, snap);
517
+ keep();
251
518
  } else if (d.kind === 'odimoff') {
252
519
  const o = sk.openings[d.id], g = o && openingSpan(sk, o);
253
520
  if (g) {
@@ -256,70 +523,132 @@ function dragTo(d: Drag, ev: PEv): void {
256
523
  else { const od = (o.dims || []).find(x => 'd:' + x.id === d.part); if (od) od.off = k; }
257
524
  }
258
525
  } else if (d.kind === 'diamlabel') {
259
- const c = sk.cons[d.id], ci = curveInfo(sk, c.c);
260
- if (ci) {
261
- const o = sk.points[ci.o.c];
262
- const r = ci.kind === 'circle' ? ci.o.r : hyp(sk.points[ci.o.a].x - o.x, sk.points[ci.o.a].y - o.y);
263
- c.ang = Math.atan2(wy - o.y, wx - o.x);
264
- c.off = Math.max(0, hyp(wx - o.x, wy - o.y) - r - 40 / mainT().s);
526
+ // (out from the face it's measured across, for one on a curved wall's face)
527
+ const c = sk.cons[d.id], g = dimRadius(sk, c);
528
+ if (g) {
529
+ c.ang = Math.atan2(wy - g.o.y, wx - g.o.x);
530
+ c.off = Math.max(0, hyp(wx - g.o.x, wy - g.o.y) - g.r - 40 / mainT().s);
265
531
  }
266
532
  } else if (d.kind === 'dimoff') {
267
- const c = sk.cons[d.id], a = sk.points[c.a!], b = sk.points[c.b!];
533
+ // (off the line it's drawn from: a wall's face, for a length measured on one)
534
+ const c = sk.cons[d.id], e = dimEnds(sk, c), a = e ? e.a : sk.points[c.a!], b = e ? e.b : sk.points[c.b!];
268
535
  const n = norm2(-(b.y - a.y), b.x - a.x);
269
536
  c.off = (wx - (a.x + b.x) / 2) * n[0] + (wy - (a.y + b.y) / 2) * n[1];
537
+ } else if (d.kind === 'wopening') {
538
+ // a locked door won't move: say so once, then nothing
539
+ if (d.stuck) return;
540
+ if (!moveOpeningDrag(d.drag, wx - d.grab.x, wy - d.grab.y, (ev.touch ? 16 : 10) / mainT().s)) { d.stuck = true; return; }
270
541
  } else if (d.kind === 'opening') {
271
542
  const o = sk.openings[d.id], l = sk.lines[o.line], A = sk.points[l.a], B = sk.points[l.b];
272
- applySlide(sk, d.group, projT(A, B, { x: wx, y: wy }) * hyp(B.x - A.x, B.y - A.y) - d.grab);
543
+ const delta = projT(A, B, { x: wx, y: wy }) * hyp(B.x - A.x, B.y - A.y) - d.grab;
544
+ // what's drawn on the wall's faces follows as it slides — a cabinet tied to a window can't be pushed off the
545
+ // wall: the slide stops where it would be (as dragging the window in the wall view does)
546
+ d.base ??= planFit(sk);
547
+ d.last = slideWithinWalls(sk, d.group, snapSlide(d.snap.at0, d.snap.w, delta, d.snap.targets, (ev.touch ? 16 : 10) / mainT().s), d.last ?? 0, d.base);
273
548
  resolveOpeningsNow();
274
549
  } else if (d.kind === 'note') {
275
550
  const n = sk.notes.find(n => n.id === d.id)!;
276
551
  const dx = wx - d.grab.x, dy = wy - d.grab.y;
277
552
  if (n.pts) n.pts = d.start.pts!.map(p => [p[0] + dx, p[1] + dy] as [number, number]);
278
553
  else { n.x = d.start.x! + dx; n.y = d.start.y! + dy; }
554
+ if (d.both && d.start.leader) n.leader = [d.start.leader[0] + dx, d.start.leader[1] + dy];
555
+ } else if (d.kind === 'group') {
556
+ let dx = wx - d.grab.x, dy = wy - d.grab.y;
557
+ // Shift: moved level, plumb or 45° only
558
+ if (ev.shift) ({ x: dx, y: dy } = snap45({ x: 0, y: 0 }, { x: dx, y: dy }));
559
+ for (const g of d.notes) {
560
+ const n = sk.notes.find(x => x.id === g.id);
561
+ if (!n) continue;
562
+ if (n.pts) n.pts = g.start.pts!.map(p => [p[0] + dx, p[1] + dy] as [number, number]);
563
+ else { n.x = g.start.x! + dx; n.y = g.start.y! + dy; }
564
+ // (a link's leader with it: what it points at is moving too)
565
+ if (g.start.leader) n.leader = [g.start.leader[0] + dx, g.start.leader[1] + dy];
566
+ }
567
+ for (const g of d.images) { const u = imageById(g.id); if (u) { u.x = g.start.x + dx; u.y = g.start.y + dy; } }
279
568
  } else if (d.kind === 'umove') {
280
- const u = page().underlay!;
569
+ const u = imageById(d.id);
570
+ if (!u) return;
281
571
  u.x = d.start.x + wx - d.grab.x;
282
572
  u.y = d.start.y + wy - d.grab.y;
283
573
  } else if (d.kind === 'nresize') {
284
- reshapeNote(d.id, d.part, d.start, wx, wy);
574
+ if (d.part === 'tip') {
575
+ // a leader points at the drawing: its tip snaps to it, as a line's end does
576
+ const n = sk.notes.find(x => x.id === d.id), s = snapAt(ev.sx, ev.sy, { touch: ev.touch });
577
+ ed.snapState = s.kind === 'free' ? null : s;
578
+ if (n) n.leader = [s.x, s.y];
579
+ } else reshapeNote(d.id, d.part, d.start, wx, wy);
285
580
  } else if (d.kind === 'uresize') {
286
- resizeUnderlay(d.part, d.start, wx, wy, ev.shift);
581
+ resizeUnderlay(d.id, d.part, d.start, wx, wy, ev.shift);
287
582
  }
288
583
  resolveOpeningsNow();
289
- requestDraw();
584
+ // on a wall face, a door or window tied to what's dragged (a dimension to a cabinet) moves on the face as it's
585
+ // solved, but it's drawn from the plan: bring the plan along at each step — as dragging the door or window itself
586
+ // does — rather than only when the drag ends
587
+ if (shapes && faceEdit.key) syncFace(planSK(), faceEdit.key);
588
+ // (a photo moved or sized: only the photos are drawn again — the drawing over them hasn't changed)
589
+ if (d.kind === 'umove' || d.kind === 'uresize') requestImageDraw(); else requestDraw();
290
590
  }
291
591
 
292
592
  /** Finish a drag: snapping onto geometry turns into a join or a constraint (when auto-constrain is on). */
293
593
  function endDrag(d: Drag): void {
294
594
  const sk = SK();
295
595
  ed.snapState = null;
596
+ if (d.kind === 'wopening' && !d.drag.started) dropHistory(); // it didn't move
296
597
  if (d.kind === 'point' && d.snap && settings.autoCons) {
297
598
  const s = d.snap;
298
599
  if (s.kind === 'point') {
299
600
  const snap = JSON.stringify(sk);
300
601
  if (mergePoints(sk, s.ref!, d.id)) {
301
602
  const r = solveSketch();
302
- if (!r.ok) { page().sk = JSON.parse(snap) as Sketch; toast('Can’t join those points — it conflicts with other constraints.', { warn: true }); }
603
+ if (!r.ok) { replaceSK(JSON.parse(snap) as Sketch); toast('Can’t join those points — it conflicts with other constraints.', { warn: true }); }
303
604
  else { ed.sel = [{ type: 'point', id: s.ref! }]; toast('Points joined'); }
304
605
  }
305
- } else if (s.kind === 'mid') tryAddConstraint({ type: 'mid', p: d.id, l: s.ref! }, { silent: false });
606
+ } else if (s.kind === 'mid') tryAddConstraint({ type: sk.lines[s.ref!]?.anchor ? 'online' : 'mid', p: d.id, l: s.ref! }, { silent: !!sk.lines[s.ref!]?.anchor }); // (on a wall face's own edge: on it, free to slide)
306
607
  else if (s.kind === 'online') tryAddConstraint({ type: 'online', p: d.id, l: s.ref! }, { silent: true });
307
608
  else if (s.kind === 'oncircle') tryAddConstraint({ type: 'oncircle', p: d.id, c: s.ref! }, { silent: true });
308
609
  }
309
610
  if (d.kind === 'point') {
310
611
  const p = SK().points[d.id];
311
- if (p && hyp(p.x - d.start.x, p.y - d.start.y) < 1e-6 && !d.snap && ed.ana.det.has(d.id)) toast('That point is fully defined. Change a dimension to move it.');
612
+ if (p && hyp(p.x - d.start.x, p.y - d.start.y) < 1e-6 && !d.snap && ed.ana.det.has(d.id)) toast('That point is fully defined. Change a dimension to move it.', { warn: true });
312
613
  }
313
614
  afterEdit();
615
+ // a cabinet, door or window picked up by its corner and dropped with that corner on a point: they join, as points do
616
+ if ((d.kind === 'multi' || d.kind === 'wopening') && d.via && settings.autoCons) joinDropped(d.via);
617
+ }
618
+
619
+ /** Join corner `via` (a cabinet's, door's or window's, just dropped) to the point it landed on, if any. */
620
+ function joinDropped(via: string): void {
621
+ const sk = SK(), P = sk.points[via];
622
+ if (!P) return;
623
+ const op = openingOfPoint(via), cab = cabOfPoint(sk, via);
624
+ const own = (id: string) => (op && id.startsWith('@o:' + op + ':')) || (cab && cabOfPoint(sk, id) === cab);
625
+ const tol = (ed.lastPointerTouch ? 16 : 10) / mainT().s;
626
+ let best: string | null = null, bd = tol;
627
+ for (const [id, q] of Object.entries(sk.points)) {
628
+ if (id === via || own(id) || (q.anchor && q.fixed && P.fixed)) continue;
629
+ const d = hyp(q.x - P.x, q.y - P.y);
630
+ if (d < bd) { bd = d; best = id; }
631
+ }
632
+ if (!best) return;
633
+ // (part of the drag's undo step)
634
+ const snap = JSON.stringify(sk);
635
+ if (!joinPoints(sk, via, best)) return;
636
+ if (!solveSketch().ok) { replaceSK(JSON.parse(snap) as Sketch); afterEdit(); toast('Can’t join those points — it conflicts with other constraints.', { warn: true }); return; }
637
+ toast('Points joined');
638
+ afterEdit();
314
639
  }
315
640
 
316
641
  /**
317
642
  * Resize the image underlay from a handle. Corners scale proportionally about the opposite corner
318
643
  * (hold Shift for a free corner); side handles stretch one direction about the opposite side.
319
644
  */
320
- function resizeUnderlay(part: HandlePart, s0: { x: number; y: number; w: number; h: number }, wx: number, wy: number, free: boolean): void {
321
- const u = page().underlay!, min = 8 / mainT().s;
322
- const left = s0.x, right = s0.x + s0.w, top = s0.y, bottom = s0.y - s0.h;
645
+ function resizeUnderlay(id: string, part: HandlePart, s0: { x: number; y: number; w: number; h: number }, ptx: number, pty: number, free: boolean): void {
646
+ const u = imageById(id), min = 8 / mainT().s;
647
+ if (!u) return;
648
+ // worked in the image's own frame (from its middle as the drag began, unturned), so a turned image resizes along its
649
+ // own sides — and put back turned, the side or corner opposite the handle staying where it was
650
+ const was: Underlay = { ...u, x: s0.x, y: s0.y, w: s0.w, h: s0.h }, { x: wx, y: wy } = underlayLocal(was, ptx, pty);
651
+ const left = -s0.w / 2, right = s0.w / 2, top = s0.h / 2, bottom = -s0.h / 2;
323
652
  const W = part.includes('w'), E = part.includes('e'), N = part.includes('n'), Sth = part.includes('s');
324
653
  let w = s0.w, h = s0.h;
325
654
  if (part.length === 2 && !free) {
@@ -335,10 +664,29 @@ function resizeUnderlay(part: HandlePart, s0: { x: number; y: number; w: number;
335
664
  if (N) h = Math.max(min, wy - bottom);
336
665
  if (Sth) h = Math.max(min, top - wy);
337
666
  }
667
+ const mid = underlayAt(was, (W ? right - w : left) + w / 2, (N ? bottom + h : top) - h / 2);
338
668
  u.w = w;
339
669
  u.h = h;
340
- u.x = W ? right - w : left;
341
- u.y = N ? bottom + h : top;
670
+ u.x = mid.x - w / 2;
671
+ u.y = mid.y + h / 2;
672
+ }
673
+
674
+ /**
675
+ * Scale a note from a corner: the opposite corner stays put and the note grows or shrinks evenly (text, padding and
676
+ * corners together) as the corner follows the pointer along the box's diagonal.
677
+ */
678
+ function scaleText(n: Note, part: string, start: Note, wx: number, wy: number): void {
679
+ const T = mainT(), b = noteTextBox(T, start);
680
+ const xs = { w: b.x, e: b.x + b.w }, ys = { n: b.y, s: b.y + b.h };
681
+ const corner = { x: xs[part[1] as 'w' | 'e'], y: ys[part[0] as 'n' | 's'] };
682
+ const fixed = { x: part[1] === 'w' ? xs.e : xs.w, y: part[0] === 'n' ? ys.s : ys.n };
683
+ const [px, py] = S(T, wx, wy), dx = corner.x - fixed.x, dy = corner.y - fixed.y;
684
+ const along = ((px - fixed.x) * dx + (py - fixed.y) * dy) / (dx * dx + dy * dy || 1);
685
+ // its size on the drawing (to the quarter inch), which then holds at any zoom
686
+ const was = noteTextIn(start), ts = Math.round(clamp(was * along, NOTE_TEXT_MIN_IN, NOTE_TEXT_MAX_IN) * 4) / 4, k = ts / was;
687
+ [n.x, n.y] = s2w(fixed.x + (b.x - fixed.x) * k, fixed.y + (b.mid - fixed.y) * k);
688
+ delete n.size;
689
+ if (ts === NOTE_TEXT_IN) delete n.ts; else n.ts = ts;
342
690
  }
343
691
 
344
692
  /**
@@ -347,6 +695,7 @@ function resizeUnderlay(part: HandlePart, s0: { x: number; y: number; w: number;
347
695
  */
348
696
  function reshapeNote(id: string, part: string, start: Note, wx: number, wy: number): void {
349
697
  const n = SK().notes.find(x => x.id === id);
698
+ if (n?.type === 'text') { scaleText(n, part, start, wx, wy); return; }
350
699
  if (!n || !n.pts || !start.pts) return;
351
700
  if (part[0] === 'p') { const i = +part.slice(1); n.pts = start.pts.map((p, k) => (k === i ? [wx, wy] : [p[0], p[1]]) as [number, number]); return; }
352
701
  const b = noteBox(start)!, min = 4 / mainT().s;