@reekon-tools/boldr-cad 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (547) hide show
  1. package/README.md +81 -9
  2. package/dist/app/edit.d.ts +12 -1
  3. package/dist/app/edit.js +96 -11
  4. package/dist/app/edit.js.map +1 -1
  5. package/dist/app/state.d.ts +31 -4
  6. package/dist/app/state.js +31 -2
  7. package/dist/app/state.js.map +1 -1
  8. package/dist/data/memory.d.ts +5 -0
  9. package/dist/data/memory.js +2 -2
  10. package/dist/data/memory.js.map +1 -1
  11. package/dist/data/provider.d.ts +19 -7
  12. package/dist/data/sync.d.ts +7 -2
  13. package/dist/data/sync.js +115 -64
  14. package/dist/data/sync.js.map +1 -1
  15. package/dist/editor/actions.d.ts +1 -2
  16. package/dist/editor/actions.js +391 -113
  17. package/dist/editor/actions.js.map +1 -1
  18. package/dist/editor/confirm.d.ts +23 -0
  19. package/dist/editor/confirm.js +35 -0
  20. package/dist/editor/confirm.js.map +1 -0
  21. package/dist/editor/elevation.d.ts +191 -90
  22. package/dist/editor/elevation.js +812 -616
  23. package/dist/editor/elevation.js.map +1 -1
  24. package/dist/editor/face.d.ts +73 -0
  25. package/dist/editor/face.js +269 -0
  26. package/dist/editor/face.js.map +1 -0
  27. package/dist/editor/files.d.ts +6 -0
  28. package/dist/editor/files.js +50 -4
  29. package/dist/editor/files.js.map +1 -1
  30. package/dist/editor/gestures.d.ts +18 -3
  31. package/dist/editor/gestures.js +145 -30
  32. package/dist/editor/gestures.js.map +1 -1
  33. package/dist/editor/keypad.d.ts +19 -4
  34. package/dist/editor/keypad.js +39 -9
  35. package/dist/editor/keypad.js.map +1 -1
  36. package/dist/editor/keys.js +36 -4
  37. package/dist/editor/keys.js.map +1 -1
  38. package/dist/editor/laser.d.ts +20 -4
  39. package/dist/editor/laser.js +104 -21
  40. package/dist/editor/laser.js.map +1 -1
  41. package/dist/editor/linkActions.d.ts +49 -0
  42. package/dist/editor/linkActions.js +210 -0
  43. package/dist/editor/linkActions.js.map +1 -0
  44. package/dist/editor/linkFollow.d.ts +2 -0
  45. package/dist/editor/linkFollow.js +53 -0
  46. package/dist/editor/linkFollow.js.map +1 -0
  47. package/dist/editor/linkWrite.d.ts +13 -0
  48. package/dist/editor/linkWrite.js +84 -0
  49. package/dist/editor/linkWrite.js.map +1 -0
  50. package/dist/editor/links.d.ts +84 -0
  51. package/dist/editor/links.js +130 -0
  52. package/dist/editor/links.js.map +1 -0
  53. package/dist/editor/markup.d.ts +29 -3
  54. package/dist/editor/markup.js +95 -10
  55. package/dist/editor/markup.js.map +1 -1
  56. package/dist/editor/mode.d.ts +40 -2
  57. package/dist/editor/mode.js +71 -4
  58. package/dist/editor/mode.js.map +1 -1
  59. package/dist/editor/ports.d.ts +36 -6
  60. package/dist/editor/ports.js +10 -1
  61. package/dist/editor/ports.js.map +1 -1
  62. package/dist/editor/project.d.ts +38 -6
  63. package/dist/editor/project.js +167 -27
  64. package/dist/editor/project.js.map +1 -1
  65. package/dist/editor/selection.d.ts +10 -0
  66. package/dist/editor/selection.js +62 -8
  67. package/dist/editor/selection.js.map +1 -1
  68. package/dist/editor/status.js +3 -0
  69. package/dist/editor/status.js.map +1 -1
  70. package/dist/editor/tooltips.d.ts +5 -0
  71. package/dist/editor/tooltips.js +48 -0
  72. package/dist/editor/tooltips.js.map +1 -0
  73. package/dist/editor/view3d.d.ts +17 -0
  74. package/dist/editor/view3d.js +135 -0
  75. package/dist/editor/view3d.js.map +1 -0
  76. package/dist/editor/view3dState.d.ts +39 -0
  77. package/dist/editor/view3dState.js +44 -0
  78. package/dist/editor/view3dState.js.map +1 -0
  79. package/dist/export/dxf.js +1 -1
  80. package/dist/export/dxf.js.map +1 -1
  81. package/dist/export/pdf.js +17 -12
  82. package/dist/export/pdf.js.map +1 -1
  83. package/dist/export/plan.d.ts +23 -6
  84. package/dist/export/plan.js +127 -44
  85. package/dist/export/plan.js.map +1 -1
  86. package/dist/index.d.ts +5 -0
  87. package/dist/index.js +2 -0
  88. package/dist/index.js.map +1 -1
  89. package/dist/model/constraints.js +15 -4
  90. package/dist/model/constraints.js.map +1 -1
  91. package/dist/model/demo.js +3 -3
  92. package/dist/model/demo.js.map +1 -1
  93. package/dist/model/elevation.d.ts +6 -9
  94. package/dist/model/elevation.js +5 -13
  95. package/dist/model/elevation.js.map +1 -1
  96. package/dist/model/faces.d.ts +58 -0
  97. package/dist/model/faces.js +197 -0
  98. package/dist/model/faces.js.map +1 -0
  99. package/dist/model/facesolve.d.ts +72 -0
  100. package/dist/model/facesolve.js +369 -0
  101. package/dist/model/facesolve.js.map +1 -0
  102. package/dist/model/history.d.ts +3 -2
  103. package/dist/model/history.js +17 -11
  104. package/dist/model/history.js.map +1 -1
  105. package/dist/model/images.d.ts +18 -0
  106. package/dist/model/images.js +26 -0
  107. package/dist/model/images.js.map +1 -0
  108. package/dist/model/links.d.ts +46 -0
  109. package/dist/model/links.js +73 -0
  110. package/dist/model/links.js.map +1 -0
  111. package/dist/model/markup.d.ts +46 -0
  112. package/dist/model/markup.js +71 -0
  113. package/dist/model/markup.js.map +1 -0
  114. package/dist/model/measurement.d.ts +11 -0
  115. package/dist/model/measurement.js +24 -0
  116. package/dist/model/measurement.js.map +1 -0
  117. package/dist/model/openings.d.ts +4 -0
  118. package/dist/model/openings.js +4 -0
  119. package/dist/model/openings.js.map +1 -1
  120. package/dist/model/planGeom.d.ts +72 -2
  121. package/dist/model/planGeom.js +184 -10
  122. package/dist/model/planGeom.js.map +1 -1
  123. package/dist/model/project.js +3 -2
  124. package/dist/model/project.js.map +1 -1
  125. package/dist/model/projectfile.js +2 -1
  126. package/dist/model/projectfile.js.map +1 -1
  127. package/dist/model/rooms.js +2 -2
  128. package/dist/model/rooms.js.map +1 -1
  129. package/dist/model/settings.d.ts +6 -0
  130. package/dist/model/settings.js +2 -2
  131. package/dist/model/settings.js.map +1 -1
  132. package/dist/model/sketch.d.ts +14 -0
  133. package/dist/model/sketch.js +143 -24
  134. package/dist/model/sketch.js.map +1 -1
  135. package/dist/model/smooth.d.ts +6 -0
  136. package/dist/model/smooth.js +52 -0
  137. package/dist/model/smooth.js.map +1 -0
  138. package/dist/model/store.d.ts +12 -0
  139. package/dist/model/store.js +35 -3
  140. package/dist/model/store.js.map +1 -1
  141. package/dist/model/types.d.ts +173 -8
  142. package/dist/model/wallfaces.d.ts +32 -0
  143. package/dist/model/wallfaces.js +133 -20
  144. package/dist/model/wallfaces.js.map +1 -1
  145. package/dist/model/wallshape.d.ts +15 -0
  146. package/dist/model/wallshape.js +89 -0
  147. package/dist/model/wallshape.js.map +1 -0
  148. package/dist/react/BottomBar.d.ts +9 -5
  149. package/dist/react/BottomBar.js +213 -24
  150. package/dist/react/BottomBar.js.map +1 -1
  151. package/dist/react/Buttons.d.ts +13 -2
  152. package/dist/react/Buttons.js +37 -5
  153. package/dist/react/Buttons.js.map +1 -1
  154. package/dist/react/CadEditor.d.ts +46 -3
  155. package/dist/react/CadEditor.js +77 -20
  156. package/dist/react/CadEditor.js.map +1 -1
  157. package/dist/react/ConfirmSheet.d.ts +4 -0
  158. package/dist/react/ConfirmSheet.js +31 -0
  159. package/dist/react/ConfirmSheet.js.map +1 -0
  160. package/dist/react/ConflictSheet.d.ts +2 -1
  161. package/dist/react/ConflictSheet.js +16 -8
  162. package/dist/react/ConflictSheet.js.map +1 -1
  163. package/dist/react/Drawer.d.ts +8 -2
  164. package/dist/react/Drawer.js +167 -38
  165. package/dist/react/Drawer.js.map +1 -1
  166. package/dist/react/Duck.d.ts +13 -0
  167. package/dist/react/Duck.js +45 -0
  168. package/dist/react/Duck.js.map +1 -0
  169. package/dist/react/Empty3D.d.ts +2 -0
  170. package/dist/react/Empty3D.js +39 -0
  171. package/dist/react/Empty3D.js.map +1 -0
  172. package/dist/react/FitScroll.d.ts +6 -0
  173. package/dist/react/FitScroll.js +14 -0
  174. package/dist/react/FitScroll.js.map +1 -0
  175. package/dist/react/Icon.native.d.ts +6 -0
  176. package/dist/react/Icon.native.js +4 -0
  177. package/dist/react/Icon.native.js.map +1 -1
  178. package/dist/react/Icon.web.d.ts +6 -0
  179. package/dist/react/Icon.web.js +4 -0
  180. package/dist/react/Icon.web.js.map +1 -1
  181. package/dist/react/KeypadSheet.d.ts +9 -2
  182. package/dist/react/KeypadSheet.js +174 -42
  183. package/dist/react/KeypadSheet.js.map +1 -1
  184. package/dist/react/LaserTray.js +8 -5
  185. package/dist/react/LaserTray.js.map +1 -1
  186. package/dist/react/MarkupPanel.d.ts +8 -0
  187. package/dist/react/MarkupPanel.js +67 -0
  188. package/dist/react/MarkupPanel.js.map +1 -0
  189. package/dist/react/PageThumb.native.d.ts +7 -2
  190. package/dist/react/PageThumb.native.js +19 -7
  191. package/dist/react/PageThumb.native.js.map +1 -1
  192. package/dist/react/PageThumb.web.d.ts +3 -2
  193. package/dist/react/PageThumb.web.js +4 -4
  194. package/dist/react/PageThumb.web.js.map +1 -1
  195. package/dist/react/PlanCanvas.native.d.ts +1 -1
  196. package/dist/react/PlanCanvas.native.js +134 -36
  197. package/dist/react/PlanCanvas.native.js.map +1 -1
  198. package/dist/react/PlanCanvas.web.js +16 -5
  199. package/dist/react/PlanCanvas.web.js.map +1 -1
  200. package/dist/react/Popovers.d.ts +12 -6
  201. package/dist/react/Popovers.js +53 -26
  202. package/dist/react/Popovers.js.map +1 -1
  203. package/dist/react/Rail.d.ts +24 -0
  204. package/dist/react/Rail.js +324 -34
  205. package/dist/react/Rail.js.map +1 -1
  206. package/dist/react/ShapeMenu.d.ts +7 -0
  207. package/dist/react/ShapeMenu.js +58 -0
  208. package/dist/react/ShapeMenu.js.map +1 -0
  209. package/dist/react/Sheet.d.ts +14 -4
  210. package/dist/react/Sheet.js +54 -9
  211. package/dist/react/Sheet.js.map +1 -1
  212. package/dist/react/Slider.d.ts +18 -0
  213. package/dist/react/Slider.js +27 -0
  214. package/dist/react/Slider.js.map +1 -0
  215. package/dist/react/Toast.js +14 -6
  216. package/dist/react/Toast.js.map +1 -1
  217. package/dist/react/ToolTip.d.ts +7 -0
  218. package/dist/react/ToolTip.js +43 -0
  219. package/dist/react/ToolTip.js.map +1 -0
  220. package/dist/react/TopBar.d.ts +6 -3
  221. package/dist/react/TopBar.js +48 -17
  222. package/dist/react/TopBar.js.map +1 -1
  223. package/dist/react/Tween.d.ts +7 -0
  224. package/dist/react/Tween.js +26 -0
  225. package/dist/react/Tween.js.map +1 -0
  226. package/dist/react/Tween.native.d.ts +6 -0
  227. package/dist/react/Tween.native.js +43 -0
  228. package/dist/react/Tween.native.js.map +1 -0
  229. package/dist/react/View3D.native.d.ts +4 -0
  230. package/dist/react/View3D.native.js +103 -0
  231. package/dist/react/View3D.native.js.map +1 -0
  232. package/dist/react/View3D.web.d.ts +4 -0
  233. package/dist/react/View3D.web.js +151 -0
  234. package/dist/react/View3D.web.js.map +1 -0
  235. package/dist/react/View3DHost.d.ts +143 -0
  236. package/dist/react/View3DHost.js +377 -0
  237. package/dist/react/View3DHost.js.map +1 -0
  238. package/dist/react/corners.d.ts +7 -0
  239. package/dist/react/corners.js +51 -0
  240. package/dist/react/corners.js.map +1 -0
  241. package/dist/react/easing.d.ts +15 -0
  242. package/dist/react/easing.js +10 -0
  243. package/dist/react/easing.js.map +1 -0
  244. package/dist/react/host.d.ts +7 -2
  245. package/dist/react/host.js +8 -3
  246. package/dist/react/host.js.map +1 -1
  247. package/dist/react/hostKeypad.d.ts +56 -0
  248. package/dist/react/hostKeypad.js +5 -0
  249. package/dist/react/hostKeypad.js.map +1 -0
  250. package/dist/react/motion.d.ts +77 -0
  251. package/dist/react/motion.js +173 -0
  252. package/dist/react/motion.js.map +1 -0
  253. package/dist/react/ports.js +90 -8
  254. package/dist/react/ports.js.map +1 -1
  255. package/dist/react/saving.d.ts +15 -0
  256. package/dist/react/saving.js +76 -0
  257. package/dist/react/saving.js.map +1 -0
  258. package/dist/react/store.d.ts +38 -1
  259. package/dist/react/store.js +31 -1
  260. package/dist/react/store.js.map +1 -1
  261. package/dist/react/theme.d.ts +10 -0
  262. package/dist/react/theme.js +8 -0
  263. package/dist/react/theme.js.map +1 -1
  264. package/dist/react/topRow.d.ts +22 -0
  265. package/dist/react/topRow.js +61 -0
  266. package/dist/react/topRow.js.map +1 -0
  267. package/dist/react/touch.d.ts +6 -0
  268. package/dist/react/touch.js +9 -0
  269. package/dist/react/touch.js.map +1 -0
  270. package/dist/render/ctx.d.ts +8 -0
  271. package/dist/render/draw.d.ts +33 -1
  272. package/dist/render/draw.js +593 -215
  273. package/dist/render/draw.js.map +1 -1
  274. package/dist/render/elevation.d.ts +65 -43
  275. package/dist/render/elevation.js +414 -140
  276. package/dist/render/elevation.js.map +1 -1
  277. package/dist/render/figmaIcons.d.ts +12 -0
  278. package/dist/render/figmaIcons.js +21 -0
  279. package/dist/render/figmaIcons.js.map +1 -0
  280. package/dist/render/hit.d.ts +22 -2
  281. package/dist/render/hit.js +168 -25
  282. package/dist/render/hit.js.map +1 -1
  283. package/dist/render/icons.js +69 -10
  284. package/dist/render/icons.js.map +1 -1
  285. package/dist/render/linkTile.d.ts +15 -0
  286. package/dist/render/linkTile.js +369 -0
  287. package/dist/render/linkTile.js.map +1 -0
  288. package/dist/render/markup.d.ts +11 -0
  289. package/dist/render/markup.js +112 -0
  290. package/dist/render/markup.js.map +1 -0
  291. package/dist/render/navigate.d.ts +88 -0
  292. package/dist/render/navigate.js +100 -0
  293. package/dist/render/navigate.js.map +1 -0
  294. package/dist/render/snap.d.ts +20 -1
  295. package/dist/render/snap.js +118 -25
  296. package/dist/render/snap.js.map +1 -1
  297. package/dist/render/squircle.d.ts +8 -0
  298. package/dist/render/squircle.js +71 -0
  299. package/dist/render/squircle.js.map +1 -0
  300. package/dist/render/view.d.ts +115 -2
  301. package/dist/render/view.js +244 -19
  302. package/dist/render/view.js.map +1 -1
  303. package/dist/skia/SkiaDrawCtx.d.ts +30 -2
  304. package/dist/skia/SkiaDrawCtx.js +198 -43
  305. package/dist/skia/SkiaDrawCtx.js.map +1 -1
  306. package/dist/skia/host.d.ts +18 -2
  307. package/dist/skia/host.js +46 -1
  308. package/dist/skia/host.js.map +1 -1
  309. package/dist/solver/equations.js +37 -2
  310. package/dist/solver/equations.js.map +1 -1
  311. package/dist/solver/gauss-newton.d.ts +17 -0
  312. package/dist/solver/gauss-newton.js +144 -36
  313. package/dist/solver/gauss-newton.js.map +1 -1
  314. package/dist/solver/linalg.d.ts +45 -2
  315. package/dist/solver/linalg.js +164 -10
  316. package/dist/solver/linalg.js.map +1 -1
  317. package/dist/solver/types.d.ts +5 -0
  318. package/dist/tools/annotate.d.ts +57 -1
  319. package/dist/tools/annotate.js +64 -43
  320. package/dist/tools/annotate.js.map +1 -1
  321. package/dist/tools/chamfer.d.ts +12 -0
  322. package/dist/tools/chamfer.js +114 -0
  323. package/dist/tools/chamfer.js.map +1 -0
  324. package/dist/tools/corner.d.ts +38 -0
  325. package/dist/tools/corner.js +99 -0
  326. package/dist/tools/corner.js.map +1 -0
  327. package/dist/tools/dim.d.ts +86 -9
  328. package/dist/tools/dim.js +581 -96
  329. package/dist/tools/dim.js.map +1 -1
  330. package/dist/tools/elevation.js +1 -1
  331. package/dist/tools/elevation.js.map +1 -1
  332. package/dist/tools/fillet.d.ts +9 -0
  333. package/dist/tools/fillet.js +86 -0
  334. package/dist/tools/fillet.js.map +1 -0
  335. package/dist/tools/freehand.d.ts +3 -2
  336. package/dist/tools/freehand.js +19 -12
  337. package/dist/tools/freehand.js.map +1 -1
  338. package/dist/tools/index.js +14 -4
  339. package/dist/tools/index.js.map +1 -1
  340. package/dist/tools/line.d.ts +8 -3
  341. package/dist/tools/line.js +66 -30
  342. package/dist/tools/line.js.map +1 -1
  343. package/dist/tools/modify.d.ts +13 -0
  344. package/dist/tools/modify.js +105 -25
  345. package/dist/tools/modify.js.map +1 -1
  346. package/dist/tools/openings.js +22 -15
  347. package/dist/tools/openings.js.map +1 -1
  348. package/dist/tools/registry.d.ts +8 -0
  349. package/dist/tools/registry.js +42 -2
  350. package/dist/tools/registry.js.map +1 -1
  351. package/dist/tools/select.d.ts +77 -3
  352. package/dist/tools/select.js +560 -74
  353. package/dist/tools/select.js.map +1 -1
  354. package/dist/tools/shapes.d.ts +14 -0
  355. package/dist/tools/shapes.js +154 -75
  356. package/dist/tools/shapes.js.map +1 -1
  357. package/dist/units/format.d.ts +10 -2
  358. package/dist/units/format.js +18 -3
  359. package/dist/units/format.js.map +1 -1
  360. package/dist/view3d/build.d.ts +35 -0
  361. package/dist/view3d/build.js +630 -0
  362. package/dist/view3d/build.js.map +1 -0
  363. package/dist/view3d/camera.d.ts +60 -0
  364. package/dist/view3d/camera.js +84 -0
  365. package/dist/view3d/camera.js.map +1 -0
  366. package/dist/view3d/fatLines.d.ts +11 -0
  367. package/dist/view3d/fatLines.js +83 -0
  368. package/dist/view3d/fatLines.js.map +1 -0
  369. package/dist/view3d/lines.d.ts +17 -0
  370. package/dist/view3d/lines.js +271 -0
  371. package/dist/view3d/lines.js.map +1 -0
  372. package/dist/view3d/mesh.d.ts +51 -0
  373. package/dist/view3d/mesh.js +101 -0
  374. package/dist/view3d/mesh.js.map +1 -0
  375. package/dist/view3d/poly.d.ts +16 -0
  376. package/dist/view3d/poly.js +109 -0
  377. package/dist/view3d/poly.js.map +1 -0
  378. package/dist/view3d/viewer.d.ts +86 -0
  379. package/dist/view3d/viewer.js +350 -0
  380. package/dist/view3d/viewer.js.map +1 -0
  381. package/package.json +15 -2
  382. package/src/app/edit.ts +83 -12
  383. package/src/app/main.ts +11 -5
  384. package/src/app/state.ts +38 -5
  385. package/src/data/memory.ts +2 -2
  386. package/src/data/provider.ts +15 -7
  387. package/src/data/sync.ts +87 -50
  388. package/src/editor/actions.ts +292 -83
  389. package/src/editor/confirm.ts +58 -0
  390. package/src/editor/elevation.ts +698 -478
  391. package/src/editor/face.ts +227 -0
  392. package/src/editor/files.ts +40 -4
  393. package/src/editor/gestures.ts +147 -31
  394. package/src/editor/keypad.ts +43 -11
  395. package/src/editor/keys.ts +25 -5
  396. package/src/editor/laser.ts +98 -20
  397. package/src/editor/linkActions.ts +194 -0
  398. package/src/editor/linkFollow.ts +43 -0
  399. package/src/editor/linkWrite.ts +77 -0
  400. package/src/editor/links.ts +181 -0
  401. package/src/editor/markup.ts +92 -8
  402. package/src/editor/mode.ts +83 -7
  403. package/src/editor/ports.ts +37 -6
  404. package/src/editor/project.ts +155 -26
  405. package/src/editor/selection.ts +54 -8
  406. package/src/editor/status.ts +2 -0
  407. package/src/editor/tooltips.ts +47 -0
  408. package/src/editor/view3d.ts +119 -0
  409. package/src/editor/view3dState.ts +61 -0
  410. package/src/export/dxf.ts +1 -1
  411. package/src/export/pdf.ts +15 -7
  412. package/src/export/plan.ts +119 -35
  413. package/src/index.ts +5 -0
  414. package/src/input/keyboard.ts +2 -4
  415. package/src/model/constraints.ts +13 -4
  416. package/src/model/demo.ts +3 -3
  417. package/src/model/elevation.ts +6 -16
  418. package/src/model/faces.ts +153 -0
  419. package/src/model/facesolve.ts +322 -0
  420. package/src/model/history.ts +19 -11
  421. package/src/model/images.ts +35 -0
  422. package/src/model/links.ts +77 -0
  423. package/src/model/markup.ts +80 -0
  424. package/src/model/measurement.ts +26 -0
  425. package/src/model/openings.ts +6 -0
  426. package/src/model/planGeom.ts +174 -8
  427. package/src/model/project.ts +3 -2
  428. package/src/model/projectfile.ts +2 -1
  429. package/src/model/rooms.ts +1 -1
  430. package/src/model/settings.ts +8 -2
  431. package/src/model/sketch.ts +101 -14
  432. package/src/model/smooth.ts +51 -0
  433. package/src/model/store.ts +35 -4
  434. package/src/model/types.ts +170 -8
  435. package/src/model/wallfaces.ts +126 -17
  436. package/src/model/wallshape.ts +97 -0
  437. package/src/react/BottomBar.tsx +263 -46
  438. package/src/react/Buttons.tsx +50 -9
  439. package/src/react/CadEditor.tsx +133 -30
  440. package/src/react/ConfirmSheet.tsx +38 -0
  441. package/src/react/ConflictSheet.tsx +19 -12
  442. package/src/react/Drawer.tsx +213 -110
  443. package/src/react/Duck.tsx +50 -0
  444. package/src/react/Empty3D.tsx +32 -0
  445. package/src/react/FitScroll.tsx +19 -0
  446. package/src/react/Icon.native.tsx +5 -0
  447. package/src/react/Icon.web.tsx +5 -0
  448. package/src/react/KeypadSheet.tsx +253 -60
  449. package/src/react/LaserTray.tsx +12 -8
  450. package/src/react/MarkupPanel.tsx +150 -0
  451. package/src/react/PageThumb.native.tsx +18 -7
  452. package/src/react/PageThumb.web.tsx +4 -4
  453. package/src/react/PlanCanvas.native.tsx +151 -34
  454. package/src/react/PlanCanvas.web.tsx +15 -5
  455. package/src/react/Popovers.tsx +116 -51
  456. package/src/react/Rail.tsx +360 -27
  457. package/src/react/ShapeMenu.tsx +66 -0
  458. package/src/react/Sheet.tsx +56 -11
  459. package/src/react/Slider.tsx +48 -0
  460. package/src/react/Toast.tsx +16 -9
  461. package/src/react/ToolTip.tsx +49 -0
  462. package/src/react/TopBar.tsx +97 -39
  463. package/src/react/Tween.native.tsx +36 -0
  464. package/src/react/Tween.tsx +24 -0
  465. package/src/react/View3D.native.tsx +131 -0
  466. package/src/react/View3D.web.tsx +131 -0
  467. package/src/react/View3DHost.ts +367 -0
  468. package/src/react/corners.ts +52 -0
  469. package/src/react/easing.ts +14 -0
  470. package/src/react/host.ts +8 -3
  471. package/src/react/hostKeypad.ts +66 -0
  472. package/src/react/motion.tsx +239 -0
  473. package/src/react/ports.ts +78 -9
  474. package/src/react/saving.ts +80 -0
  475. package/src/react/store.ts +35 -3
  476. package/src/react/theme.ts +8 -0
  477. package/src/react/topRow.ts +71 -0
  478. package/src/react/touch.ts +10 -0
  479. package/src/render/ctx.ts +8 -0
  480. package/src/render/draw.ts +437 -175
  481. package/src/render/elevation.ts +349 -143
  482. package/src/render/figmaIcons.ts +22 -0
  483. package/src/render/hit.ts +140 -21
  484. package/src/render/icons.ts +69 -10
  485. package/src/render/linkTile.ts +297 -0
  486. package/src/render/markup.ts +91 -0
  487. package/src/render/navigate.ts +114 -0
  488. package/src/render/snap.ts +105 -22
  489. package/src/render/squircle.ts +73 -0
  490. package/src/render/view.ts +255 -19
  491. package/src/skia/SkiaDrawCtx.ts +163 -41
  492. package/src/skia/host.ts +46 -1
  493. package/src/solver/equations.ts +37 -2
  494. package/src/solver/gauss-newton.ts +125 -28
  495. package/src/solver/linalg.ts +141 -5
  496. package/src/solver/types.ts +5 -0
  497. package/src/styles.css +91 -42
  498. package/src/tools/annotate.ts +58 -29
  499. package/src/tools/chamfer.ts +103 -0
  500. package/src/tools/corner.ts +99 -0
  501. package/src/tools/dim.ts +524 -92
  502. package/src/tools/elevation.ts +1 -1
  503. package/src/tools/fillet.ts +82 -0
  504. package/src/tools/freehand.ts +20 -16
  505. package/src/tools/index.ts +17 -4
  506. package/src/tools/line.ts +59 -23
  507. package/src/tools/modify.ts +86 -24
  508. package/src/tools/openings.ts +8 -13
  509. package/src/tools/registry.ts +27 -2
  510. package/src/tools/select.ts +424 -75
  511. package/src/tools/shapes.ts +119 -58
  512. package/src/ui/ctx.ts +2 -1
  513. package/src/ui/domPorts.ts +23 -4
  514. package/src/ui/drawer.ts +6 -26
  515. package/src/ui/elevation.ts +10 -101
  516. package/src/ui/keypad.ts +8 -14
  517. package/src/ui/laser.ts +2 -2
  518. package/src/ui/menu.ts +63 -17
  519. package/src/ui/noteinput.ts +3 -3
  520. package/src/ui/opacity.ts +108 -0
  521. package/src/ui/palette.ts +67 -12
  522. package/src/ui/rail.ts +21 -7
  523. package/src/ui/topbar.ts +4 -14
  524. package/src/units/format.ts +18 -3
  525. package/src/view3d/build.ts +607 -0
  526. package/src/view3d/camera.ts +113 -0
  527. package/src/view3d/fatLines.ts +83 -0
  528. package/src/view3d/lines.ts +258 -0
  529. package/src/view3d/mesh.ts +111 -0
  530. package/src/view3d/poly.ts +104 -0
  531. package/src/view3d/viewer.ts +348 -0
  532. package/dist/react/ElevCanvas.native.d.ts +0 -3
  533. package/dist/react/ElevCanvas.native.js +0 -58
  534. package/dist/react/ElevCanvas.native.js.map +0 -1
  535. package/dist/react/ElevCanvas.web.d.ts +0 -4
  536. package/dist/react/ElevCanvas.web.js +0 -44
  537. package/dist/react/ElevCanvas.web.js.map +0 -1
  538. package/dist/react/ElevationSheet.d.ts +0 -2
  539. package/dist/react/ElevationSheet.js +0 -38
  540. package/dist/react/ElevationSheet.js.map +0 -1
  541. package/dist/render/elevationView.d.ts +0 -3
  542. package/dist/render/elevationView.js +0 -100
  543. package/dist/render/elevationView.js.map +0 -1
  544. package/src/react/ElevCanvas.native.tsx +0 -68
  545. package/src/react/ElevCanvas.web.tsx +0 -42
  546. package/src/react/ElevationSheet.tsx +0 -88
  547. package/src/render/elevationView.ts +0 -73
