@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
@@ -0,0 +1,239 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
2
+ import {
3
+ AccessibilityInfo, Animated, Easing, Pressable, StyleSheet, View,
4
+ type GestureResponderEvent, type LayoutRectangle, type PressableProps, type StyleProp, type TextStyle, type ViewStyle,
5
+ } from 'react-native';
6
+ import { DURATION, EASE, NATIVE } from './easing';
7
+ import { TweenView } from './Tween';
8
+
9
+ /*
10
+ How the chrome moves: one set of rules, everywhere.
11
+ - A change of state (a toggle, a choice in a segmented control, the selected tool) moves over 150 ms: off at once,
12
+ settling softly at the end — quick to answer, nothing springy.
13
+ - A press sinks a touch (to 96 %, a little dimmer) in 70 ms and comes back over 150 ms with the same easing; wide
14
+ rows (menu items) only dim.
15
+ On a device all of it runs on the native driver — opacity, transforms and colours only (what moves a view's size or
16
+ place goes through TweenView) — so a press or a change of state plays smoothly however busy the JavaScript thread is,
17
+ as it is just after a tap (the editor answers, the plan is drawn again). Interpolations are made once, not on every
18
+ render: each one made anew has to be sent to the UI thread again.
19
+ */
20
+
21
+ export { DURATION, EASE };
22
+ const PRESS_IN = 70;
23
+
24
+ const ease = (v: Animated.Value, to: number, duration = DURATION) =>
25
+ Animated.timing(v, { toValue: to, duration, easing: EASE, useNativeDriver: NATIVE }).start();
26
+
27
+ /*
28
+ Popups — the ••• and Layers menus, the shapes menu, the markup panel, an image's slider, a toast — come and go the
29
+ same way: in about 140 ms they fade in and grow from just under their size, easing to a stop, moving out a few
30
+ points from the button that opened them; going, in about 100 ms they fade and shrink back, gathering pace, and stay
31
+ on screen until they've gone. Opacity and a transform only, on the native driver: the UI thread plays them, so a
32
+ busy JavaScript thread (a redraw, a save) can't make them stutter. With Reduce Motion on, they only fade.
33
+ */
34
+ export const POP_IN = 140, POP_OUT = 100, POP_EASE_OUT = Easing.bezier(0.4, 0, 1, 1);
35
+
36
+ let reduceMotion = false;
37
+ try {
38
+ AccessibilityInfo.isReduceMotionEnabled?.().then(v => { reduceMotion = !!v; }, () => {});
39
+ AccessibilityInfo.addEventListener?.('reduceMotionChanged', (v: boolean) => { reduceMotion = !!v; });
40
+ } catch { /* (no accessibility settings to read: full motion) */ }
41
+ /** Reduce Motion is on: things that move only fade. */
42
+ export const reducedMotion = (): boolean => reduceMotion;
43
+
44
+ /**
45
+ * A popup's coming and going: whether it's on screen (`present` — open, or still going), and the style that moves it.
46
+ * `from`: where it comes from, points (toward the button that opened it: a menu under a button at the top comes down
47
+ * from it, `{ y: -6 }`); `origin`: the point it grows from, as CSS transform-origin ('top right', 'left center'…).
48
+ *
49
+ * `keep`: once it's been open, it stays built between times — hidden (`display: none`: not laid out, not tapped, not
50
+ * read out) rather than taken down — so a menu with a lot in it opens at once, not after it's been built again (on a
51
+ * device, the wait before it moved). `mounted` says whether to draw it at all; the style hides it when it's away.
52
+ */
53
+ export function usePopup(open: boolean, from: { x?: number; y?: number }, origin: string, keep = false): {
54
+ present: boolean; mounted: boolean; style: Animated.WithAnimatedObject<ViewStyle>;
55
+ } {
56
+ const v = useRef(new Animated.Value(open ? 1 : 0)).current, [present, setPresent] = useState(open), been = useRef(open);
57
+ if (open) been.current = true;
58
+ useEffect(() => {
59
+ if (open) setPresent(true);
60
+ // (on the next frame: a popup just put on screen is mounted twice by React's strict mode, and its animation would
61
+ // stop when the first goes)
62
+ const frame = requestAnimationFrame(() => {
63
+ Animated.timing(v, open
64
+ ? { toValue: 1, duration: POP_IN, easing: EASE, useNativeDriver: NATIVE }
65
+ : { toValue: 0, duration: POP_OUT, easing: POP_EASE_OUT, useNativeDriver: NATIVE })
66
+ .start(({ finished }) => { if (finished && !open) setPresent(false); });
67
+ });
68
+ return () => cancelAnimationFrame(frame);
69
+ }, [open, v]);
70
+ const fx = from.x ?? 0, fy = from.y ?? 0;
71
+ const style = useMemo(() => (reduceMotion
72
+ ? { opacity: v }
73
+ : {
74
+ opacity: v, transformOrigin: origin,
75
+ transform: [
76
+ { translateX: v.interpolate({ inputRange: [0, 1], outputRange: [fx, 0] }) },
77
+ { translateY: v.interpolate({ inputRange: [0, 1], outputRange: [fy, 0] }) },
78
+ { scale: v.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) },
79
+ ],
80
+ }) as Animated.WithAnimatedObject<ViewStyle>, [v, fx, fy, origin]);
81
+ const shown = open || present;
82
+ const hidden = useMemo(() => [style, { display: 'none' as const }] as unknown as Animated.WithAnimatedObject<ViewStyle>, [style]);
83
+ return { present: shown, mounted: shown || (keep && been.current), style: shown ? style : hidden };
84
+ }
85
+
86
+ /** An animated value that eases to 1 when `on`, to 0 when not (starting where it is). */
87
+ export function useEased(on: boolean): Animated.Value {
88
+ const v = useRef(new Animated.Value(on ? 1 : 0)).current;
89
+ useEffect(() => { ease(v, on ? 1 : 0); }, [on, v]);
90
+ return v;
91
+ }
92
+
93
+ /**
94
+ * A press as an animated value: to 1 as it goes down, back to 0 as it lets go. Spread the handlers on a Pressable.
95
+ * `full`: a quick tap still shows the whole press — let go before it's all the way in, it finishes going in, then
96
+ * comes back (within `outMs` all told).
97
+ */
98
+ export function usePress(inMs = PRESS_IN, outMs = DURATION, easing = EASE, full = false): { v: Animated.Value; onPressIn: () => void; onPressOut: () => void } {
99
+ const v = useRef(new Animated.Value(0)).current;
100
+ const timing = (x: number, ms: number) => Animated.timing(v, { toValue: x, duration: ms, easing, useNativeDriver: NATIVE });
101
+ return {
102
+ v,
103
+ onPressIn: () => timing(1, inMs).start(),
104
+ onPressOut: () => v.stopAnimation(now => {
105
+ if (!full || now >= 0.99) { timing(0, outMs).start(); return; }
106
+ const rest = Math.round((1 - now) * inMs * 0.6);
107
+ Animated.sequence([timing(1, rest), timing(0, Math.max(60, outMs - rest))]).start();
108
+ }),
109
+ };
110
+ }
111
+
112
+ /** The toggle's own pace: a smooth ease in and out, slower than a button press, so its knob's travel reads as one glide. */
113
+ export const TOGGLE_EASE = Easing.bezier(0.4, 0, 0.2, 1);
114
+ export const TOGGLE_MS = 200, TOGGLE_PRESS_MS = 90;
115
+
116
+ /**
117
+ * A toggle's glide, started by the tap itself rather than once the setting it flips has been applied: on a device that
118
+ * meant waiting for the menu to render again and, for a layer or a snap, the drawing to be redrawn — the knob sat
119
+ * stretched for a moment before it moved. `go` starts the glide now; a change of `on` from elsewhere (the setting
120
+ * changed another way, or put back) glides it too.
121
+ */
122
+ export function useToggleEased(on: boolean): { v: Animated.Value; go: (to: boolean, change: () => void) => void } {
123
+ const v = useRef(new Animated.Value(on ? 1 : 0)).current, at = useRef(on), pending = useRef(false);
124
+ const glide = useCallback((to: boolean) => {
125
+ at.current = to;
126
+ Animated.timing(v, { toValue: to ? 1 : 0, duration: TOGGLE_MS, easing: TOGGLE_EASE, useNativeDriver: NATIVE }).start();
127
+ }, [v]);
128
+ /** Glide to `to` now, and make the change a frame later (once the glide is with the native driver). */
129
+ const go = useCallback((to: boolean, change: () => void) => {
130
+ glide(to);
131
+ pending.current = true;
132
+ requestAnimationFrame(() => { pending.current = false; change(); });
133
+ }, [glide]);
134
+ // after every render: where the setting really is wins (changed another way, or refused) — not while a tap's own
135
+ // change is still on its way
136
+ useEffect(() => { if (!pending.current && on !== at.current) glide(on); });
137
+ return { v, go };
138
+ }
139
+
140
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
141
+
142
+ type TapState = { pressed: boolean; hovered: boolean };
143
+ export type TapProps = Omit<PressableProps, 'style' | 'children'> & {
144
+ style?: StyleProp<ViewStyle> | ((s: TapState) => StyleProp<ViewStyle>);
145
+ children?: ReactNode;
146
+ /** scale: sinks and dims (buttons, keys); dim: only dims (wide rows); none: no press feedback */
147
+ feel?: 'scale' | 'dim' | 'none';
148
+ };
149
+
150
+ /** A Pressable with the chrome's press feel. `style` may be a function of { pressed, hovered }, as a Pressable's. */
151
+ export function Tap({ style, feel = 'scale', onPressIn, onPressOut, onHoverIn, onHoverOut, ...rest }: TapProps) {
152
+ const v = useRef(new Animated.Value(0)).current;
153
+ const [st, setSt] = useState<TapState>({ pressed: false, hovered: false });
154
+ const resolved = typeof style === 'function' ? style(st) : style;
155
+ const base = (StyleSheet.flatten(resolved)?.opacity as number | undefined) ?? 1;
156
+ const anim = useMemo(() => (feel === 'none' ? null : {
157
+ opacity: v.interpolate({ inputRange: [0, 1], outputRange: [base, base * (feel === 'dim' ? 0.7 : 0.82)] }),
158
+ ...(feel === 'scale' ? { transform: [{ scale: v.interpolate({ inputRange: [0, 1], outputRange: [1, 0.96] }) }] } : {}),
159
+ }), [v, base, feel]);
160
+ return (
161
+ <AnimatedPressable
162
+ {...rest}
163
+ onPressIn={(e: GestureResponderEvent) => { setSt(s => ({ ...s, pressed: true })); ease(v, 1, PRESS_IN); onPressIn?.(e); }}
164
+ onPressOut={(e: GestureResponderEvent) => { setSt(s => ({ ...s, pressed: false })); ease(v, 0); onPressOut?.(e); }}
165
+ onHoverIn={(e: Parameters<NonNullable<PressableProps['onHoverIn']>>[0]) => { setSt(s => ({ ...s, hovered: true })); onHoverIn?.(e); }}
166
+ onHoverOut={(e: Parameters<NonNullable<PressableProps['onHoverOut']>>[0]) => { setSt(s => ({ ...s, hovered: false })); onHoverOut?.(e); }}
167
+ style={[resolved, anim]}
168
+ />
169
+ );
170
+ }
171
+
172
+ /**
173
+ * One option of a segmented control: its label fades between the two colours as it's chosen. On a device that's two
174
+ * labels, one over the other, crossfaded on the native driver (it animates a fade smoothly, not a text colour); in a
175
+ * browser, one label whose colour is eased.
176
+ */
177
+ function SegOption({ label, on, onPress, onLayout, style, textStyle, onText, offText, role }: {
178
+ label: string; on: boolean; onPress: () => void; onLayout: (r: LayoutRectangle) => void;
179
+ style?: StyleProp<ViewStyle>; textStyle?: StyleProp<TextStyle>; onText: string; offText: string; role: 'button' | 'tab';
180
+ }) {
181
+ const t = useEased(on);
182
+ const ink = useMemo(() => (NATIVE ? t.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }) : t.interpolate({ inputRange: [0, 1], outputRange: [offText, onText] })), [t, offText, onText]);
183
+ return (
184
+ <Tap feel="dim" accessibilityRole={role} accessibilityLabel={label} accessibilityState={{ selected: on }} onPress={onPress}
185
+ onLayout={e => onLayout(e.nativeEvent.layout)} style={[segCenter, style, { backgroundColor: 'transparent' }]}>
186
+ {NATIVE ? (
187
+ <>
188
+ <Animated.Text style={[textStyle, { color: offText, opacity: ink }]}>{label}</Animated.Text>
189
+ {/* (the chosen colour's label, exactly over the other) */}
190
+ <View pointerEvents="none" style={segOver}>
191
+ <Animated.Text accessible={false} style={[textStyle, { color: onText, opacity: t }]}>{label}</Animated.Text>
192
+ </View>
193
+ </>
194
+ ) : <Animated.Text style={[textStyle, { color: ink }]}>{label}</Animated.Text>}
195
+ </Tap>
196
+ );
197
+ }
198
+ // (no grow: a device's layout grows a row with a growing child to all the room it's given — the control then ran the
199
+ // width of its menu row, squeezing the row's own label out)
200
+ const segRow: ViewStyle = { flexDirection: 'row', alignSelf: 'stretch' };
201
+ /** An option's label sits in its middle, both ways, whatever its padding. */
202
+ const segCenter: ViewStyle = { alignItems: 'center', justifyContent: 'center' };
203
+ // (spelt out: React Native 0.86 dropped StyleSheet.absoluteFillObject, and spreading it gave nothing — the label then sat
204
+ // under the other one, each option centring the pair)
205
+ const segOver: ViewStyle = { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, alignItems: 'center', justifyContent: 'center' };
206
+
207
+ /**
208
+ * A segmented control (Units, Precision, Draft / Annotate, Pages / Spaces): the chosen option's highlight slides to
209
+ * the one picked as the labels fade. `style` is the track, `optionStyle` each option, `onColor` the highlight.
210
+ */
211
+ export function Segmented<T extends string | number>({ value, options, onPick, style, optionStyle, textStyle, onColor = '#fff', onText, offText, role = 'button' }: {
212
+ value: T; options: [T, string][]; onPick: (v: T) => void;
213
+ style?: StyleProp<ViewStyle>; optionStyle?: StyleProp<ViewStyle>; textStyle?: StyleProp<TextStyle>;
214
+ onColor?: string; onText: string; offText: string; role?: 'button' | 'tab';
215
+ }) {
216
+ const [boxes, setBoxes] = useState<Record<string, LayoutRectangle>>({});
217
+ const box = boxes[String(value)];
218
+ const radius = (StyleSheet.flatten(optionStyle)?.borderRadius as number | undefined) ?? 999;
219
+ return (
220
+ <View style={style} accessibilityRole={role === 'tab' ? 'tablist' : undefined}>
221
+ {/* the options and their highlight share an unpadded row, the highlight the row's whole height: only where an
222
+ option starts and how wide it is are measured, so a later layout (text settling on a device) can't leave
223
+ the highlight off its label */}
224
+ <View style={segRow}>
225
+ {/* the first time it's measured it's simply there; after that it slides (and widens or narrows) to the choice */}
226
+ {box && (
227
+ <TweenView pointerEvents="none" to={{ left: box.x, width: box.width }}
228
+ style={{ position: 'absolute', top: 0, bottom: 0, borderRadius: radius, backgroundColor: onColor }} />
229
+ )}
230
+ {options.map(([v, label]) => (
231
+ <SegOption key={String(v)} label={label} on={v === value} onPress={() => onPick(v)} role={role}
232
+ onLayout={r => setBoxes(b => (b[String(v)] && b[String(v)].x === r.x && b[String(v)].width === r.width ? b : { ...b, [String(v)]: r }))}
233
+ style={optionStyle} textStyle={textStyle} onText={onText} offText={offText} />
234
+ ))}
235
+ </View>
236
+ </View>
237
+ );
238
+ }
239
+
@@ -1,11 +1,17 @@
1
1
  import { conflictEnd, conflictOpen, conflictStart } from '../editor/conflict';
