@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
@@ -4,6 +4,7 @@ import { settings } from '../model/settings';
4
4
  import { SK } from '../model/store';
5
5
  import { gridSteps } from '../model/planGeom';
6
6
  import { mainT, S, s2w } from './view';
7
+ import { intoWall } from '../editor/elevation';
7
8
 
8
9
  export interface SnapOpts {
9
10
  touch?: boolean;
@@ -15,11 +16,61 @@ export interface SnapOpts {
15
16
  prevLine?: string | null;
16
17
  }
17
18
 
18
- /** Snap a screen point: endpoints > midpoints/intersections > H/V/perp guides > on line/circle > grid. */
19
+ /** Snap a screen point: endpoints > midpoints/intersections > on line > H/V/perp guides > on circle > grid. In the wall view, never past the wall. */
20
+ /**
21
+ * Shift held: from `a`, the direction to `p` locked to the nearest 45° — level, plumb or a diagonal — keeping how
22
+ * far along it `p` is. `dir`: which (H level, V plumb, D diagonal).
23
+ */
24
+ export function snap45(a: { x: number; y: number }, p: { x: number; y: number }): { x: number; y: number; dir: 'H' | 'V' | 'D' } {
25
+ const dx = p.x - a.x, dy = p.y - a.y, L = hyp(dx, dy);
26
+ if (L < 1e-9) return { x: a.x, y: a.y, dir: 'H' };
27
+ const k = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)), th = k * (Math.PI / 4);
28
+ const ux = Math.round(Math.cos(th) * 1e12) / 1e12, uy = Math.round(Math.sin(th) * 1e12) / 1e12, t = dx * ux + dy * uy;
29
+ return { x: a.x + ux * t, y: a.y + uy * t, dir: k % 4 === 0 ? 'H' : Math.abs(k % 4) === 2 ? 'V' : 'D' };
30
+ }
31
+
32
+ /** A drawing snap with Shift held, from point `from`: locked to the nearest 45°; level or plumb comes with its guide (auto-constrain adds it). */
33
+ export function shiftSnap(from: string, s: Snap): Snap {
34
+ const a = SK().points[from];
35
+ if (!a) return s;
36
+ const q = snap45(a, s), [x, y] = intoWall(q.x, q.y);
37
+ const guides: Guide[] = q.dir === 'D' ? [] : [{ kind: q.dir, from, x1: a.x, y1: a.y, x2: x, y2: y }];
38
+ return { x, y, kind: 'free', guides };
39
+ }
40
+
19
41
  export function snapAt(sx: number, sy: number, o: SnapOpts = {}): Snap {
42
+ const s = snapRaw(sx, sy, o), [x, y] = intoWall(s.x, s.y);
43
+ return x === s.x && y === s.y ? s : { ...s, x, y, kind: s.kind === 'grid' ? 'grid' : 'free', ref: null, refs: undefined };
44
+ }
45
+
46
+ /*
47
+ The grid: with Snap to grid on, whatever a snap leaves free goes on the grid that's drawn (gridSteps: what's seen is
48
+ what's snapped to) — the whole point in open space; the other direction when a guide holds one (drawn level from a
49
+ point on the grid: a whole number of steps long); along a line, where it crosses a grid line; along a perpendicular,
50
+ whole steps from where it starts. Endpoints, midpoints and intersections still come first. And it's steady: as the
51
+ pointer moves, the snap holds the grid line it's on until the pointer is well past halfway to the next, so a
52
+ finger's wobble at the halfway point doesn't flick it back and forth.
53
+ */
54
+ const HOLD = 0.15;
55
+ /** The last snap's grid lines, while the pointer moves on from it (each axis, if it went on the grid). */
56
+ let held: { st: number; x?: number; y?: number; sx: number; sy: number; t: number } | null = null;
57
+
58
+ /** The grid line nearest v — or, `last` (the one it was on) while v isn't yet well past halfway to the next. */
59
+ export function onGrid(v: number, st: number, last?: number): number {
60
+ const r = Math.round(v / st) * st;
61
+ return last !== undefined && Math.abs(v - last) <= st * (0.5 + HOLD) ? last : r;
62
+ }
63
+
64
+ function snapRaw(sx: number, sy: number, o: SnapOpts): Snap {
20
65
  const sk = SK(), T = mainT(), tol = o.touch ? 22 : 12;
21
66
  const [wx, wy] = s2w(sx, sy);
22
67
  const ex = o.exclude || new Set<string>();
68
+ const st = settings.snapGrid ? gridSteps(T.s).minor : 0, now = performance.now();
69
+ // (the same pointer moving on: not a new tap somewhere else, nor one after a pause)
70
+ const was = held && held.st === st && now - held.t < 250 && hyp(sx - held.sx, sy - held.sy) < 48 ? held : null;
71
+ let usedX: number | undefined, usedY: number | undefined;
72
+ const gridX = (v: number) => (usedX = onGrid(v, st, was?.x)), gridY = (v: number) => (usedY = onGrid(v, st, was?.y));
73
+ const finish = (snap: Snap): Snap => { held = st ? { st, x: usedX, y: usedY, sx, sy, t: now } : null; return snap; };
23
74
  let best: (Snap & { sc: number }) | null = null;
24
75
  const cand = (x: number, y: number, kind: Snap['kind'], ref: string | [string, string], prio: number, label: string) => {
25
76
  const [px, py] = S(T, x, y);
@@ -62,7 +113,19 @@ export function snapAt(sx: number, sy: number, o: SnapOpts = {}): Snap {
62
113
  }
63
114
  if (best) {
64
115
  const { sc: _sc, ...snap } = best as Snap & { sc: number };
65
- return snap;
116
+ return finish(snap);
117
+ }
118
+ // on a line, near the pointer (checked before the alignment guides: a line under the pointer wins over lining up with
119
+ // a point elsewhere — where both apply, the snap is where the guide crosses the line)
120
+ let bestL: { d: number; x: number; y: number; id: string } | null = null;
121
+ for (const id of near) {
122
+ const l = sk.lines[id], A = sk.points[l.a], B = sk.points[l.b];
123
+ const t = projT(A, B, { x: wx, y: wy });
124
+ if (t < 0 || t > 1) continue;
125
+ const fx = A.x + (B.x - A.x) * t, fy = A.y + (B.y - A.y) * t;
126
+ const [qx, qy] = S(T, fx, fy);
127
+ const d = hyp(qx - sx, qy - sy);
128
+ if (d < tol && (!bestL || d < bestL.d)) bestL = { d, x: fx, y: fy, id };
66
129
  }
67
130
  let x = wx, y = wy, kind: Snap['kind'] = 'free';
68
131
  const guides: Guide[] = [];
@@ -77,42 +140,62 @@ export function snapAt(sx: number, sy: number, o: SnapOpts = {}): Snap {
77
140
  if (gy == null && Math.abs(sy - py) < tol) gy = { y: r.p.y, from: r.id, own: r.own };
78
141
  if (gx == null && Math.abs(sx - px) < tol) gx = { x: r.p.x, from: r.id, own: r.own };
79
142
  }
143
+ if (bestL) {
144
+ // a line under the pointer: on it — where a guide crosses it, if one does (level with the start, and on the line)
145
+ const l = sk.lines[bestL.id], A = sk.points[l.a], B = sk.points[l.b], dx = B.x - A.x, dy = B.y - A.y;
146
+ const g = gy && Math.abs(dy) > 1e-9 ? { t: (gy.y - A.y) / dy, h: gy } : gx && Math.abs(dx) > 1e-9 ? { t: (gx.x - A.x) / dx, v: gx } : null;
147
+ if (g && g.t >= 0 && g.t <= 1) {
148
+ const ix = A.x + dx * g.t, iy = A.y + dy * g.t, [qx, qy] = S(T, ix, iy);
149
+ if (hyp(qx - sx, qy - sy) < tol * 1.5) {
150
+ const gd = 'h' in g ? g.h! : g.v!, H = 'h' in g;
151
+ return finish({ x: ix, y: iy, kind: 'online', ref: bestL.id, label: 'On line', guides: [H ? { x1: ix - 1, y1: iy, x2: ix + 1, y2: iy, kind: 'H', from: gd.from, label: gd.own ? 'Horizontal' : 'Aligned' } : { x1: ix, y1: iy - 1, x2: ix, y2: iy + 1, kind: 'V', from: gd.from, label: gd.own ? 'Vertical' : 'Aligned' }] });
152
+ }
153
+ }
154
+ // on the grid, as far as the line allows: where it crosses the grid line nearest (along a level or plumb line,
155
+ // the grid point on it)
156
+ if (st) {
157
+ const on = bestL, near2 = (c: { x: number; y: number }) => hyp(c.x - on.x, c.y - on.y);
158
+ const opts: { x: number; y: number; t: number; axis: 'x' | 'y' }[] = [];
159
+ if (Math.abs(dx) > 1e-9) { const gv = onGrid(on.x, st, was?.x), t = (gv - A.x) / dx; opts.push({ x: gv, y: A.y + dy * t, t, axis: 'x' }); }
160
+ if (Math.abs(dy) > 1e-9) { const gv = onGrid(on.y, st, was?.y), t = (gv - A.y) / dy; opts.push({ x: A.x + dx * t, y: gv, t, axis: 'y' }); }
161
+ const pick = opts
162
+ .filter(c => { const [qx, qy] = S(T, c.x, c.y); return c.t >= 0 && c.t <= 1 && hyp(qx - sx, qy - sy) < tol * 1.5; })
163
+ .sort((a, b) => near2(a) - near2(b))[0];
164
+ if (pick) {
165
+ if (pick.axis === 'x') usedX = pick.x; else usedY = pick.y;
166
+ return finish({ x: pick.x, y: pick.y, kind: 'online', ref: on.id, label: 'On line', guides: [] });
167
+ }
168
+ }
169
+ return finish({ x: bestL.x, y: bestL.y, kind: 'online', ref: bestL.id, label: 'On line', guides: [] });
170
+ }
80
171
  if (gy) { y = gy.y; guides.push({ x1: wx - 1, y1: gy.y, x2: wx + 1, y2: gy.y, kind: 'H', from: gy.from, label: gy.own ? 'Horizontal' : 'Aligned' }); }
81
172
  if (gx) { x = gx.x; guides.push({ x1: gx.x, y1: wy - 1, x2: gx.x, y2: wy + 1, kind: 'V', from: gx.from, label: gx.own ? 'Vertical' : 'Aligned' }); }
82
173
  if (!guides.length && from && o.prevLine && sk.lines[o.prevLine]) {
83
174
  const l = sk.lines[o.prevLine], A = sk.points[l.a], B = sk.points[l.b];
84
175
  const d = norm2(B.x - A.x, B.y - A.y), n = [-d[1], d[0]];
85
- const t = (wx - from.x) * n[0] + (wy - from.y) * n[1];
176
+ const t0 = (wx - from.x) * n[0] + (wy - from.y) * n[1];
177
+ // (with the grid: whole steps of it from where it starts)
178
+ const t = st ? onGrid(t0, st) : t0;
86
179
  const px = from.x + n[0] * t, py = from.y + n[1] * t;
87
- const [qx, qy] = S(T, px, py);
88
- if (hyp(qx - sx, qy - sy) < tol && Math.abs(t) * T.s > tol) {
180
+ const [qx, qy] = S(T, from.x + n[0] * t0, from.y + n[1] * t0);
181
+ if (hyp(qx - sx, qy - sy) < tol && Math.abs(t0) * T.s > tol) {
89
182
  x = px; y = py;
90
183
  guides.push({ x1: from.x, y1: from.y, x2: px, y2: py, kind: 'perp', label: 'Perpendicular' });
91
184
  }
92
185
  }
93
186
  if (!guides.length) {
94
- let bestL: { d: number; x: number; y: number; id: string } | null = null;
95
- for (const id of near) {
96
- const l = sk.lines[id], A = sk.points[l.a], B = sk.points[l.b];
97
- const t = projT(A, B, { x: wx, y: wy });
98
- if (t < 0 || t > 1) continue;
99
- const fx = A.x + (B.x - A.x) * t, fy = A.y + (B.y - A.y) * t;
100
- const [qx, qy] = S(T, fx, fy);
101
- const d = hyp(qx - sx, qy - sy);
102
- if (d < tol && (!bestL || d < bestL.d)) bestL = { d, x: fx, y: fy, id };
103
- }
104
- if (bestL) return { x: bestL.x, y: bestL.y, kind: 'online', ref: bestL.id, label: 'On line', guides: [] };
105
187
  for (const id in sk.circles) {
106
188
  const ci = sk.circles[id], o2 = sk.points[ci.c];
107
189
  const d = hyp(wx - o2.x, wy - o2.y);
108
190
  if (Math.abs(d - ci.r) * T.s < tol && d > 1e-6) {
109
- return { x: o2.x + ((wx - o2.x) / d) * ci.r, y: o2.y + ((wy - o2.y) / d) * ci.r, kind: 'oncircle', ref: id, label: 'On circle', guides: [] };
191
+ return finish({ x: o2.x + ((wx - o2.x) / d) * ci.r, y: o2.y + ((wy - o2.y) / d) * ci.r, kind: 'oncircle', ref: id, label: 'On circle', guides: [] });
110
192
  }
111
193
  }
112
- if (settings.snapGrid) {
113
- const st = gridSteps(T.s).minor;
114
- x = Math.round(wx / st) * st; y = Math.round(wy / st) * st; kind = 'grid';
115
- }
194
+ if (st) { x = gridX(wx); y = gridY(wy); kind = 'grid'; }
195
+ } else if (st && !guides.some(g => g.kind === 'perp')) {
196
+ // a guide holds one direction: the other goes on the grid
197
+ if (!gx) x = gridX(wx);
198
+ if (!gy) y = gridY(wy);
116
199
  }
117
- return { x, y, kind, ref: null, label: '', guides };
200
+ return finish({ x, y, kind, ref: null, label: '', guides });
118
201
  }
@@ -0,0 +1,73 @@
1
+ import type { DrawCtx } from './ctx';
2
+
3
+ /*
4
+ Figma's corner smoothing, at the 60% it calls "iOS" (Apple's continuous corners): a corner keeps its circular arc of
5
+ radius r in the middle, but shorter, and eases into each straight side over a longer run — (1 + smoothing) · r of
6
+ the side instead of r — so the curvature has no sudden step where the side ends. The construction is Figma's own
7
+ (as published in its "Desperately seeking squircles", and the MIT figma-squircle package).
8
+ */
9
+
10
+ /** How much every rounded corner in the editor is smoothed: Figma's 60%, "iOS". */
11
+ export const CORNER_SMOOTHING = 0.6;
12
+
13
+ const rad = (deg: number): number => (deg * Math.PI) / 180;
14
+
15
+ /** One corner's measures: the run along each side (p), the two Béziers' (a, b, c, d), the arc's half-sweep (alpha). */
16
+ function cornerParams(r: number, smoothing: number, budget: number) {
17
+ // a corner can't run further along its sides than half the shorter side: less smoothing where it can't fit
18
+ const s = Math.max(0, Math.min(smoothing, budget / r - 1));
19
+ const p = Math.min((1 + s) * r, budget);
20
+ const arcMeasure = 90 * (1 - s);
21
+ const arcSection = Math.sin(rad(arcMeasure / 2)) * r * Math.SQRT2;
22
+ const alpha = (90 - arcMeasure) / 2;
23
+ const p3ToP4 = r * Math.tan(rad(alpha / 2));
24
+ const beta = 45 * s;
25
+ const c = p3ToP4 * Math.cos(rad(beta));
26
+ const d = c * Math.tan(rad(beta));
27
+ const b = (p - arcSection - c - d) / 3;
28
+ return { p, a: 2 * b, b, c, d, alpha: rad(alpha) };
29
+ }
30
+
31
+ /**
32
+ * A rounded rectangle's path with smoothed corners (x, y its top left; r each corner's radius). A pill or a circle —
33
+ * r at least half the shorter side — is left round: smoothing has nothing to ease into there, as in Figma.
34
+ */
35
+ export function smoothRectPath(c: DrawCtx, x: number, y: number, w: number, h: number, r: number, smoothing = CORNER_SMOOTHING): void {
36
+ const budget = Math.min(w, h) / 2;
37
+ r = Math.max(0, Math.min(r, budget));
38
+ c.beginPath();
39
+ if (r <= 0 || smoothing <= 0 || r >= budget - 1e-6) {
40
+ c.moveTo(x + r, y);
41
+ c.arcTo(x + w, y, x + w, y + h, r);
42
+ c.arcTo(x + w, y + h, x, y + h, r);
43
+ c.arcTo(x, y + h, x, y, r);
44
+ c.arcTo(x, y, x + w, y, r);
45
+ c.closePath();
46
+ return;
47
+ }
48
+ const k = cornerParams(r, smoothing, budget);
49
+ // each corner drawn in its own frame: u along the side coming into it, v along the side leaving it, the corner's
50
+ // vertex at the origin — then turned to its place, clockwise from the top right
51
+ const corners: { ox: number; oy: number; ux: [number, number]; vx: [number, number]; turn: number }[] = [
52
+ { ox: x + w, oy: y, ux: [1, 0], vx: [0, 1], turn: 0 },
53
+ { ox: x + w, oy: y + h, ux: [0, 1], vx: [-1, 0], turn: Math.PI / 2 },
54
+ { ox: x, oy: y + h, ux: [-1, 0], vx: [0, -1], turn: Math.PI },
55
+ { ox: x, oy: y, ux: [0, -1], vx: [1, 0], turn: (3 * Math.PI) / 2 },
56
+ ];
57
+ const { p, a, b, c: cc, d, alpha } = k;
58
+ corners.forEach((q, i) => {
59
+ const at = (u: number, v: number): [number, number] => [q.ox + q.ux[0] * u + q.vx[0] * v, q.oy + q.ux[1] * u + q.vx[1] * v];
60
+ const [sx, sy] = at(-p, 0);
61
+ if (i === 0) c.moveTo(sx, sy); else c.lineTo(sx, sy);
62
+ // into the arc, along the side
63
+ const [c1x, c1y] = at(-p + a, 0), [c2x, c2y] = at(-p + a + b, 0), [e1x, e1y] = at(-p + a + b + cc, d);
64
+ c.bezierCurveTo(c1x, c1y, c2x, c2y, e1x, e1y);
65
+ // the arc: radius r about (−r, r), its middle (90° less twice alpha) — angles turned with the corner
66
+ const [cx, cy] = at(-r, r);
67
+ c.arc(cx, cy, r, -Math.PI / 2 + alpha + q.turn, -alpha + q.turn, false);
68
+ // out of the arc onto the next side (the first Bézier mirrored across the corner's diagonal)
69
+ const [c3x, c3y] = at(0, p - a - b), [c4x, c4y] = at(0, p - a), [ex, ey] = at(0, p);
70
+ c.bezierCurveTo(c3x, c3y, c4x, c4y, ex, ey);
71
+ });
72
+ c.closePath();
73
+ }
@@ -1,9 +1,14 @@
1
1
  import { clamp } from '../model/geom';
2
+ import { faceEdit } from '../model/faces';
2
3
  import { page, SK } from '../model/store';
3
4
  import type { Note, Sketch, Underlay, View } from '../model/types';
5
+ import { CLOSED_SHAPES, isSegment, NOTE_TEXT_SIZE, noteTextIn } from '../model/markup';
6
+ import { LINK_FULL_PX, LINK_PIN_PX, LINK_TILE_PX, linkSide } from '../model/links';
4
7
  import { draw } from './draw';
5
8
  import { onSessionReset, sessionState } from '../model/session';
6
9
  import { refreshDrawerSoon } from '../editor/ports';
10
+ import { FONT } from './theme';
11
+ import { ZOOM_MAX, ZOOM_MIN } from './navigate';
7
12
 
8
13
  /** World (inches, y up) → screen transform. */
9
14
  export interface Xform { s: number; ox: number; oy: number }
@@ -21,8 +26,13 @@ export function initCanvas(el: HTMLCanvasElement): void {
21
26
  cvs.ctx = el.getContext('2d')!;
22
27
  }
23
28
 
29
+ /** The pan/zoom of each wall face that's been open, by page and face. */
30
+ const faceViews = sessionState(() => ({} as Record<string, View>));
31
+
32
+ /** The view of what's being edited: the page's plan, or the wall face open. */
24
33
  export function V(): View {
25
34
  const pg = page();
35
+ if (faceEdit.key) return (faceViews[pg.id + '|' + faceEdit.key] ??= { cx: 0, cy: 48, s: 2 });
26
36
  if (!pg.view) pg.view = { cx: 84, cy: 120, s: 2 };
27
37
  return pg.view;
28
38
  }
@@ -42,17 +52,17 @@ export function s2w(sx: number, sy: number): [number, number] {
42
52
  export function zoomAt(sx: number, sy: number, f: number): void {
43
53
  const v = V();
44
54
  const [wx, wy] = s2w(sx, sy);
45
- v.s = clamp(v.s * f, 0.04, 400);
55
+ v.s = clamp(v.s * f, ZOOM_MIN, ZOOM_MAX);
46
56
  v.cx = wx - (sx - cvs.W / 2) / v.s;
47
57
  v.cy = wy + (sy - cvs.H / 2) / v.s;
48
- requestDraw();
58
+ requestViewDraw();
49
59
  }
50
60
 
51
61
  export function panBy(dx: number, dy: number): void {
52
62
  const v = V();
53
63
  v.cx -= dx / v.s;
54
64
  v.cy += dy / v.s;
55
- requestDraw();
65
+ requestViewDraw();
56
66
  }
57
67
 
58
68
  export function sketchBounds(sk: Sketch, inclOrigin = true): { x0: number; y0: number; x1: number; y1: number } | null {
@@ -60,24 +70,42 @@ export function sketchBounds(sk: Sketch, inclOrigin = true): { x0: number; y0: n
60
70
  const add = (x: number, y: number) => { x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y); };
61
71
  for (const id in sk.points) { const p = sk.points[id]; if (p.origin && !inclOrigin) continue; add(p.x, p.y); }
62
72
  for (const id in sk.circles) { const c = sk.circles[id], o = sk.points[c.c]; if (o) { add(o.x - c.r, o.y - c.r); add(o.x + c.r, o.y + c.r); } }
63
- for (const n of sk.notes) { if (n.pts) n.pts.forEach(p => add(p[0], p[1])); else add(n.x!, n.y!); }
73
+ for (const n of sk.notes) {
74
+ if (n.pts) n.pts.forEach(p => add(p[0], p[1]));
75
+ else if (n.type === 'link') {
76
+ // its tile, and what its leader points at
77
+ const h = linkSide(n) / 2, x = n.x ?? 0, y = n.y ?? 0;
78
+ add(x - h, y - h); add(x + h, y + h);
79
+ if (n.leader) add(n.leader[0], n.leader[1]);
80
+ } else add(n.x!, n.y!);
81
+ }
64
82
  if (x0 === Infinity) return null;
65
83
  return { x0, y0, x1, y1 };
66
84
  }
67
85
 
86
+ /** The open wall's face, for the wall view to fit (registered by editor/elevation.ts; null on the plan). */
87
+ let wallBox: () => { x0: number; y0: number; x1: number; y1: number } | null = () => null;
88
+ export function setWallBox(fn: typeof wallBox): void { wallBox = fn; }
89
+
68
90
  export function fitView(): void {
69
- const sk = SK(), pg = page();
91
+ const sk = SK(), pg = page(), wallFace = wallBox();
70
92
  let b = sketchBounds(sk);
71
- if (pg.underlay) {
72
- const u = pg.underlay, h = underlayH(u);
73
- const bb = { x0: u.x, y0: u.y - h, x1: u.x + u.w, y1: u.y };
93
+ // the wall view fits the wall itself (and what's drawn on it); the page's photos and plans are under the plan, not
94
+ // on the wall — counting them zoomed a wall out to take in a plan nobody could see there
95
+ if (faceEdit.key && wallFace) b = b ? { x0: Math.min(b.x0, wallFace.x0), y0: Math.min(b.y0, wallFace.y0), x1: Math.max(b.x1, wallFace.x1), y1: Math.max(b.y1, wallFace.y1) } : wallFace;
96
+ if (!faceEdit.key) for (const u of pg.underlays) {
97
+ // (as it sits, turned)
98
+ const cs = underlayCorners(u), xs = cs.map(q => q.x), ys = cs.map(q => q.y);
99
+ const bb = { x0: Math.min(...xs), y0: Math.min(...ys), x1: Math.max(...xs), y1: Math.max(...ys) };
74
100
  b = b ? { x0: Math.min(b.x0, bb.x0), y0: Math.min(b.y0, bb.y0), x1: Math.max(b.x1, bb.x1), y1: Math.max(b.y1, bb.y1) } : bb;
75
101
  }
76
102
  const v = V();
77
103
  if (!b || (b.x1 - b.x0 < 1 && b.y1 - b.y0 < 1)) { v.cx = 0; v.cy = 0; v.s = 3; requestDraw(); return; }
78
104
  const { W, H } = cvs;
79
- const phone = W < 760;
80
- const L = phone ? 78 : 110, R = phone ? 28 : 110, Tp = phone ? 200 : 150, Bt = 120;
105
+ const phone = W < 760, wall = faceEdit.key != null;
106
+ // a wall face: room for the dimensions along the floor and beside it, and for the key plan (top right)
107
+ const L = phone ? 78 : 110, R = wall ? (phone ? 64 : Math.max(110, keyPlanSize(W) + 40)) : phone ? 28 : 110;
108
+ const Tp = phone ? 200 : wall ? 110 : 150, Bt = wall ? (phone ? 210 : 190) : 120;
81
109
  const bw = Math.max(b.x1 - b.x0, 24), bh = Math.max(b.y1 - b.y0, 24);
82
110
  v.s = clamp(Math.min((W - L - R) / bw, (H - Tp - Bt) / bh), 0.08, 60);
83
111
  const scx = (L + W - R) / 2, scy = (Tp + H - Bt) / 2;
@@ -86,6 +114,9 @@ export function fitView(): void {
86
114
  requestDraw();
87
115
  }
88
116
 
117
+ /** Size of the wall view's key plan (top right), pixels. */
118
+ export const keyPlanSize = (W: number): number => Math.min(150, Math.max(96, W * 0.16));
119
+
89
120
  /** Height of an image underlay in inches: its own, or from the picture's aspect ratio. */
90
121
  export function underlayH(u: Underlay): number {
91
122
  if (u.h) return u.h;
@@ -95,9 +126,40 @@ export function underlayH(u: Underlay): number {
95
126
 
96
127
  export type HandlePart = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';
97
128
 
98
- /** Screen positions of an underlay's resize handles: corners scale, side handles stretch. */
99
- export function underlayHandles(T: Xform, u: Underlay): { part: HandlePart; x: number; y: number }[] {
100
- return boxHandles(T, { left: u.x, right: u.x + u.w, top: u.y, bottom: u.y - underlayH(u) });
129
+ /*
130
+ An image turned (its `rot`) turns about its middle: x, y, w and h are its box before it's turned. These give its
131
+ points as they sit turned. The world's y runs up, so a turn clockwise as it's seen is the other way round in it.
132
+ */
133
+ const turnOf = (u: Underlay): number => (-(u.rot ?? 0) * Math.PI) / 180;
134
+ /** An image's middle, inches. */
135
+ export function underlayMid(u: Underlay): { x: number; y: number } {
136
+ return { x: u.x + u.w / 2, y: u.y - underlayH(u) / 2 };
137
+ }
138
+ /** The point `lx` across and `ly` up from an image's middle in its own (unturned) frame, as it sits turned (inches). */
139
+ export function underlayAt(u: Underlay, lx: number, ly: number): { x: number; y: number } {
140
+ const m = underlayMid(u), a = turnOf(u), c = Math.cos(a), s = Math.sin(a);
141
+ return { x: m.x + lx * c - ly * s, y: m.y + lx * s + ly * c };
142
+ }
143
+ /** A point (inches) as across and up from an image's middle, in its own unturned frame. */
144
+ export function underlayLocal(u: Underlay, x: number, y: number): { x: number; y: number } {
145
+ const m = underlayMid(u), a = turnOf(u), c = Math.cos(a), s = Math.sin(a), dx = x - m.x, dy = y - m.y;
146
+ return { x: dx * c + dy * s, y: -dx * s + dy * c };
147
+ }
148
+ /** Where an image's handles sit in its own frame (as fractions of its half width and half height), in boxHandles' order. */
149
+ const HANDLES: [HandlePart, number, number][] = [['nw', -1, 1], ['n', 0, 1], ['ne', 1, 1], ['e', 1, 0], ['se', 1, -1], ['s', 0, -1], ['sw', -1, -1], ['w', -1, 0]];
150
+ /** An image's corners as it sits turned: top left, top right, bottom right, bottom left (inches). */
151
+ export function underlayCorners(u: Underlay): { x: number; y: number }[] {
152
+ const hw = u.w / 2, hh = underlayH(u) / 2;
153
+ return [[-1, 1], [1, 1], [1, -1], [-1, -1]].map(([i, j]) => underlayAt(u, i * hw, j * hh));
154
+ }
155
+
156
+ /**
157
+ * Screen positions of an underlay's resize handles: corners scale, side handles stretch. `ang`: how far the image is
158
+ * turned on screen (radians clockwise), for the side handles' bars to lie along its sides.
159
+ */
160
+ export function underlayHandles(T: Xform, u: Underlay): { part: HandlePart; x: number; y: number; ang: number }[] {
161
+ const hw = u.w / 2, hh = underlayH(u) / 2, ang = ((u.rot ?? 0) * Math.PI) / 180;
162
+ return HANDLES.map(([part, i, j]) => { const p = underlayAt(u, i * hw, j * hh), [x, y] = S(T, p.x, p.y); return { part, x, y, ang }; });
101
163
  }
102
164
 
103
165
  /** A world-space box (y up: top > bottom). */
@@ -120,13 +182,100 @@ export function noteBox(n: Note): Box | null {
120
182
  return { left: Math.min(...xs), right: Math.max(...xs), top: Math.max(...ys), bottom: Math.min(...ys) };
121
183
  }
122
184
 
123
- /** Handles for adjusting markup: arrow ends ('p0', 'p1'), or the box around a cloud or pen stroke. Text has none. */
185
+ /**
186
+ * A note's text is set at one size (NOTE_TEXT_SIZE px) and the whole note scaled to its size on the drawing: its
187
+ * letters are `ts` inches tall (NOTE_TEXT_IN unless set, from NOTE_TEXT_MIN_IN to NOTE_TEXT_MAX_IN), so it zooms with
188
+ * the drawing rather than staying one size on screen. (Set at one size, the text is measured — and on a device its
189
+ * font made — once, not at every step of a zoom.)
190
+ */
191
+ export { NOTE_TEXT_SIZE, NOTE_TEXT_IN, NOTE_TEXT_MIN_IN, NOTE_TEXT_MAX_IN, noteTextIn } from '../model/markup';
192
+ export const noteFont = (size: number): string => '600 ' + size + 'px ' + FONT;
193
+
194
+ /** What measures text: a drawing context, or anything that sets a font and measures with it. */
195
+ export interface TextMeasure { font: string; measureText(s: string): { width: number } }
196
+
197
+ /**
198
+ * What measures text outside drawing a frame (picking a note, its handles, a box select): the browser's canvas, or on
199
+ * a device — which has no browser canvas — the host's (Skia, with the drawing's own fonts: skia/host.ts), so a note
200
+ * is picked by the same box it's drawn in. Failing both, an estimate rather than a crash.
201
+ */
202
+ let hostMeasure: TextMeasure | null = null;
203
+ export function setTextMeasure(m: TextMeasure | null): void { hostMeasure = m; }
204
+ const estimate: TextMeasure = {
205
+ font: '',
206
+ measureText(s: string) { const size = Number(/([\d.]+)px/.exec(this.font)?.[1] ?? 14); return { width: s.length * size * 0.6 }; },
207
+ };
208
+ export const textMeasure = (): TextMeasure => cvs.ctx ?? hostMeasure ?? estimate;
209
+
210
+ /**
211
+ * A note's box on screen at its size: left / top corner, width, height; `mid` is the text's middle line, `k` its scale
212
+ * from the unscaled size (padding and corners grow with it). Measured with `c` (the drawing's context when drawing).
213
+ */
214
+ export function noteTextBox(T: Xform, n: Note, c: TextMeasure = textMeasure()) {
215
+ const size = noteTextIn(n) * T.s, k = size / NOTE_TEXT_SIZE, [x, mid] = S(T, n.x!, n.y!);
216
+ c.font = noteFont(NOTE_TEXT_SIZE);
217
+ const w = (c.measureText(n.text!).width + 16) * k;
218
+ return { x, y: mid - 14 * k, w, h: 28 * k, mid, k, size };
219
+ }
220
+
221
+ /** How much a link's tile shows on the screen: all of it, its picture or icon alone, or a pin. */
222
+ export type LinkLod = 'full' | 'compact' | 'pin';
223
+
224
+ /** A tile on the screen: its square (top left, side), its middle, how much it shows, and its scale from the layout. */
225
+ export interface LinkBox { x: number; y: number; s: number; cx: number; cy: number; lod: LinkLod; k: number }
226
+
227
+ export function linkBox(T: Xform, n: Note): LinkBox {
228
+ const [cx, cy] = S(T, n.x ?? 0, n.y ?? 0), side = linkSide(n) * T.s;
229
+ const lod: LinkLod = side >= LINK_FULL_PX ? 'full' : side >= LINK_PIN_PX ? 'compact' : 'pin';
230
+ const s = Math.max(LINK_PIN_PX, side);
231
+ return { x: cx - s / 2, y: cy - s / 2, s, cx, cy, lod, k: s / LINK_TILE_PX };
232
+ }
233
+
234
+ /** Where a leader leaves the tile toward screen point tx, ty: the edge of its square (null: the point's under it). */
235
+ export function leaderStart(b: LinkBox, tx: number, ty: number): [number, number] | null {
236
+ const dx = tx - b.cx, dy = ty - b.cy, h = b.s / 2, m = Math.max(Math.abs(dx), Math.abs(dy));
237
+ if (m <= h) return null;
238
+ return [b.cx + (dx * h) / m, b.cy + (dy * h) / m];
239
+ }
240
+
241
+ /** A tile's leader on the screen, from the tile's edge to its tip (null: none, or its tip is under the tile). */
242
+ export function leaderPath(T: Xform, n: Note, b: LinkBox = linkBox(T, n)): [[number, number], [number, number]] | null {
243
+ if (!n.leader) return null;
244
+ const tip = S(T, n.leader[0], n.leader[1]), start = leaderStart(b, tip[0], tip[1]);
245
+ return start ? [start, tip] : null;
246
+ }
247
+
248
+ /** A tile's handle: its leader's tip (tiles are sized together, by the Tile size slider, not one by one). */
249
+ export function linkHandles(T: Xform, n: Note): { part: string; x: number; y: number }[] {
250
+ if (!n.leader) return [];
251
+ const [x, y] = S(T, n.leader[0], n.leader[1]);
252
+ return [{ part: 'tip', x, y }];
253
+ }
254
+
255
+ /** A tile's box and leader tip on the screen, for fitting and box selection: left, top, right, bottom. */
256
+ export function linkExtent(T: Xform, n: Note): [number, number, number, number] {
257
+ const b = linkBox(T, n);
258
+ let l = b.x, t = b.y, r = b.x + b.s, bt = b.y + b.s;
259
+ if (n.leader) { const [x, y] = S(T, n.leader[0], n.leader[1]); l = Math.min(l, x); r = Math.max(r, x); t = Math.min(t, y); bt = Math.max(bt, y); }
260
+ return [l, t, r, bt];
261
+ }
262
+
263
+ /** Handles for adjusting markup: a line's or arrow's ends ('p0', 'p1'), the box around a cloud, shape or pen stroke, a note's corners. */
124
264
  export function noteHandles(T: Xform, n: Note): { part: string; x: number; y: number }[] {
125
- if (n.type === 'arrow' && n.pts && n.pts.length >= 2) {
265
+ if (n.type === 'link') return linkHandles(T, n);
266
+ if (isSegment(n) && n.pts && n.pts.length >= 2) {
126
267
  const last = n.pts.length - 1;
127
268
  return [{ part: 'p0', p: n.pts[0] }, { part: 'p' + last, p: n.pts[last] }].map(({ part, p }) => { const [x, y] = S(T, p[0], p[1]); return { part, x, y }; });
128
269
  }
129
- if (n.type === 'cloud' || n.type === 'pen') { const b = noteBox(n); return b ? boxHandles(T, b) : []; }
270
+ if (n.type === 'cloud' || n.type === 'pen' || CLOSED_SHAPES.has(n.type)) { const b = noteBox(n); return b ? boxHandles(T, b) : []; }
271
+ if (n.type === 'text' && n.text) {
272
+ // a note scales as a whole (its text can't stretch): corners only
273
+ const b = noteTextBox(T, n);
274
+ return [
275
+ { part: 'nw', x: b.x, y: b.y }, { part: 'ne', x: b.x + b.w, y: b.y },
276
+ { part: 'se', x: b.x + b.w, y: b.y + b.h }, { part: 'sw', x: b.x, y: b.y + b.h },
277
+ ];
278
+ }
130
279
  return [];
131
280
  }
132
281
 
@@ -142,7 +291,7 @@ const browserImage: ImageLoader = (src, onLoad) => {
142
291
  };
143
292
  let loadImage: ImageLoader = browserImage;
144
293
  /** Load pictures some other way from now on (null: the browser's). */
145
- export function setImageLoader(fn: ImageLoader | null): void { loadImage = fn || browserImage; imgs.clear(); }
294
+ export function setImageLoader(fn: ImageLoader | null): void { loadImage = fn || browserImage; imgs.clear(); thumbs.clear(); }
146
295
 
147
296
  /** Turns a stored reference (`stored:…`, kept by a drawing in place of a URL) into a URL to load. */
148
297
  let resolveStored: ((ref: string) => Promise<string>) | null = null;
@@ -155,6 +304,31 @@ export function aliasImage(from: string, to: string): void {
155
304
  const im = imgs.get(from);
156
305
  if (im && !imgs.has(to)) imgs.set(to, im);
157
306
  }
307
+ /**
308
+ * Small pictures of linked files (a link tile's), by address: kept apart from the page's photos and plans, and only the
309
+ * last few dozen used — a drawing can link many — each drawn again as it arrives. What's let go goes between frames,
310
+ * and never what the last frame drew (so many on the screen at once don't load each other out, again and again).
311
+ */
312
+ const thumbs = new Map<string, { im: ImageLike; used: number }>(), THUMBS_KEPT = 64;
313
+ let thumbFrame = 0;
314
+ /** A new frame is being drawn: let go of the least recently used past those kept, if the last frame didn't draw them. */
315
+ export function nextThumbFrame(): void {
316
+ for (const [k, o] of thumbs) {
317
+ if (thumbs.size <= THUMBS_KEPT || o.used === thumbFrame) break;
318
+ thumbs.delete(k);
319
+ }
320
+ thumbFrame++;
321
+ }
322
+ export function linkThumb(src: string): ImageLike {
323
+ let t = thumbs.get(src);
324
+ // (the most recently used last: the first is the one to let go)
325
+ if (t) thumbs.delete(src);
326
+ else t = { im: loadImage(src, () => requestDraw()), used: 0 };
327
+ t.used = thumbFrame;
328
+ thumbs.set(src, t);
329
+ return t.im;
330
+ }
331
+
158
332
  export function imgCache(src: string | undefined): ImageLike | null {
159
333
  if (!src) return null;
160
334
  let im = imgs.get(src);
@@ -178,15 +352,77 @@ let drawFrame = 0;
178
352
  let paint: () => void = () => draw();
179
353
  export function setFrameRenderer(fn: (() => void) | null): void { paint = fn || (() => draw()); }
180
354
 
181
- /** Repaint on the next animation frame (several requests in one frame paint once). */
182
- export function requestDraw(): void {
355
+ /*
356
+ What the next frame has to redraw, for a host that keeps the drawing as a picture (see PlanCanvas.native): the
357
+ drawing itself only when something in it changed (requestDraw), or when the view moved (the host compares the view
358
+ it last drew at); otherwise just the moving parts, as a tool follows the pointer (requestOverlayDraw). The page's
359
+ photos and plans are kept apart, under it: turning, fading, moving or sizing one redraws only them
360
+ (requestImageDraw), not every wall and dimension over them. A host that redraws everything each frame (the
361
+ browser) ignores this.
362
+ */
363
+ const frame = { drawingChanged: true, imagesChanged: true };
364
+
365
+ /** Whether the drawing (not only its moving parts) has changed since the host last kept it; the host then marks it kept. */
366
+ export const drawingChanged = (): boolean => frame.drawingChanged;
367
+ export function markDrawingKept(): void { frame.drawingChanged = false; }
368
+ /** Whether the photos and plans under the drawing have changed since the host last kept them; it then marks them kept. */
369
+ export const imagesChanged = (): boolean => frame.imagesChanged;
370
+ export function markImagesKept(): void { frame.imagesChanged = false; }
371
+
372
+ function schedule(): void {
183
373
  if (drawFrame) return;
184
374
  drawFrame = requestAnimationFrame(() => { drawFrame = 0; paint(); });
185
375
  }
376
+
377
+ /**
378
+ * Paint a frame that's waiting, now. A press is tested against the tap targets as last painted (the chips), so one that
379
+ * comes between an edit and the next frame — a quick tap, a slow frame — would land on where a chip was, not where it is.
380
+ */
381
+ export function paintPending(): void {
382
+ if (!drawFrame) return;
383
+ cancelAnimationFrame(drawFrame);
384
+ drawFrame = 0;
385
+ paint();
386
+ }
387
+
388
+ /**
389
+ * The view as the host moved it itself — a device pans and zooms on its UI thread, at the screen's rate, and tells
390
+ * this thread where it got to: take it, and draw the plan there on the next frame.
391
+ */
392
+ export function setViewFromHost(s: number, cx: number, cy: number): void {
393
+ const v = V();
394
+ v.s = s; v.cx = cx; v.cy = cy;
395
+ schedule();
396
+ }
397
+
398
+ /** Repaint on the next animation frame (several requests in one frame paint once). */
399
+ export function requestDraw(): void {
400
+ frame.drawingChanged = true;
401
+ frame.imagesChanged = true;
402
+ schedule();
403
+ }
404
+
405
+ /** Only the page's photos and plans changed (one turned, faded, moved or sized): the drawing over them can be reused. */
406
+ export function requestImageDraw(): void {
407
+ frame.imagesChanged = true;
408
+ schedule();
409
+ }
410
+
411
+ /** Only the moving parts changed (a tool's preview, the snap marker, the selection box): the rest can be reused. */
412
+ export function requestOverlayDraw(): void { schedule(); }
413
+
414
+ /**
415
+ * The view panned or zoomed: draw the next frame there. The drawing itself didn't change — a host that keeps it sees
416
+ * the view moved, and draws it again at the new one, with nothing waiting for the view to settle first.
417
+ */
418
+ function requestViewDraw(): void { schedule(); }
186
419
  onSessionReset(() => {
187
420
  if (drawFrame) cancelAnimationFrame(drawFrame);
188
421
  drawFrame = 0;
422
+ frame.drawingChanged = true;
423
+ frame.imagesChanged = true;
189
424
  imgs.clear();
425
+ thumbs.clear();
190
426
  });
191
427
 
192
428
  /** The plan's size in display points, where no browser canvas measures it (a device). */