@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
@@ -2,7 +2,7 @@ import { isDim, measure, relatedCons } from '../model/constraints';
2
2
  import { addCons, restorePos, snapshotPos } from '../model/sketch';
3
3
  import type { Analysis, Constraint, Sketch, XY } from '../model/types';
4
4
  import { buildSys, getX, type System, type Vec } from './equations';
5
- import { jac, rref, solveDense, sq, type Row } from './linalg';
5
+ import { blocks, compactRows, envAt, envelopeOf, jac, rref, rrefTol, solveDense, solveEnvelope, sq, type Envelope, type Row } from './linalg';
6
6
  import type { AddResult, SolveResult, Solver } from './types';
7
7
 
8
8
  /**
@@ -13,24 +13,32 @@ export function gnSolve(sys: System, X: Vec, W: Float64Array, maxIt = 60): { X:
13
13
  const m = sys.eqs.length, n = sys.n;
14
14
  const evalR = (Y: Vec) => sys.eqs.map(e => e.f(Y));
15
15
  let r = evalR(X), nr = sq(r);
16
- for (let it = 0; it < maxIt && nr > 1e-22; it++) {
16
+ let env: Envelope | null = null;
17
+ // (solved to 1e-10 — far finer than the 1e-6 a solve must reach: past that, steps only chase rounding error)
18
+ for (let it = 0; it < maxIt && nr > 1e-20; it++) {
17
19
  const J = jac(sys, X);
18
- const A = new Float64Array(m * m);
19
- for (let i = 0; i < m; i++) {
20
- const Ji = J[i];
21
- for (let k = 0; k <= i; k++) {
22
- const Jk = J[k];
23
- let s = 0;
24
- const sm = Ji.size < Jk.size ? Ji : Jk, lg = sm === Ji ? Jk : Ji;
25
- for (const [v, a] of sm) { const b = lg.get(v); if (b !== undefined) s += (a * b) / W[v]; }
26
- A[i * m + k] = s; A[k * m + i] = s;
20
+ // A = J W⁻¹ Jᵀ, summed variable by variable: only the equations that share a variable meet in A, and each variable
21
+ // is in a handful of them — so it costs about as much as J has entries, not every pair of equations (which, on a
22
+ // plan of a few dozen rooms, made each step of a drag take longer than a frame)
23
+ const cols: { row: number[]; val: number[] }[] = Array.from({ length: n }, () => ({ row: [], val: [] }));
24
+ for (let i = 0; i < m; i++) for (const [v, a] of J[i]) { cols[v].row.push(i); cols[v].val.push(a); }
25
+ // kept in the envelope of its band (see envelopeOf) and factored there: solving A densely took time growing with
26
+ // the cube of the equations — most of each step of a drag on a big plan. Dense if the band's factor breaks down.
27
+ env ??= envelopeOf(sys.eqs.map(e => e.deps), n);
28
+ const E = new Float64Array(env.size), at = (i: number, k: number) => (i >= k ? envAt(env!, i, k) : envAt(env!, k, i));
29
+ for (let v = 0; v < n; v++) {
30
+ const { row, val } = cols[v], w = W[v];
31
+ for (let p = 0; p < row.length; p++) {
32
+ const i = env.pos[row[p]], ai = val[p] / w;
33
+ E[at(i, i)] += ai * val[p];
34
+ for (let q = 0; q < p; q++) E[at(i, env.pos[row[q]])] += ai * val[q];
27
35
  }
28
36
  }
29
37
  let md = 0;
30
- for (let i = 0; i < m; i++) md = Math.max(md, A[i * m + i]);
38
+ for (let i = 0; i < m; i++) md = Math.max(md, E[envAt(env, i, i)]);
31
39
  const mu = 1e-8 * (md || 1) + 1e-14;
32
- for (let i = 0; i < m; i++) A[i * m + i] += mu;
33
- const y = solveDense(A, Float64Array.from(r), m);
40
+ for (let i = 0; i < m; i++) E[envAt(env, i, i)] += mu;
41
+ const y = solveEnvelope(env, E, r) ?? solveDense(denseNormal(J, W, m, n, mu), Float64Array.from(r), m);
34
42
  if (!y) break;
35
43
  const dx = new Float64Array(n);
36
44
  for (let i = 0; i < m; i++) {
@@ -43,7 +51,7 @@ export function gnSolve(sys: System, X: Vec, W: Float64Array, maxIt = 60): { X:
43
51
  const Xn = new Float64Array(n);
44
52
  for (let j = 0; j < n; j++) Xn[j] = X[j] + step * dx[j];
45
53
  const rn = evalR(Xn), nn = sq(rn);
46
- if (isFinite(nn) && (nn < nr || nn < 1e-22)) { X = Xn; r = rn; nr = nn; ok = true; break; }
54
+ if (isFinite(nn) && (nn < nr || nn < 1e-20)) { X = Xn; r = rn; nr = nn; ok = true; break; }
47
55
  step *= 0.5;
48
56
  }
49
57
  if (!ok) break;
@@ -52,6 +60,23 @@ export function gnSolve(sys: System, X: Vec, W: Float64Array, maxIt = 60): { X:
52
60
  return { X, ok: maxr < 1e-6, res: maxr, r };
53
61
  }
54
62
 
63
+ /** J W⁻¹ Jᵀ + μI as a dense m × m matrix (for solveDense, when the banded factor breaks down). */
64
+ function denseNormal(J: Row[], W: Float64Array, m: number, n: number, mu: number): Float64Array {
65
+ const cols: { row: number[]; val: number[] }[] = Array.from({ length: n }, () => ({ row: [], val: [] }));
66
+ for (let i = 0; i < m; i++) for (const [v, a] of J[i]) { cols[v].row.push(i); cols[v].val.push(a); }
67
+ const A = new Float64Array(m * m);
68
+ for (let v = 0; v < n; v++) {
69
+ const { row, val } = cols[v], w = W[v];
70
+ for (let p = 0; p < row.length; p++) {
71
+ const i = row[p], ai = val[p] / w;
72
+ A[i * m + i] += ai * val[p];
73
+ for (let q = 0; q < p; q++) { const k = row[q], s = ai * val[q]; A[i * m + k] += s; A[k * m + i] += s; }
74
+ }
75
+ }
76
+ for (let i = 0; i < m; i++) A[i * m + i] += mu;
77
+ return A;
78
+ }
79
+
55
80
  export function writeX(sk: Sketch, sys: System, X: Vec): void {
56
81
  for (const [k, i] of sys.idx) {
57
82
  if (k.startsWith('r:')) { const c = sk.circles[k.slice(2)]; if (c) c.r = X[i]; }
@@ -87,11 +112,51 @@ export function culpritsFor(sys0: System, newRows: Row[]): string[] {
87
112
  return [...out];
88
113
  }
89
114
 
115
+ /** The drawing's equations with the new constraint's added ('__new'), and the new ones' Jacobian rows and residuals. */
90
116
  function newRowsFor(sk: Sketch, c: Constraint) {
91
- const sys1 = buildSys(sk, { extra: c }), X = getX(sys1), J = jac(sys1, X);
92
- const rows: Row[] = [], res: number[] = [];
93
- sys1.eqs.forEach((e, i) => { if (e.cid === '__new') { rows.push(J[i]); res.push(Math.abs(e.f(X))); } });
94
- return { sys1, J, rows, res };
117
+ const sys1 = buildSys(sk, { extra: c }), X = getX(sys1);
118
+ const rows = jac({ ...sys1, eqs: sys1.eqs.filter(e => e.cid === '__new') }, X);
119
+ const res = sys1.eqs.filter(e => e.cid === '__new').map(e => Math.abs(e.f(X)));
120
+ return { sys1, X, rows, res };
121
+ }
122
+
123
+ /**
124
+ * How much a new constraint's equations (cid '__new' in `sys1`, rows of `J1`) add to the rank of the others: 0 when
125
+ * they follow from them (a redundant or a conflicting constraint). One that holds a variable no other equation touches
126
+ * adds 1 outright — as most of a new shape's constraints do (a rectangle's corners are new points). Otherwise only the
127
+ * block of the system it joins is reduced: the rest has the same rank with it or without it. Both used to reduce the
128
+ * whole drawing, twice for each constraint — most of the time it took to finish a shape on a big plan.
129
+ */
130
+ export function rankGain(sys1: System, X: Vec, newRows?: Row[]): number {
131
+ const fresh: number[] = [], olds: number[] = [];
132
+ sys1.eqs.forEach((e, i) => (e.cid === '__new' ? fresh : olds).push(i));
133
+ if (fresh.length === 1) {
134
+ const row = newRows?.[0] ?? jac({ ...sys1, eqs: [sys1.eqs[fresh[0]]] }, X)[0];
135
+ // its variables it clearly moves (not a derivative lost in rounding, which the full reduction judges against the
136
+ // rest): one that no other equation moves at all here — a new point's, or a y only level walls hold — adds 1.
137
+ // (Only the equations on those variables are differentiated; a level wall depends on its points' x's but doesn't
138
+ // move with them.)
139
+ const strong = [...row].filter(([, v]) => Math.abs(v) >= 1e-3).map(([k]) => k);
140
+ if (strong.length) {
141
+ const want = new Set(strong), touching = olds.filter(i => sys1.eqs[i].deps.some(d => want.has(d)));
142
+ const rows = jac({ ...sys1, eqs: touching.map(i => sys1.eqs[i]) }, X);
143
+ if (strong.some(k => rows.every(r => !r.has(k)))) return 1;
144
+ }
145
+ }
146
+ return rankGainOf(sys1, jac(sys1, X), fresh, olds);
147
+ }
148
+
149
+ function rankGainOf(sys1: System, J1: Row[], fresh: number[], olds: number[]): number {
150
+ const tol0 = rrefTol(olds.map(i => J1[i])), tol1 = rrefTol(J1);
151
+ const isNew = new Set(fresh);
152
+ let r0 = 0, r1 = 0;
153
+ for (const b of blocks(J1, sys1.n)) {
154
+ if (!b.rows.some(i => isNew.has(i))) continue;
155
+ const before = b.rows.filter(i => !isNew.has(i));
156
+ r0 += rref(compactRows(before.map(i => J1[i]), b.cols), b.cols.length, false, tol0).rank;
157
+ r1 += rref(compactRows(b.rows.map(i => J1[i]), b.cols), b.cols.length, false, tol1).rank;
158
+ }
159
+ return r1 - r0;
95
160
  }
96
161
 
97
162
  export class GaussNewtonSolver implements Solver {
@@ -118,8 +183,34 @@ export class GaussNewtonSolver implements Solver {
118
183
  const snap = snapshotPos(sk);
119
184
  ids.forEach((id, i) => { sk.points[id].x = targets[i].x; sk.points[id].y = targets[i].y; });
120
185
  const s = this.solve(sk, ids);
121
- if (!s.ok) restorePos(sk, snap);
122
- return s.ok;
186
+ if (!s.ok) { restorePos(sk, snap); return false; }
187
+ this.settle(sk, ids, snap);
188
+ return true;
189
+ }
190
+
191
+ solveHolding(sk: Sketch, held: string[]): SolveResult {
192
+ const snap = snapshotPos(sk), ids = held.filter(id => sk.points[id]);
193
+ const s = this.solve(sk, ids);
194
+ if (s.ok && ids.length) this.settle(sk, ids, snap);
195
+ return s;
196
+ }
197
+
198
+ /**
199
+ * After a solve that pulled `ids` (held heavy) into line: they stay where they got to, everything else goes back to
200
+ * where it was (`before`) and is solved once more — so the rest moves only as far as the constraints need. Pulling
201
+ * the held points into line, the solve can use any free motion to ease the strain (a cabinet's sideways slack; a
202
+ * cabinet free to slide up its wall, edge-to-edge collinear with another), and that would otherwise stay behind —
203
+ * a little more at each step of a drag: things tied to what's dragged gliding when they shouldn't move at all.
204
+ */
205
+ private settle(sk: Sketch, ids: string[], before: ReturnType<typeof snapshotPos>): void {
206
+ const got = ids.map(id => ({ x: sk.points[id].x, y: sk.points[id].y }));
207
+ const solved = snapshotPos(sk);
208
+ restorePos(sk, before);
209
+ const pts = ids.map(id => sk.points[id]), had = pts.map(p => p.fixed);
210
+ pts.forEach((p, i) => { p.x = got[i].x; p.y = got[i].y; p.fixed = true; });
211
+ const settled = this.solve(sk);
212
+ pts.forEach((p, i) => { if (had[i]) p.fixed = true; else delete p.fixed; });
213
+ if (!settled.ok) restorePos(sk, solved);
123
214
  }
124
215
 
125
216
  analyzeDOF(sk: Sketch): Analysis {
@@ -128,11 +219,18 @@ export class GaussNewtonSolver implements Solver {
128
219
  for (const id in sk.points) if (sk.points[id].fixed) det.add(id);
129
220
  let dof = sys.n, ok = true;
130
221
  if (sys.eqs.length) {
131
- const J = jac(sys, X), R = rref(J, sys.n, true);
132
- dof = sys.n - R.rank;
222
+ // reduced block by block (see blocks): the same rank and determined variables as the whole at once
223
+ const J = jac(sys, X), tol = rrefTol(J), detV = new Uint8Array(sys.n);
224
+ let rank = 0;
225
+ for (const b of blocks(J, sys.n)) {
226
+ const R = rref(compactRows(b.rows.map(i => J[i]), b.cols), b.cols.length, true, tol);
227
+ rank += R.rank;
228
+ b.cols.forEach((c, k) => { if (R.det![k]) detV[c] = 1; });
229
+ }
230
+ dof = sys.n - rank;
133
231
  for (const [k, i] of sys.idx) {
134
- if (k.startsWith('r:')) { if (R.det![i]) detR.add(k.slice(2)); }
135
- else if (R.det![i] && R.det![i + 1]) det.add(k);
232
+ if (k.startsWith('r:')) { if (detV[i]) detR.add(k.slice(2)); }
233
+ else if (detV[i] && detV[i + 1]) det.add(k);
136
234
  }
137
235
  sys.eqs.forEach(e => { if (Math.abs(e.f(X)) > 1e-5) { bad.add(e.cid); ok = false; } });
138
236
  }
@@ -140,10 +238,9 @@ export class GaussNewtonSolver implements Solver {
140
238
  }
141
239
 
142
240
  addConstraint(sk: Sketch, c: Constraint, { explain = false }: { explain?: boolean } = {}): AddResult {
143
- const sys0 = buildSys(sk), r0 = rref(jac(sys0, getX(sys0)), sys0.n, false).rank;
144
241
  const N = newRowsFor(sk, c);
145
242
  if (!N.rows.length) return { ok: false };
146
- const gain = rref(N.J, N.sys1.n, false).rank - r0;
243
+ const gain = rankGain(N.sys1, N.X, N.rows);
147
244
  const resid = Math.max(...N.res);
148
245
  if (gain === 0) {
149
246
  if (isDim(c)) {
@@ -155,7 +252,7 @@ export class GaussNewtonSolver implements Solver {
155
252
  }
156
253
  if (resid < 1e-6) return { ok: false, redundant: true };
157
254
  let culprits: string[] = [];
158
- if (explain) { culprits = culpritsFor(sys0, N.rows); if (!culprits.length) culprits = relatedCons(sk, c); }
255
+ if (explain) { culprits = culpritsFor(buildSys(sk), N.rows); if (!culprits.length) culprits = relatedCons(sk, c); }
159
256
  return { ok: false, conflict: 'dependent', culprits };
160
257
  }
161
258
  const snap = snapshotPos(sk);
@@ -54,13 +54,20 @@ export const sq = (r: ArrayLike<number>): number => {
54
54
  return s;
55
55
  };
56
56
 
57
- /** Reduced row echelon form: rank, pivot columns and (optionally) which variables are fully determined. */
58
- export function rref(rows: Row[], n: number, needDet: boolean): { rank: number; piv: number[]; det: Uint8Array | null } {
57
+ /** What rref counts as zero for these rows: a billionth of their largest entry (or of 1). */
58
+ export function rrefTol(rows: Row[]): number {
59
+ let maxAbs = 0;
60
+ for (const r of rows) for (const v of r.values()) { const a = Math.abs(v); if (a > maxAbs) maxAbs = a; }
61
+ return 1e-9 * Math.max(1, maxAbs);
62
+ }
63
+
64
+ /**
65
+ * Reduced row echelon form: rank, pivot columns and (optionally) which variables are fully determined. `tol`: what
66
+ * counts as zero (by default rrefTol of these rows — pass the whole system's when reducing it block by block).
67
+ */
68
+ export function rref(rows: Row[], n: number, needDet: boolean, tol = rrefTol(rows)): { rank: number; piv: number[]; det: Uint8Array | null } {
59
69
  const m = rows.length;
60
70
  const M = rows.map(r => { const a = new Float64Array(n); r.forEach((v, k) => (a[k] = v)); return a; });
61
- let maxAbs = 0;
62
- for (const a of M) for (let k = 0; k < n; k++) { const v = Math.abs(a[k]); if (v > maxAbs) maxAbs = v; }
63
- const tol = 1e-9 * Math.max(1, maxAbs);
64
71
  const piv: number[] = [];
65
72
  let r = 0;
66
73
  for (let col = 0; col < n && r < m; col++) {
@@ -94,3 +101,132 @@ export function rref(rows: Row[], n: number, needDet: boolean): { rank: number;
94
101
  }
95
102
  return { rank: r, piv, det };
96
103
  }
104
+
105
+ /**
106
+ * The system's independent blocks: groups of rows that share no variable with any other group (directly or through
107
+ * other rows), each with its variables (ascending). A plan's level and plumb walls set its x's apart from its y's, and
108
+ * separate drawings on a page are separate blocks: a block's rank and reduced form don't depend on the rest, so the
109
+ * system is reduced block by block, at a fraction of the cost of reducing it whole. Rows with no entries are left out.
110
+ */
111
+ export function blocks(rows: Row[], n: number): { rows: number[]; cols: number[] }[] {
112
+ const parent = new Int32Array(n);
113
+ for (let i = 0; i < n; i++) parent[i] = i;
114
+ const find = (x: number): number => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; };
115
+ for (const r of rows) {
116
+ let root = -1;
117
+ for (const k of r.keys()) { const f = find(k); if (root < 0) root = f; else if (f !== root) parent[f] = root; }
118
+ }
119
+ const byRoot = new Map<number, { rows: number[]; cols: number[] }>();
120
+ rows.forEach((r, i) => {
121
+ for (const k of r.keys()) {
122
+ const root = find(k);
123
+ let b = byRoot.get(root);
124
+ if (!b) byRoot.set(root, (b = { rows: [], cols: [] }));
125
+ b.rows.push(i);
126
+ break;
127
+ }
128
+ });
129
+ for (let c = 0; c < n; c++) { const b = byRoot.get(find(c)); if (b) b.cols.push(c); }
130
+ return [...byRoot.values()];
131
+ }
132
+
133
+ /** Rows renumbered onto `cols` (ascending): variable cols[k] becomes k. */
134
+ export function compactRows(rows: Row[], cols: number[]): Row[] {
135
+ const at = new Map<number, number>();
136
+ cols.forEach((c, k) => at.set(c, k));
137
+ return rows.map(r => {
138
+ const o: Row = new Map();
139
+ r.forEach((v, c) => { const k = at.get(c); if (k !== undefined) o.set(k, v); });
140
+ return o;
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Where the nonzeros of J W⁻¹ Jᵀ can be — equations that share a variable — with the equations reordered (reverse
146
+ * Cuthill–McKee) so they sit close to the diagonal. Its LDLᵀ factor fills in only within each row's envelope (from the
147
+ * row's first nonzero to the diagonal): a narrow band for a drawing's equations, which mostly tie a point to its
148
+ * neighbours, with equations that share nothing in separate blocks of it. Worked out once a solve, from the variables
149
+ * each equation depends on (the same at every step).
150
+ */
151
+ export interface Envelope {
152
+ m: number;
153
+ /** pos[equation] = its place in the new order; ord[place] = the equation there */
154
+ pos: Int32Array; ord: Int32Array;
155
+ /** each row's (in the new order) first column, and where its entries (first … diagonal) start in the values */
156
+ first: Int32Array; start: Int32Array;
157
+ size: number;
158
+ }
159
+
160
+ /** Where entry (i, k) of the envelope (new order, k ≤ i) is kept in its values. */
161
+ export const envAt = (env: Envelope, i: number, k: number): number => env.start[i] + (k - env.first[i]);
162
+
163
+ export function envelopeOf(deps: number[][], n: number): Envelope {
164
+ const m = deps.length;
165
+ const byVar: number[][] = Array.from({ length: n }, () => []);
166
+ deps.forEach((ds, i) => { for (const v of ds) byVar[v].push(i); });
167
+ const near: Set<number>[] = Array.from({ length: m }, () => new Set<number>());
168
+ for (const eqs of byVar) for (let p = 0; p < eqs.length; p++) for (let q = p + 1; q < eqs.length; q++) { near[eqs[p]].add(eqs[q]); near[eqs[q]].add(eqs[p]); }
169
+ const adj = near.map(s => [...s]), deg = adj.map(a => a.length);
170
+ // Cuthill–McKee: breadth first from a least-connected equation of each block, fewest neighbours first; then reversed
171
+ const order: number[] = [], seen = new Uint8Array(m);
172
+ const starts = Array.from({ length: m }, (_, i) => i).sort((a, b) => deg[a] - deg[b] || a - b);
173
+ for (const s of starts) {
174
+ if (seen[s]) continue;
175
+ seen[s] = 1;
176
+ const q = [s];
177
+ for (let h = 0; h < q.length; h++) {
178
+ const next = adj[q[h]].filter(v => !seen[v]).sort((a, b) => deg[a] - deg[b] || a - b);
179
+ for (const v of next) { seen[v] = 1; q.push(v); }
180
+ }
181
+ for (const e of q) order.push(e);
182
+ }
183
+ order.reverse();
184
+ const ord = Int32Array.from(order), pos = new Int32Array(m);
185
+ ord.forEach((e, i) => { pos[e] = i; });
186
+ const first = new Int32Array(m), start = new Int32Array(m + 1);
187
+ for (let i = 0; i < m; i++) {
188
+ let f = i;
189
+ for (const e of adj[ord[i]]) { const j = pos[e]; if (j < f) f = j; }
190
+ first[i] = f;
191
+ start[i + 1] = start[i] + (i - f + 1);
192
+ }
193
+ return { m, pos, ord, first, start, size: start[m] };
194
+ }
195
+
196
+ /**
197
+ * Solve A y = b for a symmetric positive definite A kept in `env`'s envelope (its lower triangle, `val`, row by row in
198
+ * the new order — overwritten by the LDLᵀ factor); b and y in the equations' own order. Null if a pivot isn't positive
199
+ * (A isn't numerically positive definite), for the caller to solve it another way.
200
+ */
201
+ export function solveEnvelope(env: Envelope, val: Float64Array, b: ArrayLike<number>): Float64Array | null {
202
+ const { m, first, start, ord } = env, D = new Float64Array(m);
203
+ for (let i = 0; i < m; i++) {
204
+ const fi = first[i], si = start[i] - fi;
205
+ for (let j = fi; j < i; j++) {
206
+ const fj = first[j], sj = start[j] - fj;
207
+ let s = val[si + j];
208
+ for (let k = fi > fj ? fi : fj; k < j; k++) s -= val[si + k] * val[sj + k] * D[k];
209
+ val[si + j] = s / D[j];
210
+ }
211
+ let d = val[si + i];
212
+ for (let k = fi; k < i; k++) { const l = val[si + k]; d -= l * l * D[k]; }
213
+ if (!(d > 0) || !Number.isFinite(d)) return null;
214
+ D[i] = d;
215
+ }
216
+ // L z = b, then D, then Lᵀ y = z (all in the new order)
217
+ const z = new Float64Array(m);
218
+ for (let i = 0; i < m; i++) {
219
+ const fi = first[i], si = start[i] - fi;
220
+ let s = b[ord[i]];
221
+ for (let k = fi; k < i; k++) s -= val[si + k] * z[k];
222
+ z[i] = s;
223
+ }
224
+ for (let i = 0; i < m; i++) z[i] /= D[i];
225
+ for (let i = m - 1; i >= 0; i--) {
226
+ const fi = first[i], si = start[i] - fi, zi = z[i];
227
+ for (let k = fi; k < i; k++) z[k] -= val[si + k] * zi;
228
+ }
229
+ const y = new Float64Array(m);
230
+ for (let i = 0; i < m; i++) y[ord[i]] = z[i];
231
+ return y;
232
+ }
@@ -35,6 +35,11 @@ export interface Solver {
35
35
  solve(sk: Sketch, heavy?: string[]): SolveResult;
36
36
  /** Move points toward targets and re-solve; reverts and returns false if that can't be satisfied. */
37
37
  drag(sk: Sketch, ids: string[], targets: XY[]): boolean;
38
+ /**
39
+ * Solve with the points `held` kept where they are as far as the constraints allow (heavy), and the rest moved only
40
+ * as far as the constraints need: nothing free drifts while the held ones are put back in line.
41
+ */
42
+ solveHolding(sk: Sketch, held: string[]): SolveResult;
38
43
  analyzeDOF(sk: Sketch): Analysis;
39
44
  /**
40
45
  * Add a constraint safely: independent → add + solve; redundant dimension → reference dimension;
package/src/styles.css CHANGED
@@ -29,9 +29,18 @@ html{scroll-padding-top:env(safe-area-inset-top,0px)}
29
29
  *,*::before,*::after{box-sizing:inherit}
30
30
  html,body{height:100%;margin:0}
31
31
  body{background:var(--cv-bg);font-family:var(--font);color:#fff;overflow:hidden;-webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none;overscroll-behavior:none}
32
- #app{position:relative;height:100%;overflow:hidden}
32
+ /* clip, not just hidden: nothing may scroll the editor itself — the closed drawer and the sheets waiting off-screen
33
+ made it scrollable, and scrolling one into view (a test's click, the browser revealing a focused field) shifted the
34
+ whole page sideways under the pointer */
35
+ #app{position:relative;height:100%;overflow:hidden;overflow:clip}
33
36
  #cv{position:absolute;inset:0;width:100%;height:100%;touch-action:none;display:block;outline:none}
34
37
  button{font:inherit;color:inherit;border:0;background:none;padding:0;cursor:pointer;-webkit-tap-highlight-color:transparent}
38
+ /* how the chrome moves (as react/motion.tsx): changes of state move over 150 ms; a press sinks a touch in 70 ms and
39
+ comes back with the same easing; wide rows only dim, switches don't sink */
40
+ button{transition:transform .15s cubic-bezier(.2,0,0,1),opacity .15s cubic-bezier(.2,0,0,1),background-color .15s cubic-bezier(.2,0,0,1),color .15s cubic-bezier(.2,0,0,1)}
41
+ button:active:not([disabled]){transform:scale(.96);opacity:.82;transition-duration:.07s}
42
+ .mlink:active:not([disabled]),.cf-item:active{transform:none;opacity:.7}
43
+ .switch:active:not([disabled]){transform:none;opacity:1}
35
44
  button:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--yellow);outline-offset:2px}
36
45
  svg{display:block;flex-shrink:0}
37
46
  input{font:inherit}
@@ -41,6 +50,8 @@ input{font:inherit}
41
50
  .top>*{pointer-events:auto}
42
51
  .tl,.tr{display:flex;align-items:center;gap:8px;min-width:0}
43
52
  .cbtn{width:46px;height:46px;border-radius:50%;background:var(--g3);border:1px solid rgba(255,255,255,.16);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 2px 10px rgba(0,0,0,.16);color:#fff;position:relative;transition:transform .12s}
53
+ /* a round button whose menu is open (•••, Layers): its icon in yellow, as the React editor shows it */
54
+ .cbtn[aria-expanded="true"]{color:var(--yellow)}
44
55
  .cbtn:active{transform:scale(.93)}
45
56
  .cbtn.bt{background:var(--blue);border-color:transparent}
46
57
  .cbtn.off{color:var(--lg1)}
@@ -70,7 +81,7 @@ input{font:inherit}
70
81
  .hint{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
71
82
 
72
83
  /* left rail */
73
- .rail{position:absolute;left:12px;top:112px;background:var(--g2);border-radius:30px;padding:8px 6px;display:flex;flex-direction:column;align-items:center;gap:0;z-index:22;max-height:calc(100% - 196px);overflow-y:auto;scrollbar-width:none;box-shadow:0 6px 22px rgba(0,0,0,.26)}
84
+ .rail{position:absolute;left:12px;top:112px;background:var(--g2);border-radius:26px;padding:6px;display:flex;flex-direction:column;align-items:center;gap:0;z-index:22;max-height:calc(100% - 196px);overflow-y:auto;scrollbar-width:none;box-shadow:0 6px 22px rgba(0,0,0,.26)}
74
85
  @media (max-height:760px){.rail button{width:38px;height:38px}.rail .div{margin:3px 0}}
75
86
  .rail::-webkit-scrollbar{display:none}
76
87
  .rail button{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff;transition:background .12s}
@@ -85,7 +96,7 @@ input{font:inherit}
85
96
 
86
97
  /* bottom contextual bar */
87
98
  .bottom{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);display:flex;align-items:center;gap:12px;z-index:24;max-width:calc(100% - 180px)}
88
- .ctx{display:flex;align-items:center;gap:2px;background:var(--g2);border-radius:34px;padding:6px 8px;overflow-x:auto;scrollbar-width:none;box-shadow:0 6px 22px rgba(0,0,0,.26);max-width:100%}
99
+ .ctx{display:flex;align-items:center;gap:2px;background:var(--g2);border-radius:33px;padding:6px;overflow-x:auto;scrollbar-width:none;box-shadow:0 6px 22px rgba(0,0,0,.26);max-width:100%}
89
100
  .ctx::-webkit-scrollbar{display:none}
90
101
  .cb{min-width:58px;height:54px;border-radius:27px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:#fff;font-size:10.5px;font-weight:500;padding:0 8px;flex-shrink:0;white-space:nowrap;transition:background .12s}
91
102
  .cb:hover{background:rgba(255,255,255,.08)}
@@ -95,7 +106,7 @@ input{font:inherit}
95
106
  .cb.locked{color:var(--red)}
96
107
  .ctx .sep{width:1px;height:32px;background:var(--g3);margin:0 4px;flex-shrink:0}
97
108
  .ctx .lbl{font-size:12px;color:var(--lg1);padding:0 10px;white-space:nowrap;flex-shrink:0}
98
- .addbtn{width:48px;height:48px;border-radius:50%;background:var(--yellow);color:var(--dark);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:4px}
109
+ .addbtn{width:48px;height:48px;border-radius:50%;background:var(--yellow);color:var(--dark);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:4px;margin-right:3px}
99
110
  /* the + stays pinned at the end of the bar while the other actions scroll under it (phone) */
100
111
  .ctx .addbtn{position:sticky;right:0;z-index:1;box-shadow:0 0 0 6px var(--g2),-16px 0 10px -2px var(--g2)}
101
112
  .ctx:has(.addbtn){padding-right:6px}
@@ -123,12 +134,16 @@ input{font:inherit}
123
134
  .welcome .ypill{height:38px;font-size:14px}
124
135
 
125
136
  /* drawer */
126
- .drawer{position:absolute;top:0;right:0;bottom:0;width:350px;max-width:100%;background:var(--g1);z-index:50;transform:translateX(105%);transition:transform .28s cubic-bezier(.2,.8,.2,1);display:flex;flex-direction:column;box-shadow:-8px 0 30px rgba(0,0,0,.3);color:#fff}
137
+ .drawer{position:absolute;top:0;right:0;bottom:0;width:350px;max-width:100%;background:var(--g1);border-top:1px solid var(--g2);border-left:1px solid var(--g2);z-index:50;transform:translateX(calc(100% + 40px));transition:transform .22s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column;box-shadow:-8px 0 30px rgba(0,0,0,.3);color:#fff}
127
138
  .drawer.show{transform:none}
139
+ /* (sliding out it takes no taps: a tap on the drawing as it goes lands on the drawing, as with the backdrop) */
140
+ .drawer:not(.show){pointer-events:none}
128
141
  .dh{display:flex;align-items:center;justify-content:space-between;padding:16px 14px 10px 16px;gap:10px}
129
142
  .tabs{display:flex;background:var(--dark);border-radius:20px;padding:3px}
143
+ .tabs[hidden]{display:none}
130
144
  .tabs button{padding:8px 14px;border-radius:17px;font-size:13px;font-weight:600;color:var(--lg1)}
131
145
  .tabs button[aria-selected="true"]{background:#fff;color:var(--dark)}
146
+ .dtitle{font-size:17px;font-weight:700;color:#fff;padding-left:2px}
132
147
  .dclose{width:38px;height:38px;border-radius:50%;background:var(--g2);display:flex;align-items:center;justify-content:center}
133
148
  .dbody{flex:1;overflow-y:auto;padding:6px 16px 30px;scrollbar-width:thin}
134
149
  .field{width:100%;background:transparent;border:1px solid transparent;border-radius:8px;color:#fff;padding:6px 8px;margin-left:-8px;font-weight:600}
@@ -148,10 +163,14 @@ input{font:inherit}
148
163
  .sec{font-size:13px;font-weight:600;color:var(--g4);margin:18px 0 8px}
149
164
  .row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 0;border-bottom:1px solid var(--g2);font-size:14px}
150
165
  .row small{display:block;color:var(--g4);font-size:12px;margin-top:2px}
151
- .switch{width:46px;height:28px;border-radius:14px;background:var(--g3);position:relative;flex-shrink:0;transition:background .15s}
152
- .switch::after{content:"";position:absolute;top:3px;left:3px;width:22px;height:22px;border-radius:50%;background:#fff;transition:transform .15s}
153
- .switch[aria-checked="true"]{background:var(--blue)}
154
- .switch[aria-checked="true"]::after{transform:translateX(18px)}
166
+ /* BOLDR's toggle: grey with the knob left when off, green with it right when on (the Figma's 69 × 36, scaled) */
167
+ .switch{width:48px;height:25px;border-radius:100px;background:var(--g4);position:relative;flex-shrink:0;transition:background-color .26s cubic-bezier(.4,0,.2,1)}
168
+ .switch::after{content:"";position:absolute;top:2px;left:2px;width:21px;height:21px;border-radius:11px;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.2);transition:transform .26s cubic-bezier(.4,0,.2,1),width .26s cubic-bezier(.4,0,.2,1)}
169
+ .switch[aria-checked="true"]{background:#58A942}
170
+ .switch[aria-checked="true"]::after{transform:translateX(23px)}
171
+ /* pressed in, as Apple's: the knob stretches toward the side it's about to go, and rounds up again as it lets go */
172
+ .switch:active:not([disabled])::after{width:27px;transition-duration:.12s}
173
+ .switch[aria-checked="true"]:active:not([disabled])::after{transform:translateX(17px)}
155
174
  input[type=range]{width:120px;accent-color:var(--yellow)}
156
175
  .roomrow{background:var(--g2);border-radius:10px;padding:8px 10px;margin-bottom:8px}
157
176
  .roomrow .field{font-size:14px}
@@ -166,32 +185,34 @@ input[type=range]{width:120px;accent-color:var(--yellow)}
166
185
  .sheet.show{transform:none;visibility:visible;transition:transform .28s cubic-bezier(.2,.8,.2,1),visibility 0s}
167
186
  .sheet .handle{width:50px;height:5px;border-radius:3px;background:var(--g4);margin:0 auto 14px}
168
187
  @media (min-width:900px){
169
- .sheet{left:auto;right:16px;bottom:16px;width:380px;border-radius:20px}
188
+ .sheet{left:auto;right:16px;bottom:16px;width:360px;border-radius:20px;border-bottom:1px solid var(--g3);padding-top:16px}
170
189
  .sheet .handle{display:none}
171
190
  .backdrop{background:transparent}
172
191
  }
173
- .kp-title{display:flex;align-items:center;justify-content:space-between;font-size:16px;font-weight:600;margin-bottom:10px}
174
- .kp-title small{font-size:12px;color:var(--g4);font-weight:500}
175
- .kp-display{background:var(--blue);border-radius:8px;min-height:84px;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:8px 10px;margin-bottom:10px}
176
- .kp-display input{width:100%;background:transparent;border:0;color:#fff;font-size:32px;font-weight:700;text-align:center;letter-spacing:-.3px;outline:none;caret-color:var(--yellow)}
177
- .kp-display input::placeholder{color:rgba(255,255,255,.45)}
178
- .kp-prev{font-size:13px;color:var(--lblue2);min-height:17px;font-weight:500}
179
- .kp-prev.bad{color:#FFC9C4}
192
+ .kp{--key:#4B6074;--key2:#67819A;--mred:#DF6358}
193
+ .sheet.kp,.sheet.cf{border:1px solid var(--g3);border-bottom:0;padding-left:16px;padding-right:16px;box-shadow:0 5px 15px rgba(0,0,0,.75)}
194
+ @media (min-width:900px){.sheet.kp,.sheet.cf{border-bottom:1px solid var(--g3)}}
195
+ .kp-title{display:flex;align-items:center;gap:12px;font-size:16px;font-weight:600;margin-bottom:12px}
196
+ .kp-title small{flex:1;text-align:right;font-size:12px;color:var(--lg1);font-weight:500}
197
+ .kp-x{width:20px;height:20px;display:flex;align-items:center;justify-content:center;color:#fff}
198
+ .kp-display{background:var(--dark);border-radius:12px;min-height:80px;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:8px 12px;margin-bottom:12px}
199
+ .kp-display input{width:100%;background:transparent;border:0;color:#fff;font-size:34px;font-weight:700;text-align:center;letter-spacing:-.3px;outline:none;caret-color:var(--yellow)}
200
+ .kp-display input::placeholder{color:rgba(255,255,255,.4)}
201
+ .kp-prev{font-size:13px;color:var(--lg1);min-height:16px;font-weight:500;margin-top:2px}
202
+ .kp-prev.bad{color:#FFB4AE}
180
203
  .kp-extra{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
181
204
  .kp-extra:empty{display:none}
182
- .kp-extra button{height:36px;padding:0 12px;border-radius:18px;background:var(--g2);font-size:13px;font-weight:600;color:var(--lg2);display:flex;align-items:center;gap:6px}
183
- .kp-extra button.on{background:#fff;color:var(--dark)}
184
- .kp-extra button.danger{color:#FFB4AE}
185
- .kp-fr{display:grid;grid-template-columns:repeat(8,1fr);gap:5px;margin-bottom:8px}
186
- .kp-fr button{height:38px;border-radius:7px;background:var(--g3);font-size:13px;font-weight:600}
205
+ .kp-extra button{height:30px;padding:0 10px;border-radius:7px;background:var(--dark);border:.5px solid var(--g3);font-size:13px;font-weight:500;color:#fff;display:flex;align-items:center;gap:6px}
206
+ .kp-extra button.on{background:#fff;border-color:#fff;color:var(--dark)}
207
+ .kp-extra button.danger{color:var(--mred)}
208
+ .kp-fr{display:grid;grid-template-columns:repeat(8,1fr);gap:5px;margin-bottom:7px}
209
+ .kp-fr button{height:32px;border-radius:7px;background:var(--key2);font-size:12px;font-weight:600}
187
210
  .keys{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;margin-bottom:12px}
188
- .key{height:52px;border-radius:8px;background:var(--g2);font-size:21px;font-weight:600;color:#fff;transition:background .1s;display:flex;align-items:center;justify-content:center}
189
- .key:active{background:var(--g3)}
190
- .key.alt{background:var(--g3);font-size:16px}
191
- .key.laser{background:var(--blue);font-size:13px;gap:4px}
192
- .key.laser[disabled]{opacity:.35}
193
- .kp-actions{display:flex;gap:10px}
194
- .kp-actions .ypill{flex:1}
211
+ .key{height:50px;border-radius:8px;background:var(--key);font-size:22px;font-weight:700;color:#fff;display:flex;align-items:center;justify-content:center}
212
+ .key.alt{background:var(--key2)}
213
+ .key.wide{grid-column:span 3}
214
+ .key.del{background:var(--mred);font-size:17px;font-weight:500}
215
+ .kp-apply{width:100%;height:46px;border-radius:12px;background:var(--yellow);color:var(--dark);font-size:16px;font-weight:600}
195
216
  .gbtn{height:46px;padding:0 18px;border-radius:24px;background:var(--g2);color:#fff;font-weight:600;font-size:15px}
196
217
  .cf h3{margin:0 0 6px;font-size:18px;font-weight:600}
197
218
  .cf p{margin:0 0 12px;color:var(--lg1);font-size:14px;line-height:1.45}
@@ -204,6 +225,18 @@ input[type=range]{width:120px;accent-color:var(--yellow)}
204
225
  .menu{position:absolute;top:66px;right:12px;width:300px;background:var(--g1);border-radius:15px;padding:12px 14px;z-index:58;box-shadow:0 10px 30px rgba(0,0,0,.35);display:none;color:#fff;max-height:calc(100% - 90px);overflow-y:auto}
205
226
  .menu.show{display:block}
206
227
  .menu .mrow{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 0;font-size:14px}
228
+ .menu.layers{width:330px}
229
+ .menu .mrow small{display:block;color:var(--g4);font-size:12px;margin-top:2px}
230
+ .menu .thispage{margin:2px 0 8px}
231
+ /* the selected image's opacity slider, just above the bar */
232
+ .opacity-pop{position:absolute;z-index:57;width:340px;display:none;align-items:center;gap:10px;background:var(--g2);border-radius:24px;padding:8px 16px;min-height:48px;box-sizing:border-box;box-shadow:0 8px 24px rgba(0,0,0,.3);color:var(--lg2);font-size:12.5px;font-weight:600}
233
+ .opacity-pop.show{display:flex}
234
+ .opacity-pop input{flex:1;accent-color:#fff}
235
+ .opacity-pop b{width:48px;text-align:right;color:#fff}
236
+ /* Reset sits concentric in the slider: the same 8 above, below and beside it, its radius the slider's less that */
237
+ #rotatePop{padding-right:8px}
238
+ .opacity-pop .pop-reset{height:32px;padding:0 16px;border-radius:16px;background:var(--g3);color:#fff;font-size:12.5px;font-weight:600}
239
+ .opacity-pop .pop-reset:disabled{opacity:.4}
207
240
  .seg{display:flex;background:var(--dark);border-radius:16px;padding:3px}
208
241
  .seg button{padding:6px 10px;border-radius:13px;font-size:12.5px;font-weight:600;color:var(--lg1)}
209
242
  .seg button[aria-pressed="true"]{background:#fff;color:var(--dark)}
@@ -213,10 +246,34 @@ input[type=range]{width:120px;accent-color:var(--yellow)}
213
246
  .keys-list{display:none;font-size:12.5px;color:var(--lg1);line-height:1.7;padding-bottom:6px}
214
247
  .keys-list.show{display:block}
215
248
  .keys-list kbd{font-family:inherit;color:var(--yellow);font-weight:600;display:inline-block;min-width:64px}
216
- .palette{position:absolute;z-index:58;background:var(--g1);border-radius:24px;padding:8px;display:none;gap:6px;box-shadow:0 8px 24px rgba(0,0,0,.3)}
249
+ .palette{position:absolute;z-index:58;width:264px;max-height:calc(100% - 16px);overflow-y:auto;background:var(--g1);border:1px solid var(--g2);border-radius:20px;padding:12px;display:none;flex-direction:column;gap:8px;box-shadow:0 8px 24px rgba(0,0,0,.3)}
217
250
  .palette.show{display:flex}
218
- .palette button{width:30px;height:30px;border-radius:50%;border:2px solid transparent}
219
- .palette button.on{border-color:#fff}
251
+ .pl-label{font-size:11px;font-weight:600;color:var(--g4);text-transform:uppercase;letter-spacing:.5px;margin-top:2px}
252
+ .pl-swatches{display:flex;flex-wrap:wrap;gap:10px}
253
+ .pl-swatch{width:40px;height:32px;border-radius:16px;border:2px solid rgba(255,255,255,.18)}
254
+ .pl-swatch.on{border:3px solid #fff}
255
+ .pl-row{display:flex;gap:8px;align-items:center}
256
+ .pl-opt{flex:1;height:40px;border-radius:12px;border:1px solid rgba(255,255,255,.18);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;color:#fff;font-size:10px;font-weight:500}
257
+ .pl-opt.tall{height:48px}
258
+ .pl-opt.on{border-color:#fff;background:var(--blue)}
259
+ .pl-opt i{display:block;background:#fff}
260
+ .pv-dot{border-radius:10px}
261
+ .pv-bar{width:24px;height:5px}
262
+ .pv-row{display:flex;align-items:center;gap:3px}
263
+ .pv-dash{width:6px;height:5px}
264
+ .pv-row .pv-dot{width:5px;height:5px;border-radius:3px}
265
+ .pv-cap{width:22px;height:7px}
266
+ .pv-cap.round{border-radius:4px}
267
+ .pv-arrow{gap:0}
268
+ .pv-arrow.start{flex-direction:row-reverse}
269
+ .pv-shaft{width:13px;height:5px}
270
+ .pl-opt i.pv-head{width:0;height:0;background:none;border-top:6px solid transparent;border-bottom:6px solid transparent;border-left:9px solid #fff}
271
+ .pl-opt .pv-arrow.start i.pv-head{border-left:0;border-right:9px solid #fff}
272
+
273
+ /* what a tool just picked does: a line under the tabs that goes by itself */
274
+ .tooltip-note{position:absolute;top:66px;left:50%;transform:translate(-50%,-4px);max-width:min(460px,calc(100% - 24px));background:var(--g1);border:1px solid var(--g2);color:var(--lg2);border-radius:16px;padding:8px 14px;font-size:13px;text-align:center;z-index:29;opacity:0;pointer-events:none;transition:opacity .24s cubic-bezier(.4,0,.2,1),transform .24s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 11px rgba(0,0,0,.22)}
275
+ .tooltip-note.show{opacity:1;transform:translate(-50%,0);transition-duration:.16s}
276
+ @media (max-width:760px){.tooltip-note{top:120px}}
220
277
 
221
278
  /* toast */
222
279
  .toast{position:absolute;left:50%;bottom:96px;transform:translate(-50%,12px);background:var(--dark);border:1px solid var(--g2);color:#fff;border-radius:12px;padding:11px 14px;font-size:14px;line-height:1.35;z-index:70;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;display:flex;align-items:center;gap:14px;max-width:min(520px,calc(100% - 32px));box-shadow:0 8px 24px rgba(0,0,0,.3)}
@@ -252,7 +309,8 @@ input[type=range]{width:120px;accent-color:var(--yellow)}
252
309
  @media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
253
310
 
254
311
  /* ---------- wall elevation sheet ---------- */
255
- .elev{position:absolute;inset:0;z-index:54;background:var(--cv-bg);display:none;flex-direction:column}
312
+ .elev{position:absolute;top:0;left:0;right:0;z-index:54;display:none;flex-direction:column;pointer-events:none}
313
+ .elev>*{pointer-events:auto}
256
314
  .elev.show{display:flex;animation:elevIn .22s cubic-bezier(.2,.8,.2,1)}
257
315
  @keyframes elevIn{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:none}}
258
316
  .elev-top{display:flex;align-items:center;gap:10px;padding:12px;padding-top:max(12px,env(safe-area-inset-top));flex-shrink:0}
@@ -260,12 +318,3 @@ input[type=range]{width:120px;accent-color:var(--yellow)}
260
318
  .elev-title b{font-size:14px;color:#fff;white-space:nowrap}
261
319
  .elev-title small{font-size:12px;color:var(--lg1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
262
320
  .elev-flip{color:#fff;font-size:14px;font-weight:600;gap:8px}
263
- .elev-body{position:relative;flex:1;min-height:0}
264
- .elev-body canvas{position:absolute;inset:0;width:100%;height:100%;touch-action:none;display:block}
265
- .elev-bar{position:absolute;left:50%;bottom:max(16px,env(safe-area-inset-bottom));transform:translateX(-50%);display:flex;align-items:center;gap:6px;background:var(--g2);border-radius:34px;padding:6px 6px 6px 20px;box-shadow:0 6px 22px rgba(0,0,0,.26);color:#fff;font-size:14px;font-weight:600;white-space:nowrap}
266
- .elev-bar[hidden]{display:none}
267
- .elev .trash{position:absolute;right:16px;bottom:max(16px,env(safe-area-inset-bottom))}
268
- .elev-acts{display:flex;align-items:center;gap:2px;margin-left:6px}
269
- .elev-acts:empty{display:none}
270
- .elev-rail{top:12px;max-height:calc(100% - 24px)}
271
- .elev-bar .addbtn[hidden]{display:none}