@reekon-tools/boldr-cad 0.1.0 → 1.1.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 (555) hide show
  1. package/README.md +99 -16
  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 +9 -1
  9. package/dist/data/memory.js +13 -8
  10. package/dist/data/memory.js.map +1 -1
  11. package/dist/data/provider.d.ts +30 -10
  12. package/dist/data/sync.d.ts +14 -3
  13. package/dist/data/sync.js +133 -66
  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 +20 -5
  34. package/dist/editor/keypad.js +62 -15
  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 +38 -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 +170 -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 +7 -1
  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 +54 -4
  155. package/dist/react/CadEditor.js +79 -21
  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 +582 -97
  329. package/dist/tools/dim.js.map +1 -1
  330. package/dist/tools/elevation.js +5 -5
  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 +25 -18
  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 +11 -3
  358. package/dist/units/format.js +27 -6
  359. package/dist/units/format.js.map +1 -1
  360. package/dist/units/parse.d.ts +2 -1
  361. package/dist/units/parse.js +5 -4
  362. package/dist/units/parse.js.map +1 -1
  363. package/dist/units/prefs.d.ts +9 -1
  364. package/dist/units/prefs.js +6 -0
  365. package/dist/units/prefs.js.map +1 -1
  366. package/dist/view3d/build.d.ts +35 -0
  367. package/dist/view3d/build.js +630 -0
  368. package/dist/view3d/build.js.map +1 -0
  369. package/dist/view3d/camera.d.ts +60 -0
  370. package/dist/view3d/camera.js +84 -0
  371. package/dist/view3d/camera.js.map +1 -0
  372. package/dist/view3d/fatLines.d.ts +11 -0
  373. package/dist/view3d/fatLines.js +83 -0
  374. package/dist/view3d/fatLines.js.map +1 -0
  375. package/dist/view3d/lines.d.ts +17 -0
  376. package/dist/view3d/lines.js +271 -0
  377. package/dist/view3d/lines.js.map +1 -0
  378. package/dist/view3d/mesh.d.ts +51 -0
  379. package/dist/view3d/mesh.js +101 -0
  380. package/dist/view3d/mesh.js.map +1 -0
  381. package/dist/view3d/poly.d.ts +16 -0
  382. package/dist/view3d/poly.js +109 -0
  383. package/dist/view3d/poly.js.map +1 -0
  384. package/dist/view3d/viewer.d.ts +86 -0
  385. package/dist/view3d/viewer.js +350 -0
  386. package/dist/view3d/viewer.js.map +1 -0
  387. package/package.json +15 -2
  388. package/src/app/edit.ts +83 -12
  389. package/src/app/main.ts +11 -5
  390. package/src/app/state.ts +38 -5
  391. package/src/data/memory.ts +15 -10
  392. package/src/data/provider.ts +25 -10
  393. package/src/data/sync.ts +107 -53
  394. package/src/editor/actions.ts +292 -83
  395. package/src/editor/confirm.ts +58 -0
  396. package/src/editor/elevation.ts +698 -478
  397. package/src/editor/face.ts +227 -0
  398. package/src/editor/files.ts +40 -4
  399. package/src/editor/gestures.ts +147 -31
  400. package/src/editor/keypad.ts +67 -18
  401. package/src/editor/keys.ts +25 -5
  402. package/src/editor/laser.ts +98 -20
  403. package/src/editor/linkActions.ts +194 -0
  404. package/src/editor/linkFollow.ts +43 -0
  405. package/src/editor/linkWrite.ts +77 -0
  406. package/src/editor/links.ts +181 -0
  407. package/src/editor/markup.ts +92 -8
  408. package/src/editor/mode.ts +83 -7
  409. package/src/editor/ports.ts +39 -6
  410. package/src/editor/project.ts +157 -26
  411. package/src/editor/selection.ts +54 -8
  412. package/src/editor/status.ts +2 -0
  413. package/src/editor/tooltips.ts +47 -0
  414. package/src/editor/view3d.ts +119 -0
  415. package/src/editor/view3dState.ts +61 -0
  416. package/src/export/dxf.ts +1 -1
  417. package/src/export/pdf.ts +15 -7
  418. package/src/export/plan.ts +119 -35
  419. package/src/index.ts +7 -1
  420. package/src/input/keyboard.ts +2 -4
  421. package/src/model/constraints.ts +13 -4
  422. package/src/model/demo.ts +3 -3
  423. package/src/model/elevation.ts +6 -16
  424. package/src/model/faces.ts +153 -0
  425. package/src/model/facesolve.ts +322 -0
  426. package/src/model/history.ts +19 -11
  427. package/src/model/images.ts +35 -0
  428. package/src/model/links.ts +77 -0
  429. package/src/model/markup.ts +80 -0
  430. package/src/model/measurement.ts +26 -0
  431. package/src/model/openings.ts +6 -0
  432. package/src/model/planGeom.ts +174 -8
  433. package/src/model/project.ts +3 -2
  434. package/src/model/projectfile.ts +2 -1
  435. package/src/model/rooms.ts +1 -1
  436. package/src/model/settings.ts +8 -2
  437. package/src/model/sketch.ts +101 -14
  438. package/src/model/smooth.ts +51 -0
  439. package/src/model/store.ts +35 -4
  440. package/src/model/types.ts +170 -8
  441. package/src/model/wallfaces.ts +126 -17
  442. package/src/model/wallshape.ts +97 -0
  443. package/src/react/BottomBar.tsx +263 -46
  444. package/src/react/Buttons.tsx +50 -9
  445. package/src/react/CadEditor.tsx +142 -34
  446. package/src/react/ConfirmSheet.tsx +38 -0
  447. package/src/react/ConflictSheet.tsx +19 -12
  448. package/src/react/Drawer.tsx +213 -110
  449. package/src/react/Duck.tsx +50 -0
  450. package/src/react/Empty3D.tsx +32 -0
  451. package/src/react/FitScroll.tsx +19 -0
  452. package/src/react/Icon.native.tsx +5 -0
  453. package/src/react/Icon.web.tsx +5 -0
  454. package/src/react/KeypadSheet.tsx +253 -60
  455. package/src/react/LaserTray.tsx +12 -8
  456. package/src/react/MarkupPanel.tsx +150 -0
  457. package/src/react/PageThumb.native.tsx +18 -7
  458. package/src/react/PageThumb.web.tsx +4 -4
  459. package/src/react/PlanCanvas.native.tsx +151 -34
  460. package/src/react/PlanCanvas.web.tsx +15 -5
  461. package/src/react/Popovers.tsx +116 -51
  462. package/src/react/Rail.tsx +360 -27
  463. package/src/react/ShapeMenu.tsx +66 -0
  464. package/src/react/Sheet.tsx +56 -11
  465. package/src/react/Slider.tsx +48 -0
  466. package/src/react/Toast.tsx +16 -9
  467. package/src/react/ToolTip.tsx +49 -0
  468. package/src/react/TopBar.tsx +97 -39
  469. package/src/react/Tween.native.tsx +36 -0
  470. package/src/react/Tween.tsx +24 -0
  471. package/src/react/View3D.native.tsx +131 -0
  472. package/src/react/View3D.web.tsx +131 -0
  473. package/src/react/View3DHost.ts +367 -0
  474. package/src/react/corners.ts +52 -0
  475. package/src/react/easing.ts +14 -0
  476. package/src/react/host.ts +8 -3
  477. package/src/react/hostKeypad.ts +66 -0
  478. package/src/react/motion.tsx +239 -0
  479. package/src/react/ports.ts +78 -9
  480. package/src/react/saving.ts +80 -0
  481. package/src/react/store.ts +35 -3
  482. package/src/react/theme.ts +8 -0
  483. package/src/react/topRow.ts +71 -0
  484. package/src/react/touch.ts +10 -0
  485. package/src/render/ctx.ts +8 -0
  486. package/src/render/draw.ts +437 -175
  487. package/src/render/elevation.ts +349 -143
  488. package/src/render/figmaIcons.ts +22 -0
  489. package/src/render/hit.ts +140 -21
  490. package/src/render/icons.ts +69 -10
  491. package/src/render/linkTile.ts +297 -0
  492. package/src/render/markup.ts +91 -0
  493. package/src/render/navigate.ts +114 -0
  494. package/src/render/snap.ts +105 -22
  495. package/src/render/squircle.ts +73 -0
  496. package/src/render/view.ts +255 -19
  497. package/src/skia/SkiaDrawCtx.ts +163 -41
  498. package/src/skia/host.ts +46 -1
  499. package/src/solver/equations.ts +37 -2
  500. package/src/solver/gauss-newton.ts +125 -28
  501. package/src/solver/linalg.ts +141 -5
  502. package/src/solver/types.ts +5 -0
  503. package/src/styles.css +91 -42
  504. package/src/tools/annotate.ts +58 -29
  505. package/src/tools/chamfer.ts +103 -0
  506. package/src/tools/corner.ts +99 -0
  507. package/src/tools/dim.ts +525 -93
  508. package/src/tools/elevation.ts +5 -5
  509. package/src/tools/fillet.ts +82 -0
  510. package/src/tools/freehand.ts +20 -16
  511. package/src/tools/index.ts +17 -4
  512. package/src/tools/line.ts +59 -23
  513. package/src/tools/modify.ts +86 -24
  514. package/src/tools/openings.ts +11 -16
  515. package/src/tools/registry.ts +27 -2
  516. package/src/tools/select.ts +424 -75
  517. package/src/tools/shapes.ts +119 -58
  518. package/src/ui/ctx.ts +2 -1
  519. package/src/ui/domPorts.ts +23 -4
  520. package/src/ui/drawer.ts +6 -26
  521. package/src/ui/elevation.ts +10 -101
  522. package/src/ui/keypad.ts +8 -14
  523. package/src/ui/laser.ts +2 -2
  524. package/src/ui/menu.ts +63 -17
  525. package/src/ui/noteinput.ts +3 -3
  526. package/src/ui/opacity.ts +108 -0
  527. package/src/ui/palette.ts +67 -12
  528. package/src/ui/rail.ts +21 -7
  529. package/src/ui/topbar.ts +4 -14
  530. package/src/units/format.ts +24 -6
  531. package/src/units/parse.ts +4 -3
  532. package/src/units/prefs.ts +13 -1
  533. package/src/view3d/build.ts +607 -0
  534. package/src/view3d/camera.ts +113 -0
  535. package/src/view3d/fatLines.ts +83 -0
  536. package/src/view3d/lines.ts +258 -0
  537. package/src/view3d/mesh.ts +111 -0
  538. package/src/view3d/poly.ts +104 -0
  539. package/src/view3d/viewer.ts +348 -0
  540. package/dist/react/ElevCanvas.native.d.ts +0 -3
  541. package/dist/react/ElevCanvas.native.js +0 -58
  542. package/dist/react/ElevCanvas.native.js.map +0 -1
  543. package/dist/react/ElevCanvas.web.d.ts +0 -4
  544. package/dist/react/ElevCanvas.web.js +0 -44
  545. package/dist/react/ElevCanvas.web.js.map +0 -1
  546. package/dist/react/ElevationSheet.d.ts +0 -2
  547. package/dist/react/ElevationSheet.js +0 -38
  548. package/dist/react/ElevationSheet.js.map +0 -1
  549. package/dist/render/elevationView.d.ts +0 -3
  550. package/dist/render/elevationView.js +0 -100
  551. package/dist/render/elevationView.js.map +0 -1
  552. package/src/react/ElevCanvas.native.tsx +0 -68
  553. package/src/react/ElevCanvas.web.tsx +0 -42
  554. package/src/react/ElevationSheet.tsx +0 -88
  555. package/src/render/elevationView.ts +0 -73
