@dxos/react-ui-canvas 0.13.0 → 0.14.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 (341) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/index.mjs +9044 -1135
  3. package/dist/lib/index.mjs.map +1 -1
  4. package/dist/types/src/components/About/About.d.ts +13 -0
  5. package/dist/types/src/components/About/About.d.ts.map +1 -0
  6. package/dist/types/src/components/About/About.stories.d.ts +7 -0
  7. package/dist/types/src/components/About/About.stories.d.ts.map +1 -0
  8. package/dist/types/src/components/About/index.d.ts +2 -0
  9. package/dist/types/src/components/About/index.d.ts.map +1 -0
  10. package/dist/types/src/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -1
  11. package/dist/types/src/components/ControlFrame/ControlFrame.d.ts +8 -2
  12. package/dist/types/src/components/ControlFrame/ControlFrame.d.ts.map +1 -1
  13. package/dist/types/src/components/Layers/Layers.d.ts +30 -0
  14. package/dist/types/src/components/Layers/Layers.d.ts.map +1 -0
  15. package/dist/types/src/components/Layers/Layers.stories.d.ts +11 -0
  16. package/dist/types/src/components/Layers/Layers.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/Layers/index.d.ts +2 -0
  18. package/dist/types/src/components/Layers/index.d.ts.map +1 -0
  19. package/dist/types/src/components/Palette/Palette.d.ts +20 -4
  20. package/dist/types/src/components/Palette/Palette.d.ts.map +1 -1
  21. package/dist/types/src/components/Palette/Palette.stories.d.ts +22 -0
  22. package/dist/types/src/components/Palette/Palette.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/PartEditor/PartEditor.d.ts.map +1 -1
  24. package/dist/types/src/components/Properties/AlignField.d.ts +23 -0
  25. package/dist/types/src/components/Properties/AlignField.d.ts.map +1 -0
  26. package/dist/types/src/components/Properties/ClassField.d.ts +23 -0
  27. package/dist/types/src/components/Properties/ClassField.d.ts.map +1 -0
  28. package/dist/types/src/components/Properties/FontField.d.ts +7 -0
  29. package/dist/types/src/components/Properties/FontField.d.ts.map +1 -0
  30. package/dist/types/src/components/Properties/LayerField.d.ts +17 -0
  31. package/dist/types/src/components/Properties/LayerField.d.ts.map +1 -0
  32. package/dist/types/src/components/Properties/Properties.d.ts +27 -3
  33. package/dist/types/src/components/Properties/Properties.d.ts.map +1 -1
  34. package/dist/types/src/components/Properties/Properties.stories.d.ts +20 -0
  35. package/dist/types/src/components/Properties/Properties.stories.d.ts.map +1 -0
  36. package/dist/types/src/components/Properties/SceneField.d.ts +8 -0
  37. package/dist/types/src/components/Properties/SceneField.d.ts.map +1 -0
  38. package/dist/types/src/components/Properties/StyleGrid.d.ts +15 -5
  39. package/dist/types/src/components/Properties/StyleGrid.d.ts.map +1 -1
  40. package/dist/types/src/components/Properties/StyleGrid.stories.d.ts +7 -0
  41. package/dist/types/src/components/Properties/StyleGrid.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/SceneLayer/SceneLayer.d.ts +31 -7
  43. package/dist/types/src/components/SceneLayer/SceneLayer.d.ts.map +1 -1
  44. package/dist/types/src/components/SceneView/Dock.d.ts +22 -0
  45. package/dist/types/src/components/SceneView/Dock.d.ts.map +1 -0
  46. package/dist/types/src/components/SceneView/Dock.stories.d.ts +15 -0
  47. package/dist/types/src/components/SceneView/Dock.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/SceneView/SceneView.d.ts +33 -7
  49. package/dist/types/src/components/SceneView/SceneView.d.ts.map +1 -1
  50. package/dist/types/src/components/SceneView/SceneView.stories.d.ts +16 -6
  51. package/dist/types/src/components/SceneView/SceneView.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/SceneView/SceneViewContext.d.ts +36 -4
  53. package/dist/types/src/components/SceneView/SceneViewContext.d.ts.map +1 -1
  54. package/dist/types/src/components/SceneView/Scored.stories.d.ts +4 -4
  55. package/dist/types/src/components/SceneView/Scored.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/SceneView/usePointerMachine.d.ts +3 -1
  57. package/dist/types/src/components/SceneView/usePointerMachine.d.ts.map +1 -1
  58. package/dist/types/src/components/SceneView/useSceneCamera.d.ts +2 -2
  59. package/dist/types/src/components/SceneView/useSceneCamera.d.ts.map +1 -1
  60. package/dist/types/src/components/SceneView/useSceneClipboard.d.ts.map +1 -1
  61. package/dist/types/src/components/SceneView/useSceneKeys.d.ts.map +1 -1
  62. package/dist/types/src/components/SceneView/useSceneNavigation.d.ts +7 -4
  63. package/dist/types/src/components/SceneView/useSceneNavigation.d.ts.map +1 -1
  64. package/dist/types/src/components/Toolbar/Toolbar.d.ts +2 -0
  65. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  66. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/index.d.ts +2 -0
  68. package/dist/types/src/components/index.d.ts.map +1 -1
  69. package/dist/types/src/hooks/useWheel.d.ts +5 -0
  70. package/dist/types/src/hooks/useWheel.d.ts.map +1 -1
  71. package/dist/types/src/index.d.ts +4 -1
  72. package/dist/types/src/index.d.ts.map +1 -1
  73. package/dist/types/src/model/atoms.d.ts +21 -3
  74. package/dist/types/src/model/atoms.d.ts.map +1 -1
  75. package/dist/types/src/model/projection.d.ts.map +1 -1
  76. package/dist/types/src/model/projections/constrained.d.ts +2 -1
  77. package/dist/types/src/model/projections/constrained.d.ts.map +1 -1
  78. package/dist/types/src/model/projections/dynamic.d.ts.map +1 -1
  79. package/dist/types/src/model/projections/lattice.d.ts.map +1 -1
  80. package/dist/types/src/model/registry.d.ts +21 -3
  81. package/dist/types/src/model/registry.d.ts.map +1 -1
  82. package/dist/types/src/model/store.d.ts +10 -2
  83. package/dist/types/src/model/store.d.ts.map +1 -1
  84. package/dist/types/src/model/types.d.ts +699 -338
  85. package/dist/types/src/model/types.d.ts.map +1 -1
  86. package/dist/types/src/playwright/SceneManager.d.ts +1 -1
  87. package/dist/types/src/playwright/SceneManager.d.ts.map +1 -1
  88. package/dist/types/src/utils/builder.d.ts +22 -41
  89. package/dist/types/src/utils/builder.d.ts.map +1 -1
  90. package/dist/types/src/utils/camera.d.ts +19 -10
  91. package/dist/types/src/utils/camera.d.ts.map +1 -1
  92. package/dist/types/src/utils/clipboard.d.ts +4 -2
  93. package/dist/types/src/utils/clipboard.d.ts.map +1 -1
  94. package/dist/types/src/utils/diagram.d.ts.map +1 -1
  95. package/dist/types/src/utils/group.d.ts +14 -0
  96. package/dist/types/src/utils/group.d.ts.map +1 -0
  97. package/dist/types/src/utils/group.test.d.ts +2 -0
  98. package/dist/types/src/utils/group.test.d.ts.map +1 -0
  99. package/dist/types/src/utils/gutter-route.d.ts +7 -0
  100. package/dist/types/src/utils/gutter-route.d.ts.map +1 -1
  101. package/dist/types/src/utils/hit.d.ts +3 -20
  102. package/dist/types/src/utils/hit.d.ts.map +1 -1
  103. package/dist/types/src/utils/index.d.ts +3 -0
  104. package/dist/types/src/utils/index.d.ts.map +1 -1
  105. package/dist/types/src/utils/lattice.d.ts +5 -0
  106. package/dist/types/src/utils/lattice.d.ts.map +1 -1
  107. package/dist/types/src/utils/layers.d.ts +35 -0
  108. package/dist/types/src/utils/layers.d.ts.map +1 -0
  109. package/dist/types/src/utils/layers.test.d.ts +2 -0
  110. package/dist/types/src/utils/layers.test.d.ts.map +1 -0
  111. package/dist/types/src/utils/parts.d.ts +25 -9
  112. package/dist/types/src/utils/parts.d.ts.map +1 -1
  113. package/dist/types/src/utils/ports.d.ts +2 -0
  114. package/dist/types/src/utils/ports.d.ts.map +1 -1
  115. package/dist/types/src/utils/properties.d.ts +4 -3
  116. package/dist/types/src/utils/properties.d.ts.map +1 -1
  117. package/dist/types/src/utils/route.d.ts +5 -0
  118. package/dist/types/src/utils/route.d.ts.map +1 -1
  119. package/dist/types/src/utils/scenes.d.ts +14 -0
  120. package/dist/types/src/utils/scenes.d.ts.map +1 -0
  121. package/dist/types/src/utils/scenes.test.d.ts +2 -0
  122. package/dist/types/src/utils/scenes.test.d.ts.map +1 -0
  123. package/dist/types/src/utils/shapes.d.ts +23 -9
  124. package/dist/types/src/utils/shapes.d.ts.map +1 -1
  125. package/dist/types/src/utils/style.d.ts +29 -6
  126. package/dist/types/src/utils/style.d.ts.map +1 -1
  127. package/dist/types/src/utils/style.test.d.ts +2 -0
  128. package/dist/types/src/utils/style.test.d.ts.map +1 -0
  129. package/dist/types/src/utils/testing.d.ts +2 -2
  130. package/dist/types/src/utils/testing.d.ts.map +1 -1
  131. package/dist/types/src/utils/undo.d.ts +15 -4
  132. package/dist/types/src/utils/undo.d.ts.map +1 -1
  133. package/dist/types/tsconfig.tsbuildinfo +1 -1
  134. package/package.json +20 -26
  135. package/src/components/About/About.stories.tsx +32 -0
  136. package/src/components/About/About.tsx +38 -0
  137. package/src/components/About/index.ts +5 -0
  138. package/src/components/Breadcrumbs/Breadcrumbs.tsx +17 -3
  139. package/src/components/ControlFrame/ControlFrame.tsx +138 -82
  140. package/src/components/Grid/Grid.stories.tsx +1 -1
  141. package/src/components/Layers/Layers.stories.tsx +131 -0
  142. package/src/components/Layers/Layers.tsx +216 -0
  143. package/src/components/Layers/index.ts +5 -0
  144. package/src/components/Palette/Palette.stories.tsx +135 -0
  145. package/src/components/Palette/Palette.tsx +230 -39
  146. package/src/components/PartEditor/PartEditor.tsx +4 -3
  147. package/src/components/Properties/AlignField.tsx +117 -0
  148. package/src/components/Properties/ClassField.tsx +44 -0
  149. package/src/components/Properties/FontField.tsx +73 -0
  150. package/src/components/Properties/LayerField.tsx +61 -0
  151. package/src/components/Properties/Properties.stories.tsx +113 -0
  152. package/src/components/Properties/Properties.tsx +240 -60
  153. package/src/components/Properties/SceneField.tsx +22 -0
  154. package/src/components/Properties/StyleGrid.stories.tsx +50 -0
  155. package/src/components/Properties/StyleGrid.tsx +54 -26
  156. package/src/components/SceneLayer/SceneLayer.tsx +497 -188
  157. package/src/components/SceneView/Constrained.stories.tsx +1 -1
  158. package/src/components/SceneView/Dock.stories.tsx +95 -0
  159. package/src/components/SceneView/Dock.tsx +146 -0
  160. package/src/components/SceneView/Dynamic.stories.tsx +1 -1
  161. package/src/components/SceneView/SceneView.stories.tsx +170 -18
  162. package/src/components/SceneView/SceneView.tsx +565 -151
  163. package/src/components/SceneView/SceneViewContext.ts +23 -2
  164. package/src/components/SceneView/Scored.stories.tsx +11 -11
  165. package/src/components/SceneView/usePointerMachine.ts +80 -20
  166. package/src/components/SceneView/useSceneCamera.ts +15 -8
  167. package/src/components/SceneView/useSceneClipboard.ts +4 -0
  168. package/src/components/SceneView/useSceneKeys.ts +6 -4
  169. package/src/components/SceneView/useSceneNavigation.ts +44 -33
  170. package/src/components/Toolbar/Toolbar.stories.tsx +14 -15
  171. package/src/components/Toolbar/Toolbar.tsx +15 -4
  172. package/src/components/index.ts +2 -0
  173. package/src/hooks/useWheel.ts +35 -0
  174. package/src/index.ts +4 -3
  175. package/src/model/atoms.ts +27 -3
  176. package/src/model/projection.ts +5 -0
  177. package/src/model/projections/constrained.test.ts +18 -10
  178. package/src/model/projections/constrained.ts +19 -8
  179. package/src/model/projections/dynamic.ts +5 -2
  180. package/src/model/projections/lattice.ts +15 -3
  181. package/src/model/registry.test.ts +14 -9
  182. package/src/model/registry.ts +39 -34
  183. package/src/model/store.ts +12 -2
  184. package/src/model/types.ts +124 -36
  185. package/src/playwright/SceneManager.ts +2 -2
  186. package/src/playwright/create.spec.ts +5 -3
  187. package/src/playwright/demo.spec.ts +2 -2
  188. package/src/playwright/lattice.spec.ts +4 -5
  189. package/src/playwright/scene.spec.ts +27 -25
  190. package/src/utils/builder.test.ts +46 -9
  191. package/src/utils/builder.ts +42 -18
  192. package/src/utils/camera.test.ts +22 -51
  193. package/src/utils/camera.ts +39 -32
  194. package/src/utils/clipboard.test.ts +23 -2
  195. package/src/utils/clipboard.ts +8 -4
  196. package/src/utils/diagram.test.ts +3 -1
  197. package/src/utils/diagram.ts +3 -25
  198. package/src/utils/group.test.ts +69 -0
  199. package/src/utils/group.ts +94 -0
  200. package/src/utils/gutter-route.test.ts +49 -0
  201. package/src/utils/gutter-route.ts +142 -16
  202. package/src/utils/hit.test.ts +4 -38
  203. package/src/utils/hit.ts +4 -26
  204. package/src/utils/index.ts +3 -0
  205. package/src/utils/lattice.ts +7 -1
  206. package/src/utils/layers.test.ts +139 -0
  207. package/src/utils/layers.ts +175 -0
  208. package/src/utils/parts.test.ts +66 -32
  209. package/src/utils/parts.ts +63 -41
  210. package/src/utils/ports.ts +7 -4
  211. package/src/utils/properties.test.ts +23 -6
  212. package/src/utils/properties.ts +66 -16
  213. package/src/utils/route.test.ts +51 -2
  214. package/src/utils/route.ts +133 -23
  215. package/src/utils/scenes.test.ts +38 -0
  216. package/src/utils/scenes.ts +54 -0
  217. package/src/utils/shapes.test.ts +33 -8
  218. package/src/utils/shapes.ts +51 -35
  219. package/src/utils/style.test.ts +54 -0
  220. package/src/utils/style.ts +153 -40
  221. package/src/utils/testing.test.ts +3 -3
  222. package/src/utils/testing.ts +21 -30
  223. package/src/utils/undo.test.ts +25 -2
  224. package/src/utils/undo.ts +50 -13
  225. package/dist/lib/chunk-Grid.mjs +0 -99
  226. package/dist/lib/chunk-Grid.mjs.map +0 -1
  227. package/dist/lib/scene.mjs +0 -6835
  228. package/dist/lib/scene.mjs.map +0 -1
  229. package/dist/lib/types.mjs +0 -16
  230. package/dist/lib/types.mjs.map +0 -1
  231. package/dist/types/src/archive/components/Canvas/Canvas.d.ts +0 -15
  232. package/dist/types/src/archive/components/Canvas/Canvas.d.ts.map +0 -1
  233. package/dist/types/src/archive/components/Canvas/Canvas.stories.d.ts +0 -18
  234. package/dist/types/src/archive/components/Canvas/Canvas.stories.d.ts.map +0 -1
  235. package/dist/types/src/archive/components/Canvas/index.d.ts +0 -2
  236. package/dist/types/src/archive/components/Canvas/index.d.ts.map +0 -1
  237. package/dist/types/src/archive/components/CellGrid/CellGrid.d.ts +0 -21
  238. package/dist/types/src/archive/components/CellGrid/CellGrid.d.ts.map +0 -1
  239. package/dist/types/src/archive/components/CellGrid/CellGrid.stories.d.ts +0 -21
  240. package/dist/types/src/archive/components/CellGrid/CellGrid.stories.d.ts.map +0 -1
  241. package/dist/types/src/archive/components/CellGrid/headers/Ruler.d.ts +0 -15
  242. package/dist/types/src/archive/components/CellGrid/headers/Ruler.d.ts.map +0 -1
  243. package/dist/types/src/archive/components/CellGrid/headers/TrackHeader.d.ts +0 -19
  244. package/dist/types/src/archive/components/CellGrid/headers/TrackHeader.d.ts.map +0 -1
  245. package/dist/types/src/archive/components/CellGrid/headers/index.d.ts +0 -3
  246. package/dist/types/src/archive/components/CellGrid/headers/index.d.ts.map +0 -1
  247. package/dist/types/src/archive/components/CellGrid/index.d.ts +0 -6
  248. package/dist/types/src/archive/components/CellGrid/index.d.ts.map +0 -1
  249. package/dist/types/src/archive/components/CellGrid/input/index.d.ts +0 -3
  250. package/dist/types/src/archive/components/CellGrid/input/index.d.ts.map +0 -1
  251. package/dist/types/src/archive/components/CellGrid/input/pointer.d.ts +0 -33
  252. package/dist/types/src/archive/components/CellGrid/input/pointer.d.ts.map +0 -1
  253. package/dist/types/src/archive/components/CellGrid/input/wheel.d.ts +0 -14
  254. package/dist/types/src/archive/components/CellGrid/input/wheel.d.ts.map +0 -1
  255. package/dist/types/src/archive/components/CellGrid/render/index.d.ts +0 -3
  256. package/dist/types/src/archive/components/CellGrid/render/index.d.ts.map +0 -1
  257. package/dist/types/src/archive/components/CellGrid/render/overlay-layer.d.ts +0 -21
  258. package/dist/types/src/archive/components/CellGrid/render/overlay-layer.d.ts.map +0 -1
  259. package/dist/types/src/archive/components/CellGrid/render/static-layer.d.ts +0 -36
  260. package/dist/types/src/archive/components/CellGrid/render/static-layer.d.ts.map +0 -1
  261. package/dist/types/src/archive/components/CellGrid/state/atoms.d.ts +0 -23
  262. package/dist/types/src/archive/components/CellGrid/state/atoms.d.ts.map +0 -1
  263. package/dist/types/src/archive/components/CellGrid/state/index.d.ts +0 -4
  264. package/dist/types/src/archive/components/CellGrid/state/index.d.ts.map +0 -1
  265. package/dist/types/src/archive/components/CellGrid/state/types.d.ts +0 -39
  266. package/dist/types/src/archive/components/CellGrid/state/types.d.ts.map +0 -1
  267. package/dist/types/src/archive/components/CellGrid/state/viewport.d.ts +0 -52
  268. package/dist/types/src/archive/components/CellGrid/state/viewport.d.ts.map +0 -1
  269. package/dist/types/src/archive/components/CellGrid/state/viewport.test.d.ts +0 -2
  270. package/dist/types/src/archive/components/CellGrid/state/viewport.test.d.ts.map +0 -1
  271. package/dist/types/src/archive/components/FPS.d.ts +0 -9
  272. package/dist/types/src/archive/components/FPS.d.ts.map +0 -1
  273. package/dist/types/src/archive/components/Grid/Grid.d.ts +0 -6
  274. package/dist/types/src/archive/components/Grid/Grid.d.ts.map +0 -1
  275. package/dist/types/src/archive/components/Grid/index.d.ts +0 -2
  276. package/dist/types/src/archive/components/Grid/index.d.ts.map +0 -1
  277. package/dist/types/src/archive/components/index.d.ts +0 -5
  278. package/dist/types/src/archive/components/index.d.ts.map +0 -1
  279. package/dist/types/src/archive/hooks/index.d.ts +0 -5
  280. package/dist/types/src/archive/hooks/index.d.ts.map +0 -1
  281. package/dist/types/src/archive/hooks/projection.d.ts +0 -60
  282. package/dist/types/src/archive/hooks/projection.d.ts.map +0 -1
  283. package/dist/types/src/archive/hooks/useCanvasContext.d.ts +0 -13
  284. package/dist/types/src/archive/hooks/useCanvasContext.d.ts.map +0 -1
  285. package/dist/types/src/archive/hooks/useDrag.d.ts +0 -6
  286. package/dist/types/src/archive/hooks/useDrag.d.ts.map +0 -1
  287. package/dist/types/src/archive/hooks/useWheel.d.ts +0 -8
  288. package/dist/types/src/archive/hooks/useWheel.d.ts.map +0 -1
  289. package/dist/types/src/archive/index.d.ts +0 -5
  290. package/dist/types/src/archive/index.d.ts.map +0 -1
  291. package/dist/types/src/archive/types.d.ts +0 -19
  292. package/dist/types/src/archive/types.d.ts.map +0 -1
  293. package/dist/types/src/archive/util/index.d.ts +0 -4
  294. package/dist/types/src/archive/util/index.d.ts.map +0 -1
  295. package/dist/types/src/archive/util/svg-path.d.ts +0 -4
  296. package/dist/types/src/archive/util/svg-path.d.ts.map +0 -1
  297. package/dist/types/src/archive/util/svg.d.ts +0 -32
  298. package/dist/types/src/archive/util/svg.d.ts.map +0 -1
  299. package/dist/types/src/archive/util/svg.stories.d.ts +0 -15
  300. package/dist/types/src/archive/util/svg.stories.d.ts.map +0 -1
  301. package/dist/types/src/archive/util/util.d.ts +0 -17
  302. package/dist/types/src/archive/util/util.d.ts.map +0 -1
  303. package/dist/types/src/scene.d.ts +0 -5
  304. package/dist/types/src/scene.d.ts.map +0 -1
  305. package/src/archive/components/Canvas/Canvas.stories.tsx +0 -111
  306. package/src/archive/components/Canvas/Canvas.tsx +0 -87
  307. package/src/archive/components/Canvas/index.ts +0 -5
  308. package/src/archive/components/CellGrid/CellGrid.stories.tsx +0 -239
  309. package/src/archive/components/CellGrid/CellGrid.tsx +0 -270
  310. package/src/archive/components/CellGrid/headers/Ruler.tsx +0 -71
  311. package/src/archive/components/CellGrid/headers/TrackHeader.tsx +0 -58
  312. package/src/archive/components/CellGrid/headers/index.ts +0 -6
  313. package/src/archive/components/CellGrid/index.ts +0 -9
  314. package/src/archive/components/CellGrid/input/index.ts +0 -6
  315. package/src/archive/components/CellGrid/input/pointer.ts +0 -236
  316. package/src/archive/components/CellGrid/input/wheel.ts +0 -68
  317. package/src/archive/components/CellGrid/render/index.ts +0 -6
  318. package/src/archive/components/CellGrid/render/overlay-layer.ts +0 -66
  319. package/src/archive/components/CellGrid/render/static-layer.ts +0 -112
  320. package/src/archive/components/CellGrid/state/atoms.ts +0 -43
  321. package/src/archive/components/CellGrid/state/index.ts +0 -7
  322. package/src/archive/components/CellGrid/state/types.ts +0 -40
  323. package/src/archive/components/CellGrid/state/viewport.test.ts +0 -50
  324. package/src/archive/components/CellGrid/state/viewport.ts +0 -94
  325. package/src/archive/components/FPS.tsx +0 -98
  326. package/src/archive/components/Grid/Grid.tsx +0 -17
  327. package/src/archive/components/Grid/index.ts +0 -5
  328. package/src/archive/components/index.ts +0 -8
  329. package/src/archive/hooks/index.ts +0 -8
  330. package/src/archive/hooks/projection.tsx +0 -156
  331. package/src/archive/hooks/useCanvasContext.ts +0 -32
  332. package/src/archive/hooks/useDrag.tsx +0 -96
  333. package/src/archive/hooks/useWheel.tsx +0 -80
  334. package/src/archive/index.ts +0 -8
  335. package/src/archive/types.ts +0 -14
  336. package/src/archive/util/index.ts +0 -7
  337. package/src/archive/util/svg-path.ts +0 -13
  338. package/src/archive/util/svg.stories.tsx +0 -46
  339. package/src/archive/util/svg.tsx +0 -128
  340. package/src/archive/util/util.ts +0 -50
  341. package/src/scene.ts +0 -9