@@ -1,26 +1,33 @@
1
- import { Pressable, StyleSheet, Text, View } from 'react-native';
2
- import { notify, ui } from './store';
1
+ import { useRef } from 'react';
2
+ import { Animated, Text, View } from 'react-native';
3
+ import { notify, ui, useEditorKey } from './store';
3
4
  import { C, FONT, shadow } from './theme';
5
+ import { Tap, usePopup } from './motion';
6
+ import { createSheet } from './corners';
4
7
 
5
8
  /** A short message above the bottom bar, sometimes with an action (Undo). */
6
9
  export function Toast({ phone }: { phone: boolean }) {
7
- const t = ui.toast;
8
- if (!t) return null;
10
+ useEditorKey(() => (ui.toast ? String(ui.toast.id) : ''));
11
+ // (it rises in, and fades as it goes, still saying what it said)
12
+ const open = !!ui.toast, last = useRef(ui.toast), pop = usePopup(open, { y: 8 }, 'bottom center');
13
+ if (ui.toast) last.current = ui.toast;
14
+ const t = last.current;
15
+ if (!pop.present || !t) return null;
9
16
  return (
10
17
  <View style={[s.wrap, { bottom: phone ? 160 : 96 }]}>
11
- <View style={[s.toast, t.warn && s.warn]} accessibilityRole="alert" testID="toast" key={t.id}>
18
+ <Animated.View style={[s.toast, t.warn && s.warn, pop.style]} pointerEvents={open ? 'auto' : 'none'} accessibilityRole="alert" testID={open ? 'toast' : undefined} key={t.id}>
12
19
  <Text style={s.text}>{t.msg}</Text>
13
20
  {t.action && (
14
- <Pressable accessibilityRole="button" onPress={() => { const fn = t.action!.fn; ui.toast = null; notify(); fn(); }}>
21
+ <Tap accessibilityRole="button" onPress={() => { const fn = t.action!.fn; ui.toast = null; notify(); fn(); }}>
15
22
  <Text style={s.action}>{t.action.label}</Text>
16
- </Pressable>
23
+ </Tap>
17
24
  )}
18
- </View>
25
+ </Animated.View>
19
26
  </View>
20
27
  );
21
28
  }