@@ -1,16 +1,31 @@
1
- import { clamp, hyp, norm2, pointInPoly } from './geom';
2
- import { roomValid } from './sketch';
3
- import type { Arc, Sketch, XY } from './types';
1
+ import { clamp, hyp, norm2, normAng, pointInPoly } from './geom';
2
+ import { roomGeom } from './rooms';
3
+ import { frameMemo, roomValid } from './sketch';
4
+ import type { Arc, Constraint, LineKind, Sketch, XY } from './types';
5
+ import { faceCorner } from './wallfaces';
6
+ import { getUnitPrefs, isMetric, type Unit } from '../units';
4
7
 
5
8
  /*
6
9
  Plan geometry shared by drawing, hit testing, snapping and the tools: grid steps, where a door/window sits on
7
10
  its wall, arc angles, and which side of a wall faces out. Pure (no view, no canvas).
8
11
  */
9
12
 
10
- export function gridSteps(s: number): { minor: number; major: number } {
11
- const list = [1 / 16, 1 / 8, 1 / 4, 1 / 2, 1, 2, 6, 12, 24, 60, 120, 240, 600, 1200, 2400, 6000];
12
- const minor = list.find(v => v * s >= 11) || 6000;
13
- const major = [12, 60, 120, 600, 1200, 6000].find(v => v >= minor * 4) || minor * 5;
13
+ /**
14
+ * The grid at a zoom (`s` px per inch): its finest lines (`minor`, what snapping uses) and its heavier ones (`major`),
15
+ * in inches. It follows the zoom in the drawing's units — 1/16", 1/8" … 6", 1', 2', 5'; or 1, 2, 5, 10 mm … — as fine
16
+ * as keeps its lines at least GRID_MIN_PX apart: what's drawn is exactly what's snapped to, spaced so each line can be
17
+ * picked out with a finger.
18
+ */
19
+ export const GRID_MIN_PX = 16;
20
+ const IMPERIAL = [1 / 16, 1 / 8, 1 / 4, 1 / 2, 1, 2, 6, 12, 24, 60, 120, 240, 600, 1200, 2400, 6000];
21
+ const IMPERIAL_MAJOR = [12, 60, 120, 600, 1200, 6000];
22
+ const MM = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000].map(v => v / 25.4);
23
+ const MM_MAJOR = [10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000].map(v => v / 25.4);
24
+
25
+ export function gridSteps(s: number, unit: Unit = getUnitPrefs().unit): { minor: number; major: number } {
26
+ const metric = isMetric(unit), list = metric ? MM : IMPERIAL;
27
+ const minor = list.find(v => v * s >= GRID_MIN_PX) || list[list.length - 1];
28
+ const major = (metric ? MM_MAJOR : IMPERIAL_MAJOR).find(v => v >= minor * 4 - 1e-9) || minor * 5;
14
29
  return { minor, major };
15
30
  }
