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