2
2
  import { elevSyncFrame } from '../editor/elevation';
3
- import { keypadEnd, keypadOpen, keypadStart, keypadTyped } from '../editor/keypad';
3
+ import { keypadEnd, keypadOnHost, keypadOpen, keypadSpec, keypadStart, keypadSubmit, keypadTyped } from '../editor/keypad';
4
+ import type { KeypadSpec } from '../editor/ports';
5
+ import { hostKeypad, type HostKeypadRequest } from './hostKeypad';
6
+ import { offersJobMeasurements } from '../editor/links';
7
+ import type { LinkTarget } from '../model/types';
4
8
  import type { EditorPorts } from '../editor/ports';
5
9
  import { onSessionReset } from '../model/session';
6
10
  import { saveNow } from '../model/store';
7
- import { requestDraw } from '../render/view';
11
+ import { manualSave, noteEdits, saveDrawing } from './saving';
12
+ import { requestDraw, requestOverlayDraw } from '../render/view';
8
13
  import { notify, ui } from './store';
14
+ import { setInteracting } from './Duck';
9
15
 
10
16
  /* The editor's ports for the React Native interface: each request updates the interface's state and re-renders. */
11
17
 
@@ -34,13 +40,58 @@ function downloadInBrowser(name: string, mime: string, data: string): void {
34
40
  setTimeout(() => URL.revokeObjectURL(a.href), 2000);
35
41
  }