16
31
 
@@ -47,6 +62,157 @@ export function outsideSide(sk: Sketch, lid: string): number {
47
62
  const l = sk.lines[lid], a = sk.points[l.a], b = sk.points[l.b];
48
63
  const n = norm2(-(b.y - a.y), b.x - a.x);
49
64
  const m = { x: (a.x + b.x) / 2 + n[0] * 12, y: (a.y + b.y) / 2 + n[1] * 12 };
50
- for (const id in sk.rooms) { const r = sk.rooms[id]; if (roomValid(sk, r) && pointInPoly(m, r.loop.map(p => sk.points[p]))) return -1; }
65
+ // (asked for every door and window on every frame: the rooms' outlines, and the boxes around them, are worked out
66
+ // once a frame — a room the box rules out isn't looked at more closely)
67
+ const rooms = frameMemo(sk, 'roomOutlines', () => Object.values(sk.rooms).filter(r => roomValid(sk, r)).map(r => {
68
+ const poly = r.loop.map(p => sk.points[p]), xs = poly.map(p => p.x), ys = poly.map(p => p.y);
69
+ return { poly, x0: Math.min(...xs), x1: Math.max(...xs), y0: Math.min(...ys), y1: Math.max(...ys) };
70
+ }));
71
+ for (const r of rooms) if (m.x >= r.x0 && m.x <= r.x1 && m.y >= r.y0 && m.y <= r.y1 && pointInPoly(m, r.poly)) return -1;
51
72
  return 1;
52
73
  }