@@ -7,7 +7,13 @@ import { type KeyboardEvent, type MouseEvent, type RefObject } from 'react';
7
7
  import { createContext } from '@dxos/react-hooks';
8
8
 
9
9
  import { type useRegistry } from '../../hooks/index.ts';
10
- import { type ControlPointRef, type Drag, type EditingPart, type SceneViewAtoms } from '../../model/atoms.ts';
10
+ import {
11
+ type ControlPointRef,
12
+ type Drag,
13
+ type EditingPart,
14
+ type PanelMode,
15
+ type SceneViewAtoms,
16
+ } from '../../model/atoms.ts';
11
17
  import { type Projection } from '../../model/projection.ts';
12
18
  import { type LinkRegistry, type NodeRegistry } from '../../model/registry.ts';
13
19
  import { type SceneStore } from '../../model/store.ts';
@@ -16,6 +22,8 @@ import {
16
22
  type Camera,
17
23
  type Capabilities,
18
24
  type ElementId,
25
+ type LinkId,
26
+ type NodeId,
19
27
  type Scene,
20
28
  type SceneId,
21
29
  type Tool,
@@ -39,6 +47,8 @@ export type SceneViewContextValue = {
39
47
  store: SceneStore;
40
48
  projection: Projection;
41
49
  capabilities: Capabilities;
50
+ /** The scene is only looked at: the parts that edit it (selection frame, ports, actions, properties) hide. */
51
+ readonly: boolean;
42
52
  nodeRegistry: NodeRegistry;
43
53
  linkRegistry: LinkRegistry;
44
54
 
@@ -49,16 +59,19 @@ export type SceneViewContextValue = {
49
59
  blocked: boolean;
50
60
  /** The current scene's frame. */
51
61
  bounds: Bounds;
62
+ /** The scene's frame and the visible viewport together, for the lattice cells. */
63
+ latticeBounds: Bounds;
52
64
  path: SceneId[];
53
65
  camera: Camera;
54
66
  /** The zoom against this level's own 1:1 rather than the root's; display only. */
55
- nominalZoom: number;
56
67
  /** The centre of the view in scene coordinates. */
57
68
  pointer: { x: number; y: number };
58
69
  /** Whether the viewport has been measured; the scene stays hidden until it has. */
59
70
  measured: boolean;
60
71
  /** One screen pixel in scene units, for chrome that should not grow with the camera. */
61
72
  frameUnit: number;
73
+ /** The portal filling most of the view and the opacity of the rest of the layer around it (`layerOpacity`). */
74
+ focus: { id: ElementId; opacity: number } | undefined;
62
75
  /** Minor grid spacing in scene px. */
63
76
  grid: number;
64
77
  snapEnabled: boolean;
@@ -70,14 +83,22 @@ export type SceneViewContextValue = {
70
83
  hover: ElementId | undefined;
71
84
  selectedPoint: ControlPointRef | undefined;
72
85
  editing: EditingPart | undefined;
86
+ /** The node whose embedded content takes input. */
87
+ active: NodeId | undefined;
73
88
  clipboard: Clipboard | undefined;
74
89
  drag: Drag | undefined;
75
90
  tool: Tool;
76
91
  debug: boolean;
92
+ /** Where the properties and layers panels sit. */
93
+ panels: PanelMode;
77
94
 
78
95
  /** The bounds a create gesture would land, drawn whether or not the node itself previews. */
79
96
  createFrame: Bounds | undefined;
97
+ /** Where the nodes of a move in flight will land, while they follow the pointer. */
98
+ landing: readonly Bounds[] | undefined;
80
99
  handlers: ElementHandlers;
100
+ /** The link under the pointer. */
101
+ linkHover: LinkId | undefined;
81
102
  select: (ids: Iterable<ElementId>) => void;
82
103
  toolbarActions: ToolbarActions;
83
104
  } & Pick<SceneCamera, 'navigating' | 'opening'> &
@@ -21,7 +21,7 @@ import { type SceneStore, createMemoryStore } from '../../model/store.ts';
21
21
  import { type Scene, type SceneId } from '../../model/types.ts';
22
22
  import { SceneBuilder } from '../../utils/builder.ts';
23
23
  import { diagnosticElements, toDiagramObjects } from '../../utils/diagram.ts';
24
- import { createClassSceneTree } from '../../utils/testing.ts';
24
+ import { createModelSceneTree } from '../../utils/testing.ts';
25
25
  import { SceneView } from './SceneView.tsx';
26
26
 
27
27
  /**
@@ -95,7 +95,7 @@ const fromMermaid = async (source: string): Promise<Seed> => {
95
95
  ...graph.edges.map(({ from, to }, index) =>
96
96
  SceneBuilder.link('smart', nodeId(from), nodeId(to))
97
97
  .id(nodeId(`${from}-${to}-${index}`))
98
- .properties({ directed: true }),
98
+ .properties({ ends: { end: 'arrow' } }),
99
99
  ),
100
100
  ]).build();
101
101
  return { scenes, root: ROOT, engine: result.chosen.evaluation };
@@ -319,9 +319,9 @@ const Editor = ({ store, root, engine }: EditorProps) => {
319
319
  };
320
320
 
321
321
  type StoryArgs = {
322
- /** Mermaid flowchart laid out by the engine; ignored when `fixture` is `classes`. */
322
+ /** Mermaid flowchart laid out by the engine; ignored when `fixture` is `model`. */
323
323
  source?: string;
324
- fixture?: 'mermaid' | 'classes';
324
+ fixture?: 'mermaid' | 'model';
325
325
  };
326
326
 
327
327
  const DefaultStory = ({ source = BASIC, fixture = 'mermaid' }: StoryArgs) => {
@@ -330,8 +330,8 @@ const DefaultStory = ({ source = BASIC, fixture = 'mermaid' }: StoryArgs) => {
330
330
  const [failure, setFailure] = useState<string>();
331
331
  useEffect(() => {
332
332
  const key = `${fixture}:${source}`;
333
- if (fixture === 'classes') {
334
- setSeed({ ...createClassSceneTree(), key });
333
+ if (fixture === 'model') {
334
+ setSeed({ ...createModelSceneTree(), key });
335
335
  setFailure(undefined);
336
336
  return;
337
337
  }
@@ -364,12 +364,12 @@ const DefaultStory = ({ source = BASIC, fixture = 'mermaid' }: StoryArgs) => {
364
364
  };
365
365
 
366
366
  const meta: Meta<StoryArgs> = {
367
- title: 'ui/react-ui-canvas/scene/Scored',
367
+ title: 'ui/react-ui-canvas/Scored',
368
368
  render: DefaultStory,
369
369
  decorators: [withRegistry, withTheme(), withLayout({ layout: 'fullscreen' })],
370
370
  argTypes: {
371
371
  source: { control: 'text', description: 'Mermaid flowchart laid out by the engine' },
372
- fixture: { control: 'radio', options: ['mermaid', 'classes'] },
372
+ fixture: { control: 'radio', options: ['mermaid', 'model'] },
373
373
  },
374
374
  };
375
375
 
@@ -387,7 +387,7 @@ export const Platform: Story = {
387
387
  args: { source: PLATFORM.trim(), fixture: 'mermaid' },
388
388
  };
389
389
 
390
- /** The scene engine's own class fixture, graded as drawn. */
391
- export const Classes: Story = {
392
- args: { fixture: 'classes' },
390
+ /** The scene engine's own model fixture, graded as drawn. */
391
+ export const Model: Story = {
392
+ args: { fixture: 'model' },
393
393
  };
@@ -12,7 +12,7 @@ import {
12
12
  useState,
13
13
  } from 'react';
14
14
 
15
- import { type useRegistry } from '../../hooks/index.ts';
15
+ import { SCENE_OVERLAY_ATTRIBUTE, type useRegistry } from '../../hooks/index.ts';
16
16
  import { type ControlPointRef, type Drag, type Handle, type SceneViewAtoms } from '../../model/atoms.ts';
17
17
  import { nodeDef } from '../../model/node-def.ts';
18
18
  import { type Projection } from '../../model/projection.ts';
@@ -33,17 +33,18 @@ import {
33
33
  type SplineLink,
34
34
  type Tool,
35
35
  endpointNode,
36
+ isFrameNode,
36
37
  isPointEndpoint,
37
- isPortalNode,
38
38
  } from '../../model/types.ts';
39
39
  import { boundsCenter, panBy, screenToScene } from '../../utils/camera.ts';
40
40
  import { duplicateSelection } from '../../utils/clipboard.ts';
41
41
  import { boundsFromPoints, hitTest, nodesIntersecting } from '../../utils/hit.ts';
42
+ import { DEFAULT_LAYER, activeLayer } from '../../utils/layers.ts';
42
43
  import { topZ } from '../../utils/order.ts';
43
44
  import { nodePorts, portAccepts, portPoint } from '../../utils/ports.ts';
44
45
  import { resizeBounds } from '../../utils/resize.ts';
45
46
  import { insertIndex, linkGeometry, sideToward } from '../../utils/route.ts';
46
- import { DEFAULT_SIZES, cloneShape, createLink, createNode, nodeBounds } from '../../utils/shapes.ts';
47
+ import { NOMINAL_SIZES, cloneShape, createLink, createNode, nodeBounds, nominalSize } from '../../utils/shapes.ts';
47
48
  import { type LinkEnd, handlePoint } from '../ControlFrame/ControlFrame.tsx';
48
49
  import { type SceneCamera } from './useSceneCamera.ts';
49
50
  import { type SceneSnap } from './useSceneSnap.ts';
@@ -91,6 +92,8 @@ export type UsePointerMachineOptions = {
91
92
  /** Set once the user takes the camera over, which stops the view re-fitting itself. */
92
93
  interactedRef: MutableRefObject<boolean>;
93
94
  select: (ids: Iterable<ElementId>) => void;
95
+ /** Scene px of one nominal unit (the model's major grid cell), which a new node's default size is counted in. */
96
+ cell: number;
94
97
  } & Pick<SceneCamera, 'setCamera' | 'cancelAnimation' | 'isNavigating'> &
95
98
  Pick<SceneSnap, 'minor' | 'major' | 'snap' | 'snapMinor'>;
96
99
 
@@ -153,6 +156,7 @@ export const usePointerMachine = ({
153
156
  setCamera,
154
157
  cancelAnimation,
155
158
  isNavigating,
159
+ cell,
156
160
  minor,
157
161
  major,
158
162
  snap,
@@ -248,6 +252,17 @@ export const usePointerMachine = ({
248
252
  [registry, atoms.selection, select],
249
253
  );
250
254
 
255
+ /** Makes a node's embedded content live (`SceneViewAtoms.active`), when it has any. */
256
+ const activate = useCallback(
257
+ (id: NodeId) => {
258
+ // Escaped: a host's node id may hold characters a selector would misread.
259
+ if (rootRef.current?.querySelector(`[data-node-id="${CSS.escape(id)}"] [${SCENE_OVERLAY_ATTRIBUTE}]`)) {
260
+ registry.set(atoms.active, id);
261
+ }
262
+ },
263
+ [rootRef, registry, atoms.active],
264
+ );
265
+
251
266
  const onNodePointerDown = useCallback(
252
267
  (node: Node, event: PointerEvent) => {
253
268
  const currentTool = registry.get(atoms.tool);
@@ -265,6 +280,12 @@ export const usePointerMachine = ({
265
280
  return;
266
281
  }
267
282
  const next = clickSelect(node.id, event);
283
+ // A press on the content of the active node (its own toolbar or editor) starts no move: the drag's pointer
284
+ // capture would take the click from that content's controls. Inactive, the content is inert and the node moves.
285
+ const embedded = event.target instanceof Element && event.target.closest(`[${SCENE_OVERLAY_ATTRIBUTE}]`);
286
+ if (embedded && registry.get(atoms.active) === node.id) {
287
+ return;
288
+ }
268
289
  if (capabilities.move && !node.locked) {
269
290
  const { x, y } = nodeBounds(node);
270
291
  const ids = [...next].filter((id) => scene.nodes[id] !== undefined);
@@ -272,9 +293,23 @@ export const usePointerMachine = ({
272
293
  { kind: 'move', ids, origin: toScene(event), anchor: { x, y }, delta: { x: 0, y: 0 }, copy: event.metaKey },
273
294
  event,
274
295
  );
296
+ } else if (next.size === 1) {
297
+ // A node that cannot move has no drag to end in a click, so the press itself activates it.
298
+ activate(node.id);
275
299
  }
276
300
  },
277
- [registry, atoms.tool, clickSelect, capabilities.move, capabilities.link, scene.nodes, toScene, startDrag],
301
+ [
302
+ registry,
303
+ atoms.tool,
304
+ atoms.active,
305
+ activate,
306
+ clickSelect,
307
+ capabilities.move,
308
+ capabilities.link,
309
+ scene.nodes,
310
+ toScene,
311
+ startDrag,
312
+ ],
278
313
  );
279
314
 
280
315
  const onLinkPointerDown = useCallback(
@@ -529,6 +564,7 @@ export const usePointerMachine = ({
529
564
  const raw = { x: point.x - current.origin.x, y: point.y - current.origin.y };
530
565
  setDrag({
531
566
  ...current,
567
+ raw,
532
568
  delta: {
533
569
  x: snapMinor(current.anchor.x + raw.x) - current.anchor.x,
534
570
  y: snapMinor(current.anchor.y + raw.y) - current.anchor.y,
@@ -652,7 +688,7 @@ export const usePointerMachine = ({
652
688
  if (!drag.dropped && (drawn.width === 0 || drawn.height === 0)) {
653
689
  return undefined;
654
690
  }
655
- const size = drag.dropped ? { ...def.defaultSize } : { ...drawn };
691
+ const size = drag.dropped ? nominalSize(def.defaultSize, cell) : { ...drawn };
656
692
  const center = drag.dropped
657
693
  ? { x: drag.from.x + size.width / 2, y: drag.from.y + size.height / 2 }
658
694
  : { x: drawn.x + drawn.width / 2, y: drawn.y + drawn.height / 2 };
@@ -663,21 +699,27 @@ export const usePointerMachine = ({
663
699
  pendingRef.current = { type: drag.type, node };
664
700
  return node;
665
701
  },
666
- [nodeRegistry, scene.nodes],
702
+ [nodeRegistry, scene.nodes, cell],
667
703
  );
668
704
 
669
705
  /** Adds a new node; a new portal opens onto a fresh scene of its own, whichever path created it. */
670
706
  const addNode = useCallback(
671
707
  (node: Node) => {
672
- if (isPortalNode(node)) {
708
+ if (isFrameNode(node)) {
673
709
  registry.set(store.scenes, {
674
710
  ...registry.get(store.scenes),
675
- [node.scene]: { id: node.scene, name: 'Untitled', nodes: {}, links: {} },
711
+ [node.scene]: {
712
+ id: node.scene,
713
+ name: 'Untitled',
714
+ nodes: {},
715
+ links: {},
716
+ layers: { [DEFAULT_LAYER.id]: DEFAULT_LAYER },
717
+ },
676
718
  });
677
719
  }
678
- projection.apply({ kind: 'create', node });
720
+ projection.apply({ kind: 'create', node: { ...node, layer: activeLayer(scene, registry.get(atoms.layer)) } });
679
721
  },
680
- [projection, registry, store],
722
+ [projection, registry, store, scene, atoms.layer],
681
723
  );
682
724
 
683
725
  /** The node the gesture made, committed: the next gesture starts from a fresh `create`. */
@@ -722,6 +764,10 @@ export const usePointerMachine = ({
722
764
  }
723
765
  case 'move': {
724
766
  if (current.delta.x === 0 && current.delta.y === 0) {
767
+ // A click (a press that never moved) on a node with embedded content makes that content live.
768
+ if (current.ids.length === 1) {
769
+ activate(current.ids[0]);
770
+ }
725
771
  break;
726
772
  }
727
773
  // ⌘-drag leaves the selection where it was and drops a copy, which becomes the selection.
@@ -750,27 +796,34 @@ export const usePointerMachine = ({
750
796
  break;
751
797
  }
752
798
  let target = current.target;
799
+ let created: Node | undefined;
753
800
  if (!target && isPointEndpoint(current.source)) {
754
801
  // A free-ended link that never reached a node ends free too.
755
802
  target = { point: current.to };
756
803
  } else if (!target && capabilities.create) {
757
804
  // Dropping a link drag on empty canvas creates a node there and links to it (canvas-editor
758
- // behaviour): a copy of the shape it left, without its text, else a rectangle. Its top-left is
759
- // what snaps, so the edges land on the grid.
805
+ // behaviour): a copy of the shape it left, without its text, else a rectangle. It is centred on
806
+ // the release point (not the settled end, already snapped to the coarse grid) with its top-left
807
+ // on the minor grid, as a palette drop lands.
808
+ const drop = raw.kind === 'link' ? raw.to : current.to;
760
809
  const source = scene.nodes[endpointNode(current.source) ?? ''];
761
810
  const def = source ? nodeRegistry[source.type] : undefined;
762
- const size = source?.size ?? DEFAULT_SIZES.rect;
811
+ const size = source?.size ?? nominalSize(NOMINAL_SIZES.rect, cell);
763
812
  const props = {
764
813
  id: createId(source?.type ?? 'rect'),
765
814
  z: topZ(Object.values(scene.nodes)),
766
815
  center: {
767
- x: snap(current.to.x - size.width / 2) + size.width / 2,
768
- y: snap(current.to.y - size.height / 2) + size.height / 2,
816
+ x: snapMinor(drop.x - size.width / 2) + size.width / 2,
817
+ y: snapMinor(drop.y - size.height / 2) + size.height / 2,
769
818
  },
770
819
  size,
771
820
  };
772
- const node = source && def ? cloneShape(source, def.create(props)) : createNode({ type: 'rect', ...props });
821
+ const node =
822
+ source && def
823
+ ? cloneShape(source, def.create(props), nodeDef(nodeRegistry, source)?.parts)
824
+ : createNode({ type: 'rect', ...props });
773
825
  addNode(node);
826
+ created = node;
774
827
  target = { node: node.id };
775
828
  }
776
829
  if (target) {
@@ -782,11 +835,15 @@ export const usePointerMachine = ({
782
835
  target,
783
836
  midpoint: { x: snap((current.from.x + current.to.x) / 2), y: snap((current.from.y + current.to.y) / 2) },
784
837
  // A link between ports that declare a direction is drawn with one.
785
- directed: isDirected(scene, nodeRegistry, current.source, target),
838
+ ends: isDirected(scene, nodeRegistry, current.source, target) ? { end: 'arrow' } : undefined,
839
+ });
840
+ projection.apply({
841
+ kind: 'link',
842
+ link: { ...link, layer: activeLayer(scene, registry.get(atoms.layer)) },
786
843
  });
787
- projection.apply({ kind: 'link', link });
788
- // The new link is what the user just made, so it is what they act on next (style, delete).
789
- select([link.id]);
844
+ // What the user just made is what they act on next (style, delete): the link, and the node
845
+ // the drop created with it.
846
+ select(created ? [link.id, created.id] : [link.id]);
790
847
  }
791
848
  break;
792
849
  }
@@ -832,6 +889,9 @@ export const usePointerMachine = ({
832
889
  commitCreated,
833
890
  addNode,
834
891
  minor,
892
+ cell,
893
+ snapMinor,
894
+ activate,
835
895
  ],
836
896
  );
837
897
 
@@ -15,8 +15,8 @@ const NAVIGATION_SETTLE_MS = 150;
15
15
  export type SceneCamera = {
16
16
  /** Write the camera, from a value or from its predecessor. */
17
17
  setCamera: (next: Camera | ((camera: Camera) => Camera)) => void;
18
- /** Animate to `target`, calling `done` when the last frame lands. */
19
- animateTo: (target: Camera, done?: () => void) => void;
18
+ /** Animate to `target`, calling `done` when the last frame lands; `duration` fixes its length in ms. */
19
+ animateTo: (target: Camera, done?: () => void, duration?: number) => void;
20
20
  /** Abandon an animation in flight, leaving the camera where it reached. */
21
21
  cancelAnimation: () => void;
22
22
  /** The camera is moving on its own, so the canvas ignores the pointer. */
@@ -88,14 +88,21 @@ export const useSceneCamera = (
88
88
  }, [setNavigation]);
89
89
 
90
90
  const animateTo = useCallback(
91
- (target: Camera, done?: () => void) => {
91
+ (target: Camera, done?: () => void, duration?: number) => {
92
92
  cancelAnimation();
93
93
  setNavigation(true);
94
- cancelRef.current = animateCamera(registry.get(atoms.camera), target, viewport, setCamera, () => {
95
- cancelRef.current = undefined;
96
- setNavigation(false);
97
- done?.();
98
- });
94
+ cancelRef.current = animateCamera(
95
+ registry.get(atoms.camera),
96
+ target,
97
+ viewport,
98
+ setCamera,
99
+ () => {
100
+ cancelRef.current = undefined;
101
+ setNavigation(false);
102
+ done?.();
103
+ },
104
+ duration,
105
+ );
99
106
  },
100
107
  [registry, atoms.camera, viewport, setCamera, cancelAnimation, setNavigation],
101
108
  );
@@ -9,6 +9,7 @@ import { type SceneViewAtoms } from '../../model/atoms.ts';
9
9
  import { type Projection } from '../../model/projection.ts';
10
10
  import { type Capabilities, type ElementId, type Point, type Scene } from '../../model/types.ts';
11
11
  import { clipboardBounds, copySelection, pasteFragment } from '../../utils/clipboard.ts';
12
+ import { activeLayer } from '../../utils/layers.ts';
12
13
  import { between, topZ } from '../../utils/order.ts';
13
14
  import { type SceneSnap } from './useSceneSnap.ts';
14
15
 
@@ -78,6 +79,7 @@ export const useSceneClipboard = ({
78
79
  createId,
79
80
  nodeZ: () => (nodeZ = between(nodeZ, undefined)),
80
81
  linkZ: () => (linkZ = between(linkZ, undefined)),
82
+ layer: activeLayer(scene, registry.get(atoms.layer)),
81
83
  });
82
84
  projection.apply(intent);
83
85
  registry.set(atoms.clipboard, { ...fragment, pasted: at ? fragment.pasted : fragment.pasted + 1 });
@@ -91,6 +93,8 @@ export const useSceneClipboard = ({
91
93
  snap,
92
94
  scene.nodes,
93
95
  scene.links,
96
+ scene,
97
+ atoms.layer,
94
98
  projection,
95
99
  select,
96
100
  createId,
@@ -11,7 +11,7 @@ import { nodeDef } from '../../model/node-def.ts';
11
11
  import { type Projection } from '../../model/projection.ts';
12
12
  import { type LinkRegistry, type NodeRegistry } from '../../model/registry.ts';
13
13
  import { type Bounds, type Capabilities, type ElementId, type Scene, type Size } from '../../model/types.ts';
14
- import { fitBounds, zoomAt } from '../../utils/camera.ts';
14
+ import { NOMINAL_ZOOM, fitBounds, zoomAt } from '../../utils/camera.ts';
15
15
  import { unionBounds } from '../../utils/hit.ts';
16
16
  import { nodeBounds } from '../../utils/shapes.ts';
17
17
  import { toolForKey } from '../Palette/Palette.tsx';
@@ -119,18 +119,20 @@ export const useSceneKeys = ({
119
119
  break;
120
120
  }
121
121
  case 'fit':
122
- animateTo(fitBounds(bounds, viewport, inset));
122
+ animateTo(fitBounds(bounds, viewport, inset, NOMINAL_ZOOM));
123
123
  event.preventDefault();
124
124
  break;
125
125
  case 'fitSelection': {
126
126
  const union = unionBounds(selectedNodes.map((id) => nodeBounds(scene.nodes[id])));
127
127
  if (union) {
128
- animateTo(fitBounds(union, viewport, inset));
128
+ animateTo(fitBounds(union, viewport, inset, NOMINAL_ZOOM));
129
129
  }
130
130
  break;
131
131
  }
132
132
  case 'zoomReset':
133
- animateTo(zoomAt(registry.get(atoms.camera), { x: viewport.width / 2, y: viewport.height / 2 }, 1));
133
+ animateTo(
134
+ zoomAt(registry.get(atoms.camera), { x: viewport.width / 2, y: viewport.height / 2 }, NOMINAL_ZOOM),
135
+ );
134
136
  break;
135
137
  case 'back':
136
138
  goHistory(-1);
@@ -6,6 +6,8 @@ import { type MutableRefObject, useCallback, useEffect, useMemo, useRef } from '
6
6
 
7
7
  import { type useRegistry } from '../../hooks/index.ts';
8
8
  import { type Drag, type HistoryEntry, type SceneViewAtoms } from '../../model/atoms.ts';
9
+ import { nodeDef } from '../../model/node-def.ts';
10
+ import { type NodeRegistry } from '../../model/registry.ts';
9
11
  import { type SceneStore } from '../../model/store.ts';
10
12
  import {
11
13
  type Bounds,
@@ -15,13 +17,27 @@ import {
15
17
  type Scene,
16
18
  type SceneId,
17
19
  type Size,
18
- isPortalNode,
20
+ isFrameNode,
19
21
  } from '../../model/types.ts';
20
- import { coverage, enterPortal, exitPortal, fitBounds, portalFrame, portalScale } from '../../utils/camera.ts';
21
- import { contentBounds, sceneBounds } from '../../utils/hit.ts';
22
+ import {
23
+ DRILL_ANIMATION_MS,
24
+ NOMINAL_ZOOM,
25
+ coverage,
26
+ enterPortal,
27
+ exitPortal,
28
+ fitBounds,
29
+ portalFrame,
30
+ } from '../../utils/camera.ts';
31
+ import { contentBounds } from '../../utils/hit.ts';
22
32
  import { nodeBounds } from '../../utils/shapes.ts';
23
33
  import { type SceneCamera } from './useSceneCamera.ts';
24
34
 
35
+ /** An empty root has nothing to frame: fitting it centres the origin at true size. */
36
+ const EMPTY_FRAME: Bounds = { x: 0, y: 0, width: 0, height: 0 };
37
+
38
+ /** The shapes of a scene, unpadded: what Fit frames, with its inset as the only margin. */
39
+ const fitFrame = (scene: Scene): Bounds => contentBounds(scene, 0, 1) ?? EMPTY_FRAME;
40
+
25
41
  /** A portal covering this much of the viewport becomes the root; a root below this yields to its parent. */
26
42
  const AUTO_ENTER = 0.85;
27
43
  const AUTO_EXIT = 0.3;
@@ -30,6 +46,7 @@ const AUTO_DRILL_MS = 150;
30
46
  export type UseSceneNavigationOptions = {
31
47
  registry: ReturnType<typeof useRegistry>;
32
48
  atoms: SceneViewAtoms;
49
+ nodeRegistry: NodeRegistry;
33
50
  store: SceneStore;
34
51
  scenes: Record<SceneId, Scene>;
35
52
  scene: Scene;
@@ -48,13 +65,14 @@ export type SceneNavigation = {
48
65
  nameOf: (id: SceneId) => string;
49
66
  /** The portal in `parent` that shows `childId`, if any. */
50
67
  portalTo: (parentId: SceneId | undefined, childId: SceneId) => Node | undefined;
51
- /** The frame of the scene at the head of `path`: its parent portal's frame, or its derived bounds. */
68
+ /** What fitting the scene at the head of `path` shows: its parent portal's frame, or its content. */
52
69
  frameOf: (path: SceneId[], scene: Scene) => Bounds;
53
70
  /** The current scene's frame, held for the visit rather than re-derived as its content changes. */
54
71
  bounds: Bounds;
55
- /** The zoom against this level's own 1:1 rather than the root's; display only. */
56
- nominalZoom: number;
72
+ /** What Fit shows: the current scene's shapes themselves (the margin is the fit's own inset). */
73
+ fitTarget: Bounds;
57
74
  pushHistory: (entry: HistoryEntry) => void;
75
+ /** Drills into a frame's scene, or opens the node in the host when its type has `hostOpen` for it. */
58
76
  drillIn: (portal: Node, animate?: boolean) => void;
59
77
  drillOut: (levels?: number, animate?: boolean) => void;
60
78
  goHistory: (offset: number) => void;
@@ -68,6 +86,7 @@ export type SceneNavigation = {
68
86
  export const useSceneNavigation = ({
69
87
  registry,
70
88
  atoms,
89
+ nodeRegistry,
71
90
  scenes,
72
91
  scene,
73
92
  path,
@@ -88,7 +107,7 @@ export const useSceneNavigation = ({
88
107
  (parentId: SceneId | undefined, childId: SceneId): Node | undefined => {
89
108
  const parent = parentId ? scenes[parentId] : undefined;
90
109
  return parent
91
- ? Object.values(parent.nodes).find((node) => isPortalNode(node) && node.scene === childId)
110
+ ? Object.values(parent.nodes).find((node) => isFrameNode(node) && node.scene === childId)
92
111
  : undefined;
93
112
  },
94
113
  [scenes],
@@ -97,7 +116,7 @@ export const useSceneNavigation = ({
97
116
  const frameOf = useCallback(
98
117
  (scenePath: SceneId[], current: Scene): Bounds => {
99
118
  const portal = portalTo(scenePath[scenePath.length - 2], current.id);
100
- return portal ? portalFrame(portal, contentBounds(current)) : sceneBounds(current);
119
+ return portal ? portalFrame(portal, contentBounds(current)) : (contentBounds(current) ?? EMPTY_FRAME);
101
120
  },
102
121
  [portalTo],
103
122
  );
@@ -118,23 +137,7 @@ export const useSceneNavigation = ({
118
137
  return frameRef.current.frame;
119
138
  }, [frameOf, path, scene]);
120
139
 
121
- /**
122
- * A portal frame is the portal's box times a power of the grid ratio, so entering one divides the
123
- * camera by that factor; reported raw, the number would drop fourfold on a drill-in that changed
124
- * nothing the user can see. Display only — nothing derives geometry from it, so the frame growing
125
- * with its content cannot feed back.
126
- */
127
- const nominalZoom = useMemo(() => {
128
- const scale = path.slice(1).reduce((accumulated, sceneId, index) => {
129
- const parent = scenes[path[index]];
130
- const child = scenes[sceneId];
131
- const portal = parent && child ? portalTo(parent.id, sceneId) : undefined;
132
- return portal && child
133
- ? accumulated * portalScale(portal, portalFrame(portal, contentBounds(child)))
134
- : accumulated;
135
- }, 1);
136
- return camera.zoom / scale;
137
- }, [path, scenes, portalTo, camera.zoom]);
140
+ const fitTarget = useMemo(() => fitFrame(scene), [scene]);
138
141
 
139
142
  const pushHistory = useCallback(
140
143
  (entry: HistoryEntry) => {
@@ -145,9 +148,16 @@ export const useSceneNavigation = ({
145
148
  [registry, atoms.history],
146
149
  );
147
150
 
151
+ const hostOpen = useCallback((node: Node) => nodeDef(nodeRegistry, node)?.hostOpen?.(node), [nodeRegistry]);
152
+
148
153
  const drillIn = useCallback(
149
154
  (portal: Node, animate = true) => {
150
- const child = isPortalNode(portal) ? scenes[portal.scene] : undefined;
155
+ const open = hostOpen(portal);
156
+ if (open) {
157
+ open();
158
+ return;
159
+ }
160
+ const child = isFrameNode(portal) ? scenes[portal.scene] : undefined;
151
161
  if (!child) {
152
162
  return;
153
163
  }
@@ -168,14 +178,15 @@ export const useSceneNavigation = ({
168
178
  // Land the child where fitting it would, margin and all, but never past 1:1, so its text lands at
169
179
  // its natural size rather than magnified to fill the view. Expressed in the child's own space and
170
180
  // mapped back out, so the zoom ends exactly where the swap puts the camera.
171
- const target = exitPortal(fitBounds(childBounds, viewport, inset, 1), portal, childBounds);
172
- animateTo(target, () => swap(target));
181
+ const target = exitPortal(fitBounds(fitFrame(child), viewport, inset, NOMINAL_ZOOM), portal, childBounds);
182
+ animateTo(target, () => swap(target), DRILL_ANIMATION_MS);
173
183
  setOpening(portal.id);
174
184
  } else {
175
185
  swap(registry.get(atoms.camera));
176
186
  }
177
187
  },
178
188
  [
189
+ hostOpen,
179
190
  scenes,
180
191
  registry,
181
192
  atoms.path,
@@ -205,7 +216,7 @@ export const useSceneNavigation = ({
205
216
  const child = scenes[next[next.length - 1]];
206
217
  const parent = scenes[next[next.length - 2]];
207
218
  const portal = parent
208
- ? Object.values(parent.nodes).find((node) => isPortalNode(node) && node.scene === child?.id)
219
+ ? Object.values(parent.nodes).find((node) => isFrameNode(node) && node.scene === child?.id)
209
220
  : undefined;
210
221
  if (!child || !portal) {
211
222
  break;
@@ -220,7 +231,7 @@ export const useSceneNavigation = ({
220
231
  setCamera(exited);
221
232
  const parent = scenes[next[next.length - 1]];
222
233
  if (animate && parent) {
223
- animateTo(fitBounds(frameOf(next, parent), viewport, inset));
234
+ animateTo(fitBounds(fitFrame(parent), viewport, inset, NOMINAL_ZOOM), undefined, DRILL_ANIMATION_MS);
224
235
  }
225
236
  pushHistory({ path: next, camera: exited });
226
237
  },
@@ -266,7 +277,7 @@ export const useSceneNavigation = ({
266
277
  }
267
278
  const timer = setTimeout(() => {
268
279
  const portal = Object.values(scene.nodes).find(
269
- (node) => isPortalNode(node) && coverage(camera, nodeBounds(node), viewport) >= AUTO_ENTER,
280
+ (node) => isFrameNode(node) && !hostOpen(node) && coverage(camera, nodeBounds(node), viewport) >= AUTO_ENTER,
270
281
  );
271
282
  if (portal) {
272
283
  drillIn(portal, false);
@@ -282,7 +293,7 @@ export const useSceneNavigation = ({
282
293
  }
283
294
  }, AUTO_DRILL_MS);
284
295
  return () => clearTimeout(timer);
285
- }, [camera, scene, bounds, path, viewport, drag, drillIn, drillOut, registry, atoms.history, isAnimating]);
296
+ }, [camera, scene, bounds, path, viewport, drag, hostOpen, drillIn, drillOut, registry, atoms.history, isAnimating]);
286
297
 
287
- return { nameOf, portalTo, frameOf, bounds, nominalZoom, pushHistory, drillIn, drillOut, goHistory };
298
+ return { nameOf, portalTo, frameOf, bounds, fitTarget, pushHistory, drillIn, drillOut, goHistory };
288
299
  };