36
42
 
43
+ /** What the host's keypad is asked for (see hostKeypad.ts): its answer goes to this keypad, if it's still the one open. */
44
+ function hostRequest(spec: KeypadSpec): HostKeypadRequest {
45
+ const mine = () => keypadSpec() === spec;
46
+ return {
47
+ title: spec.title,
48
+ sub: spec.sub,
49
+ kind: spec.numeric ? 'angle' : 'length',
50
+ current: spec.initial || undefined,
51
+ extras: () => (spec.extras?.() ?? []).map(x => ({ label: x.label, ariaLabel: x.ariaLabel, on: x.on, danger: x.danger, press: () => { if (mine()) { x.onPress(); notify(); } } })),
52
+ apply: v => { if (mine()) keypadSubmit(v); },
53
+ cancel: () => { if (mine()) reactPorts.closeKeypad(); },
54
+ ...(spec.measurements && offersJobMeasurements() ? {
55
+ measurementKind: spec.measurements.kind,
56
+ pickMeasurement: (t: LinkTarget) => { if (mine()) { spec.measurements!.pick(t); notify(); } },
57
+ } : {}),
58
+ };
59
+ }
60
+
61
+ /** A slider above the bar (an image's opacity or rotation, the tiles' size): opened, swapped for another, or closed by its button again. */
62
+ function toggleImagePop(kind: 'opacity' | 'rotate' | 'tilesize'): void {
63
+ ui.imgPop = ui.imgPop === kind ? null : kind;
64
+ ui.menu = false;
65
+ // (a toast would sit over the slider: it's done with once the slider opens)
66
+ if (ui.imgPop) ui.toast = null;
67
+ notify();
68
+ }
69
+
70
+ /**
71
+ * One re-render for all an edit refreshes: an edit alone refreshes the bar, the status, the drawer and the wall view,
72
+ * and each refresh had every component on screen work out again what it shows — four to seven times for one tap.
73
+ * React draws once at the end of the task anyway; this has them look once too.
74
+ */
75
+ let refreshQueued = false;
76
+ function notifySoon(): void {
77
+ if (refreshQueued) return;
78
+ refreshQueued = true;
79
+ queueMicrotask(() => { refreshQueued = false; notify(); });
80
+ }
81
+ onSessionReset(() => { refreshQueued = false; });
82
+
37
83
  export const reactPorts: EditorPorts = {
38
84
  refresh(part) {
39
- if (part === 'keypad') requestDraw(); // canvas previews follow the typed value
85
+ // canvas previews follow the typed value: they're drawn with the moving parts, not the whole drawing again
86
+ if (part === 'keypad') requestOverlayDraw();
40
87
  if (part === 'elevation') elevSyncFrame(); // the open wall's frame from the live drawing (closes the view if the wall is gone)
41
- notify();
88
+ noteEdits();
89
+ notifySoon();
42
90
  },
91
+ // only what needs attention: something asked for that couldn't be done, and why. What did happen shows on the
92
+ // drawing itself — no status messages ("Deleted", "Undone", "Points joined"…)
43
93
  toast(msg, o = {}) {
94
+ if (!o.warn) return;
44
95
  ui.toast = { id: ++toastId, msg, warn: o.warn, action: o.action };
45
96
  clearTimeout(toastTimer);
46
97
  toastTimer = setTimeout(() => { ui.toast = null; notify(); }, o.warn ? 5200 : 3200);
@@ -48,6 +99,17 @@ export const reactPorts: EditorPorts = {
48
99
  },
49
100
  openKeypad(spec) {
50
101
  reactPorts.dismissConflict();
102
+ // a phone's host asks with its own keypad: the editor keeps the request (a laser reading, Delete, another tool can
103
+ // still close it) but draws nothing of its own
104
+ const host = hostKeypad();
105
+ if (host) {
106
+ if (keypadOpen()) reactPorts.closeKeypad();
107
+ keypadStart(spec, true);
108
+ host(hostRequest(spec));
109
+ notify();
110
+ requestDraw();
111
+ return;
112
+ }
51
113
  // the keypad has no text field: let typed keys reach the editor, not the button that opened it (Enter would press it again)
52
114
  if (typeof document !== 'undefined') (document.activeElement as HTMLElement | null)?.blur?.();
53
115
  keypadStart(spec);
@@ -57,7 +119,8 @@ export const reactPorts: EditorPorts = {
57
119
  requestDraw();
58
120
  },
59
121
  closeKeypad() {
60
- const spec = keypadEnd();
122
+ const onHost = keypadOnHost(), spec = keypadEnd();
123
+ if (onHost) hostKeypad()?.(null);
61
124
  ui.keypad = false;
62
125
  notify();
63
126
  requestDraw();
@@ -78,18 +141,24 @@ export const reactPorts: EditorPorts = {
78
141
  return true;
79
142
  },
80
143
  openDrawer(tab) { ui.drawer = tab ?? 'pages'; notify(); },
81
- showNoteInput(x, y, cb) { ui.note = { x, y, cb }; notify(); },
144
+ showNoteInput(x, y, cb, initial) { ui.note = { x, y, cb, initial }; notify(); },
82
145
  hideNoteInput() { if (ui.note) { ui.note = null; notify(); } },
83
- openPalette() { ui.palette = true; notify(); },
146
+ // (its button again closes it)
147
+ openPalette() { ui.palette = !ui.palette; ui.menu = false; ui.shapeMenu = null; notify(); },
148
+ toggleImageOpacity() { toggleImagePop('opacity'); },
149
+ toggleImageRotation() { toggleImagePop('rotate'); },
150
+ toggleTileSize() { toggleImagePop('tilesize'); },
84
151
  editRoomName(roomId) { ui.drawer = 'rooms'; ui.renameRoom = roomId; notify(); },
85
152
  setCursor(cursor, where = 'plan') { cursorTargets[where]?.(cursor); },
153
+ interacting: setInteracting,
86
154
  dismissPopovers() {
87
- const was = ui.palette || ui.menu;
88
- if (was) { ui.palette = false; ui.menu = false; notify(); }
155
+ const was = ui.palette || !!ui.menu || !!ui.imgPop || !!ui.shapeMenu;
156
+ if (was) { ui.palette = false; ui.menu = false; ui.imgPop = null; ui.shapeMenu = null; notify(); }
89
157
  return was;
90
158
  },
91
159
  saveFile(name, mime, data) { (fileSaver ?? downloadInBrowser)(name, mime, data); },
92
160
  save() {
161
+ if (manualSave()) { void saveDrawing(); return; }
93
162
  const ok = saveNow();
94
163
  reactPorts.toast(ok ? 'Saved on this device' : 'Saved, but the image was too large to keep offline.', { warn: !ok });
95
164
  },
@@ -0,0 +1,80 @@
1
+ import { ed } from '../app/state';
2
+ import type { CadSync } from '../data/sync';
3
+ import { sessionState } from '../model/session';
4
+ import { store } from '../model/store';
5
+ import { notify } from './store';
6
+ import { renderThumbnail } from './thumbnail';
7
+
8
+ /*
9
+ Manual saving (BOLDR's annotation behaviour): edits stay in the editor until Save writes them — the drawing, its
10
+ preview and its measurements — and leaving with unsaved edits is the host's to warn about (it gets the state through
11
+ CadEditor's onSaveState, and can save on the way out with saveCadDrawing).
12
+ */
13
+
14
+ export type CadSaveState = 'saved' | 'dirty' | 'saving' | 'error';
15
+
16
+ const sv = sessionState(() => ({
17
+ manual: false,
18
+ state: 'saved' as CadSaveState,
19
+ /** the edit count when the drawing last matched what's saved */
20
+ cleanSeq: 0,
21
+ sync: null as CadSync | null,
22
+ listener: null as ((s: CadSaveState) => void) | null,
23
+ }));
24
+
25
+ export const manualSave = (): boolean => sv.manual;
26
+ export const saveState = (): CadSaveState => sv.state;
27
+
28
+ function setState(s: CadSaveState): void {
29
+ if (sv.state === s) return;
30
+ sv.state = s;
31
+ sv.listener?.(s);
32
+ notify();
33
+ }
34
+
35
+ /** Start manual saving for the drawing open in `sync`; `listener` hears each change of state. */
36
+ export function startManualSave(sync: CadSync, listener: ((s: CadSaveState) => void) | null): void {
37
+ Object.assign(sv, { manual: true, state: 'saved', cleanSeq: ed.editSeq, sync, listener });
38
+ listener?.('saved');
39
+ }
40
+
41
+ export function stopManualSave(): void {
42
+ Object.assign(sv, { manual: false, sync: null, listener: null });
43
+ }
44
+
45
+ /** An edit was made: unsaved from now on (cheap, so it runs with every refresh of the editor). */
46
+ export function noteEdits(): void {
47
+ if (sv.manual && sv.state !== 'saving' && ed.editSeq !== sv.cleanSeq) setState('dirty');
48
+ }
49
+
50
+ /** Compare the drawing with what's saved (an undo back to the saved drawing is clean again). */
51
+ export function checkChanges(): void {
52
+ if (!sv.manual || !sv.sync || sv.state === 'saving') return;
53
+ if (sv.sync.hasChanges()) setState('dirty');
54
+ else { sv.cleanSeq = ed.editSeq; setState('saved'); }
55
+ }
56
+
57
+ /** Whether the drawing has edits that aren't saved (or a save that failed or is still going). */
58
+ export function hasUnsavedChanges(): boolean {
59
+ checkChanges();
60
+ return sv.manual && sv.state !== 'saved';
61
+ }
62
+
63
+ /** Save: write the drawing, its preview and its measurements. Resolves true once written, false if that failed. */
64
+ export async function saveDrawing(): Promise<boolean> {
65
+ const sync = sv.sync;
66
+ if (!sv.manual || !sync || !store.project) return true;
67
+ setState('saving');
68
+ const before = sync.failures();
69
+ sync.persistence.save();
70
+ const thumb = renderThumbnail(store.project.pages[0]);
71
+ if (thumb) sync.saveThumbnail(thumb);
72
+ await sync.idle();
73
+ await sync.idle(); // the measurement ids the save stamped on the pages
74
+ if (sync.failures() !== before) { setState('error'); return false; }
75
+ // edited while it saved: still unsaved
76
+ const dirty = sync.hasChanges();
77
+ if (!dirty) sv.cleanSeq = ed.editSeq;
78
+ setState(dirty ? 'dirty' : 'saved');
79
+ return true;
80
+ }
@@ -1,4 +1,4 @@
1
- import { useSyncExternalStore } from 'react';
1
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
2
2
  import type { ConflictSpec, DrawerTab, ToastOpts } from '../editor/ports';
3
3
  import { sessionState } from '../model/session';
4
4
 
@@ -22,8 +22,24 @@ const getVersion = () => version;
22
22
  /** Re-render this component whenever the editor changes. */
23
23
  export const useEditorVersion = (): number => useSyncExternalStore(subscribe, getVersion, getVersion);
24
24
 
25
+ /** The editor's change count (for a key that should change with every change: see useEditorKey). */
26
+ export const editorVersion = (): number => version;
27
+
28
+ /**
29
+ * Re-render this component only when what it shows changes, not on every change to the editor: `key` sums up all it
30
+ * reads, as one string, worked out again after each change — the same string, and it isn't rendered again. Keep it
31
+ * cheap and free of side effects; a component that's only briefly on screen can key on editorVersion() instead.
32
+ * (Re-rendering the whole interface on every change was most of the time an interaction took on a phone.)
33
+ */
34
+ export function useEditorKey(key: () => string): string {
35
+ const latest = useRef(key);
36
+ latest.current = key;
37
+ const get = useCallback(() => latest.current(), []);
38
+ return useSyncExternalStore(subscribe, get, get);
39
+ }
40
+
25
41
  export interface ToastState { id: number; msg: string; warn?: boolean; action?: ToastOpts['action'] }
26
- export interface NoteInputState { x: number; y: number; cb: (text: string) => void }
42
+ export interface NoteInputState { x: number; y: number; cb: (text: string) => void; initial?: string }
27
43
 
28
44
  export const ui = sessionState(() => ({
29
45
  toast: null as ToastState | null,
@@ -37,7 +53,23 @@ export const ui = sessionState(() => ({
37
53
  renameRoom: null as string | null,
38
54
  note: null as NoteInputState | null,
39
55
  palette: false,
40
- menu: false,
56
+ /** what the tool just picked from the rail does (a note under the tabs that goes by itself); seq tells one pick from the next */
57
+ toolTip: null as { text: string; seq: number } | null,
58
+ /** the slider open above the bar: the selected image's opacity, or how far it's turned; or the link tiles' size */
59
+ imgPop: null as null | 'opacity' | 'rotate' | 'tilesize',
60
+ /** the tool each shared place on the rail draws with (the last picked; editor/mode.ts TOOL_GROUPS), and the menu of
61
+ a group's tools, open beside its place at x, y */
62
+ picked: { shapes: 'line', marks: 'arrow' } as Record<string, string>,
63
+ shapeMenu: null as { x: number; y: number; group: string } | null,
64
+ /** the top bar's open menu: ••• (exports, files and settings) or Layers (the stack) */
65
+ menu: false as false | 'more' | 'layers',
41
66
  /** when a sheet opened (taps on the backdrop right after are the opening tap's ghost click) */
42
67
  sheetOpenedAt: 0,
68
+ /**
69
+ * How wide the top row's mode tabs and right-hand buttons laid out (0: not yet), and whether those buttons were
70
+ * measured in 3D, which has no Layers (see topRow.ts)
71
+ */
72
+ topRoom: { tabs: 0, right: 0, right3d: false },
73
+ /** Annotate's + floats in the corner (as a phone's) on a screen too short for it under the rail (see Rail) */
74
+ linkFab: false,
43
75
  }));
@@ -3,9 +3,17 @@ import { Platform } from 'react-native';
3
3
  /* The editor chrome's colors and type (dark in both themes; the canvas has its own light and dark colors). */
4
4
 
5
5
  export const C = {
6
+ /** card: the Pages panel's cards (Figma 2548:101989), a step between grey-1 and grey-2 */
7
+ card: '#3A3B3C',
6
8
  dark: '#181A1C', g1: '#2B2C2E', g2: '#424345', g3: '#626364', g4: '#9FA0A2', lg1: '#BCBEC2', lg2: '#E8EAED',
7
9
  yellow: '#FFAD00', blue: '#0066FF', lblue: '#50A6FF', lblue2: '#89C3FF', red: '#E83D2F', wred: '#DA1100',
8
10
  purple: '#BD30A8', green: '#00E920', white: '#FFFFFF', softRed: '#FFB4AE',
11
+ /** the keypad's keys: digits, unit and symbol keys, Del */
12
+ key: '#4B6074', key2: '#67819A', mutedRed: '#DF6358',
13
+ /** a toggle that's on */
14
+ on: '#58A942',
15
+ /** BOLDR's measurement tiles (Other/Active Blue): the job's lengths under a line's length keys */
16
+ tile: '#0364F4',
9
17
  } as const;
10
18
 
11
19
  /** The chrome's typeface: Montserrat in a browser (the host loads the web font); the system font on a device. */
@@ -0,0 +1,71 @@
1
+ import { is3d } from '../editor/view3dState';
2
+ import { notify, ui } from './store';
3
+
4
+ /*
5
+ The top row: the back button and the crumb on the left, the round buttons on the right, and the mode tabs centred
6
+ between them. Where they don't all fit:
7
+ - a tall screen (a phone; an iPad held upright) puts the tabs on a row of their own under it, as a phone has them;
8
+ - a short one (a phone on its side) has no height to spare for that: the crumb goes, and the tabs sit between the
9
+ back button and the buttons.
10
+ Worked out from their widths as they lay out, so it holds for whatever buttons the host adds. (Centred regardless,
11
+ an 11" iPad's tabs covered the crumb and, in Draft, the Fit button.)
12
+ */
13
+
14
+ /** From the screen's edges to the row, and between the tabs and what's beside them. */
15
+ const EDGE = 12, GAP = 10;
16
+ /** The back button's gap to the crumb. */
17
+ const BACK_GAP = 8;
18
+ /** A crumb narrower than this isn't worth showing; the crumb's widest. */
19
+ const CRUMB_MIN = 140, CRUMB_MAX = 420;
20
+ /** Shorter than this (points), a screen keeps the tabs in the top row. */
21
+ const SHORT = 560;
22
+ /** Until they've laid out: about how wide the tabs and the right-hand buttons are. */
23
+ const GUESS = { tabs: 232, right: 330 };
24
+
25
+ /** Where the row the tabs sit on starts (from the editor's top), and the line under them (the tool tips). */
26
+ export const TABS_TOP = { row: 12, below: 66 }, TIP_TOP = { row: 66, below: 120 };
27
+
28
+ export interface TopLayout {
29
+ /** the tabs on a row of their own under the top row */
30
+ below: boolean;
31
+ /** in the top row, but not centred on the screen: their left edge (null: centred) */
32
+ left: number | null;
33
+ /** how wide the crumb may be (0: it isn't shown) */
34
+ crumb: number;
35
+ }
36
+
37
+ /** A part of the row laid out `w` wide (the right-hand buttons: as they are in this mode). Nothing changes for less than a point. */
38
+ export function setTopRoom(part: 'tabs' | 'right', w: number): void {
39
+ const r = ui.topRoom, now3d = part === 'right' && is3d();
40
+ if (Math.abs(r[part] - w) < 1 && (part === 'tabs' || r.right3d === now3d)) return;
41
+ ui.topRoom = part === 'tabs' ? { ...r, tabs: w } : { ...r, right: w, right3d: now3d };
42
+ notify();
43
+ }
44
+
45
+ /**
46
+ * The tabs' width, and the right-hand buttons' as Draft has them: 3D has no Layers button, and it's counted anyway, so
47
+ * switching between them never moves the tabs.
48
+ */
49
+ function widths(size: number): { tabs: number; right: number } {
50
+ const r = ui.topRoom;
51
+ return { tabs: r.tabs || GUESS.tabs, right: r.right ? r.right + (r.right3d ? size + BACK_GAP : 0) : GUESS.right };
52
+ }
53
+
54
+ /** The top row on a screen `width` by `height` (round buttons `size` across). */
55
+ export function topLayout(width: number, height: number, phone: boolean, size = 46): TopLayout {
56
+ const m = widths(size), back = EDGE + size + BACK_GAP, end = width - EDGE - m.right - GAP;
57
+ if (phone) return { below: true, left: null, crumb: 0 };
58
+ // centred: the crumb up to the tabs, the buttons clear of them
59
+ const side = width / 2 - m.tabs / 2 - GAP, centred = side >= m.right + EDGE;
60
+ if (centred && side - back >= CRUMB_MIN) return { below: false, left: null, crumb: Math.min(CRUMB_MAX, side - back) };
61
+ if (height < SHORT) {
62
+ if (centred) return { below: false, left: null, crumb: 0 };
63
+ const from = EDGE + size + GAP;
64
+ if (end - from >= m.tabs) return { below: false, left: (from + end) / 2 - m.tabs / 2, crumb: 0 };
65
+ }
66
+ const crumb = Math.min(CRUMB_MAX, end - back);
67
+ return { below: true, left: null, crumb: crumb >= CRUMB_MIN ? crumb : 0 };
68
+ }
69
+
70
+ /** The layout as a string, for a component's key (it's drawn again when this changes). */
71
+ export const topKey = (l: TopLayout): string => [l.below, l.left == null ? '' : Math.round(l.left), Math.round(l.crumb)].join(',');
@@ -0,0 +1,10 @@
1
+ import { Platform } from 'react-native';
2
+ import { ed } from '../app/state';
3
+
4
+ /**
5
+ * Whether the person is on a touch screen: a device, a coarse pointer in a browser (an iPad), or a finger the last
6
+ * thing on the drawing. A keypad swipes to the job's measurements there, as BOLDR's own keypads do; a desktop has a
7
+ * button for them.
8
+ */
9
+ export const touchUI = (): boolean =>
10
+ Platform.OS !== 'web' || ed.lastPointerTouch || (typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches);
package/src/render/ctx.ts CHANGED
@@ -23,17 +23,25 @@ export interface DrawCtx {
23
23
  translate(x: number, y: number): void;
24
24
  /** radians, clockwise on screen */
25
25
  rotate(angle: number): void;
26
+ scale(x: number, y: number): void;
26
27
  setLineDash(segments: number[]): void;
27
28
 
28
29
  beginPath(): void;
29
30
  moveTo(x: number, y: number): void;
30
31
  lineTo(x: number, y: number): void;
31
32
  closePath(): void;
33
+ bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void;
32
34
  /** as the HTML canvas: angles in radians, clockwise on screen unless `counterclockwise` */
33
35
  arc(x: number, y: number, r: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
34
36
  arcTo(x1: number, y1: number, x2: number, y2: number, r: number): void;
35
37
  rect(x: number, y: number, w: number, h: number): void;
36
38
  roundRect(x: number, y: number, w: number, h: number, r: number): void;
39
+ /**
40
+ * Optional: start a path that is a shape `draw` makes (drawn there with its top left at x, y), for a shape drawn many
41
+ * times a frame — every chip's and badge's box. An engine that has it keeps each shape (by `key`) once and moves it
42
+ * into place, instead of describing it again each time; without it, `draw` draws it there.
43
+ */
44
+ shape?(key: string, x: number, y: number, draw: (c: DrawCtx, x: number, y: number) => void): void;
37
45
  fill(): void;
38
46
  stroke(): void;
39
47
  clip(): void;