74
+
75
+ /** A wall's own record — a straight one's line, a curved one's circle or arc — when `id` is a wall (not a guide). */
76
+ export function wallOf(sk: Sketch, id: string): { kind?: LineKind; thick?: number; constr?: boolean } | null {
77
+ const o = sk.lines[id] ?? sk.circles[id] ?? sk.arcs[id];
78
+ return o && o.kind === 'wall' && !o.constr ? o : null;
79
+ }
80
+
81
+ /** How thick a wall is (a curved one with none set: 4½", as it's drawn); 0 for a line. */
82
+ export const wallThick = (o: { kind?: LineKind; thick?: number }): number => (o.kind === 'wall' ? o.thick || 4.5 : 0);
83
+
84
+ /** A circle's or arc's centre and centreline radius; an arc's angles too (it runs counter-clockwise from a to b). */
85
+ export function curveFrame(sk: Sketch, id: string): { o: XY; r: number; arc?: { t0: number; t1: number } } | null {
86
+ const ci = sk.circles[id];
87
+ if (ci) { const o = sk.points[ci.c]; return o ? { o, r: ci.r } : null; }
88
+ const a = sk.arcs[id];
89
+ if (!a || !sk.points[a.c] || !sk.points[a.a] || !sk.points[a.b]) return null;
90
+ const g = arcAngles(sk, a);
91
+ return { o: g.o, r: g.r, arc: { t0: g.t0, t1: g.t1 } };
92
+ }
93
+
94
+ /**
95
+ * The space against one side of a wall, if any (a point just past its face there is inside it): its id. A straight
96
+ * wall's sides are the left (+1) and right (−1) of its A → B; a curved one's, its outer (+1) and inner (−1) faces —
97
+ * nothing is inside a circle (a space has corners: it can't be bounded by one).
98
+ */
99
+ export function spaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null {
100
+ const l = sk.lines[id];
101
+ if (!l) return curveSpaceBeside(sk, id, side);
102
+ const a = sk.points[l.a], b = sk.points[l.b];
103
+ if (!a || !b) return null;
104
+ const n = norm2(-(b.y - a.y), b.x - a.x), k = side * ((l.kind === 'wall' ? l.thick / 2 : 0) + 1);
105
+ const m = { x: (a.x + b.x) / 2 + n[0] * k, y: (a.y + b.y) / 2 + n[1] * k };
106
+ for (const rid in sk.rooms) {
107
+ const r = sk.rooms[rid];
108
+ if (roomValid(sk, r) && pointInPoly(m, r.loop.map(p => sk.points[p]))) return rid;
109
+ }
110
+ return null;
111
+ }
112
+
113
+ function curveSpaceBeside(sk: Sketch, id: string, side: 1 | -1): string | null {
114
+ const f = curveFrame(sk, id), w = sk.circles[id] ?? sk.arcs[id];
115
+ if (!f || !w || (side === -1 && !f.arc)) return null;
116
+ // (just past the face at the middle of the arc, or anywhere around a circle)
117
+ const at = f.arc ? f.arc.t0 + normAng(f.arc.t1 - f.arc.t0) / 2 : 0, rr = f.r + side * (wallThick(w) / 2 + 1);
118
+ if (rr <= 0) return null;
119
+ const m = { x: f.o.x + Math.cos(at) * rr, y: f.o.y + Math.sin(at) * rr };
120
+ // (a space's outline as it's drawn: its arcs as arcs, not chords)
121
+ for (const rid in sk.rooms) {
122
+ const r = sk.rooms[rid];
123
+ if (roomValid(sk, r) && pointInPoly(m, roomGeom(sk, r).poly)) return rid;
124
+ }
125
+ return null;
126
+ }
127
+
128
+ /**
129
+ * The face a wall is measured on unless one is picked (a straight wall's length, a curved one's diameter): the side a
130
+ * space is on — the side it was tapped on (`tapped`), when there are spaces both sides — or none (its centreline) when
131
+ * there's no space beside it, or it isn't a wall.
132
+ */
133
+ export function defaultFace(sk: Sketch, id: string, tapped?: 1 | -1): 1 | -1 | undefined {
134
+ if (!wallOf(sk, id)) return undefined;
135
+ const left = spaceBeside(sk, id, 1), right = spaceBeside(sk, id, -1);
136
+ if (left && right) return tapped ?? 1;
137
+ return left ? 1 : right ? -1 : undefined;
138
+ }
139
+
140
+ /**
141
+ * What one face of a wall is called. A straight wall's: its space's side between two spaces, inside or outside one —
142
+ * or, with no space against it (or one space either side of it: a wall standing in a room; or two spaces of one name),
143
+ * which way it faces on the page (Top, Bottom, Left, Right). A curved wall's: Outer or Inner, whatever is beside it.
144
+ */
145
+ export function faceName(sk: Sketch, lid: string, side: 1 | -1): string {
146
+ if (!sk.lines[lid]) return side === 1 ? 'Outer' : 'Inner';
147
+ const here = spaceBeside(sk, lid, side), there = spaceBeside(sk, lid, side === 1 ? -1 : 1);
148
+ if (here && there) { if (sk.rooms[here].name !== sk.rooms[there].name) return sk.rooms[here].name + ' side'; }
149
+ else if (here) return 'Inside';
150
+ else if (there) return 'Outside';
151
+ const l = sk.lines[lid], a = sk.points[l.a], b = sk.points[l.b];
152
+ // (the face's outward direction: the left of A → B for +1; y is up)
153
+ const nx = -(b.y - a.y) * side, ny = (b.x - a.x) * side;
154
+ return Math.abs(ny) >= Math.abs(nx) ? (ny > 0 ? 'Top' : 'Bottom') : nx > 0 ? 'Right' : 'Left';
155
+ }
156
+
157
+ /**
158
+ * Where a length dimension's two ends are: its points — or, measured on walls' faces, the faces': along a wall, its
159
+ * face's two corners (what the number measures); between two walls, the foot on each face. Null if it isn't a length
160
+ * dimension or what it measures is gone.
161
+ */
162
+ export function dimEnds(sk: Sketch, c: Constraint): { a: XY; b: XY } | null {
163
+ const P = (id?: string) => (id ? sk.points[id] : undefined);
164
+ if (c.type === 'dist') {
165
+ const a = P(c.a), b = P(c.b);
166
+ if (!a || !b) return null;
167
+ const w = c.face && c.line ? sk.lines[c.line] : undefined;
168
+ if (w && w.kind === 'wall' && (!c.kind || c.kind === 'al')) {
169
+ const fa = faceCorner(sk, c.line!, c.face!, 'a'), fb = faceCorner(sk, c.line!, c.face!, 'b');
170
+ if (fa && fb) return c.a === w.a ? { a: fa, b: fb } : { a: fb, b: fa };
171
+ }
172
+ return { a, b };
173
+ }
174
+ if (c.type === 'pldist') {
175
+ const l = c.l ? sk.lines[c.l] : undefined, p = P(c.p);
176
+ if (!l || !p) return null;
177
+ const A = sk.points[l.a], B = sk.points[l.b], len2 = (B.x - A.x) ** 2 + (B.y - A.y) ** 2 || 1;
178
+ const t = ((p.x - A.x) * (B.x - A.x) + (p.y - A.y) * (B.y - A.y)) / len2;
179
+ const f = { x: A.x + (B.x - A.x) * t, y: A.y + (B.y - A.y) * t };
180
+ if (!c.faces) return { a: p, b: f };
181
+ // each end moved onto its face: toward the other for a face toward it, away for a far face
182
+ const d = hyp(p.x - f.x, p.y - f.y), L = Math.sqrt(len2);
183
+ const [ux, uy] = d > 1e-9 ? [(p.x - f.x) / d, (p.y - f.y) / d] : [-(B.y - A.y) / L, (B.x - A.x) / L];
184
+ const th = (id?: string) => { const w = id ? sk.lines[id] : undefined; return w && w.kind === 'wall' ? w.thick : 0; };
185
+ const kl = (c.faces[0] * th(c.l)) / 2, kp = (c.faces[1] * th(c.l2)) / 2;
186
+ return { a: { x: p.x - ux * kp, y: p.y - uy * kp }, b: { x: f.x + ux * kl, y: f.y + uy * kl } };
187
+ }
188
+ return null;
189
+ }
190
+
191
+ /**
192
+ * Where a diameter (or radius) dimension's arrows go: its curve's centre, and the radius it measures at — the
193
+ * centreline's, or on a curved wall's face, that face's.
194
+ */
195
+ export function dimRadius(sk: Sketch, c: Constraint): { o: XY; r: number } | null {
196
+ const f = c.c ? curveFrame(sk, c.c) : null, w = c.c ? sk.circles[c.c] ?? sk.arcs[c.c] : undefined;
197
+ if (!f || !w) return null;
198
+ return { o: f.o, r: f.r + (c.face && w.kind === 'wall' ? (c.face * wallThick(w)) / 2 : 0) };
199
+ }
200
+
201
+ /** A wall's edge as said to people: 'Inside edge', 'Outside edge', 'Kitchen side', 'Top edge', 'Inner edge'. */
202
+ export function edgeLabel(sk: Sketch, lid: string, side: 1 | -1): string {
203
+ const n = faceName(sk, lid, side);
204
+ return n.endsWith(' side') ? n : n + ' edge';
205
+ }
206
+
207
+ /** A wall edge's selection id, and back: '<line id>|1' or '<line id>|-1'. */
208
+ export const edgeId = (lid: string, side: 1 | -1): string => lid + '|' + side;
209
+ export function edgeOf(id: string): [string, 1 | -1] {
210
+ const i = id.lastIndexOf('|');
211
+ return [id.slice(0, i), id.slice(i + 1) === '-1' ? -1 : 1];
212
+ }
213
+
214
+ /** Which side of line `lid` a point is on: +1 the left of its A → B, −1 the right. */
215
+ export function sideOf(sk: Sketch, lid: string, p: XY): 1 | -1 {
216
+ const l = sk.lines[lid], a = sk.points[l.a], b = sk.points[l.b];
217
+ return (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x) >= 0 ? 1 : -1;
218
+ }
@@ -1,3 +1,4 @@
1
+ import { normalizeImages } from './images';
1
2
  import { newPage } from './demo';
2
3
  import type { Page, Project } from './types';
3
4
 
@@ -6,9 +7,9 @@ import type { Page, Project } from './types';
6
7
  * name appears in more than one level, it's prefixed with the level's name ("Level 2 · Floor plan").
7
8
  */