22
29
 
23
- const s = StyleSheet.create({
30
+ const s = createSheet({
24
31
  wrap: { pointerEvents: 'box-none', position: 'absolute', left: 16, right: 16, alignItems: 'center', zIndex: 70 },
25
32
  toast: { backgroundColor: C.dark, borderWidth: 1, borderColor: C.g2, borderRadius: 12, paddingVertical: 11, paddingHorizontal: 14, flexDirection: 'row', alignItems: 'center', gap: 14, maxWidth: 520, ...shadow },
26
33
  warn: { borderLeftWidth: 4, borderLeftColor: C.wred },
@@ -0,0 +1,49 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Animated, useWindowDimensions, View } from 'react-native';
3
+ import { TIP_TOP, topLayout } from './topRow';
4
+ import { RichText } from './RichText';
5
+ import { ui, useEditorKey } from './store';
6
+ import { C, FONT, shadow } from './theme';
7
+ import { TOGGLE_EASE } from './motion';
8
+ import { NATIVE } from './easing';
9
+ import { createSheet } from './corners';
10
+
11
+ /** How long the note stays, and how it comes and goes. */
12
+ const SHOW_MS = 2600, IN_MS = 160, OUT_MS = 240;
13
+
14
+ /**
15
+ * What the tool just picked from the rail does, in a line, just under the Annotate / Draft tabs: it fades in, stays a
16
+ * moment and goes by itself; picking another tool replaces it at once.
17
+ */
18
+ export function ToolTip({ phone }: { phone: boolean }) {
19
+ const { width, height } = useWindowDimensions();
20
+ useEditorKey(() => String(ui.toolTip?.seq ?? '') + '|' + topLayout(width, height, phone).below);
21
+ const tip = ui.toolTip, v = useRef(new Animated.Value(0)).current, [shown, setShown] = useState<{ text: string; seq: number } | null>(null);
22
+ const slide = useRef(v.interpolate({ inputRange: [0, 1], outputRange: [-4, 0] })).current;
23
+ useEffect(() => {
24
+ if (!tip) return;
25
+ setShown(tip);
26
+ v.setValue(0);
27
+ const go = (to: number, ms: number) => Animated.timing(v, { toValue: to, duration: ms, easing: TOGGLE_EASE, useNativeDriver: NATIVE });
28
+ go(1, IN_MS).start();
29
+ const t = setTimeout(() => go(0, OUT_MS).start(({ finished }) => { if (finished) setShown(s => (s?.seq === tip.seq ? null : s)); }), SHOW_MS);
30
+ return () => clearTimeout(t);
31
+ }, [tip?.seq]); // eslint-disable-line react-hooks/exhaustive-deps
32
+ // (always mounted, see-through when idle: a view put on screen as its fade starts can be detached for a moment by
33
+ // React's strict mode, which stops the fade part-way)
34
+ return (
35
+ <View pointerEvents="none" style={[s.wrap, { top: topLayout(width, height, phone).below ? TIP_TOP.below : TIP_TOP.row }]}>
36
+ <Animated.View testID={shown ? 'toolTip' : undefined} accessibilityLiveRegion="polite" aria-hidden={!shown}
37
+ style={[s.tip, !shown && s.idle, { opacity: v, transform: [{ translateY: slide }] }]}>
38
+ {shown ? <RichText html={shown.text} style={s.text} /> : null}
39
+ </Animated.View>
40
+ </View>
41
+ );
42
+ }
43
+
44
+ const s = createSheet({
45
+ wrap: { position: 'absolute', left: 12, right: 12, alignItems: 'center', zIndex: 29 },
46
+ tip: { maxWidth: 460, backgroundColor: C.g1, borderRadius: 999, paddingVertical: 8, paddingHorizontal: 14, borderWidth: 1, borderColor: C.g2, ...shadow },
47
+ text: { fontFamily: FONT, fontSize: 13, color: C.lg2, textAlign: 'center' },
48
+ idle: { borderWidth: 0, padding: 0, boxShadow: 'none' },
49
+ });
@@ -1,75 +1,133 @@
1
1
  import type { ReactNode } from 'react';
2
- import { Pressable, StyleSheet, Text, View } from 'react-native';
3
- import { applyCtxAction } from '../editor/actions';
2
+ import { Text, useWindowDimensions, View } from 'react-native';
3
+ import { closeElevation, elev, elevOpen, elevTitle, flipElevation } from '../editor/elevation';
4
4
  import { setMode } from '../editor/mode';
5
5
  import { openDrawer } from '../editor/ports';
6
- import { ed } from '../app/state';
7
- import { settings } from '../model/settings';
6
+ import { close3d, open3d } from '../editor/view3d';
7
+ import { command3d, is3d } from '../editor/view3dState';
8
+ import { ed, type Mode } from '../app/state';
8
9
  import { page, store } from '../model/store';
9
10
  import { fitView } from '../render/view';
10
11
  import { CircleButton, Pill } from './Buttons';
11
12
  import { Icon } from './Icon';
12
13
  import { reactPorts } from './ports';
13
- import { notify, ui } from './store';
14
+ import { manualSave, saveState } from './saving';
15
+ import { notify, ui, useEditorKey } from './store';
14
16
  import { C, FONT, shadow } from './theme';
17
+ import { Segmented, Tap } from './motion';
18
+ import { createSheet } from './corners';
19
+ import { setTopRoom, TABS_TOP, topKey, topLayout } from './topRow';
15
20
 
16
- /** The top bar: back, the project and page, view controls, the host's own buttons, settings, and Save. */
17
- export function TopBar({ onBack, headerRight, phone, readOnly }: { onBack?: () => void; headerRight?: ReactNode; phone: boolean; readOnly?: boolean }) {
18
- const size = phone ? 42 : 46;
21
+ /**
22
+ * The top bar: back, the project and page, view controls, the host's own buttons, settings, and Save. In the wall
23
+ * view: back to the plan, the wall and the side it's seen from, and Flip to look from the other side.
24
+ */
25
+ export function TopBar({ onBack, headerRight, phone, readOnly }: {
26
+ onBack?: () => void; headerRight?: ReactNode | ((buttonSize: number) => ReactNode); phone: boolean; readOnly?: boolean;
27
+ }) {
28
+ const { width, height } = useWindowDimensions(), size = phone ? 42 : 46;
29
+ useEditorKey(() => {
30
+ const w = elevOpen() && !!elev.frame, title = w ? elevTitle() : null;
31
+ return [w, title?.title, title?.sub, store.project.name, page().name, ui.menu, saveState(), manualSave(), is3d(), topKey(topLayout(width, height, phone, size))].join('|');
32
+ });
33
+ const wall = elevOpen() && !!elev.frame, t = wall ? elevTitle() : null, crumb = topLayout(width, height, phone, size).crumb;
19
34
  return (
20
35
  <View style={s.top}>
21
- <View style={s.side}>
22
- <CircleButton icon="back" label={onBack ? 'Back' : 'Project pages'} size={size} onPress={onBack ?? (() => openDrawer('pages'))} testID="btnPages" />
23
- {!phone && (
24
- <Pressable accessibilityRole="button" accessibilityLabel="Open pages" testID="crumb" onPress={() => openDrawer('pages')} style={s.crumb}>
36
+ <View style={[s.side, wall && { flexShrink: 1 }]}>
37
+ {wall ? (
38
+ <>
39
+ <CircleButton icon="back" label="Back to the plan" size={size} onPress={closeElevation} testID="elevClose" />
40
+ {/* on a phone the wall's name gets the row below (where Draft / Annotate sit on the plan) */}
41
+ <View style={[s.crumb, s.wallCrumb, { height: size }, phone && s.wallCrumbPhone]} accessibilityLabel="Wall elevation" testID="elev">
42
+ <Text numberOfLines={1} style={s.wallTitle} testID="elevTitle">{t!.title}</Text>
43
+ <Text numberOfLines={1} style={s.wallSub} testID="elevSub">{t!.sub}</Text>
44
+ </View>
45
+ </>
46
+ ) : (
47
+ <CircleButton icon="back" label={onBack ? 'Back' : 'Project pages'} size={size} onPress={onBack ?? (() => openDrawer('pages'))} testID="btnPages" />
48
+ )}
49
+ {/* (where there's room: beside the tabs, or with the tabs below it, up to the buttons) */}
50
+ {!phone && !wall && crumb > 0 && (
51
+ <Tap accessibilityRole="button" accessibilityLabel="Open pages" testID="crumb" onPress={() => openDrawer('pages')} style={[s.crumb, { maxWidth: crumb }]}>
25
52
  <Text numberOfLines={1} style={s.crumbProj}>{store.project.name}</Text>
26
53
  <Text style={s.crumbSep}>/</Text>
27
54
  <Text numberOfLines={1} style={s.crumbPage}>{page().name}</Text>
28
- </Pressable>
55
+ </Tap>
29
56
  )}
30
57
  </View>
31
- <View style={[s.side, phone && { gap: 6 }]}>
32
- <CircleButton icon="fit" label="Fit to screen" size={size} onPress={() => fitView()} testID="btnFit" />
33
- <CircleButton icon={settings.showCons ? 'eye' : 'eyeoff'} label="Constraint icons" size={size} iconColor={settings.showCons ? '#fff' : C.lg1}
34
- onPress={() => applyCtxAction('showcons')} testID="btnIcons" />
35
- <CircleButton icon="layers" label="Layers" size={size} onPress={() => openDrawer('layers')} testID="btnLayers" />
36
- {headerRight}
37
- <Pill kind="grey" accessibilityLabel="Settings and more" testID="btnMore" style={[s.more, { height: size }, phone && { paddingHorizontal: 13, gap: 10 }]}
38
- onPress={() => { ui.menu = !ui.menu; ui.palette = false; notify(); }}>
39
- <Icon name="share" /><Icon name="dots" />
40
- </Pill>
41
- {!readOnly && <Pill accessibilityLabel="Save" testID="btnSave" style={[{ height: size }, phone && { minWidth: 52, paddingHorizontal: 14 }]} onPress={() => reactPorts.save()}><Icon name="save" color={C.dark} /></Pill>}
58
+ {/* (how wide the buttons are decides whether the tabs fit between them and the crumb: the wall view's own aren't
59
+ the plan's, and its tabs are hidden) */}
60
+ <View style={[s.side, phone && { gap: 6 }]} onLayout={e => { if (!elevOpen()) setTopRoom('right', e.nativeEvent.layout.width); }}>
61
+ {wall && (
62
+ <Pill kind="grey" accessibilityLabel="Flip: look from the other side" testID="elevFlip" onPress={flipElevation} style={[{ height: size, minWidth: 0 }, phone && { paddingHorizontal: 12 }]}>
63
+ <Icon name="flip" />{!phone && <Text style={s.flipText}>Flip</Text>}
64
+ </Pill>
65
+ )}
66
+ {/* (on a phone, Fit is the first item in •••: the row has no room for it) */}
67
+ {!phone && <CircleButton icon="fit" label="Fit to screen" size={size} onPress={() => (is3d() ? command3d('fit') : fitView())} testID="btnFit" />}
68
+ {/* the stack: what's shown (the Layers menu), open or closed — the plan's layers, so not in 3D */}
69
+ {!(wall && phone) && !is3d() && (
70
+ <CircleButton icon="layers" label="Layers" size={size} iconColor={ui.menu === 'layers' ? C.yellow : '#fff'}
71
+ onPress={() => { ui.menu = ui.menu === 'layers' ? false : 'layers'; ui.palette = false; ui.shapeMenu = null; notify(); }} testID="btnLayers" />
72
+ )}
73
+ {/* the room: the project panel, pages and spaces */}
74
+ {!wall && <CircleButton icon="spaces" label="Pages and spaces" size={size} onPress={() => openDrawer('pages')} testID="btnProject" />}
75
+ {typeof headerRight === 'function' ? headerRight(size) : headerRight}
76
+ {/* •••: exports, files and settings */}
77
+ <CircleButton icon="dots" label="Share, settings and more" size={size} iconColor={ui.menu === 'more' ? C.yellow : '#fff'}
78
+ onPress={() => { ui.menu = ui.menu === 'more' ? false : 'more'; ui.palette = false; ui.shapeMenu = null; notify(); }} testID="btnMore" />
79
+ {!readOnly && (
80
+ <Pill accessibilityLabel="Save" testID="btnSave" style={{ height: size }}
81
+ disabled={saveState() === 'saving'} onPress={() => reactPorts.save()}>
82
+ <Icon name="save" color={C.dark} />
83
+ {/* saving manually: what Save will do, or has done */}
84
+ {manualSave() && !phone && (
85
+ <Text style={s.saveText}>{saveState() === 'saving' ? 'Saving…' : saveState() === 'saved' ? 'Saved' : 'Save'}</Text>
86
+ )}
87
+ </Pill>
88
+ )}
42
89
  </View>
43
90
  </View>
44
91
  );
45
92
  }
46
93
 
47
- /** Draft / Annotate. */
94
+ const TABS: [Mode | '3d', string][] = [['annotate', 'Annotate'], ['draft', 'Draft'], ['3d', '3D']];
95
+
96
+ /** Annotate / Draft / 3D: centred in the top row, or on a row of their own under it (see topRow.ts). */
48
97
  export function ModeTabs({ phone }: { phone: boolean }) {
98
+ const { width, height } = useWindowDimensions();
99
+ useEditorKey(() => (is3d() ? '3d' : ed.mode) + '|' + topKey(topLayout(width, height, phone)));
100
+ const at = topLayout(width, height, phone);
101
+ // (straight over: no line under the tabs saying how to get about in 3D)
102
+ const pick = (m: Mode | '3d') => {
103
+ if (m !== '3d') { close3d(); setMode(m); return; }
104
+ open3d();
105
+ };
49
106
  return (
50
- <View style={[s.modes, phone && { top: 66 }]} accessibilityRole="tablist">
51
- {(['annotate', 'draft'] as const).map(m => {
52
- const on = ed.mode === m;
53
- return (
54
- <Pressable key={m} accessibilityRole="tab" accessibilityState={{ selected: on }} accessibilityLabel={m === 'draft' ? 'Draft' : 'Annotate'}
55
- onPress={() => setMode(m)} style={[s.mode, on && { backgroundColor: '#fff' }]}>
56
- <Text style={[s.modeText, on && { color: C.dark }]}>{m === 'draft' ? 'Draft' : 'Annotate'}</Text>
57
- </Pressable>
58
- );
59
- })}
107
+ <View pointerEvents="box-none" style={[s.modesAt, at.left == null ? s.modesCentred : { left: at.left }, { top: at.below ? TABS_TOP.below : TABS_TOP.row }]}
108
+ onLayout={e => setTopRoom('tabs', e.nativeEvent.layout.width)}>
109
+ <Segmented role="tab" value={is3d() ? '3d' : ed.mode} options={TABS} onPick={pick}
110
+ style={s.modes} optionStyle={s.mode} textStyle={s.modeText} onText={C.dark} offText={C.lg1} />
60
111
  </View>
61
112
  );
62
113
  }
63
114
 
64
- const s = StyleSheet.create({
115
+ const s = createSheet({
116
+ wallCrumb: { flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center', gap: 0, flexShrink: 1, minWidth: 0 },
117
+ wallCrumbPhone: { position: 'absolute', top: 54, left: 0, maxWidth: 260 },
118
+ wallTitle: { fontFamily: FONT, fontSize: 14, fontWeight: '700', color: '#fff' },
119
+ wallSub: { fontFamily: FONT, fontSize: 12, color: C.lg1 },
120
+ flipText: { fontFamily: FONT, fontSize: 14, fontWeight: '600', color: '#fff', marginLeft: 6 },
121
+ saveText: { fontFamily: FONT, fontSize: 14, fontWeight: '600', color: C.dark, marginLeft: 6 },
65
122
  top: { pointerEvents: 'box-none', position: 'absolute', top: 12, left: 12, right: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10, zIndex: 30 },
66
123
  side: { pointerEvents: 'box-none', flexDirection: 'row', alignItems: 'center', gap: 8, minWidth: 0 },
67
124
  crumb: { height: 46, borderRadius: 24, backgroundColor: C.g1, paddingHorizontal: 16, flexDirection: 'row', alignItems: 'center', gap: 7, maxWidth: 420, ...shadow },
68
125
  crumbProj: { fontFamily: FONT, fontSize: 14, color: C.lg1, flexShrink: 1 },
69
126
  crumbSep: { fontFamily: FONT, fontSize: 14, color: C.g4 },
70
127
  crumbPage: { fontFamily: FONT, fontSize: 14, color: '#fff', fontWeight: '600', flexShrink: 1 },
71
- more: { minWidth: 0, paddingHorizontal: 16, gap: 14 },
72
- modes: { position: 'absolute', top: 12, alignSelf: 'center', flexDirection: 'row', backgroundColor: C.g1, borderRadius: 24, padding: 4, height: 46, zIndex: 31, ...shadow },
73
- mode: { paddingHorizontal: 18, borderRadius: 20, justifyContent: 'center' },
128
+ modesAt: { position: 'absolute', zIndex: 31 },
129
+ modesCentred: { alignSelf: 'center' },
130
+ modes: { flexDirection: 'row', backgroundColor: C.g1, borderRadius: 24, padding: 4, height: 46, ...shadow },
131
+ mode: { paddingHorizontal: 18, borderRadius: 999, justifyContent: 'center' },
74
132
  modeText: { fontFamily: FONT, fontSize: 14, fontWeight: '600', color: C.lg1 },
75
133
  });
@@ -0,0 +1,36 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import type { ViewProps } from 'react-native';
3
+ import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
4
+ import { DURATION, type TweenTo } from './easing';
5
+
6
+ /*
7
+ TweenView on a device (see Tween.tsx): its box eases on the UI thread with Reanimated, so a rail growing longer or a
8
+ segmented control's highlight sliding never waits for the JavaScript thread. Same curve as the rest of the chrome.
9
+ */
10
+ const EASE_UI = Easing.bezier(0.2, 0, 0, 1);
11
+
12
+ export function TweenView({ to, duration = DURATION, style, ...rest }: ViewProps & { to: TweenTo; duration?: number }) {
13
+ const left = useSharedValue(to.left ?? 0), top = useSharedValue(to.top ?? 0);
14
+ const width = useSharedValue(to.width ?? 0), height = useSharedValue(to.height ?? 0);
15
+ const hasL = to.left != null, hasT = to.top != null, hasW = to.width != null, hasH = to.height != null;
16
+ // (each set at once the first time it's given, as in a browser — one given after the first render too)
17
+ const given = useRef({ left: hasL, top: hasT, width: hasW, height: hasH }).current;
18
+ useEffect(() => {
19
+ const cfg = { duration, easing: EASE_UI };
20
+ const go = (k: keyof typeof given, v: { value: number }, to: number | undefined) => {
21
+ if (to == null) return;
22
+ v.value = given[k] ? withTiming(to, cfg) : to;
23
+ given[k] = true;
24
+ };
25
+ go('left', left, to.left); go('top', top, to.top); go('width', width, to.width); go('height', height, to.height);
26
+ }, [to.left, to.top, to.width, to.height]); // eslint-disable-line react-hooks/exhaustive-deps
27
+ const box = useAnimatedStyle(() => {
28
+ const o: { left?: number; top?: number; width?: number; height?: number } = {};
29
+ if (hasL) o.left = left.value;
30
+ if (hasT) o.top = top.value;
31
+ if (hasW) o.width = width.value;
32
+ if (hasH) o.height = height.value;
33
+ return o;
34
+ });
35
+ return <Animated.View {...rest} style={[style, box]} />;
36
+ }
@@ -0,0 +1,24 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { Animated, type ViewProps } from 'react-native';
3
+ import { DURATION, EASE, type TweenTo } from './easing';
4
+
5
+ /*
6
+ A view whose size and place ease to each new value: what a transform can't do — a rail growing longer, the highlight
7
+ of a segmented control moving to a wider option. In a browser JavaScript draws every frame anyway; on a device the
8
+ same view runs on the UI thread (Tween.native.tsx), so it never waits for the JavaScript thread.
9
+ */
10
+
11
+ const KEYS = ['left', 'top', 'width', 'height'] as const;
12
+
13
+ /** `to`: the box it eases to (set at once the first time); `duration` in ms. */
14
+ export function TweenView({ to, duration = DURATION, style, ...rest }: ViewProps & { to: TweenTo; duration?: number }) {
15
+ const vals = useRef<Partial<Record<(typeof KEYS)[number], Animated.Value>>>({}).current;
16
+ for (const k of KEYS) if (to[k] != null && !vals[k]) vals[k] = new Animated.Value(to[k]!);
17
+ useEffect(() => {
18
+ for (const k of KEYS) {
19
+ const v = vals[k];
20
+ if (v && to[k] != null) Animated.timing(v, { toValue: to[k]!, duration, easing: EASE, useNativeDriver: false }).start();
21
+ }
22
+ }, [to.left, to.top, to.width, to.height]); // eslint-disable-line react-hooks/exhaustive-deps
23
+ return <Animated.View {...rest} style={[style, vals]} />;
24
+ }
@@ -0,0 +1,131 @@
1
+ import { useEffect, useMemo, useRef, useState, type ComponentType } from 'react';
2
+ import { PixelRatio, StyleSheet, View, type LayoutChangeEvent, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
4
+ import type { WebGLRenderer } from 'three';
5
+ import { toast } from '../editor/ports';
6
+ import { view3dKey } from '../editor/view3d';
7
+ import { rendererOn, sizeOn } from '../view3d/viewer';
8
+ import { useEditorKey } from './store';
9
+ import { View3DHost } from './View3DHost';
10
+
11
+ /*
12
+ The 3D tab on a device: an expo-gl view over the plan (which stays as it was underneath), drawn by three.js on the
13
+ JavaScript thread only when something changed. One finger turns the model, two move it and pinch to zoom, a tap
14
+ picks what's under it, a double tap frames everything.
15
+
16
+ Sized from its layout at the screen's pixel ratio, never from gl.drawingBufferWidth: expo-gl sets that once, as the
17
+ view is made, and leaves it as the device turns (iOS resizes the buffer itself as the view lays out; Android's
18
+ surface follows the view).
19
+
20
+ The GL view is made only once this view has a size, as react-three-fiber makes its own: resized after it's made,
21
+ expo-gl (iOS) deletes the framebuffer three.js draws to, makes another and binds framebuffers behind three.js's back,
22
+ and three.js keeps track of what it thinks is bound. So each resize (a device turned) binds the default framebuffer
23
+ afresh (sizeOn — not three.js's resetState(), which reads gl.canvas: expo-gl's context has none, and it threw), and
24
+ the frames just after are drawn again, in case one went while expo-gl was making its buffers — this view draws
25
+ only when something changes, so a frame lost stays lost. Anything that goes wrong making or sizing it is said in a
26
+ toast, never thrown: thrown from a GL view's callbacks, it takes the app down.
27
+
28
+ Left, the tab's view is kept (hidden, untouchable, drawing nothing: `active`), so coming back is at once — made
29
+ again each time, its GL context, renderer and shaders took a pause every time the tab opened. CadEditor lets it go
30
+ after a while away.
31
+
32
+ expo-gl comes from the host app (BOLDR has it, for its layout viewer); its types aren't installed here, so what's
33
+ used of it is written out below.
34
+ */
35
+
36
+ /** expo-gl's context: WebGL 2, plus the call that puts a drawn frame on the screen. */
37
+ interface ExpoGL extends WebGL2RenderingContext { endFrameEXP(): void }
38
+ interface GLViewProps { style?: StyleProp<ViewStyle>; onContextCreate: (gl: ExpoGL) => void; msaaSamples?: number }
39
+ declare const require: (id: 'expo-gl') => { GLView: ComponentType<GLViewProps> };
40
+ const { GLView } = require('expo-gl');
41
+
42
+ export function View3D({ theme, active = true }: { theme: 'light' | 'dark'; active?: boolean }) {
43
+ const host = useRef<View3DHost | null>(null), renderer = useRef<WebGLRenderer | null>(null), context = useRef<ExpoGL | null>(null);
44
+ const size = useRef({ w: 0, h: 0 }), themeNow = useRef(theme), failed = useRef(false);
45
+ // (the GL view waits for this view's size: see above)
46
+ const [laidOut, setLaidOut] = useState(false);
47
+ themeNow.current = theme;
48
+ const key = useEditorKey(view3dKey);
49
+ const drop = () => { host.current?.dispose(); renderer.current?.dispose(); host.current = null; renderer.current = null; context.current = null; };
50
+ useEffect(() => drop, []);
51
+ // (hidden, it follows nothing: shown again, it catches up with the plan at once)
52
+ const activeNow = useRef(active);
53
+ activeNow.current = active;
54
+ useEffect(() => {
55
+ host.current?.setVisible(active);
56
+ if (active) host.current?.sync(theme);
57
+ }, [key, theme, active]);
58
+
59
+ /** What went wrong making or sizing the view, in a toast (once): see above. */
60
+ const fail = (e: unknown) => {
61
+ console.error('3D view:', e);
62
+ if (failed.current) return;
63
+ failed.current = true;
64
+ toast('The 3D view couldn’t start (' + (e instanceof Error ? e.message : String(e)) + '). Leave the tab and open it again to retry.', { warn: true });
65
+ };
66
+ /** The drawing as big as the view (in points, at the screen's pixel density): again as the device turns. */
67
+ const fit = () => {
68
+ const { w, h } = size.current;
69
+ if (!renderer.current || !context.current || w < 1 || h < 1) return;
70
+ try {
71
+ sizeOn(renderer.current, context.current, w, h, PixelRatio.get());
72
+ host.current?.resize(w, h);
73
+ host.current?.settle();
74
+ } catch (e) { fail(e); }
75
+ };
76
+ const onContextCreate = (gl: ExpoGL) => {
77
+ drop();
78
+ try {
79
+ renderer.current = rendererOn(gl);
80
+ context.current = gl;
81
+ // (the GL view is made after this view's first layout: its size is known)
82
+ host.current = new View3DHost(renderer.current, themeNow.current, () => gl.endFrameEXP());
83
+ host.current.setVisible(activeNow.current);
84
+ fit();
85
+ host.current.sync(themeNow.current);
86
+ } catch (e) {
87
+ drop();
88
+ fail(e);
89
+ }
90
+ };
91
+ const onLayout = (e: LayoutChangeEvent) => {
92
+ const { width, height } = e.nativeEvent.layout;
93
+ size.current = { w: width, h: height };
94
+ if (width >= 1 && height >= 1) setLaidOut(true);
95
+ fit();
96
+ };
97
+
98
+ const gesture = useMemo(() => {
99
+ // the chrome steps aside while either moves the camera: a pinch with its fingers spreading evenly needn't move
100
+ // their middle far enough to start the pan
101
+ let panning = false, pinching = false;
102
+ const moving = () => host.current?.moving(panning || pinching);
103
+ // one finger turns the model, two move it (and pinch to zoom, at the same time)
104
+ const pan = Gesture.Pan().runOnJS(true).minPointers(1).maxPointers(2).averageTouches(true)
105
+ .onStart(() => { panning = true; moving(); })
106
+ .onChange(e => { if (e.numberOfPointers >= 2) host.current?.pan(e.changeX, e.changeY); else host.current?.orbit(e.changeX, e.changeY); })
107
+ .onFinalize(() => { panning = false; moving(); });
108
+ const pinch = Gesture.Pinch().runOnJS(true)
109
+ .onStart(() => { pinching = true; moving(); })
110
+ .onChange(e => { if (e.scaleChange > 0) host.current?.zoom(1 / e.scaleChange, e.focalX, e.focalY); })
111
+ .onFinalize(() => { pinching = false; moving(); });
112
+ // a tap is told at once, and the host makes a second one soon after a double tap: a double-tap gesture would hold
113
+ // every single tap back until it failed (half a second)
114
+ const tap = Gesture.Tap().runOnJS(true).maxDuration(400).onEnd((e, ok) => { if (ok) host.current?.tapAt(e.x, e.y); });
115
+ return Gesture.Race(Gesture.Simultaneous(pan, pinch), tap);
116
+ }, []);
117
+
118
+ return (
119
+ <GestureDetector gesture={gesture}>
120
+ <View style={[s.fill, !active && s.away]} pointerEvents={active ? 'auto' : 'none'} aria-hidden={!active}
121
+ onLayout={onLayout} collapsable={false} testID="view3d" accessibilityLabel="3D view of the plan">
122
+ {laidOut && <GLView style={s.fill} onContextCreate={onContextCreate} msaaSamples={4} />}
123
+ </View>
124
+ </GestureDetector>
125
+ );
126
+ }
127
+
128
+ // (no zIndex: it's drawn after the plan and before the chrome, which stays on top. Spelt out: React Native 0.86
129
+ // dropped StyleSheet.absoluteFillObject, and with it undefined the view got no style — the screen's width, no height —
130
+ // so the 3D tab showed nothing over the plan.)
131
+ const s = StyleSheet.create({ fill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, away: { opacity: 0 } });
@@ -0,0 +1,131 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { WebGLRenderer } from 'three';
3
+ import { toast } from '../editor/ports';
4
+ import { view3dKey } from '../editor/view3d';
5
+ import { useEditorKey } from './store';
6
+ import { View3DHost } from './View3DHost';
7
+
8
+ /*
9
+ The 3D tab in a browser: a WebGL <canvas> over the plan (which stays as it was underneath). A finger or the mouse
10
+ drags to turn the model; two fingers, the right or middle button, or Shift and a drag move it; a pinch, the wheel or
11
+ a trackpad's pinch zooms toward the pointer. A tap picks what's under it; a double tap frames everything.
12
+
13
+ Left, the tab's canvas is kept (hidden, untouchable, drawing nothing: `active`), so coming back is at once — made
14
+ again each time, its WebGL context, renderer and shaders took a pause every time the tab opened. CadEditor lets it
15
+ go after a while away.
16
+ */
17
+
18
+ /** Moving less than this (points) between press and release is a tap (a double tap is the host's: View3DHost.tapAt). */
19
+ const TAP_SLOP = 6;
20
+
21
+ export function View3D({ theme, active = true }: { theme: 'light' | 'dark'; active?: boolean }) {
22
+ const ref = useRef<HTMLCanvasElement>(null), host = useRef<View3DHost | null>(null), activeNow = useRef(active);
23
+ activeNow.current = active;
24
+ const key = useEditorKey(view3dKey);
25
+ useEffect(() => {
26
+ const cv = ref.current!;
27
+ let renderer: WebGLRenderer;
28
+ try {
29
+ renderer = new WebGLRenderer({ canvas: cv, antialias: true, powerPreference: 'high-performance' });
30
+ } catch {
31
+ toast('3D needs WebGL, which this browser has turned off.', { warn: true });
32
+ return;
33
+ }
34
+ renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
35
+ const h = new View3DHost(renderer, theme);
36
+ host.current = h;
37
+ h.setVisible(activeNow.current);
38
+ // (only for a new size: setting a canvas's size makes its drawing buffer again and clears it, the same size or not —
39
+ // and the observer reports the size just set as it starts)
40
+ let was = { w: -1, h: -1 };
41
+ const size = () => {
42
+ const w = cv.clientWidth, ht = cv.clientHeight;
43
+ if (w === was.w && ht === was.h) return;
44
+ was = { w, h: ht };
45
+ renderer.setSize(w, ht, false);
46
+ h.resize(w, ht);
47
+ };
48
+ size();
49
+ h.sync(theme);
50
+ const ro = new ResizeObserver(size);
51
+ ro.observe(cv);
52
+
53
+ // the pointers down, and what they're doing
54
+ const ac = new AbortController(), on = { signal: ac.signal };
55
+ const pts = new Map<number, { x: number; y: number }>();
56
+ let mode: 'none' | 'orbit' | 'pan' | 'pinch' = 'none', moved = false, downAt = 0, start = { x: 0, y: 0 };
57
+ let pinch = { d: 0, x: 0, y: 0 };
58
+ const at = (e: PointerEvent | WheelEvent) => { const r = cv.getBoundingClientRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; };
59
+ const two = () => {
60
+ const [a, b] = [...pts.values()];
61
+ return { d: Math.hypot(b.x - a.x, b.y - a.y), x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
62
+ };
63
+ cv.addEventListener('pointerdown', e => {
64
+ cv.focus({ preventScroll: true });
65
+ try { cv.setPointerCapture(e.pointerId); } catch { /* not capturable (synthetic) */ }
66
+ const p = at(e);
67
+ pts.set(e.pointerId, p);
68
+ if (pts.size === 1) {
69
+ mode = e.button === 1 || e.button === 2 || e.shiftKey ? 'pan' : 'orbit';
70
+ moved = false; downAt = performance.now(); start = p;
71
+ } else if (pts.size === 2) {
72
+ // (two fingers never tap)
73
+ mode = 'pinch'; moved = true; pinch = two();
74
+ h.moving(true);
75
+ }
76
+ }, on);
77
+ cv.addEventListener('pointermove', e => {
78
+ const prev = pts.get(e.pointerId);
79
+ if (!prev) return;
80
+ const p = at(e);
81
+ pts.set(e.pointerId, p);
82
+ if (mode === 'pinch' && pts.size >= 2) {
83
+ const now = two();
84
+ if (pinch.d > 0 && now.d > 0) h.zoom(pinch.d / now.d, now.x, now.y);
85
+ h.pan(now.x - pinch.x, now.y - pinch.y);
86
+ pinch = now;
87
+ return;
88
+ }
89
+ if (mode !== 'orbit' && mode !== 'pan') return;
90
+ if (!moved) {
91
+ if (Math.hypot(p.x - start.x, p.y - start.y) < TAP_SLOP) return;
92
+ moved = true;
93
+ h.moving(true);
94
+ }
95
+ if (mode === 'orbit') h.orbit(p.x - prev.x, p.y - prev.y);
96
+ else h.pan(p.x - prev.x, p.y - prev.y);
97
+ }, on);
98
+ const up = (e: PointerEvent, cancel: boolean) => {
99
+ if (!pts.has(e.pointerId)) return;
100
+ pts.delete(e.pointerId);
101
+ if (!cancel && (mode === 'orbit' || mode === 'pan') && !moved && performance.now() - downAt < 600) {
102
+ const p = at(e);
103
+ h.tapAt(p.x, p.y);
104
+ }
105
+ if (pts.size === 1 && mode === 'pinch') { mode = 'orbit'; return; } // (one finger left: it turns the model)
106
+ if (!pts.size) { if (moved) h.moving(false); mode = 'none'; }
107
+ };
108
+ cv.addEventListener('pointerup', e => up(e, false), on);
109
+ cv.addEventListener('pointercancel', e => up(e, true), on);
110
+ cv.addEventListener('contextmenu', e => e.preventDefault(), on);
111
+ // (a browser can take the GL context back — Safari does in the background — and three.js makes it again: draw then)
112
+ cv.addEventListener('webglcontextrestored', () => h.redraw(), on);
113
+ cv.addEventListener('wheel', e => {
114
+ e.preventDefault();
115
+ const p = at(e), d = e.deltaY || e.deltaX, unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 400 : 1;
116
+ // a trackpad's pinch arrives as the wheel with Ctrl held, in smaller steps
117
+ h.zoom(Math.exp(d * unit * (e.ctrlKey ? 0.01 : 0.0015)), p.x, p.y);
118
+ }, { passive: false, signal: ac.signal });
119
+ return () => { ac.abort(); ro.disconnect(); h.dispose(); renderer.dispose(); host.current = null; };
120
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
121
+ // (hidden, it follows nothing: shown again, it catches up with the plan at once)
122
+ useEffect(() => {
123
+ host.current?.setVisible(active);
124
+ if (active) host.current?.sync(theme);
125
+ }, [key, theme, active]);
126
+ return (
127
+ <canvas ref={ref} tabIndex={-1} aria-label="3D view of the plan" aria-hidden={!active} data-testid="view3d" id="cv3d"
128
+ style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', touchAction: 'none', display: 'block', outline: 'none',
129
+ visibility: active ? 'visible' : 'hidden', pointerEvents: active ? 'auto' : 'none' }} />
130
+ );
131
+ }