@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
@@ -9,9 +9,9 @@ import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
9
9
  import { SceneManager } from './SceneManager.ts';
10
10
 
11
11
  const PORT = 9006;
12
- const FREEHAND_URL = storybookUrl('ui-react-ui-canvas-scene-sceneview--freehand', PORT);
12
+ const FREEHAND_URL = storybookUrl('ui-react-ui-canvas-sceneview--freehand', PORT);
13
13
 
14
- // The fixture (`createSceneTree(1)`): rectangle A, ellipse B, text T, class C; links A→B (curve), A→C
14
+ // The fixture (`createSceneTree(1)`): rectangle A, ellipse B, text T, rectangle C; links A→B (curve), A→C
15
15
  // (line, directed) and B→C (spline). Every edge sits on the major grid.
16
16
  test.describe('SceneView', () => {
17
17
  let page: Page;
@@ -37,8 +37,8 @@ test.describe('SceneView', () => {
37
37
  test('draws the fixture with an arrowhead on the directed link', async () => {
38
38
  await expect(page.locator('[data-node-id]')).toHaveCount(4);
39
39
  expect(await scene.linkCount()).toBe(3);
40
- // One marker set per layer (arrow and circle, start and end); the directed line uses the end arrow.
41
- await expect(page.locator('[data-testid="scene-view"] marker')).toHaveCount(4);
40
+ // One marker set per layer (arrow, triangle and circle, start and end); the directed line uses the end arrow.
41
+ await expect(page.locator('[data-testid="scene-view"] marker')).toHaveCount(6);
42
42
  await expect(page.locator('[data-testid="scene-view"] path[marker-end]')).toHaveCount(1);
43
43
  });
44
44
 
@@ -83,7 +83,7 @@ test.describe('SceneView', () => {
83
83
  await expect(ports).toHaveCount(0);
84
84
  const box = await scene.box(scene.node('scene:root/a'));
85
85
  await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
86
- await expect(scene.node('scene:root/a')).toHaveClass(/border-primary-500\/50/);
86
+ await expect(scene.node('scene:root/a')).toHaveClass(/border-focus\/50/);
87
87
  await expect(ports).not.toHaveCount(0);
88
88
  await page.mouse.move(box.x + box.width + 200, box.y + box.height + 200);
89
89
  await expect(ports).toHaveCount(0);
@@ -139,6 +139,8 @@ test.describe('SceneView', () => {
139
139
  });
140
140
 
141
141
  test('a marquee replaces the selection, shift adds and alt subtracts', async () => {
142
+ // Fit frames the shapes tightly, so step out to leave empty canvas around A for the marquee to start on.
143
+ await scene.zoomOut();
142
144
  const a = await scene.box(scene.node('scene:root/a'));
143
145
  // The empty canvas above and left of A, dragging back over A's corner. Not the other corner: the
144
146
  // B→C spline passes below and right of A, and a press on a link starts an endpoint drag.
@@ -217,12 +219,12 @@ test.describe('SceneView', () => {
217
219
  // No node was created: the drop landed on an existing one rather than on empty canvas.
218
220
  expect(await scene.nodeCount()).toBe(4);
219
221
  // The new link is the selection, and nothing else is.
220
- await expect(page.locator('[data-link-id].stroke-primary-500')).toHaveCount(1);
222
+ await expect(page.locator('[data-link-id].stroke-focus')).toHaveCount(1);
221
223
  expect(await scene.selectedNodes()).toEqual([]);
222
224
  // The hover follows the pointer, not the gesture: the source is no longer highlighted, the target under
223
225
  // the released pointer is.
224
- await expect(scene.node('scene:root/a')).not.toHaveClass(/border-primary-500\/50/);
225
- await expect(scene.node('scene:root/c')).toHaveClass(/border-primary-500\/50/);
226
+ await expect(scene.node('scene:root/a')).not.toHaveClass(/border-focus\/50/);
227
+ await expect(scene.node('scene:root/c')).toHaveClass(/border-focus\/50/);
226
228
  });
227
229
 
228
230
  test('the line tool draws nothing when a press on a node never moves', async () => {
@@ -245,7 +247,7 @@ test.describe('SceneView', () => {
245
247
  // One step is ×1.25; both readouts round, so they can disagree by a point.
246
248
  await expect.poll(async () => Math.abs((await scene.zoom()) - fitted * 1.25)).toBeLessThanOrEqual(1);
247
249
  await page.getByTestId('toolbar-create').click();
248
- await page.getByTestId('create-class').click();
250
+ await page.getByTestId('create-note').click();
249
251
  await expect(page.locator('[data-node-id]')).toHaveCount(5);
250
252
  await page.getByTestId('toolbar-delete').click();
251
253
  await expect(page.locator('[data-node-id]')).toHaveCount(4);
@@ -260,25 +262,25 @@ test.describe('SceneView', () => {
260
262
  return rows[0] < rows[1] && rows[1] < rows[2];
261
263
  })
262
264
  .toBe(true);
263
- // A portal made from the toolbar gets its child scene, so Enter opens it.
265
+ // A frame made from the toolbar gets its child scene, so Enter opens it.
264
266
  await page.getByTestId('toolbar-create').click();
265
- await page.getByTestId('create-scene').click();
267
+ await page.getByTestId('create-frame').click();
266
268
  await scene.focus();
267
269
  await page.keyboard.press('Enter');
268
270
  await expect(page.getByTestId('toolbar-up')).toBeEnabled();
269
271
  });
270
272
 
271
- test('a scene shape opens from its zoom-in control', async () => {
273
+ test('a frame opens from its zoom-in control', async () => {
272
274
  await page.getByTestId('toolbar-create').click();
273
- await page.getByTestId('create-scene').click();
275
+ await page.getByTestId('create-frame').click();
274
276
  await page.getByTestId('portal-open').first().click();
275
277
  await expect(page.getByTestId('toolbar-up')).toBeEnabled();
276
278
  });
277
279
 
278
- test('the properties panel edits the selected class', async () => {
280
+ test('the properties panel edits the selected node', async () => {
279
281
  await scene.clickNode('scene:root/c');
280
282
  const labels = await page.locator('[data-testid="properties"] label').allTextContents();
281
- expect(labels).toEqual(expect.arrayContaining(['Name', 'Attributes', 'Methods', 'Style']));
283
+ expect(labels).toEqual(expect.arrayContaining(['Label', 'Style']));
282
284
  // Geometry is two labelled number fields per row.
283
285
  expect(labels).toEqual(expect.arrayContaining(['X', 'Y', 'Width', 'Height', 'Ports per side']));
284
286
  });
@@ -289,14 +291,15 @@ test.describe('SceneView', () => {
289
291
  const option = (key: string) => grid.locator(`[data-style-option="${key}"]`);
290
292
  // An unstyled node matches no swatch.
291
293
  await expect(grid.locator('[aria-checked="true"]')).toHaveCount(0);
292
- await option('blue:3').click();
293
- await expect(option('blue:3')).toHaveAttribute('aria-checked', 'true');
294
- await expect(scene.node('scene:root/a')).toHaveClass(/bg-blue-bg/);
294
+ // Tone 1 is the strongest fill.
295
+ await option('blue:1').click();
296
+ await expect(option('blue:1')).toHaveAttribute('aria-checked', 'true');
297
+ await expect(scene.node('scene:root/a')).toHaveClass(/bg-blue-500/);
295
298
  await option('neutral:0').click();
296
299
  await expect(option('neutral:0')).toHaveAttribute('aria-checked', 'true');
297
300
  // Outline: the fill goes (the selected frame's border shows the selection, not the hue).
298
301
  await expect(scene.node('scene:root/a')).toHaveClass(/bg-transparent/);
299
- await expect(scene.node('scene:root/a')).not.toHaveClass(/bg-blue-bg/);
302
+ await expect(scene.node('scene:root/a')).not.toHaveClass(/bg-blue-500/);
300
303
  });
301
304
 
302
305
  test('the geometry fields step by the grid and move the node', async () => {
@@ -315,10 +318,10 @@ test.describe('SceneView', () => {
315
318
  await expect.poll(async () => (await scene.box(scene.node('scene:root/c'))).x).toBeGreaterThan(node.x);
316
319
  });
317
320
 
318
- test('a read-only view selects but draws no handles and applies no edit', async ({ browser }) => {
321
+ test('a read-only view selects nothing, draws no handles and applies no edit', async ({ browser }) => {
319
322
  await close?.();
320
323
  ({ page, close } = await setupPage(browser, {
321
- url: storybookUrl('ui-react-ui-canvas-scene-sceneview--readonly', PORT),
324
+ url: storybookUrl('ui-react-ui-canvas-sceneview--readonly', PORT),
322
325
  viewportSize: { width: 1400, height: 800 },
323
326
  }));
324
327
  page.on('pageerror', (error) => errors.push(error.message));
@@ -326,16 +329,15 @@ test.describe('SceneView', () => {
326
329
  await scene.ready();
327
330
  const before = await scene.nodeCount();
328
331
  await scene.clickNode('scene:root/a');
329
- expect(await scene.selectedNodes()).toEqual(['scene:root/a']);
332
+ expect(await scene.selectedNodes()).toEqual([]);
330
333
  await expect(page.locator('[data-testid="scene-view"] svg rect[style*="cursor"]')).toHaveCount(0);
331
334
  const hovered = await scene.box(scene.node('scene:root/a'));
332
335
  await page.mouse.move(hovered.x + hovered.width / 2, hovered.y + hovered.height / 2);
333
336
  await expect(page.locator('[data-testid="scene-view"] circle.cursor-crosshair')).toHaveCount(0);
334
337
  await page.keyboard.press('Delete');
335
338
  expect(await scene.nodeCount()).toBe(before);
336
- // Only the select and pan tools remain, and nothing on the toolbar can change the scene.
339
+ // Only the select and pan tools remain, and the actions bar, which only edits, is gone.
337
340
  await expect(page.locator('[data-testid="palette"] button')).toHaveCount(2);
338
- await expect(page.getByTestId('toolbar-create')).toBeDisabled();
339
- await expect(page.getByTestId('toolbar-delete')).toBeDisabled();
341
+ await expect(page.getByTestId('canvas-actions')).toHaveCount(0);
340
342
  });
341
343
  });
@@ -4,8 +4,9 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
- import { isClassNode, isEllipseNode, isPortalNode, isRectNode } from '../model/types.ts';
7
+ import { isEllipseNode, isFrameNode, isNoteNode, isRectNode } from '../model/types.ts';
8
8
  import { SceneBuilder } from './builder.ts';
9
+ import { DEFAULT_LAYER } from './layers.ts';
9
10
 
10
11
  const box = (x: number, y: number) => ({ x, y, width: 200, height: 100 });
11
12
 
@@ -14,10 +15,7 @@ describe('SceneBuilder', () => {
14
15
  const { root, scenes } = SceneBuilder.scene('s', [
15
16
  SceneBuilder.rect('a', box(0, 0)).properties({ label: 'A' }),
16
17
  SceneBuilder.ellipse('b', box(400, 0)).properties({ label: 'B' }),
17
- SceneBuilder.class('c', { x: 0, y: 300, width: 200, height: 150 }).properties({
18
- name: 'C',
19
- attributes: ['id: string'],
20
- }),
18
+ SceneBuilder.note('c', { x: 0, y: 300, width: 200, height: 150 }).properties({ style: { hue: 'teal' } }),
21
19
  SceneBuilder.link('line', 'a#e2', 'b#w2'),
22
20
  SceneBuilder.link('spline', 'b', 'c').properties({ points: [{ x: 300, y: 250 }] }),
23
21
  SceneBuilder.link('line', '@10,20', 'a')
@@ -33,9 +31,9 @@ describe('SceneBuilder', () => {
33
31
  expect(Object.keys(scene.nodes)).toEqual(['a', 'b', 'c']);
34
32
  expect(scene.nodes.a.center).toEqual({ x: 100, y: 50 });
35
33
  expect(isEllipseNode(scene.nodes.b) && scene.nodes.b.size).toEqual({ width: 200, height: 100 });
36
- expect(isClassNode(scene.nodes.c) && scene.nodes.c.attributes).toEqual(['id: string']);
37
- // A class keeps its type's default methods where the fixture does not set them.
38
- expect(isClassNode(scene.nodes.c) && scene.nodes.c.methods).toEqual(['save(): void']);
34
+ expect(scene.nodes.c.style).toEqual({ hue: 'teal' });
35
+ // A note keeps its type's default text where the fixture does not set it.
36
+ expect(isNoteNode(scene.nodes.c) && scene.nodes.c.text).toEqual('Note');
39
37
  expect(scene.links['a-b'].source).toEqual({ node: 'a', port: 'e2' });
40
38
  expect(scene.links['a-b'].target).toEqual({ node: 'b', port: 'w2' });
41
39
  expect(scene.links['b-c'].type === 'spline' && scene.links['b-c'].points).toEqual([{ x: 300, y: 250 }]);
@@ -70,7 +68,7 @@ describe('SceneBuilder', () => {
70
68
  expect(root).toBe('root');
71
69
  expect(scenes.map((scene) => scene.id)).toEqual(['root', 'f']);
72
70
  const portal = scenes[0].nodes.f;
73
- expect(isPortalNode(portal) && [portal.scene, portal.label, portal.center]).toEqual([
71
+ expect(isFrameNode(portal) && [portal.scene, portal.label, portal.center]).toEqual([
74
72
  'f',
75
73
  'Inner',
76
74
  { x: 500, y: 50 },
@@ -80,6 +78,45 @@ describe('SceneBuilder', () => {
80
78
  expect(() => SceneBuilder.scene('root', [SceneBuilder.scene('f')]).build()).toThrow(/needs a frame/);
81
79
  });
82
80
 
81
+ test('every scene has the default layer, and every element is on it', ({ expect }) => {
82
+ const { scenes } = SceneBuilder.scene('root', [
83
+ SceneBuilder.rect('a', box(0, 0)),
84
+ SceneBuilder.rect('b', box(400, 0)).properties({ layer: 'top' }),
85
+ SceneBuilder.link('line', 'a', 'b').id('ab'),
86
+ SceneBuilder.scene('f', [SceneBuilder.rect('f1', box(0, 0))]).at(box(0, 200)),
87
+ ]).build();
88
+ for (const scene of scenes) {
89
+ expect(scene.layers).toEqual({ [DEFAULT_LAYER.id]: DEFAULT_LAYER });
90
+ }
91
+ expect([scenes[0].nodes.a.layer, scenes[0].nodes.f.layer, scenes[0].links.ab.layer]).toEqual([
92
+ DEFAULT_LAYER.id,
93
+ DEFAULT_LAYER.id,
94
+ DEFAULT_LAYER.id,
95
+ ]);
96
+ // A layer the fixture names is kept.
97
+ expect(scenes[0].nodes.b.layer).toBe('top');
98
+ expect(scenes[1].nodes.f1.layer).toBe(DEFAULT_LAYER.id);
99
+ });
100
+
101
+ test('two elements with one id fail the build', ({ expect }) => {
102
+ const rect = SceneBuilder.rect('a', box(0, 0));
103
+ expect(() => SceneBuilder.scene('s', [rect, rect]).build()).toThrow(/Duplicate id a in scene s/);
104
+ const link = SceneBuilder.link('line', 'a', 'a').id('l');
105
+ expect(() => SceneBuilder.scene('s', [rect, link, link]).build()).toThrow(/Duplicate id l/);
106
+ // An explicit id that repeats one already generated clashes too.
107
+ const generated = SceneBuilder.link('line', 'a', 'a');
108
+ expect(() => SceneBuilder.scene('s', [rect, generated, generated.id('a-a')]).build()).toThrow(/Duplicate id a-a/);
109
+ });
110
+
111
+ test('an id an object inherits is not taken, and any id is kept', ({ expect }) => {
112
+ const rect = SceneBuilder.rect('constructor', box(0, 0));
113
+ const proto = SceneBuilder.rect('__proto__', box(0, 0));
114
+ const link = SceneBuilder.link('line', 'constructor', '__proto__').id('toString');
115
+ const [scene] = SceneBuilder.scene('s', [rect, proto, link]).build().scenes;
116
+ expect(Object.keys(scene.nodes)).toEqual(['constructor', '__proto__']);
117
+ expect(Object.keys(scene.links)).toEqual(['toString']);
118
+ });
119
+
83
120
  test('a node its type does not describe fails the build', ({ expect }) => {
84
121
  const host = SceneBuilder.node('rect', 'a', box(0, 0)).properties({ portsPerSide: 2 });
85
122
  expect(() => SceneBuilder.scene('s', [host]).build()).not.toThrow();
@@ -17,16 +17,17 @@ import {
17
17
  type BuiltinNode,
18
18
  type BuiltinNodeType,
19
19
  type Endpoint,
20
+ type FrameNode,
20
21
  type Link,
21
22
  type LinkType,
22
23
  type Node,
23
24
  type NodeBase,
24
25
  type NodeStyle,
25
26
  type Point,
26
- type PortalNode,
27
27
  type Scene,
28
28
  type SceneId,
29
29
  } from '../model/types.ts';
30
+ import { DEFAULT_LAYER } from './layers.ts';
30
31
  import { initialKeys } from './order.ts';
31
32
  import { createNode } from './shapes.ts';
32
33
 
@@ -110,7 +111,7 @@ export class SceneElement {
110
111
  readonly elements: readonly BuilderElement[],
111
112
  readonly frame?: Box,
112
113
  readonly sceneName?: string,
113
- readonly portal: NodeProperties<PortalNode> = {},
114
+ readonly portal: NodeProperties<FrameNode> = {},
114
115
  ) {}
115
116
 
116
117
  /** The scene's name (breadcrumbs, previews). */
@@ -124,7 +125,7 @@ export class SceneElement {
124
125
  }
125
126
 
126
127
  /** The scene shape's own properties (label, contents, style). */
127
- properties(values: NodeProperties<PortalNode>): SceneElement {
128
+ properties(values: NodeProperties<FrameNode>): SceneElement {
128
129
  return new SceneElement(this.id, this.elements, this.frame, this.sceneName, {
129
130
  ...this.portal,
130
131
  ...values,
@@ -143,42 +144,59 @@ export class SceneElement {
143
144
  export type SceneTree = { root: SceneId; scenes: Scene[] };
144
145
 
145
146
  const buildScene = (element: SceneElement, registry: NodeRegistry, scenes: Scene[]): void => {
146
- const nodes: Record<string, Node> = {};
147
- const links: Record<string, Link> = {};
147
+ // Maps, so any id (`constructor`, `__proto__`) is an ordinary key; `fromEntries` keeps them own properties.
148
+ const nodes = new Map<string, Node>();
149
+ const links = new Map<string, Link>();
148
150
  const keys = initialKeys(element.elements.length);
149
- const index = scenes.push({ id: element.id, name: element.sceneName, nodes, links }) - 1;
151
+ // Every element names its layer, so a fixture reads as a scene the editor wrote.
152
+ const index =
153
+ scenes.push({
154
+ id: element.id,
155
+ name: element.sceneName,
156
+ nodes: {},
157
+ links: {},
158
+ layers: { [DEFAULT_LAYER.id]: DEFAULT_LAYER },
159
+ }) - 1;
150
160
  element.elements.forEach((child, order) => {
151
161
  const z = keys[order];
152
162
  switch (child.kind) {
153
163
  case 'node': {
154
- const node = { ...child.node, z };
164
+ // A host's type starts from its own `create`, as a built-in starts from `createNode`, so a
165
+ // fixture sets only what it means and the type supplies the rest of its content.
166
+ const def = isBuiltinType(child.node.type) ? undefined : registry[child.node.type];
167
+ const fresh = def?.create({ id: child.node.id, z, center: child.node.center, size: child.node.size });
168
+ const node = { layer: DEFAULT_LAYER.id, ...fresh, ...child.node, z };
155
169
  check(registry, node);
156
- nodes[node.id] = node;
170
+ claim(nodes, node.id, element.id);
171
+ nodes.set(node.id, node);
157
172
  break;
158
173
  }
159
174
  case 'link': {
160
175
  const id = child.linkId ?? uniqueId(links, linkId(child.link.source, child.link.target));
161
- links[id] = { ...child.link, id, z };
176
+ claim(links, id, element.id);
177
+ links.set(id, { layer: DEFAULT_LAYER.id, ...child.link, id, z });
162
178
  break;
163
179
  }
164
180
  case 'scene': {
165
181
  if (!child.frame) {
166
182
  throw new Error(`Nested scene ${child.id} needs a frame: call at(box).`);
167
183
  }
168
- const node: PortalNode = {
169
- ...createNode({ type: 'scene', id: child.id, z, center: center(child.frame), size: sizeOf(child.frame) }),
184
+ const node: FrameNode = {
185
+ ...createNode({ type: 'frame', id: child.id, z, center: center(child.frame), size: sizeOf(child.frame) }),
186
+ layer: DEFAULT_LAYER.id,
170
187
  ...child.portal,
171
- type: 'scene',
188
+ type: 'frame',
172
189
  scene: child.id,
173
190
  };
174
191
  check(registry, node);
175
- nodes[node.id] = node;
192
+ claim(nodes, node.id, element.id);
193
+ nodes.set(node.id, node);
176
194
  buildScene(child, registry, scenes);
177
195
  break;
178
196
  }
179
197
  }
180
198
  });
181
- scenes[index] = { ...scenes[index], nodes, links };
199
+ scenes[index] = { ...scenes[index], nodes: Object.fromEntries(nodes), links: Object.fromEntries(links) };
182
200
  };
183
201
 
184
202
  /** A node its type does not describe (a wrong value, or a property the type does not have) fails the build. */
@@ -193,14 +211,21 @@ const check = (registry: NodeRegistry, node: Node): void => {
193
211
  }
194
212
  };
195
213
 
214
+ /** Two elements given one id would leave only the second, so the fixture fails instead. */
215
+ const claim = (taken: ReadonlyMap<string, unknown>, id: string, sceneId: SceneId): void => {
216
+ if (taken.has(id)) {
217
+ throw new Error(`Duplicate id ${id} in scene ${sceneId}.`);
218
+ }
219
+ };
220
+
196
221
  const linkId = (source: Endpoint, target: Endpoint): string => {
197
222
  const name = (end: Endpoint) => ('node' in end ? end.node : `${end.point.x},${end.point.y}`);
198
223
  return `${name(source)}-${name(target)}`;
199
224
  };
200
225
 
201
- const uniqueId = (taken: Record<string, unknown>, id: string): string => {
226
+ const uniqueId = (taken: ReadonlyMap<string, unknown>, id: string): string => {
202
227
  let candidate = id;
203
- for (let suffix = 2; candidate in taken; ++suffix) {
228
+ for (let suffix = 2; taken.has(candidate); ++suffix) {
204
229
  candidate = `${id}-${suffix}`;
205
230
  }
206
231
  return candidate;
@@ -221,7 +246,7 @@ function node(type: string, id: string, box: Box): NodeElement<NodeBase> {
221
246
  return new NodeElement<NodeBase>(base);
222
247
  }
223
248
 
224
- const BUILTIN_TYPES: readonly string[] = ['rect', 'ellipse', 'class', 'note', 'scene'] satisfies BuiltinNodeType[];
249
+ const BUILTIN_TYPES: readonly string[] = ['rect', 'ellipse', 'note', 'frame'] satisfies BuiltinNodeType[];
225
250
  const isBuiltinType = (type: string): type is BuiltinNodeType => BUILTIN_TYPES.includes(type);
226
251
 
227
252
  type Ends = Pick<Link, 'source' | 'target'>;
@@ -244,7 +269,6 @@ export const SceneBuilder = {
244
269
  node,
245
270
  rect: (id: string, box: Box) => node('rect', id, box),
246
271
  ellipse: (id: string, box: Box) => node('ellipse', id, box),
247
- class: (id: string, box: Box) => node('class', id, box),
248
272
  note: (id: string, box: Box) => node('note', id, box),
249
273
  link,
250
274
  };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type ExpectStatic, describe, test } from 'vitest';
6
6
 
7
- import { type Bounds, type Camera, type PortalNode, type Scene } from '../model/types.ts';
7
+ import { type Bounds, type Camera, type FrameNode } from '../model/types.ts';
8
8
  import {
9
9
  MAX_ZOOM,
10
10
  MIN_ZOOM,
@@ -13,17 +13,16 @@ import {
13
13
  exitPortal,
14
14
  fitBounds,
15
15
  portalFrame,
16
- portalScale,
17
16
  sceneToScreen,
18
17
  screenToScene,
19
18
  zoomAt,
20
19
  } from './camera.ts';
21
- import { contentBounds, sceneBounds } from './hit.ts';
20
+ import { nodeBounds } from './shapes.ts';
22
21
 
23
22
  const viewport = { width: 800, height: 600 };
24
23
 
25
- const portal: PortalNode = {
26
- type: 'scene',
24
+ const portal: FrameNode = {
25
+ type: 'frame',
27
26
  id: 'p',
28
27
  z: 'V',
29
28
  center: { x: 640, y: 620 },
@@ -101,58 +100,30 @@ describe('camera', () => {
101
100
  expect(coverage(camera, { x: -100, y: -100, width: 10, height: 10 }, viewport)).toBe(0);
102
101
  });
103
102
 
104
- test('portalFrame is a power-of-four multiple of the portal on the grid, containing the child bounds', ({
103
+ test("portalFrame centres the child in the smallest region of the portal's proportions that holds it", ({
105
104
  expect,
106
105
  }) => {
107
106
  const frame = portalFrame(portal, child);
108
- // 1600 × 1000 needs four 480 × 300 portals across; the frame is 1920 × 1200.
109
- expect([frame.width, frame.height]).toEqual([1920, 1200]);
110
- // On the major grid scaled by the factor, so the child's major lines land on the parent's major lines
111
- // (a frame on the plain major grid only matches their spacing, up to three minor cells out of phase).
112
- expect(Math.abs(frame.x % 256)).toBe(0);
113
- expect(Math.abs(frame.y % 256)).toBe(0);
114
- expect(frame.x).toBeLessThanOrEqual(child.x);
115
- expect(frame.y).toBeLessThanOrEqual(child.y);
116
- expect(frame.x + frame.width).toBeGreaterThanOrEqual(child.x + child.width);
117
- expect(frame.y + frame.height).toBeGreaterThanOrEqual(child.y + child.height);
118
- // As near the child's centre as containing it allows.
119
- expect(Math.abs(frame.x + frame.width / 2 - (child.x + child.width / 2))).toBeLessThanOrEqual(256);
120
- expect(Math.abs(frame.y + frame.height / 2 - (child.y + child.height / 2))).toBeLessThanOrEqual(256);
107
+ const { width, height } = nodeBounds(portal);
108
+ // The portal's proportions, just large enough for the child on its tighter axis.
109
+ expect(frame.width / frame.height).toBeCloseTo(width / height);
110
+ expect(Math.max(child.width / frame.width, child.height / frame.height)).toBeCloseTo(1);
111
+ // Centred on the child.
112
+ expect(frame.x + frame.width / 2).toBeCloseTo(child.x + child.width / 2);
113
+ expect(frame.y + frame.height / 2).toBeCloseTo(child.y + child.height / 2);
121
114
  // A frame is its own frame, so drilling in and out is stable.
122
115
  expect(portalFrame(portal, frame)).toEqual(frame);
123
- // A child smaller than the portal still gets four portals across: the parent's minor grid is the
124
- // child's major one, never the same level.
125
- const small = portalFrame(portal, { x: 64, y: 64, width: 128, height: 128 });
126
- expect([small.width, small.height]).toEqual([1920, 1200]);
127
- expect(small.x).toBeLessThanOrEqual(64);
128
- expect(small.x + small.width).toBeGreaterThanOrEqual(192);
129
- // A child past four portals across jumps to sixteen, never to an in-between factor whose grid
130
- // would miss the parent's lines.
131
- const large = portalFrame(portal, { x: 0, y: 0, width: 2400, height: 600 });
132
- expect([large.width, large.height]).toEqual([7680, 4800]);
133
116
  });
134
117
 
135
- test('a small child in a small portal is framed by its content, not by the editing floor', ({ expect }) => {
136
- // A portal small enough that the default extent does not fit four of it across: framed by the floor
137
- // the child would need sixteen and so draw a quarter of the size, for content it does not have.
138
- const tile: PortalNode = {
139
- type: 'scene',
140
- id: 'p',
141
- z: 'A',
142
- center: { x: 0, y: 0 },
143
- size: { width: 256, height: 160 },
144
- scene: 'c',
145
- };
146
- const scene: Scene = {
147
- id: 'c',
148
- nodes: { a: { type: 'rect', id: 'a', z: 'A', center: { x: 0, y: 0 }, size: { width: 128, height: 128 } } },
149
- links: {},
150
- };
151
- const framed = portalFrame(tile, contentBounds(scene));
152
- const floored = portalFrame(tile, sceneBounds(scene));
153
- expect([framed.width, framed.height]).toEqual([1024, 640]);
154
- expect([floored.width, floored.height]).toEqual([4096, 2560]);
155
- // The child therefore draws four times the size in the same tile.
156
- expect(portalScale(tile, framed) / portalScale(tile, floored)).toBe(4);
118
+ test("a small or empty child is never magnified: the frame is at least the portal's own box", ({ expect }) => {
119
+ const { width, height } = nodeBounds(portal);
120
+ const small = portalFrame(portal, { x: 64, y: 64, width: 128, height: 128 });
121
+ expect([small.width, small.height]).toEqual([width, height]);
122
+ expect(small.x + small.width / 2).toBe(128);
123
+ const empty = portalFrame(portal, undefined);
124
+ expect(empty).toEqual({ x: -width / 2, y: -height / 2, width, height });
125
+ // So a shape inside keeps its size through the drill-in: entering maps the zoom one to one.
126
+ const camera = { x: 0, y: 0, zoom: 0.79 };
127
+ expect(enterPortal(camera, portal, empty).zoom).toBeCloseTo(0.79);
157
128
  });
158
129
  });
@@ -9,20 +9,15 @@
9
9
 
10
10
  import { interpolateZoom } from 'd3';
11
11
 
12
- import {
13
- type Bounds,
14
- type Camera,
15
- MAJOR_GRID,
16
- MAJOR_GRID_RATIO,
17
- type Node,
18
- type Point,
19
- type Size,
20
- } from '../model/types.ts';
12
+ import { type Bounds, type Camera, type Node, type Point, type Size } from '../model/types.ts';
21
13
  import { nodeBounds } from './shapes.ts';
22
14
 
23
15
  export const MIN_ZOOM = 1 / 32;
24
16
  export const MAX_ZOOM = 32;
25
17
 
18
+ /** True size: one scene px is one screen px, at every level. Fit never zooms past it. */
19
+ export const NOMINAL_ZOOM = 1;
20
+
26
21
  export const clampZoom = (zoom: number) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
27
22
 
28
23
  export const sceneToScreen = (camera: Camera, point: Point): Point => ({
@@ -82,35 +77,25 @@ export const portalScale = (portal: Node, region: Bounds) => {
82
77
  };
83
78
 
84
79
  /**
85
- * The frame a portal gives its child: the child-space region that maps exactly onto the portal. It is
86
- * the portal's box scaled by the smallest power of the grid ratio, at least one, that contains the child's
87
- * derived bounds, placed on the major grid scaled by that factor as near their centre as containing them
88
- * allows. A power of the ratio maps every child grid level onto a parent level (the child's major grid is
89
- * the parent's minor one, one level down), and placing the frame on the scaled grid puts the child's lines
90
- * on the parent's, not merely at their spacing, so the grids stay aligned through a drill-in. Drilling in
91
- * shows this frame; a portal draws the child centred in it.
80
+ * The frame a portal gives its child: the child-space region, of the portal's proportions, that maps onto
81
+ * the portal's box. It is the child's content (`child`) centred in the smallest such region that holds it,
82
+ * and never smaller than the box itself, so the preview shrinks a large scene to fit but never magnifies a
83
+ * small one. Every scene keeps its own scale: the frame only says where the child sits under the portal,
84
+ * and entering it maps the camera through the frame, so a shape is the same size at the same zoom on any
85
+ * level. An empty child is the box itself, centred on its origin.
92
86
  */
93
- export const portalFrame = (portal: Node, child: Bounds, unit = MAJOR_GRID, ratio = MAJOR_GRID_RATIO): Bounds => {
87
+ export const portalFrame = (portal: Node, child: Bounds | undefined): Bounds => {
94
88
  const { width, height } = nodeBounds(portal);
95
- let factor = ratio;
96
- while (factor * width < child.width || factor * height < child.height) {
97
- factor *= ratio;
98
- }
89
+ const content = child ?? { x: 0, y: 0, width: 0, height: 0 };
90
+ const factor = Math.max(1, content.width / width, content.height / height);
99
91
  const frame = { width: width * factor, height: height * factor };
100
92
  return {
101
- x: place(child.x, child.width, frame.width, unit * factor),
102
- y: place(child.y, child.height, frame.height, unit * factor),
93
+ x: content.x + (content.width - frame.width) / 2,
94
+ y: content.y + (content.height - frame.height) / 2,
103
95
  ...frame,
104
96
  };
105
97
  };
106
98
 
107
- /** The grid-aligned start of a span of `length` centred on `[start, start + inner]` as far as containing it allows. */
108
- const place = (start: number, inner: number, length: number, unit: number): number => {
109
- const centred = Math.round((start + inner / 2 - length / 2) / unit) * unit;
110
- // Rounding a small negative yields -0, which `toEqual` and `Object.is` tell apart from 0.
111
- return Math.max(Math.min(centred, start), start + inner - length) + 0;
112
- };
113
-
114
99
  /** Child-scene CSS transform inside a portal node whose own origin is the node's top-left. */
115
100
  export const portalTransform = (portal: Node, frame: Bounds) => {
116
101
  const scale = portalScale(portal, frame);
@@ -142,6 +127,24 @@ export const exitPortal = (camera: Camera, portal: Node, child: Bounds): Camera
142
127
  };
143
128
  };
144
129
 
130
+ /** A portal filling this share of the viewport (on its tighter axis) leaves the rest of its layer fully shown… */
131
+ const FADE_START = 0.25;
132
+ /** …and this share hides it: zooming into a portal fades out what is around it, and zooming out fades it back. */
133
+ const FADE_END = 0.75;
134
+
135
+ /**
136
+ * The opacity of a layer around a portal, from how much of the viewport the portal fills on its tighter axis:
137
+ * 1 while it is small, falling to 0 as it grows towards filling the view. A wheel zoom, a drill-in and a
138
+ * drill-out all move the camera, so all three fade alike.
139
+ */
140
+ export const layerOpacity = (camera: Camera, bounds: Bounds, viewport: Size): number => {
141
+ if (viewport.width === 0 || viewport.height === 0) {
142
+ return 1;
143
+ }
144
+ const fill = Math.min((bounds.width * camera.zoom) / viewport.width, (bounds.height * camera.zoom) / viewport.height);
145
+ return Math.min(1, Math.max(0, (FADE_END - fill) / (FADE_END - FADE_START)));
146
+ };
147
+
145
148
  /** Fraction of the viewport covered by the bounds' screen rectangle. */
146
149
  export const coverage = (camera: Camera, bounds: Bounds, viewport: Size) => {
147
150
  if (viewport.width === 0 || viewport.height === 0) {
@@ -168,16 +171,20 @@ const fromView = ([cx, cy, width]: [number, number, number], viewport: Size): Ca
168
171
  export const MIN_ANIMATION_MS = 250;
169
172
  export const MAX_ANIMATION_MS = 800;
170
173
 
171
- /** Animate between cameras; returns a cancel function. */
174
+ /** How long a drill in or out takes, either way, so entering and leaving a scene feel the same. */
175
+ export const DRILL_ANIMATION_MS = 1000;
176
+
177
+ /** Animate between cameras, over `fixedDuration` ms or a time fitted to the distance; returns a cancel function. */
172
178
  export const animateCamera = (
173
179
  from: Camera,
174
180
  to: Camera,
175
181
  viewport: Size,
176
182
  apply: (camera: Camera) => void,
177
183
  done?: () => void,
184
+ fixedDuration?: number,
178
185
  ): (() => void) => {
179
186
  const interpolate = interpolateZoom(toView(from, viewport), toView(to, viewport));
180
- const duration = Math.min(MAX_ANIMATION_MS, Math.max(MIN_ANIMATION_MS, interpolate.duration));
187
+ const duration = fixedDuration ?? Math.min(MAX_ANIMATION_MS, Math.max(MIN_ANIMATION_MS, interpolate.duration));
181
188
  const start = performance.now();
182
189
  let frame = 0;
183
190
  const tick = (now: number) => {