8
9
  export function migrateProject(p: { name: string; pages?: Page[]; spaces?: { name: string; pages: Page[] }[] }): Project {
9
- if (p.pages) return { name: p.name, pages: p.pages };
10
+ if (p.pages) return { name: p.name, pages: p.pages.map(normalizeImages) };
10
11
  const spaces = p.spaces || [], count: Record<string, number> = {};
11
12
  for (const s of spaces) for (const pg of s.pages) count[pg.name] = (count[pg.name] || 0) + 1;
12
- const pages = spaces.flatMap(s => s.pages.map(pg => (count[pg.name] > 1 ? { ...pg, name: s.name + ' · ' + pg.name } : pg)));
13
+ const pages = spaces.flatMap(s => s.pages.map(pg => normalizeImages(count[pg.name] > 1 ? { ...pg, name: s.name + ' · ' + pg.name } : pg)));
13
14
  return { name: p.name, pages: pages.length ? pages : [newPage('Floor plan')] };
14
15
  }
@@ -1,3 +1,4 @@
1
+ import { normalizeImages } from './images';
1
2
  import { migrateProject } from './project';
2
3
  import type { Project } from './types';
3
4
 
@@ -38,7 +39,7 @@ export function projectFromFile(text: string): Project {
38
39
  for (const k of ['circles', 'arcs', 'cons', 'rooms', 'openings']) if (!sk[k] || typeof sk[k] !== 'object') sk[k] = {};
39
40
  if (!Array.isArray(sk.notes)) sk.notes = [];
40
41
  if (!sk.seq) sk.seq = { room: 1 };
41
- pg.underlay = pg.underlay ?? null;
42
+ normalizeImages(pg);
42
43
  pg.view = pg.view ?? null;
43
44
  }
44
45
  return p;
@@ -171,7 +171,7 @@ export function roomGeom(sk: Sketch, r: Room): RoomGeom {
171
171
  * leave it out for loops of straight lines. Null if it's under 1 sq ft.
172
172
  */
173
173
  export function maybeRoom(sk: Sketch, loop: string[], edges?: string[]): string | null {
174
- if (loop.length < 2) return null;
174
+ if (loop.length < 2 || 'cabs' in sk) return null; // spaces are the plan's: not on a wall face
175
175
  const es = edges || roomEdges(sk, { loop, name: '' });
176
176
  if (!es) return null;
177
177
  if (Math.abs(polyArea(outline(sk, loop, es).pts)) < 144) return null;
@@ -8,11 +8,17 @@ export interface Layers {
8
8
  print: boolean;
9
9
  rooms: boolean;
10
10
  annot: boolean;
11
+ /** cabinets, doors and windows snap to corners and each other as they're placed and dragged */
12
+ objSnap: boolean;
13
+ /** cabinets' footprints in the plan */
14
+ cabinets: boolean;
11
15
  }
12
16
 
13
17
  export interface Settings extends UnitPrefs {
14
18
  snapGrid: boolean;
15
19
  autoCons: boolean;
20
+ /** picking a tool says in a line what it does (under the tabs, for a moment) */
21
+ toolTips: boolean;
16
22
  showCons: boolean;
17
23
  theme: 'system' | 'light' | 'dark';
18
24
  welcomed: boolean;
@@ -21,10 +27,10 @@ export interface Settings extends UnitPrefs {
21
27
  wallH: number;
22
28
  }
23
29
 
24
- export const defaultLayers = (): Layers => ({ underlay: true, construction: true, dims: true, print: true, rooms: true, annot: true });
30
+ export const defaultLayers = (): Layers => ({ underlay: true, construction: true, dims: true, print: true, rooms: true, annot: true, cabinets: true, objSnap: true });
25
31
 
26
32
  export const settings = sessionState((): Settings => ({
27
- unit: 'ftin', denom: 16, snapGrid: false, autoCons: true, showCons: true, theme: 'system', welcomed: false,
33
+ unit: 'ftin', denom: 16, snapGrid: false, autoCons: true, toolTips: true, showCons: true, theme: 'system', welcomed: false,
28
34
  layers: defaultLayers(), wallH: 96,
29
35
  }));
30
36
 
@@ -1,7 +1,8 @@
1
1
  import { hyp } from './geom';
2
+ import { faceEdit, faceEmpty, faceKeyParts, gcCabinets } from './faces';
2
3
  import { uid } from './ids';
3
4
  import { pruneOpeningDims } from './openings';
4
- import type { Arc, Circle, Constraint, Line, LineKind, Pt, Room, SelItem, Sketch } from './types';
5
+ import type { Arc, Circle, Constraint, Line, LineKind, Pt, Room, SelItem, Sketch, WallFace } from './types';
5
6
  import { onSessionReset } from './session';
6
7
 
7
8
  export function emptySketch(): Sketch {
@@ -23,9 +24,12 @@ export function addPoint(sk: Sketch, x: number, y: number, extra?: Partial<Pt>):
23
24
  return id;
24
25
  }
25
26
 
27
+ /** A new wall's thickness, inches (4 1/2": a stud wall with drywall both sides), until it's changed. */
28
+ export const WALL_THICK = 4.5;
29
+
26
30
  export function addLine(sk: Sketch, a: string, b: string, props: { kind?: LineKind; constr?: boolean; thick?: number } = {}): string {
27
31
  const id = uid('l');
28
- sk.lines[id] = { a, b, kind: props.kind || 'line', constr: !!props.constr, thick: props.thick || 4.5 };
32
+ sk.lines[id] = { a, b, kind: props.kind || 'line', constr: !!props.constr, thick: props.thick || WALL_THICK };
29
33
  return id;
30
34
  }
31
35
 
@@ -72,14 +76,17 @@ export function gc(sk: Sketch): void {
72
76
  for (const k of REFKEYS) {
73
77
  const v = c[k];
74
78
  if (v != null && typeof v === 'string' && !refExists(sk, v)) {
75
- if (k === 'line') c.line = null;
79
+ if (k === 'line') {
80
+ c.line = null;
81
+ // (measured on its wall's face, the wall gone: end to end, as it is now — nothing moves)
82
+ if (c.face) { delete c.face; const a = sk.points[c.a!], b = sk.points[c.b!]; if (a && b && c.driving !== false) c.value = Math.hypot(b.x - a.x, b.y - a.y); }
83
+ }
76
84
  else { delete sk.cons[id]; changed = true; break; }
77
85
  }
78
86
  }
79
87
  if (sk.cons[id] && (c.type === 'h' || c.type === 'v' || c.type === 'dist') && c.a === c.b) { delete sk.cons[id]; changed = true; }
80
88
  }
81
89
  for (const id in sk.openings) if (!sk.lines[sk.openings[id].line]) { delete sk.openings[id]; changed = true; }
82
- if (sk.elev) for (const k in sk.elev) if (!sk.lines[k.split('|')[0]] || !sk.elev[k].length) delete sk.elev[k];
83
90
  if (pruneOpeningDims(sk)) changed = true;
84
91
  const used = new Set<string>();
85
92
  for (const id in sk.lines) { used.add(sk.lines[id].a); used.add(sk.lines[id].b); }
@@ -88,13 +95,70 @@ export function gc(sk: Sketch): void {
88
95
  for (const id in sk.openings) for (const d of sk.openings[id].dims || []) if ('pt' in d.ref) used.add(d.ref.pt);
89
96
  for (const id in sk.points) {
90
97
  const p = sk.points[id];
91
- if (!used.has(id) && !p.origin && !p.datum && !pinnedPoints.has(id)) { delete sk.points[id]; changed = true; }
98
+ if (!used.has(id) && !p.origin && !p.datum && !p.anchor && !pinnedPoints.has(id)) { delete sk.points[id]; changed = true; }
92
99
  }
93
100
  }
94
- for (const id in sk.rooms) if (!roomValid(sk, sk.rooms[id])) delete sk.rooms[id];
101
+ // (each room's edges looked up by their ends, not by scanning every line for each edge: this runs after every edit)
102
+ withLineIndex(sk, () => { for (const id in sk.rooms) if (!roomValid(sk, sk.rooms[id])) delete sk.rooms[id]; });
103
+ if (sk.faces) gcFaces(sk);
104
+ }
105
+
106
+ /** Wall faces: each goes with its wall, and one with nothing drawn on it isn't kept (unless it's open); each is cleaned up as a sketch. */
107
+ function gcFaces(sk: Sketch): void {
108
+ for (const key in sk.faces!) {
109
+ const face = sk.faces![key];
110
+ if (!sk.lines[faceKeyParts(key).lid]) { delete sk.faces![key]; continue; }
111
+ gc(face);
112
+ gcCabinets(face);
113
+ if (key !== faceEdit.key && faceEmpty(face)) delete sk.faces![key];
114
+ }
115
+ if (!Object.keys(sk.faces!).length) delete sk.faces;
116
+ }
117
+
118
+ /*
119
+ While a frame is drawn the sketch can't change, so its lines are indexed by their two ends once for the frame
120
+ (withLineIndex) rather than scanned on every lookup: drawing asks which lines join two points for every room edge,
121
+ for every door and window (which side of its wall faces out) and for every H/V badge — rooms × edges × lines per
122
+ opening — which on a 20-room plan was most of a frame's time.
123
+ */
124
+ /** each end → the other end → the lines joining them (one list for both ways round) */
125
+ let lineIndex: { sk: Sketch; pairs: Map<string, Map<string, string[]>>; memo: Map<string, unknown> } | null = null;
126
+
127
+ /** Run `fn` with `sk`'s lines indexed by their ends (nothing in `fn` may change the sketch). */
128
+ export function withLineIndex<T>(sk: Sketch, fn: () => T): T {
129
+ if (lineIndex) return fn();
130
+ const pairs = new Map<string, Map<string, string[]>>();
131
+ const from = (p: string) => pairs.get(p) ?? pairs.set(p, new Map()).get(p)!;
132
+ for (const id in sk.lines) {
133
+ const l = sk.lines[id];
134
+ let ids = pairs.get(l.a)?.get(l.b);
135
+ if (!ids) { ids = []; from(l.a).set(l.b, ids); from(l.b).set(l.a, ids); }
136
+ ids.push(id);
137
+ }
138
+ lineIndex = { sk, pairs, memo: new Map() };
139
+ try { return fn(); } finally { lineIndex = null; }
140
+ }
141
+
142
+ /** Inside withLineIndex, what `compute` gives is worked out once for the frame; outside it, every time. */
143
+ export function frameMemo<T>(sk: Sketch, key: string, compute: () => T): T {
144
+ if (!lineIndex || lineIndex.sk !== sk) return compute();
145
+ const memo = lineIndex.memo;
146
+ if (!memo.has(key)) memo.set(key, compute());
147
+ return memo.get(key) as T;
148
+ }
149
+
150
+ /** The first line joining two points, if any (linesBetween(…)[0], without making a list for it). */
151
+ export function lineBetween(sk: Sketch, a: string, b: string): string | undefined {
152
+ if (lineIndex && lineIndex.sk === sk) return lineIndex.pairs.get(a)?.get(b)?.[0];
153
+ for (const id in sk.lines) {
154
+ const l = sk.lines[id];
155
+ if ((l.a === a && l.b === b) || (l.a === b && l.b === a)) return id;
156
+ }
157
+ return undefined;
95
158
  }
96
159
 
97
160
  export function linesBetween(sk: Sketch, a: string, b: string): string[] {
161
+ if (lineIndex && lineIndex.sk === sk) return lineIndex.pairs.get(a)?.get(b)?.slice() ?? [];
98
162
  const out: string[] = [];
99
163
  for (const id in sk.lines) {
100
164
  const l = sk.lines[id];
@@ -113,18 +177,20 @@ export function roomValid(sk: Sketch, r: Room): boolean {
113
177
  const id = r.edges[i], l = sk.lines[id], ar = sk.arcs[id];
114
178
  const ends = l ? [l.a, l.b] : ar ? [ar.a, ar.b] : null;
115
179
  if (!ends || !((ends[0] === a && ends[1] === b) || (ends[0] === b && ends[1] === a))) return false;
116
- } else if (!linesBetween(sk, a, b).length) return false;
180
+ } else if (!lineBetween(sk, a, b)) return false;
117
181
  }
118
182
  return true;
119
183
  }
120
184
 
121
185
  export function deleteSel(sk: Sketch, items: SelItem[]): void {
186
+ const cabCorners = new Set(Object.values((sk as WallFace).cabs ?? {}).flatMap(cb => [cb.bl, cb.br, cb.tr, cb.tl]));
122
187
  for (const it of items) {
123
- if (it.type === 'point') { const p = sk.points[it.id]; if (p && !p.origin) delete sk.points[it.id]; }
124
- else if (it.type === 'line') delete sk.lines[it.id];
188
+ // (a cabinet's corner goes with its cabinet, not on its own)
189
+ if (it.type === 'point') { const p = sk.points[it.id]; if (p && !p.origin && !p.anchor && !cabCorners.has(it.id)) delete sk.points[it.id]; }
190
+ else if (it.type === 'line') { if (!sk.lines[it.id]?.anchor && !sk.lines[it.id]?.cab) delete sk.lines[it.id]; } // (a cabinet's edge goes with it)
125
191
  else if (it.type === 'circle') delete sk.circles[it.id];
126
192
  else if (it.type === 'arc') delete sk.arcs[it.id];
127
- else if (it.type === 'cons' || it.type === 'dim') delete sk.cons[it.id];
193
+ else if (it.type === 'cons' || it.type === 'dim') { if (!sk.cons[it.id]?.anchor) delete sk.cons[it.id]; }
128
194
  else if (it.type === 'room') delete sk.rooms[it.id];
129
195
  else if (it.type === 'opening') delete sk.openings[it.id];
130
196
  else if (it.type === 'odim') {
@@ -136,9 +202,26 @@ export function deleteSel(sk: Sketch, items: SelItem[]): void {
136
202
  gc(sk);
137
203
  }
138
204
 
205
+ /**
206
+ * Join two points: one takes the other's place (a shared id) — or, two of a wall face's anchors (named by the plan,
207
+ * so neither can go), they're held together where they are (level and plumb with each other, hidden). False if they
208
+ * can't be (two the plan fixes).
209
+ */
210
+ export function joinPoints(sk: Sketch, keep: string, drop: string): boolean {
211
+ const A = sk.points[keep], B = sk.points[drop];
212
+ if (!A || !B || keep === drop) return false;
213
+ if (!(A.anchor && B.anchor)) return mergePoints(sk, keep, drop);
214
+ if (A.fixed && B.fixed) return false;
215
+ addCons(sk, { type: 'h', a: keep, b: drop, elev: true });
216
+ addCons(sk, { type: 'v', a: keep, b: drop, elev: true });
217
+ return true;
218
+ }
219
+
139
220
  /** Replace every reference to `drop` with `keep`. Coincidence = shared id. */
140
221
  export function mergePoints(sk: Sketch, keep: string, drop: string): boolean {
141
222
  if (keep === drop || !sk.points[keep] || !sk.points[drop]) return false;
223
+ // a wall face's anchor (set by the plan) is the one that stays
224
+ if (sk.points[drop].anchor) { if (sk.points[keep].anchor) return false; [keep, drop] = [drop, keep]; }
142
225
  for (const id of linesBetween(sk, keep, drop)) delete sk.lines[id];
143
226
  const rep = (v: string) => (v === drop ? keep : v);
144
227
  for (const id in sk.lines) { const l = sk.lines[id]; l.a = rep(l.a); l.b = rep(l.b); }
@@ -150,6 +233,8 @@ export function mergePoints(sk: Sketch, keep: string, drop: string): boolean {
150
233
  }
151
234
  for (const id in sk.rooms) sk.rooms[id].loop = sk.rooms[id].loop.map(rep);
152
235
  for (const id in sk.openings) for (const d of sk.openings[id].dims || []) if ('pt' in d.ref) d.ref.pt = rep(d.ref.pt);
236
+ // a wall face's cabinets: a corner joined to another point is that point now (else the cabinet would lose a corner and go)
237
+ for (const cb of Object.values((sk as WallFace).cabs ?? {})) { cb.bl = rep(cb.bl); cb.br = rep(cb.br); cb.tr = rep(cb.tr); cb.tl = rep(cb.tl); }
153
238
  if (sk.points[drop].fixed) sk.points[keep].fixed = true;
154
239
  delete sk.points[drop];
155
240
  // drop duplicate lines
@@ -194,19 +279,21 @@ export function restorePos(sk: Sketch, s: PosSnapshot): void {
194
279
 
195
280
  /** Every line and arc joined end to end with the given ones (through shared points), including them. */
196
281
  export function connectedGeometry(sk: Sketch, lineIds: string[], arcIds: string[] = []): { lines: string[]; arcs: string[] } {
282
+ // on a wall face, the plan's corners and edges aren't part of a shape: nothing joins through them
197
283
  const pts = new Set<string>(), lines = new Set(lineIds), arcs = new Set(arcIds);
198
- lineIds.forEach(id => { const l = sk.lines[id]; if (l) { pts.add(l.a); pts.add(l.b); } });
199
- arcIds.forEach(id => { const a = sk.arcs[id]; if (a) { pts.add(a.a); pts.add(a.b); } });
284
+ const join = (p: string) => { if (!sk.points[p]?.anchor) pts.add(p); };
285
+ lineIds.forEach(id => { const l = sk.lines[id]; if (l) { join(l.a); join(l.b); } });
286
+ arcIds.forEach(id => { const a = sk.arcs[id]; if (a) { join(a.a); join(a.b); } });
200
287
  let grow = true;
201
288
  while (grow) {
202
289
  grow = false;
203
290
  for (const id in sk.lines) {
204
291
  const l = sk.lines[id];
205
- if (!lines.has(id) && (pts.has(l.a) || pts.has(l.b))) { lines.add(id); pts.add(l.a); pts.add(l.b); grow = true; }
292
+ if (!lines.has(id) && !l.anchor && !l.cab && (pts.has(l.a) || pts.has(l.b))) { lines.add(id); join(l.a); join(l.b); grow = true; }
206
293
  }
207
294
  for (const id in sk.arcs) {
208
295
  const a = sk.arcs[id];
209
- if (!arcs.has(id) && (pts.has(a.a) || pts.has(a.b))) { arcs.add(id); pts.add(a.a); pts.add(a.b); grow = true; }
296
+ if (!arcs.has(id) && (pts.has(a.a) || pts.has(a.b))) { arcs.add(id); join(a.a); join(a.b); grow = true; }
210
297
  }
211
298
  }
212
299
  return { lines: [...lines], arcs: [...arcs] };
@@ -0,0 +1,51 @@
1
+ type P = [number, number];
2
+
3
+ /*
4
+ A pen stroke is drawn as a smooth curve through the points it was sampled at, not straight from one to the next —
5
+ which shows a corner at every sample wherever they're far apart (a quick stroke, a finger's coarser samples, or just
6
+ zoomed in). Centripetal Catmull-Rom, as cubic Béziers: it passes through every point, doesn't loop or overshoot where
7
+ the spacing changes, and is the same curve at any zoom — so the screen, a tap on it and an export all agree.
8
+ */
9
+
10
+ /** The curve through `pts` as one cubic Bézier per span, [first control, second control, end], from the first point. */
11
+ export function smoothSpans(pts: P[]): [P, P, P][] {
12
+ // (a repeated point makes no span)
13
+ const q: P[] = [];
14
+ for (const p of pts) { const l = q[q.length - 1]; if (!l || p[0] !== l[0] || p[1] !== l[1]) q.push(p); }
15
+ const n = q.length, out: [P, P, P][] = [];
16
+ // past each end, the point mirrored through it: the curve leaves along its first span and arrives along its last
17
+ const at = (i: number): P =>
18
+ i < 0 ? [2 * q[0][0] - q[1][0], 2 * q[0][1] - q[1][1]] : i >= n ? [2 * q[n - 1][0] - q[n - 2][0], 2 * q[n - 1][1] - q[n - 2][1]] : q[i];
19
+ // (centripetal: each span weighs in by the square root of its length)
20
+ const w = (a: P, b: P) => Math.sqrt(Math.hypot(b[0] - a[0], b[1] - a[1]));
21
+ for (let i = 0; i + 1 < n; i++) {
22
+ const p0 = at(i - 1), p1 = q[i], p2 = q[i + 1], p3 = at(i + 2);
23
+ const a = w(p0, p1), b = w(p1, p2), c = w(p2, p3);
24
+ const k1 = 3 * a * (a + b), k2 = 3 * c * (c + b), m1 = 2 * a * a + 3 * a * b + b * b, m2 = 2 * c * c + 3 * c * b + b * b;
25
+ out.push([
26
+ [(a * a * p2[0] - b * b * p0[0] + m1 * p1[0]) / k1, (a * a * p2[1] - b * b * p0[1] + m1 * p1[1]) / k1],
27
+ [(c * c * p1[0] - b * b * p3[0] + m2 * p2[0]) / k2, (c * c * p1[1] - b * b * p3[1] + m2 * p2[1]) / k2],
28
+ p2,
29
+ ]);
30
+ }
31
+ return out;
32
+ }
33
+
34
+ /** The same curve as straight pieces, never more than `tol` off it — for whatever draws only lines (an export, a tap). */
35
+ export function smoothPolyline(pts: P[], tol: number): P[] {
36
+ if (pts.length < 3) return pts.slice();
37
+ const out: P[] = [pts[0]];
38
+ let from = pts[0];
39
+ for (const [c1, c2, to] of smoothSpans(pts)) {
40
+ // how many pieces keep within `tol` (Wang’s bound, from how sharply the controls turn; the cap only stops a runaway)
41
+ const dd = Math.max(Math.hypot(from[0] - 2 * c1[0] + c2[0], from[1] - 2 * c1[1] + c2[1]), Math.hypot(c1[0] - 2 * c2[0] + to[0], c1[1] - 2 * c2[1] + to[1]));
42
+ const k = Math.min(256, Math.max(1, Math.ceil(Math.sqrt((0.75 * dd) / tol))));
43
+ for (let j = 1; j < k; j++) {
44
+ const t = j / k, s = 1 - t, A = s * s * s, B = 3 * s * s * t, C = 3 * s * t * t, D = t * t * t;
45
+ out.push([A * from[0] + B * c1[0] + C * c2[0] + D * to[0], A * from[1] + B * c1[1] + C * c2[1] + D * to[1]]);
46
+ }
47
+ out.push(to);
48
+ from = to;
49
+ }
50
+ return out;
51
+ }
@@ -2,7 +2,8 @@ import { seedProject } from './demo';
2
2
  import { deep } from './ids';
3
3
  import { defaultLayers, settings } from './settings';
4
4
  import { migrateProject } from './project';
5
- import type { Page, Project, Sketch } from './types';
5
+ import { faceEdit, faceOf } from './faces';
6
+ import type { Page, Project, Sketch, WallFace } from './types';
6
7
  import { onSessionReset, sessionState } from './session';
7
8
 
8
9
  export { migrateProject };
@@ -18,16 +19,29 @@ export interface Persistence {
18
19
  load(): void;
19
20
  /** Save the project now. Returns false if images had to be left out. */
20
21
  save(): boolean;
22
+ /** It keeps where each page was viewed from, so a pan or zoom alone is worth saving (not so a drawing synced to a job). */
23
+ keepsView?: boolean;
21
24
  }
22
25
 
23
26
  export const store = sessionState((): { project: Project; curPageId: string } => ({ project: null as unknown as Project, curPageId: '' }));
24
27
 
25
28
  export const allPages = (): Page[] => store.project.pages;
26
29
  export const page = (): Page => allPages().find(p => p.id === store.curPageId)!;
27
- export const SK = (): Sketch => page().sk;
30
+ /** The page's floor plan (whatever is being edited). */
31
+ export const planSK = (): Sketch => page().sk;
32
+ /** The sketch being edited: the floor plan, or the wall face open in the wall view (see model/faces.ts faceEdit). */
33
+ export const SK = (): Sketch => (faceEdit.key ? faceOf(page().sk, faceEdit.key, true) : page().sk);
34
+ /** Whether a wall face is being edited (the wall view is open). */
35
+ export const editingFace = (): boolean => faceEdit.key != null;
36
+ /** Put back a copy of the sketch being edited (a change that couldn't be made). */
37
+ export function replaceSK(next: Sketch): void {
38
+ if (faceEdit.key) page().sk.faces![faceEdit.key] = next as WallFace;
39
+ else page().sk = next;
40
+ }
28
41
 
29
42
  /** This browser's localStorage (the default). */
30
43
  export const localPersistence: Persistence = {
44
+ keepsView: true,
31
45
  load() {
32
46
  try {
33
47
  const raw = localStorage.getItem(STORE_KEY);
@@ -50,7 +64,7 @@ export const localPersistence: Persistence = {
50
64
  } catch {
51
65
  try {
52
66
  const p = deep(store.project);
53
- p.pages.forEach(pg => (pg.underlay = null));
67
+ p.pages.forEach(pg => (pg.underlays = []));
54
68
  localStorage.setItem(STORE_KEY, payload(p));
55
69
  } catch { /* nothing more we can do */ }
56
70
  return false;
@@ -75,9 +89,26 @@ export function load(): void {
75
89
 
76
90
  let saveT: ReturnType<typeof setTimeout> | undefined;
77
91
  onSessionReset(() => clearTimeout(saveT));
92
+
93
+ /**
94
+ * Whether nothing is being done on the drawing just now (no finger or pointer down): a save waits for that — saving
95
+ * takes the JavaScript thread a moment, felt as a hitch in a drag or a stroke started just after an edit. (Never for
96
+ * long: a lift that was lost mustn't hold saving back.)
97
+ */
98
+ let idle: () => boolean = () => true;
99
+ export function setSaveIdle(fn: (() => boolean) | null): void { idle = fn ?? (() => true); }
100
+ const SAVE_MS = 700, WAIT_MS = 300, WAITS = 10;
101
+
78
102
  export function saveSoon(): void {
79
103
  clearTimeout(saveT);
80
- saveT = setTimeout(saveNow, 700);
104
+ let waits = 0;
105
+ const attempt = () => { if (!idle() && waits++ < WAITS) saveT = setTimeout(attempt, WAIT_MS); else saveNow(); };
106
+ saveT = setTimeout(attempt, SAVE_MS);
107
+ }
108
+
109
+ /** The view moved (a pan or a zoom): saved only where that's kept (see Persistence.keepsView). */
110
+ export function saveViewSoon(): void {
111
+ if (persistence.keepsView) saveSoon();
81
112
  }
82
113
 
83
114
  /** Save now. Returns false if underlays had to be left out. */