@react-x11/components 0.9.0 → 0.11.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 (579) hide show
  1. package/README.md +10 -4
  2. package/dist/charts/node.d.ts +11 -1
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +44 -14
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/charts/render.d.ts +12 -3
  7. package/dist/charts/render.d.ts.map +1 -1
  8. package/dist/charts/render.js +94 -25
  9. package/dist/charts/render.js.map +1 -1
  10. package/dist/code/index.d.ts.map +1 -1
  11. package/dist/code/index.js +27 -24
  12. package/dist/code/index.js.map +1 -1
  13. package/dist/code-editor/completion.d.ts.map +1 -1
  14. package/dist/code-editor/completion.js +3 -2
  15. package/dist/code-editor/completion.js.map +1 -1
  16. package/dist/code-editor/index.d.ts.map +1 -1
  17. package/dist/code-editor/index.js +19 -0
  18. package/dist/code-editor/index.js.map +1 -1
  19. package/dist/code-editor/node.d.ts +177 -7
  20. package/dist/code-editor/node.d.ts.map +1 -1
  21. package/dist/code-editor/node.js +1329 -164
  22. package/dist/code-editor/node.js.map +1 -1
  23. package/dist/code-language/index.d.ts +1 -1
  24. package/dist/code-language/index.d.ts.map +1 -1
  25. package/dist/code-language/index.js +1 -1
  26. package/dist/code-language/index.js.map +1 -1
  27. package/dist/code-language/languages/glsl.d.ts.map +1 -1
  28. package/dist/code-language/languages/glsl.js +10 -2
  29. package/dist/code-language/languages/glsl.js.map +1 -1
  30. package/dist/code-language/languages/javascript.d.ts.map +1 -1
  31. package/dist/code-language/languages/javascript.js +18 -3
  32. package/dist/code-language/languages/javascript.js.map +1 -1
  33. package/dist/code-language/languages/json.d.ts.map +1 -1
  34. package/dist/code-language/languages/json.js +10 -2
  35. package/dist/code-language/languages/json.js.map +1 -1
  36. package/dist/code-language/languages/shell.d.ts.map +1 -1
  37. package/dist/code-language/languages/shell.js +10 -2
  38. package/dist/code-language/languages/shell.js.map +1 -1
  39. package/dist/code-language/languages/sql.d.ts.map +1 -1
  40. package/dist/code-language/languages/sql.js +20 -2
  41. package/dist/code-language/languages/sql.js.map +1 -1
  42. package/dist/code-language/runs.d.ts +31 -0
  43. package/dist/code-language/runs.d.ts.map +1 -1
  44. package/dist/code-language/runs.js +105 -0
  45. package/dist/code-language/runs.js.map +1 -1
  46. package/dist/code-language/stream.d.ts +22 -0
  47. package/dist/code-language/stream.d.ts.map +1 -1
  48. package/dist/code-language/stream.js +259 -9
  49. package/dist/code-language/stream.js.map +1 -1
  50. package/dist/code-language/types.d.ts +5 -4
  51. package/dist/code-language/types.d.ts.map +1 -1
  52. package/dist/codeblock/index.d.ts +26 -2
  53. package/dist/codeblock/index.d.ts.map +1 -1
  54. package/dist/codeblock/index.js +57 -4
  55. package/dist/codeblock/index.js.map +1 -1
  56. package/dist/flow/draw.d.ts +50 -0
  57. package/dist/flow/draw.d.ts.map +1 -1
  58. package/dist/flow/draw.js +132 -33
  59. package/dist/flow/draw.js.map +1 -1
  60. package/dist/flow/gl/color.d.ts +13 -0
  61. package/dist/flow/gl/color.d.ts.map +1 -0
  62. package/dist/flow/gl/color.js +82 -0
  63. package/dist/flow/gl/color.js.map +1 -0
  64. package/dist/flow/gl/index.d.ts +60 -0
  65. package/dist/flow/gl/index.d.ts.map +1 -0
  66. package/dist/flow/gl/index.js +283 -0
  67. package/dist/flow/gl/index.js.map +1 -0
  68. package/dist/flow/gl/pack.d.ts +133 -0
  69. package/dist/flow/gl/pack.d.ts.map +1 -0
  70. package/dist/flow/gl/pack.js +408 -0
  71. package/dist/flow/gl/pack.js.map +1 -0
  72. package/dist/flow/gl/renderer.d.ts +95 -0
  73. package/dist/flow/gl/renderer.d.ts.map +1 -0
  74. package/dist/flow/gl/renderer.js +459 -0
  75. package/dist/flow/gl/renderer.js.map +1 -0
  76. package/dist/flow/gl/shaders.d.ts +79 -0
  77. package/dist/flow/gl/shaders.d.ts.map +1 -0
  78. package/dist/flow/gl/shaders.js +346 -0
  79. package/dist/flow/gl/shaders.js.map +1 -0
  80. package/dist/flow/gl/text.d.ts +318 -0
  81. package/dist/flow/gl/text.d.ts.map +1 -0
  82. package/dist/flow/gl/text.js +1103 -0
  83. package/dist/flow/gl/text.js.map +1 -0
  84. package/dist/flow/index.d.ts +1 -1
  85. package/dist/flow/index.d.ts.map +1 -1
  86. package/dist/flow/index.js +619 -64
  87. package/dist/flow/index.js.map +1 -1
  88. package/dist/flow/node.d.ts +621 -72
  89. package/dist/flow/node.d.ts.map +1 -1
  90. package/dist/flow/node.js +2253 -1018
  91. package/dist/flow/node.js.map +1 -1
  92. package/dist/flow/paint.d.ts +35 -0
  93. package/dist/flow/paint.d.ts.map +1 -0
  94. package/dist/flow/paint.js +393 -0
  95. package/dist/flow/paint.js.map +1 -0
  96. package/dist/flow/paths.d.ts +14 -0
  97. package/dist/flow/paths.d.ts.map +1 -1
  98. package/dist/flow/paths.js +45 -19
  99. package/dist/flow/paths.js.map +1 -1
  100. package/dist/flow/scene.d.ts +584 -0
  101. package/dist/flow/scene.d.ts.map +1 -0
  102. package/dist/flow/scene.js +1453 -0
  103. package/dist/flow/scene.js.map +1 -0
  104. package/dist/flow/types.d.ts +111 -1
  105. package/dist/flow/types.d.ts.map +1 -1
  106. package/dist/html/controls.d.ts +28 -0
  107. package/dist/html/controls.d.ts.map +1 -1
  108. package/dist/html/controls.js +52 -2
  109. package/dist/html/controls.js.map +1 -1
  110. package/dist/html/css/calc.d.ts +14 -0
  111. package/dist/html/css/calc.d.ts.map +1 -0
  112. package/dist/html/css/calc.js +344 -0
  113. package/dist/html/css/calc.js.map +1 -0
  114. package/dist/html/css/cascade.d.ts +262 -6
  115. package/dist/html/css/cascade.d.ts.map +1 -1
  116. package/dist/html/css/cascade.js +1373 -67
  117. package/dist/html/css/cascade.js.map +1 -1
  118. package/dist/html/css/color.d.ts +23 -0
  119. package/dist/html/css/color.d.ts.map +1 -0
  120. package/dist/html/css/color.js +740 -0
  121. package/dist/html/css/color.js.map +1 -0
  122. package/dist/html/css/content.d.ts +92 -0
  123. package/dist/html/css/content.d.ts.map +1 -0
  124. package/dist/html/css/content.js +388 -0
  125. package/dist/html/css/content.js.map +1 -0
  126. package/dist/html/css/counter-styles.d.ts +76 -0
  127. package/dist/html/css/counter-styles.d.ts.map +1 -0
  128. package/dist/html/css/counter-styles.js +893 -0
  129. package/dist/html/css/counter-styles.js.map +1 -0
  130. package/dist/html/css/decode.d.ts +12 -0
  131. package/dist/html/css/decode.d.ts.map +1 -0
  132. package/dist/html/css/decode.js +116 -0
  133. package/dist/html/css/decode.js.map +1 -0
  134. package/dist/html/css/parse.d.ts +110 -10
  135. package/dist/html/css/parse.d.ts.map +1 -1
  136. package/dist/html/css/parse.js +1473 -180
  137. package/dist/html/css/parse.js.map +1 -1
  138. package/dist/html/css/style.d.ts +501 -15
  139. package/dist/html/css/style.d.ts.map +1 -1
  140. package/dist/html/css/style.js +3729 -206
  141. package/dist/html/css/style.js.map +1 -1
  142. package/dist/html/css/ua.d.ts.map +1 -1
  143. package/dist/html/css/ua.js +65 -16
  144. package/dist/html/css/ua.js.map +1 -1
  145. package/dist/html/css/values.d.ts +37 -3
  146. package/dist/html/css/values.d.ts.map +1 -1
  147. package/dist/html/css/values.js +122 -48
  148. package/dist/html/css/values.js.map +1 -1
  149. package/dist/html/css/vars.d.ts +52 -0
  150. package/dist/html/css/vars.d.ts.map +1 -0
  151. package/dist/html/css/vars.js +358 -0
  152. package/dist/html/css/vars.js.map +1 -0
  153. package/dist/html/dom.d.ts +30 -4
  154. package/dist/html/dom.d.ts.map +1 -1
  155. package/dist/html/dom.js +278 -17
  156. package/dist/html/dom.js.map +1 -1
  157. package/dist/html/fonts.d.ts +106 -0
  158. package/dist/html/fonts.d.ts.map +1 -0
  159. package/dist/html/fonts.js +565 -0
  160. package/dist/html/fonts.js.map +1 -0
  161. package/dist/html/gif.d.ts +15 -0
  162. package/dist/html/gif.d.ts.map +1 -0
  163. package/dist/html/gif.js +229 -0
  164. package/dist/html/gif.js.map +1 -0
  165. package/dist/html/index.d.ts +50 -11
  166. package/dist/html/index.d.ts.map +1 -1
  167. package/dist/html/index.js +115 -9
  168. package/dist/html/index.js.map +1 -1
  169. package/dist/html/layout/block.d.ts +226 -3
  170. package/dist/html/layout/block.d.ts.map +1 -1
  171. package/dist/html/layout/block.js +3077 -219
  172. package/dist/html/layout/block.js.map +1 -1
  173. package/dist/html/layout/boxes.d.ts +367 -17
  174. package/dist/html/layout/boxes.d.ts.map +1 -1
  175. package/dist/html/layout/boxes.js +1742 -182
  176. package/dist/html/layout/boxes.js.map +1 -1
  177. package/dist/html/layout/cache.d.ts +58 -0
  178. package/dist/html/layout/cache.d.ts.map +1 -0
  179. package/dist/html/layout/cache.js +224 -0
  180. package/dist/html/layout/cache.js.map +1 -0
  181. package/dist/html/layout/collapse.d.ts +45 -0
  182. package/dist/html/layout/collapse.d.ts.map +1 -0
  183. package/dist/html/layout/collapse.js +246 -0
  184. package/dist/html/layout/collapse.js.map +1 -0
  185. package/dist/html/layout/css-grid.d.ts +7 -0
  186. package/dist/html/layout/css-grid.d.ts.map +1 -0
  187. package/dist/html/layout/css-grid.js +900 -0
  188. package/dist/html/layout/css-grid.js.map +1 -0
  189. package/dist/html/layout/flex.d.ts.map +1 -1
  190. package/dist/html/layout/flex.js +1025 -89
  191. package/dist/html/layout/flex.js.map +1 -1
  192. package/dist/html/layout/floats.d.ts +45 -6
  193. package/dist/html/layout/floats.d.ts.map +1 -1
  194. package/dist/html/layout/floats.js +113 -14
  195. package/dist/html/layout/floats.js.map +1 -1
  196. package/dist/html/layout/grid-lines.d.ts +39 -0
  197. package/dist/html/layout/grid-lines.d.ts.map +1 -0
  198. package/dist/html/layout/grid-lines.js +119 -0
  199. package/dist/html/layout/grid-lines.js.map +1 -0
  200. package/dist/html/layout/grid.d.ts +53 -0
  201. package/dist/html/layout/grid.d.ts.map +1 -0
  202. package/dist/html/layout/grid.js +153 -0
  203. package/dist/html/layout/grid.js.map +1 -0
  204. package/dist/html/layout/inline.d.ts +154 -1
  205. package/dist/html/layout/inline.d.ts.map +1 -1
  206. package/dist/html/layout/inline.js +3598 -175
  207. package/dist/html/layout/inline.js.map +1 -1
  208. package/dist/html/layout/shaping.d.ts +6 -0
  209. package/dist/html/layout/shaping.d.ts.map +1 -0
  210. package/dist/html/layout/shaping.js +100 -0
  211. package/dist/html/layout/shaping.js.map +1 -0
  212. package/dist/html/layout/table.d.ts +3 -0
  213. package/dist/html/layout/table.d.ts.map +1 -1
  214. package/dist/html/layout/table.js +681 -175
  215. package/dist/html/layout/table.js.map +1 -1
  216. package/dist/html/layout/tracks.d.ts +44 -0
  217. package/dist/html/layout/tracks.d.ts.map +1 -0
  218. package/dist/html/layout/tracks.js +349 -0
  219. package/dist/html/layout/tracks.js.map +1 -0
  220. package/dist/html/node.d.ts +169 -4
  221. package/dist/html/node.d.ts.map +1 -1
  222. package/dist/html/node.js +1429 -137
  223. package/dist/html/node.js.map +1 -1
  224. package/dist/html/paint.d.ts +92 -3
  225. package/dist/html/paint.d.ts.map +1 -1
  226. package/dist/html/paint.js +3635 -107
  227. package/dist/html/paint.js.map +1 -1
  228. package/dist/html/resources.d.ts +60 -14
  229. package/dist/html/resources.d.ts.map +1 -1
  230. package/dist/html/resources.js +122 -36
  231. package/dist/html/resources.js.map +1 -1
  232. package/dist/html/surfaces.d.ts +23 -0
  233. package/dist/html/surfaces.d.ts.map +1 -0
  234. package/dist/html/surfaces.js +68 -0
  235. package/dist/html/surfaces.js.map +1 -0
  236. package/dist/html/svg.d.ts +80 -0
  237. package/dist/html/svg.d.ts.map +1 -0
  238. package/dist/html/svg.js +442 -0
  239. package/dist/html/svg.js.map +1 -0
  240. package/dist/html/url.d.ts +23 -0
  241. package/dist/html/url.d.ts.map +1 -0
  242. package/dist/html/url.js +80 -0
  243. package/dist/html/url.js.map +1 -0
  244. package/dist/index.d.ts +1 -1
  245. package/dist/index.d.ts.map +1 -1
  246. package/dist/index.js.map +1 -1
  247. package/dist/internal/codelines.d.ts +42 -0
  248. package/dist/internal/codelines.d.ts.map +1 -0
  249. package/dist/internal/codelines.js +170 -0
  250. package/dist/internal/codelines.js.map +1 -0
  251. package/dist/internal/collate.d.ts +3 -0
  252. package/dist/internal/collate.d.ts.map +1 -0
  253. package/dist/internal/collate.js +11 -0
  254. package/dist/internal/collate.js.map +1 -0
  255. package/dist/internal/markdown/ast.d.ts +11 -0
  256. package/dist/internal/markdown/ast.d.ts.map +1 -1
  257. package/dist/internal/markdown/parse.d.ts.map +1 -1
  258. package/dist/internal/markdown/parse.js +109 -8
  259. package/dist/internal/markdown/parse.js.map +1 -1
  260. package/dist/internal/prewarm.d.ts +18 -0
  261. package/dist/internal/prewarm.d.ts.map +1 -0
  262. package/dist/internal/prewarm.js +42 -0
  263. package/dist/internal/prewarm.js.map +1 -0
  264. package/dist/internal/sdf.d.ts +20 -0
  265. package/dist/internal/sdf.d.ts.map +1 -0
  266. package/dist/internal/sdf.js +216 -0
  267. package/dist/internal/sdf.js.map +1 -0
  268. package/dist/internal/splice.d.ts +12 -0
  269. package/dist/internal/splice.d.ts.map +1 -0
  270. package/dist/internal/splice.js +23 -0
  271. package/dist/internal/splice.js.map +1 -0
  272. package/dist/internal/window.d.ts +12 -0
  273. package/dist/internal/window.d.ts.map +1 -1
  274. package/dist/internal/window.js +23 -6
  275. package/dist/internal/window.js.map +1 -1
  276. package/dist/maps/anchors.d.ts +6 -1
  277. package/dist/maps/anchors.d.ts.map +1 -1
  278. package/dist/maps/anchors.js +260 -1
  279. package/dist/maps/anchors.js.map +1 -1
  280. package/dist/maps/controller.d.ts +57 -0
  281. package/dist/maps/controller.d.ts.map +1 -1
  282. package/dist/maps/controller.js +97 -18
  283. package/dist/maps/controller.js.map +1 -1
  284. package/dist/maps/gl/buckets.d.ts +5 -1
  285. package/dist/maps/gl/buckets.d.ts.map +1 -1
  286. package/dist/maps/gl/buckets.js +5 -2
  287. package/dist/maps/gl/buckets.js.map +1 -1
  288. package/dist/maps/gl/build-worker.d.ts +2 -0
  289. package/dist/maps/gl/build-worker.d.ts.map +1 -1
  290. package/dist/maps/gl/build-worker.js +1 -1
  291. package/dist/maps/gl/build-worker.js.map +1 -1
  292. package/dist/maps/gl/cover.d.ts +23 -4
  293. package/dist/maps/gl/cover.d.ts.map +1 -1
  294. package/dist/maps/gl/cover.js +61 -48
  295. package/dist/maps/gl/cover.js.map +1 -1
  296. package/dist/maps/gl/placement.d.ts +21 -8
  297. package/dist/maps/gl/placement.d.ts.map +1 -1
  298. package/dist/maps/gl/placement.js +133 -58
  299. package/dist/maps/gl/placement.js.map +1 -1
  300. package/dist/maps/gl/renderer.d.ts +36 -4
  301. package/dist/maps/gl/renderer.d.ts.map +1 -1
  302. package/dist/maps/gl/renderer.js +81 -24
  303. package/dist/maps/gl/renderer.js.map +1 -1
  304. package/dist/maps/gl/shaders.d.ts +18 -17
  305. package/dist/maps/gl/shaders.d.ts.map +1 -1
  306. package/dist/maps/gl/shaders.js +34 -32
  307. package/dist/maps/gl/shaders.js.map +1 -1
  308. package/dist/maps/gl/store.d.ts +10 -0
  309. package/dist/maps/gl/store.d.ts.map +1 -1
  310. package/dist/maps/gl/store.js +47 -4
  311. package/dist/maps/gl/store.js.map +1 -1
  312. package/dist/maps/gl/text.d.ts +153 -24
  313. package/dist/maps/gl/text.d.ts.map +1 -1
  314. package/dist/maps/gl/text.js +379 -63
  315. package/dist/maps/gl/text.js.map +1 -1
  316. package/dist/maps/gl/view.d.ts +293 -2
  317. package/dist/maps/gl/view.d.ts.map +1 -1
  318. package/dist/maps/gl/view.js +317 -82
  319. package/dist/maps/gl/view.js.map +1 -1
  320. package/dist/maps/icons.d.ts +32 -0
  321. package/dist/maps/icons.d.ts.map +1 -0
  322. package/dist/maps/icons.js +151 -0
  323. package/dist/maps/icons.js.map +1 -0
  324. package/dist/maps/index.d.ts +2 -0
  325. package/dist/maps/index.d.ts.map +1 -1
  326. package/dist/maps/index.js +2 -0
  327. package/dist/maps/index.js.map +1 -1
  328. package/dist/maps/labels.d.ts +56 -4
  329. package/dist/maps/labels.d.ts.map +1 -1
  330. package/dist/maps/labels.js +213 -46
  331. package/dist/maps/labels.js.map +1 -1
  332. package/dist/maps/node.d.ts +17 -0
  333. package/dist/maps/node.d.ts.map +1 -1
  334. package/dist/maps/node.js +64 -30
  335. package/dist/maps/node.js.map +1 -1
  336. package/dist/maps/proj.d.ts +48 -0
  337. package/dist/maps/proj.d.ts.map +1 -1
  338. package/dist/maps/proj.js +73 -0
  339. package/dist/maps/proj.js.map +1 -1
  340. package/dist/maps/sources.d.ts +5 -3
  341. package/dist/maps/sources.d.ts.map +1 -1
  342. package/dist/maps/sources.js +5 -3
  343. package/dist/maps/sources.js.map +1 -1
  344. package/dist/maps/style.d.ts +35 -0
  345. package/dist/maps/style.d.ts.map +1 -1
  346. package/dist/maps/style.js.map +1 -1
  347. package/dist/maps/styles.d.ts +12 -0
  348. package/dist/maps/styles.d.ts.map +1 -1
  349. package/dist/maps/styles.js +120 -6
  350. package/dist/maps/styles.js.map +1 -1
  351. package/dist/maps/tiles.d.ts +31 -11
  352. package/dist/maps/tiles.d.ts.map +1 -1
  353. package/dist/maps/tiles.js +61 -27
  354. package/dist/maps/tiles.js.map +1 -1
  355. package/dist/maps/types.d.ts +28 -4
  356. package/dist/maps/types.d.ts.map +1 -1
  357. package/dist/markdown/index.d.ts.map +1 -1
  358. package/dist/markdown/index.js +209 -68
  359. package/dist/markdown/index.js.map +1 -1
  360. package/dist/markdown/spans.d.ts.map +1 -1
  361. package/dist/markdown/spans.js +6 -0
  362. package/dist/markdown/spans.js.map +1 -1
  363. package/dist/qml/containers.d.ts.map +1 -1
  364. package/dist/qml/containers.js +6 -1
  365. package/dist/qml/containers.js.map +1 -1
  366. package/dist/qml/interaction.d.ts.map +1 -1
  367. package/dist/qml/interaction.js +38 -5
  368. package/dist/qml/interaction.js.map +1 -1
  369. package/dist/qml/layouts.d.ts.map +1 -1
  370. package/dist/qml/layouts.js +21 -6
  371. package/dist/qml/layouts.js.map +1 -1
  372. package/dist/qml/qtquick.d.ts.map +1 -1
  373. package/dist/qml/qtquick.js +9 -2
  374. package/dist/qml/qtquick.js.map +1 -1
  375. package/dist/qml/react.d.ts.map +1 -1
  376. package/dist/qml/react.js +9 -7
  377. package/dist/qml/react.js.map +1 -1
  378. package/dist/reorder/clock.d.ts +7 -0
  379. package/dist/reorder/clock.d.ts.map +1 -0
  380. package/dist/reorder/clock.js +26 -0
  381. package/dist/reorder/clock.js.map +1 -0
  382. package/dist/reorder/index.d.ts +25 -3
  383. package/dist/reorder/index.d.ts.map +1 -1
  384. package/dist/reorder/index.js +64 -19
  385. package/dist/reorder/index.js.map +1 -1
  386. package/dist/rich-text-editor/index.d.ts +0 -11
  387. package/dist/rich-text-editor/index.d.ts.map +1 -1
  388. package/dist/rich-text-editor/index.js +38 -9
  389. package/dist/rich-text-editor/index.js.map +1 -1
  390. package/dist/rich-text-editor/keymap.d.ts.map +1 -1
  391. package/dist/rich-text-editor/keymap.js +2 -1
  392. package/dist/rich-text-editor/keymap.js.map +1 -1
  393. package/dist/rich-text-editor/keys.d.ts +17 -6
  394. package/dist/rich-text-editor/keys.d.ts.map +1 -1
  395. package/dist/rich-text-editor/keys.js +135 -26
  396. package/dist/rich-text-editor/keys.js.map +1 -1
  397. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  398. package/dist/rich-text-editor/markdown.js +30 -1
  399. package/dist/rich-text-editor/markdown.js.map +1 -1
  400. package/dist/rich-text-editor/marks.d.ts +9 -0
  401. package/dist/rich-text-editor/marks.d.ts.map +1 -0
  402. package/dist/rich-text-editor/marks.js +83 -0
  403. package/dist/rich-text-editor/marks.js.map +1 -0
  404. package/dist/rich-text-editor/render.d.ts.map +1 -1
  405. package/dist/rich-text-editor/render.js +28 -10
  406. package/dist/rich-text-editor/render.js.map +1 -1
  407. package/dist/rich-text-editor/replace.d.ts +13 -0
  408. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  409. package/dist/rich-text-editor/replace.js +64 -0
  410. package/dist/rich-text-editor/replace.js.map +1 -0
  411. package/dist/rich-text-editor/toolbar.d.ts.map +1 -1
  412. package/dist/rich-text-editor/toolbar.js +1 -1
  413. package/dist/rich-text-editor/toolbar.js.map +1 -1
  414. package/dist/rich-text-editor/virtual.d.ts.map +1 -1
  415. package/dist/rich-text-editor/virtual.js +17 -0
  416. package/dist/rich-text-editor/virtual.js.map +1 -1
  417. package/dist/richtext/node.d.ts +46 -2
  418. package/dist/richtext/node.d.ts.map +1 -1
  419. package/dist/richtext/node.js +187 -4
  420. package/dist/richtext/node.js.map +1 -1
  421. package/dist/richtext/runs.d.ts +13 -8
  422. package/dist/richtext/runs.d.ts.map +1 -1
  423. package/dist/richtext/runs.js +18 -8
  424. package/dist/richtext/runs.js.map +1 -1
  425. package/dist/table/index.d.ts.map +1 -1
  426. package/dist/table/index.js +23 -0
  427. package/dist/table/index.js.map +1 -1
  428. package/dist/table/rows.d.ts +3 -2
  429. package/dist/table/rows.d.ts.map +1 -1
  430. package/dist/table/rows.js +20 -6
  431. package/dist/table/rows.js.map +1 -1
  432. package/dist/terminal/index.d.ts.map +1 -1
  433. package/dist/terminal/index.js +4 -0
  434. package/dist/terminal/index.js.map +1 -1
  435. package/dist/terminal/vt/pty.d.ts +13 -2
  436. package/dist/terminal/vt/pty.d.ts.map +1 -1
  437. package/dist/terminal/vt/pty.js +18 -3
  438. package/dist/terminal/vt/pty.js.map +1 -1
  439. package/dist/terminal-output/index.d.ts.map +1 -1
  440. package/dist/terminal-output/index.js +103 -23
  441. package/dist/terminal-output/index.js.map +1 -1
  442. package/dist/terminal-output/runs.d.ts +4 -1
  443. package/dist/terminal-output/runs.d.ts.map +1 -1
  444. package/dist/terminal-output/runs.js +5 -1
  445. package/dist/terminal-output/runs.js.map +1 -1
  446. package/dist/three/canvas.d.ts.map +1 -1
  447. package/dist/three/canvas.js +85 -7
  448. package/dist/three/canvas.js.map +1 -1
  449. package/dist/three/events.d.ts +22 -4
  450. package/dist/three/events.d.ts.map +1 -1
  451. package/dist/three/events.js +34 -4
  452. package/dist/three/events.js.map +1 -1
  453. package/dist/tree/index.d.ts.map +1 -1
  454. package/dist/tree/index.js +17 -0
  455. package/dist/tree/index.js.map +1 -1
  456. package/package.json +42 -33
  457. package/src/charts/node.ts +62 -26
  458. package/src/charts/render.ts +120 -30
  459. package/src/code/index.ts +36 -32
  460. package/src/code-editor/completion.ts +3 -2
  461. package/src/code-editor/index.ts +19 -0
  462. package/src/code-editor/node.ts +1574 -184
  463. package/src/code-language/index.ts +1 -1
  464. package/src/code-language/languages/glsl.ts +12 -2
  465. package/src/code-language/languages/javascript.ts +21 -4
  466. package/src/code-language/languages/json.ts +12 -2
  467. package/src/code-language/languages/shell.ts +12 -2
  468. package/src/code-language/languages/sql.ts +22 -2
  469. package/src/code-language/runs.ts +111 -1
  470. package/src/code-language/stream.ts +257 -17
  471. package/src/code-language/types.ts +5 -4
  472. package/src/codeblock/index.ts +68 -4
  473. package/src/flow/draw.ts +167 -41
  474. package/src/flow/gl/color.ts +85 -0
  475. package/src/flow/gl/index.ts +342 -0
  476. package/src/flow/gl/pack.ts +585 -0
  477. package/src/flow/gl/renderer.ts +679 -0
  478. package/src/flow/gl/shaders.ts +371 -0
  479. package/src/flow/gl/text.ts +1295 -0
  480. package/src/flow/index.ts +794 -67
  481. package/src/flow/node.ts +2496 -1256
  482. package/src/flow/paint.ts +511 -0
  483. package/src/flow/paths.ts +80 -15
  484. package/src/flow/scene.ts +2199 -0
  485. package/src/flow/types.ts +113 -1
  486. package/src/html/controls.ts +73 -2
  487. package/src/html/css/calc.ts +385 -0
  488. package/src/html/css/cascade.ts +1481 -62
  489. package/src/html/css/color.ts +755 -0
  490. package/src/html/css/content.ts +417 -0
  491. package/src/html/css/counter-styles.ts +973 -0
  492. package/src/html/css/decode.ts +130 -0
  493. package/src/html/css/parse.ts +1514 -160
  494. package/src/html/css/style.ts +4131 -223
  495. package/src/html/css/ua.ts +66 -17
  496. package/src/html/css/values.ts +150 -50
  497. package/src/html/css/vars.ts +348 -0
  498. package/src/html/dom.ts +284 -17
  499. package/src/html/fonts.ts +641 -0
  500. package/src/html/gif.ts +233 -0
  501. package/src/html/index.ts +180 -20
  502. package/src/html/layout/block.ts +3560 -208
  503. package/src/html/layout/boxes.ts +2141 -190
  504. package/src/html/layout/cache.ts +289 -0
  505. package/src/html/layout/collapse.ts +317 -0
  506. package/src/html/layout/css-grid.ts +1063 -0
  507. package/src/html/layout/flex.ts +1115 -106
  508. package/src/html/layout/floats.ts +128 -14
  509. package/src/html/layout/grid-lines.ts +141 -0
  510. package/src/html/layout/grid.ts +181 -0
  511. package/src/html/layout/inline.ts +4187 -192
  512. package/src/html/layout/shaping.ts +140 -0
  513. package/src/html/layout/table.ts +696 -186
  514. package/src/html/layout/tracks.ts +437 -0
  515. package/src/html/node.ts +1585 -156
  516. package/src/html/paint.ts +4582 -267
  517. package/src/html/resources.ts +161 -50
  518. package/src/html/surfaces.ts +81 -0
  519. package/src/html/svg.ts +504 -0
  520. package/src/html/url.ts +86 -0
  521. package/src/index.ts +2 -0
  522. package/src/internal/codelines.ts +200 -0
  523. package/src/internal/collate.ts +12 -0
  524. package/src/internal/markdown/ast.ts +11 -0
  525. package/src/internal/markdown/parse.ts +154 -11
  526. package/src/internal/prewarm.ts +45 -0
  527. package/src/internal/sdf.ts +227 -0
  528. package/src/internal/splice.ts +25 -0
  529. package/src/internal/window.ts +36 -5
  530. package/src/maps/anchors.ts +307 -1
  531. package/src/maps/controller.ts +116 -31
  532. package/src/maps/gl/buckets.ts +5 -1
  533. package/src/maps/gl/build-worker.ts +13 -2
  534. package/src/maps/gl/cover.ts +79 -44
  535. package/src/maps/gl/placement.ts +221 -69
  536. package/src/maps/gl/renderer.ts +104 -20
  537. package/src/maps/gl/shaders.ts +35 -32
  538. package/src/maps/gl/store.ts +66 -17
  539. package/src/maps/gl/text.ts +444 -71
  540. package/src/maps/gl/view.ts +361 -96
  541. package/src/maps/icons.ts +183 -0
  542. package/src/maps/index.ts +4 -0
  543. package/src/maps/labels.ts +269 -49
  544. package/src/maps/node.ts +82 -49
  545. package/src/maps/proj.ts +87 -0
  546. package/src/maps/sources.ts +5 -3
  547. package/src/maps/style.ts +32 -0
  548. package/src/maps/styles.ts +215 -4
  549. package/src/maps/tiles.ts +64 -31
  550. package/src/maps/types.ts +28 -4
  551. package/src/markdown/index.ts +293 -92
  552. package/src/markdown/spans.ts +5 -0
  553. package/src/qml/containers.tsx +6 -1
  554. package/src/qml/interaction.tsx +42 -5
  555. package/src/qml/layouts.tsx +21 -6
  556. package/src/qml/qtquick.tsx +9 -2
  557. package/src/qml/react.tsx +7 -5
  558. package/src/reorder/clock.ts +27 -0
  559. package/src/reorder/index.ts +100 -27
  560. package/src/rich-text-editor/index.ts +37 -12
  561. package/src/rich-text-editor/keymap.ts +1 -1
  562. package/src/rich-text-editor/keys.ts +135 -26
  563. package/src/rich-text-editor/markdown.ts +36 -1
  564. package/src/rich-text-editor/marks.ts +101 -0
  565. package/src/rich-text-editor/render.ts +49 -12
  566. package/src/rich-text-editor/replace.ts +85 -0
  567. package/src/rich-text-editor/toolbar.ts +1 -1
  568. package/src/rich-text-editor/virtual.ts +25 -0
  569. package/src/richtext/node.ts +235 -14
  570. package/src/richtext/runs.ts +23 -12
  571. package/src/table/index.ts +29 -0
  572. package/src/table/rows.ts +18 -8
  573. package/src/terminal/index.ts +4 -0
  574. package/src/terminal/vt/pty.ts +16 -2
  575. package/src/terminal-output/index.ts +129 -31
  576. package/src/terminal-output/runs.ts +16 -2
  577. package/src/three/canvas.ts +102 -8
  578. package/src/three/events.ts +49 -9
  579. package/src/tree/index.ts +19 -0
package/dist/flow/node.js CHANGED
@@ -21,11 +21,12 @@
21
21
  // `handleWheel`/`handleHover` — with the same veto, checked here.
22
22
  import * as ntk from 'react-x11/ntk';
23
23
  import { Node } from 'react-x11/node';
24
- import { tint } from 'react-x11/style';
25
24
  import { ctrlChordLetter, keysymOf, XK_BACKSPACE, XK_DELETE, XK_DOWN, XK_ESCAPE, XK_HOME, XK_LEFT, XK_RIGHT, XK_UP, } from 'react-x11/keysyms';
26
- import { createPainter, measureText, toDevice } from './draw.js';
27
- import { boundsOf, canConnect, clamp, DEFAULT_MARKER_SIZE, DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM, EDGE_SLOP, EDGE_STEP_OFFSET, fitViewport, handleAnchor, HANDLE_RADIUS, HANDLE_SLOP, gripPoint, inflateRect, intersectRects, measureNode, MIN_NODE_HEIGHT, MIN_NODE_WIDTH, NODE_BODY_INSET, NODE_DESC_SIZE, NODE_HEADER, NODE_LABEL_SIZE, NODE_PAD_X, NODE_PAD_Y, normalizeBackground, normalizeMarker, orientConnection, rectContains, rectsOverlap, RENDER_ZOOM, RESIZE_DIRECTIONS, RESIZE_GRIP, RESIZE_SLOP, resizeRect, resolveHandles, resolvePalette, snapTo, unionRects, ZOOM_STEP, } from './model.js';
28
- import { distanceToPath, edgePath, endAngle, pathBounds, pointAtFraction, startAngle, trimEnd, } from './paths.js';
25
+ import { createPainter, measureText, snapToGrid, toDevice } from './draw.js';
26
+ import { boundsOf, canConnect, clamp, DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM, EDGE_SLOP, fitViewport, HANDLE_RADIUS, HANDLE_SLOP, gripPoint, inflateRect, intersectRects, measureNode, MIN_NODE_HEIGHT, MIN_NODE_WIDTH, NODE_BODY_INSET, NODE_HEADER, normalizeBackground, orientConnection, rectContains, rectsOverlap, RENDER_ZOOM, RESIZE_DIRECTIONS, RESIZE_GRIP, RESIZE_SLOP, resizeRect, resolveHandles, resolvePalette, snapTo, unionRects, ZOOM_STEP, } from './model.js';
27
+ import { distanceToPath, pathBounds } from './paths.js';
28
+ import { paintFloat, paintGraph, paintGraphEdges, paintGraphNodes, paintGround, paintNodeItem, paintPanels, paintScene, } from './paint.js';
29
+ import { anchorsOf, buildNodeItems, buildScene, connectionPath, SceneCache, CULL_MARGIN, edgeCoarseBox, edgeInkBox, withLabelPlate, edgeRoute, batchesEdges, endpoint, HANDLE_ZOOM, MINIMAP_BLEED, miniMapPlace, screenRect, screenViewport, } from './scene.js';
29
30
  /** Registration key, `kind` and JSX tag, one string — react-x11 rejects a
30
31
  * node whose `kind` is not the name it was registered under, because `kind`
31
32
  * is what paint order, the test queries and the DEV style assertion all
@@ -35,31 +36,55 @@ export const ELEMENT = 'flowgraph';
35
36
  * global that wandered in would become an implicit `@types/node` dependency
36
37
  * a consumer has to satisfy. */
37
38
  const timers = globalThis;
39
+ /** How long the zoom has to hold still before mounted bodies come back at
40
+ * the new scale. Wheel notches in a flick land well inside it. */
41
+ const BODY_ZOOM_REST_MS = 150;
42
+ /** How long held bodies wait for the frame that draws a zoom's last step
43
+ * before they come back without it (`_restFrom`). */
44
+ const BODY_REST_CAP_MS = 1000;
45
+ /** What re-scaling the mounted bodies may add to one step of a zoom
46
+ * gesture before they sit the gesture out (`_holdBodies`). */
47
+ const BODY_BUDGET_MS = 8;
48
+ /** What one body costs a zoom step until the pane has measured it: 1.1–1.2
49
+ * ms on an M-series Mac, the 400-widget scene on Cocoa (42 bodies added
50
+ * 49 ms to a 13 ms step, 9 added 10). Learned from then on. */
51
+ const BODY_STEP_PRIOR_MS = 1.2;
52
+ /** Under GL, how long a zoom holds still before the world is rebuilt at it
53
+ * — until then each step draws the world already on the GPU, scaled
54
+ * (`glFrame`) — and how far that scaling may go before a step rebuilds
55
+ * anyway, so a long gesture never magnifies one build by more than this.
56
+ * The 2D renderer's zoom picture keeps to both. */
57
+ const GL_ZOOM_REST_MS = 120;
58
+ const GL_ZOOM_SPAN = 2;
59
+ /** How much of the pane a 2D zoom step may paint live, round the picture it
60
+ * composites — the ring a zoom out uncovers — before it paints a new one. */
61
+ const ZOOM_SHOT_LIVE = 0.5;
62
+ /**
63
+ * What painting the graph whole has to cost, ms, before a zoom paints a
64
+ * picture of it and composites that (`_zoomShotFor`) — and what it has to
65
+ * fall under before a zoom paints it live again. A picture pays for a graph
66
+ * too big to paint at every step and costs one that is not: on macOS at 2x
67
+ * its composite is ~20 ms a step whatever the graph, on X11 each new
68
+ * picture was a 50–150 ms step, and either way it holds a surface the size
69
+ * of the pane. A thirty-node graph painted live in 7 ms a step on Cocoa and
70
+ * zoomed at 90 frames a second where its picture held it to 45. A frame at
71
+ * 60 Hz to start, and a margin under it to stop, so a graph near the line
72
+ * does not go back and forth; judged on the median of the last
73
+ * `GRAPH_PAINTS_KEPT` whole paints, so a slow paint or two — a collection,
74
+ * the server catching up — decide nothing.
75
+ */
76
+ const ZOOM_SHOT_WORTH_MS = 16;
77
+ const ZOOM_SHOT_DROP_MS = 12;
78
+ const GRAPH_PAINTS_KEPT = 5;
79
+ /** How far round a box selection's outline its step claims: the pen, the
80
+ * corner's radius and a pixel of antialiasing, with room to spare. */
81
+ const SELECT_BAND = 4;
38
82
  /** How often the dash on an animated edge moves. Slow enough that a graph
39
83
  * full of them is not a repaint storm, fast enough to read as motion. */
40
84
  const ANIMATION_MS = 60;
41
85
  const ANIMATION_SPEED = 1.4; // px of dash travel per tick, at zoom 1
42
- const DEFAULT_DASH = [7, 5];
43
- /** Below these zooms the pane stops drawing detail nobody could read — the
44
- * cheapest optimisation there is, and the one that keeps a zoomed-out
45
- * overview interactive. */
46
- const LABEL_ZOOM = 0.45;
47
- const HANDLE_ZOOM = 0.5;
48
- const DESC_ZOOM = 0.6;
49
86
  /** Screen pixels the pointer may travel before a press becomes a drag. */
50
87
  const DRAG_THRESHOLD = 3;
51
- /** The grid never draws denser than this on screen, whatever the zoom. */
52
- const MIN_GRID_PX = 16;
53
- /** See `StrokeBuckets.BATCH_MIN`. */
54
- const MARKER_BATCH_MIN = 24;
55
- /**
56
- * How far outside its own box a node's ink can land: handles (capped at
57
- * 6 × 1.4 plus their outline), resize grips, the selection border, a pixel
58
- * of antialiasing. Damage rects grow by this before they are invalidated,
59
- * and cull tests grow their target by the same amount — the two must agree,
60
- * or a moved node leaves crumbs of its old handles behind.
61
- */
62
- const CULL_MARGIN = 16;
63
88
  /**
64
89
  * The props whose change means different pixels. `applyProps` repaints when
65
90
  * one of these moves and stays quiet otherwise — the event handlers are
@@ -81,7 +106,21 @@ const VISUAL_PROPS = [
81
106
  'nodesConnectable',
82
107
  'nodesResizable',
83
108
  'disabled',
109
+ 'renderer',
84
110
  ];
111
+ /** The clock, through `globalThis`: `src/` compiles with `types: []`. */
112
+ const clock = globalThis;
113
+ /**
114
+ * The pane's time: what a pan is stamped with, and what the dashes' march
115
+ * reads to decide it has held still. A test holds it (test/flow.test.ts):
116
+ * a runner that spends longer than the dashes' wait over one step of a pan
117
+ * lets them march in the middle of it — as they should — and a test of the
118
+ * pan cannot tell that from a march it should not have made.
119
+ */
120
+ export const flowClock = {
121
+ now: () => clock.performance?.now() ?? Date.now(),
122
+ };
123
+ const now = () => flowClock.now();
85
124
  const CONTROL_SIZE = 26;
86
125
  const PANEL_MARGIN = 10;
87
126
  const MINIMAP_W = 190;
@@ -124,6 +163,7 @@ export const SELF_DAMAGED_PROPS = [
124
163
  'zoomOnDoubleClick',
125
164
  'selectionOnDrag',
126
165
  'deleteOnKey',
166
+ 'adaptive',
127
167
  'snapToGrid',
128
168
  'snapGrid',
129
169
  ];
@@ -175,71 +215,13 @@ function unionMaybe(a, b) {
175
215
  return a;
176
216
  return unionRects(a, b);
177
217
  }
178
- /**
179
- * Edge strokes, grouped by the pen that will draw them.
180
- *
181
- * Everything a graph strokes is the same two or three pens — the default
182
- * edge, the selected one, the animated dash — so grouping collapses a
183
- * per-edge request into a per-pen one. The key has to carry the dash *and*
184
- * its offset: two edges marching out of phase cannot share a path, because
185
- * the offset is set on the context, not on the subpath.
186
- */
187
- class StrokeBuckets {
188
- byPen = new Map();
189
- bucket(stroke, lineWidth, dash, phase, zoom) {
190
- const scaled = dash?.map((d) => d * zoom);
191
- const key = `${stroke}|${lineWidth}|${scaled?.join(',') ?? ''}|${phase}`;
192
- let entry = this.byPen.get(key);
193
- if (!entry) {
194
- entry = {
195
- options: {
196
- stroke,
197
- lineWidth,
198
- dash: scaled,
199
- dashOffset: phase ? -phase * zoom : 0,
200
- },
201
- runs: [],
202
- };
203
- this.byPen.set(key, entry);
204
- }
205
- return entry.runs;
206
- }
207
- /**
208
- * Below this many edges in one pen, they are stroked one at a time.
209
- *
210
- * A path's mask is its bounding box, so batching scattered geometry trades
211
- * many small masks for one the size of the pane — about three quarters of
212
- * a megabyte at a normal window size. That is a large win at seven hundred
213
- * edges (measured: 3.9 MB a frame down to 1.3) and a loss at twenty, where
214
- * the individual masks never add up to a paneful. The threshold is where
215
- * they start to.
216
- */
217
- static BATCH_MIN = 24;
218
- paint(painter) {
219
- for (const { options, runs } of this.byPen.values()) {
220
- if (runs.length >= StrokeBuckets.BATCH_MIN) {
221
- painter.strokeRuns(runs, options);
222
- }
223
- else {
224
- for (const run of runs)
225
- painter.polyline(run, options);
226
- }
227
- }
228
- }
218
+ function sameRect(p, q) {
219
+ return (p.x === q.x && p.y === q.y && p.width === q.width && p.height === q.height);
229
220
  }
230
- /**
231
- * Are these the same handle? Anchors are rebuilt from the node on every read
232
- * — a drag moves them between commits — so identity says nothing, and the
233
- * `id` alone says too little: the node every graph starts with has *two*
234
- * handles with no id at all. Node, id, type and side together are what
235
- * distinguish them.
236
- */
237
- function sameHandle(a, b) {
238
- return (a != null &&
239
- a.nodeId === b.nodeId &&
240
- (a.id ?? null) === (b.id ?? null) &&
241
- a.type === b.type &&
242
- a.position === b.position);
221
+ function sameFit(a, b) {
222
+ return (sameRect(a.panel, b.panel) &&
223
+ sameRect(a.bounds, b.bounds) &&
224
+ a.scale === b.scale);
243
225
  }
244
226
  export class FlowGraphNode extends Node {
245
227
  // --- viewport, owned here unless the `viewport` prop takes it over ------
@@ -268,21 +250,146 @@ export class FlowGraphNode extends Node {
268
250
  * when the app applies the changes it is being sent. Cleared on release,
269
251
  * which is also how a refused drag snaps back. */
270
252
  _dragTo = null;
253
+ /** Whether the pane's edges are stroked a pen at a time (`EDGE_BATCH`):
254
+ * chosen by a pass over the whole pane, and followed by every pass over
255
+ * part of it until the next. */
256
+ _batch = false;
271
257
  /** The box a resize is currently making, for the same reason. */
272
258
  _resizeTo = null;
273
- /** What was last handed to `onNodeBodies`, so the React half is told only
274
- * when something actually moved. */
275
- _bodiesKey = '';
259
+ /** The bodies last sent, re-sent as the same array when only the origin
260
+ * moved, and that origin — and each one by id, reused as the same object
261
+ * when nothing about it changed, so the React half re-renders the card
262
+ * that moved and not the ones beside it. */
263
+ _bodies = [];
264
+ _bodiesById = new Map();
265
+ _bodiesOrigin = { x: 0, y: 0 };
266
+ /** Bodies held back while the zoom moves (`_holdBodies`): whether they
267
+ * are, the zoom last seen, the timer that brings them back and when the
268
+ * last held step was. */
269
+ _bodiesHeld = false;
270
+ _seenZoom = NaN;
271
+ _bodiesRest = null;
272
+ _bodiesAt = 0;
273
+ /** Mounted cards `<Flow>` shows, painted in the bodies' layer — which
274
+ * the 2D pane then leaves out (`setShownBodies`, `_cardInLayer`). */
275
+ _shownBodies = new Set();
276
+ /** The canvases `<Flow>` paints the minimap and controls on, over the
277
+ * bodies — while there are any, the graph leaves the panels out. */
278
+ _panelCanvases = [];
279
+ /** The fit the minimap on screen was painted with: every pass over it
280
+ * since its corner was last claimed whole resolved the same one. Set by
281
+ * the pass that paints it (`_sceneInput`), read by what a move claims
282
+ * there (`_miniMapMoves`). */
283
+ _miniMapShown = null;
284
+ _panelsKey = '';
285
+ /** The budget's model (`_holdBodies`, `_frameTick`): what a body adds to a
286
+ * zoom step, what a step costs with none re-scaled, the last frame's
287
+ * time, and what the step awaiting its frame did. */
288
+ _bodyStepMs = BODY_STEP_PRIOR_MS;
289
+ _baseStepMs = NaN;
290
+ _lastFrameAt = -Infinity;
291
+ _zoomStep = null;
276
292
  _dashPhase = 0;
293
+ /** The box the bodies are laid out in, which a 2D pan carries. */
294
+ _bodiesLayer = null;
295
+ /** When a pan last asked to blit — the dash timer waits for it. */
296
+ _blittedPanAt = -Infinity;
297
+ /** When the view last moved, and when the dash timer last came round,
298
+ * ticking or not: a 2D pan's dashes wait for a view that held still. */
299
+ _viewMovedAt = -Infinity;
300
+ _tickAt = -Infinity;
301
+ /** When the dash timer last marched, and what a 2D frame has cost lately
302
+ * — the paint, and the server's answer to the frame before it; a running
303
+ * mean, ms: what a tick waits on. */
304
+ _marchedAt = -Infinity;
305
+ _tickCost = 0;
277
306
  _animTimer = null;
278
307
  /** Inside `paint`, where an invalidation would only schedule a redraw of
279
308
  * the frame being drawn. */
280
309
  _painting = false;
310
+ /** Routes that survived the last frame — see {@link SceneCache}. */
311
+ _sceneCache = new SceneCache();
312
+ /** Asks the GL surface for a frame, while one is drawing this pane. */
313
+ _glRequest = null;
314
+ /**
315
+ * Bumped by every repaint this element asks for that is *not* a pure pan
316
+ * — which is how the GL surface knows the graph it holds on the GPU is
317
+ * still the graph, and a frame is only a new offset. Errs one way on
318
+ * purpose: anything it cannot see is covered by the rest of the key
319
+ * (`glFrame`), and a rebuild too many costs a frame's packing, where one
320
+ * too few draws the wrong graph.
321
+ */
322
+ _worldVersion = 0;
323
+ /**
324
+ * The same for the nodes a drag is moving under GL, which are lifted out
325
+ * of the world for as long as it moves them (`_lifted`): a step changes
326
+ * them and nothing else, so it moves this and the world stays on the GPU.
327
+ * Rebuilding the world was the whole of a drag step's cost — 5-7 ms of
328
+ * scene and packing a step at 2,000 nodes, for one card.
329
+ */
330
+ _liftVersion = 0;
331
+ /** The lifted ids, for the drag gesture they were made for, and a count
332
+ * of every change to them — the world's key, since the world is built
333
+ * without them. */
334
+ _liftSet = null;
335
+ _liftFor = null;
336
+ _liftGen = 0;
337
+ /** Whether a marching edge is in the world, or in the lifted layer. */
338
+ _worldAnimated = false;
339
+ _liftedAnimated = false;
340
+ /** The last nodes commit changed lifted nodes and nothing else. */
341
+ _liftOnly = false;
342
+ /** The overscan the GL world was culled to, in its own pinned
343
+ * coordinates, and the key it was built under. */
344
+ _worldCull = null;
345
+ _worldCullId = 0;
346
+ _worldZoom = NaN;
347
+ /** When the zoom last moved, whether that move continued a stream of
348
+ * them — a gesture, where one alone is a jump — and the timer that asks
349
+ * for the frame that rebuilds the GL world once the stream stops. */
350
+ _zoomAt = -Infinity;
351
+ _zoomStream = false;
352
+ /** When the frame that drew the latest zoom step began, and when it last
353
+ * finished a pass: an animation's next step follows the frame that drew
354
+ * its last, however long that frame took (`_applyViewport`). */
355
+ _zoomFrameAt = -Infinity;
356
+ _zoomDrawnAt = -Infinity;
357
+ _zoomRest = null;
358
+ /** A 2D paint drew labels scaled mid-zoom, and owes them at their own
359
+ * sizes once it rests. */
360
+ _textApproximated = false;
361
+ /**
362
+ * Mid-zoom on the 2D renderer: the graph and its ground as the gesture
363
+ * drew them once, on a surface of their own, composited scaled for every
364
+ * step after — the GL renderer's world drawn scaled, in a bitmap. A step
365
+ * used to stroke every edge, fill every card and lay the grid again, 20 ms
366
+ * of Direct2D a step over the stress lattice, for a picture the next step
367
+ * moved off. Dropped when the zoom rests, which paints it all again
368
+ * exactly, and whenever the graph itself changes.
369
+ */
370
+ _zoomShot = null;
371
+ /** What the last `GRAPH_PAINTS_KEPT` whole paints of the graph cost, ms,
372
+ * newest last: the steps of a zoom painted live and the pictures painted
373
+ * for zooms, the two passes that say what a step would cost live — and
374
+ * not a zoom's first step, which sets every label at its size once and
375
+ * says what the steps after it will not. Whether a zoom is worth a
376
+ * picture (`ZOOM_SHOT_WORTH_MS`); empty until a zoom has painted, so the
377
+ * first one starts live. */
378
+ _graphPaints = [];
379
+ /** A 2D drag's pictures (`_liftShotFor`), and the gesture that is not to
380
+ * have any: one whose view moved under it, where a picture a step would
381
+ * cost two full paints a step. */
382
+ _liftShot = null;
383
+ _liftShotRefused = null;
281
384
  /** Inside a live input dispatch — what makes a body emission `sync`.
282
385
  * Motion and the wheel run at continuous priority, whose React updates
283
386
  * can trail the pane's own painting by frames; an emission made under
284
387
  * this flag asks the receiver to commit before the dispatch returns. */
285
388
  _gestureSync = false;
389
+ /** Inside a call through `FlowInstance` (`fromHandle`) — an application's
390
+ * handler, timer or animation loop moving the viewport. Its emissions ask
391
+ * to commit inline too. */
392
+ _handleSync = false;
286
393
  /** The scene has been offered to assistive tech at least once — items
287
394
  * only exist once layout has placed them, which no commit marks. */
288
395
  _sceneAnnounced = false;
@@ -355,9 +462,29 @@ export class FlowGraphNode extends Node {
355
462
  };
356
463
  }
357
464
  _measure = (text, options) => measureText(this._textOptions(), text, options).width;
465
+ /** The same shaping, both dimensions: what a scene's label chips are
466
+ * sized from. `_measure` answers a width because `measureNode` only ever
467
+ * wanted one. */
468
+ _measureBox = (text, options) => measureText(this._textOptions(), text, options);
469
+ /**
470
+ * The palette, resolved once for the theme and the `palette` prop it was
471
+ * resolved from. Every text measured asks for it (`_textOptions`), and
472
+ * resolving it tints the accent and reads the background's lightness out
473
+ * of a colour string: a 2D pan's passes measured their labels with a
474
+ * fresh one each. The theme object is replaced when it changes.
475
+ */
358
476
  _palette() {
359
- return resolvePalette(this.theme, this._prop('palette'));
477
+ const theme = this.theme;
478
+ const overrides = this._prop('palette');
479
+ const held = this._paletteHeld;
480
+ if (held && held.theme === theme && held.overrides === overrides) {
481
+ return held.palette;
482
+ }
483
+ const palette = resolvePalette(theme, overrides);
484
+ this._paletteHeld = { theme, overrides, palette };
485
+ return palette;
360
486
  }
487
+ _paletteHeld = null;
361
488
  // --- the derived graph ---------------------------------------------------
362
489
  /**
363
490
  * Rebuild the node index when — and only when — one of its inputs changed
@@ -422,12 +549,12 @@ export class FlowGraphNode extends Node {
422
549
  // a fresh literal saying the same thing: keep the built entry
423
550
  continue;
424
551
  }
425
- const wasBox = this._edgeCoarseBox(this._edges[i]);
552
+ const wasBox = this._edgeInkBox(this._edges[i]);
426
553
  this._edges[i] = defaults
427
554
  ? { ...defaults, ...next }
428
555
  : next;
429
556
  this._edgesRaw[i] = next;
430
- const isBox = this._edgeCoarseBox(this._edges[i]);
557
+ const isBox = this._edgeInkBox(this._edges[i]);
431
558
  if (wasBox)
432
559
  damage = damage ? unionRects(damage, wasBox) : wasBox;
433
560
  if (isBox)
@@ -461,8 +588,20 @@ export class FlowGraphNode extends Node {
461
588
  * and `null` when nothing visual changed at all.
462
589
  */
463
590
  _applyNodes(nodes) {
591
+ const sameCardText = (a, b) => {
592
+ const da = a.data;
593
+ const db = b.data;
594
+ return ((da?.label ?? a.id) === (db?.label ?? b.id) &&
595
+ da?.description === db?.description);
596
+ };
464
597
  const prev = this._entries;
465
598
  this._nodesSeen = this.props.nodes;
599
+ // Whether every node this commit changed is one a drag has lifted out
600
+ // of the GL world — the commit of the drag's own step, when the app
601
+ // stores it — so the world on the GPU is still the world.
602
+ const lift = this._lifted();
603
+ let onlyLifted = lift != null;
604
+ this._liftOnly = false;
466
605
  let structural = nodes.length !== prev.length;
467
606
  if (!structural) {
468
607
  for (let i = 0; i < nodes.length; i++) {
@@ -472,7 +611,6 @@ export class FlowGraphNode extends Node {
472
611
  continue;
473
612
  if (n.id !== o.id ||
474
613
  n.type !== o.type ||
475
- n.selected !== o.selected ||
476
614
  n.hidden !== o.hidden ||
477
615
  n.zIndex !== o.zIndex ||
478
616
  n.handles !== o.handles ||
@@ -489,15 +627,40 @@ export class FlowGraphNode extends Node {
489
627
  this._rebuildNodes();
490
628
  return 'full';
491
629
  }
492
- let damage = null;
630
+ const damage = [];
631
+ // A node that is selected, or no longer is, is restyled and restacked —
632
+ // selection lifts it over its neighbours — and both happen inside its
633
+ // own box: edges are drawn under every node. Treated as structural, a
634
+ // selection change re-measured every node and repainted the pane, which
635
+ // a box selection does on every step that takes a node in.
636
+ let reordered = false;
637
+ // The minimap draws every node where it is, at its size, marked when it
638
+ // is selected — and, to a `nodeColor` function, as anything at all.
639
+ const colour = this._miniMapOptions()?.nodeColor;
640
+ let mapped = false;
641
+ // …and where the pane draws it, a node that only moved or changed size
642
+ // changed its own places in it, while the fit holds (`_miniMapMoves`)
643
+ const drawsMap = this._drawsMiniMap();
644
+ const moves = [];
645
+ // round the box: its handles, and the resize grips a selected node
646
+ // grows at its corners, which at a deep zoom reach past the cull margin
647
+ const margin = Math.max(CULL_MARGIN, RESIZE_GRIP * this._viewport().zoom + 2);
493
648
  for (let i = 0; i < nodes.length; i++) {
494
649
  const entry = prev[i];
495
650
  const next = nodes[i];
496
651
  const old = entry.node;
497
652
  if (next === old)
498
653
  continue;
654
+ if (!lift?.has(next.id))
655
+ onlyLifted = false;
656
+ if (next.selected !== old.selected)
657
+ reordered = true;
658
+ if (typeof colour === 'function')
659
+ mapped = true;
499
660
  const moved = next.position.x !== old.position.x ||
500
661
  next.position.y !== old.position.y;
662
+ if (moved)
663
+ mapped = true;
501
664
  // `data`, an explicit size or a paint style are the node's own to
502
665
  // change: they re-measure and repaint *this* node, never the graph. A
503
666
  // keystroke into a mounted node's textarea patches `data` on every
@@ -507,7 +670,7 @@ export class FlowGraphNode extends Node {
507
670
  const reshaped = next.data !== old.data ||
508
671
  next.width !== old.width ||
509
672
  next.height !== old.height;
510
- const restyled = next.style !== old.style;
673
+ const restyled = next.style !== old.style || next.selected !== old.selected;
511
674
  if (!moved && !reshaped && !restyled) {
512
675
  // a behavioural flag (draggable, deletable…) — nothing drawn reads it
513
676
  entry.node = next;
@@ -515,6 +678,7 @@ export class FlowGraphNode extends Node {
515
678
  }
516
679
  const widthBefore = entry.width;
517
680
  const heightBefore = entry.height;
681
+ const placeBefore = drawsMap && (moved || reshaped) ? this.rectOf(entry) : null;
518
682
  let box = moved ? this._nodeDamage(entry) : this._screenRect(entry);
519
683
  entry.node = next;
520
684
  if (reshaped) {
@@ -524,14 +688,51 @@ export class FlowGraphNode extends Node {
524
688
  entry.specs = resolveHandles(next, entry.type);
525
689
  }
526
690
  const grew = entry.width !== widthBefore || entry.height !== heightBefore;
691
+ if (grew)
692
+ mapped = true;
693
+ // A drag's own step, stored by the app, moves nothing on screen:
694
+ // the node was already where the drag had it
695
+ if (placeBefore && !sameRect(placeBefore, this.rectOf(entry))) {
696
+ moves.push({ entry, before: placeBefore });
697
+ }
698
+ // Nothing the pane draws changed, while the node holds its place and
699
+ // its size, when its card is the bodies' layer's on the 2D renderer —
700
+ // the layer repaints it, if anything it shows changed — or when what
701
+ // changed is data its card does not show: a card shows the label, the
702
+ // description and the style, and a type that paints its own may show
703
+ // anything. A board of live widgets, a tenth of them patching their
704
+ // data ten times a second, rebuilt the GL world on every patch, and on
705
+ // the 2D renderer repainted cards that showed none of it.
706
+ if (!moved && !grew) {
707
+ if (this._cardInLayer(next.id))
708
+ continue;
709
+ if (!restyled && !entry.type?.paint && sameCardText(old, next)) {
710
+ continue;
711
+ }
712
+ }
527
713
  // The edges ride along only when an endpoint actually moved — a
528
714
  // label edit that kept the box is the box's own business, and a
529
715
  // keystroke that unioned its node's edges swept half the layer's
530
716
  // neighbours into every repaint.
531
717
  box = unionRects(box, moved || grew ? this._nodeDamage(entry) : this._screenRect(entry));
532
- damage = damage ? unionRects(damage, box) : box;
718
+ damage.push(inflateRect(box, margin));
719
+ }
720
+ if (reordered)
721
+ this._sortOrder();
722
+ if (reordered || mapped) {
723
+ // A node that moved, grew or was picked out is a change in the
724
+ // minimap's corner too. Only a selection used to claim it, so a node
725
+ // the app moved repainted its own box and edges and the minimap went
726
+ // on showing it where it had been. A selection, or a colour worked
727
+ // out per node, is the whole corner's: it changes how the nodes there
728
+ // are filled together (`SceneMiniMapNode`).
729
+ const whole = !drawsMap || reordered || typeof colour === 'function';
730
+ const map = whole ? this._miniMapCorner() : this._miniMapMoves(moves);
731
+ if (map)
732
+ damage.push(map);
533
733
  }
534
- return damage ? inflateRect(damage, CULL_MARGIN) : null;
734
+ this._liftOnly = onlyLifted;
735
+ return damage;
535
736
  }
536
737
  /**
537
738
  * The screen box a node's redraw touches: its own rect and every edge on
@@ -549,13 +750,17 @@ export class FlowGraphNode extends Node {
549
750
  let box = this._screenRect(entry);
550
751
  const edges = this._edgesByNode.get(entry.node.id) ?? [];
551
752
  const tight = edges.length <= 16;
753
+ const sv = this._screenViewport();
552
754
  for (const edge of edges) {
553
755
  const geometry = tight ? this._edgeGeometry(edge) : null;
554
- const bounds = geometry
555
- ? pathBounds(geometry.points)
556
- : this._edgeCoarseBox(edge);
557
- if (bounds)
558
- box = unionRects(box, bounds);
756
+ // the route's own ink, heads and pen and all — the pen only past the
757
+ // margin every caller grows this by — or the coarse box, whose slack
758
+ // holds both
759
+ const ink = geometry
760
+ ? edgeInkBox(sv, edge, geometry.points, CULL_MARGIN)
761
+ : this._edgeInkBox(edge);
762
+ if (ink)
763
+ box = unionRects(box, ink);
559
764
  }
560
765
  return box;
561
766
  }
@@ -580,7 +785,12 @@ export class FlowGraphNode extends Node {
580
785
  }
581
786
  this._entries = entries;
582
787
  this._byId = byId;
583
- this._order = entries
788
+ this._sortOrder();
789
+ }
790
+ /** The paint order: by `zIndex`, a selected node over the rest of its
791
+ * layer, and declaration order breaking ties. */
792
+ _sortOrder() {
793
+ this._order = this._entries
584
794
  .map((entry, index) => ({ entry, index }))
585
795
  .sort((a, b) => {
586
796
  const az = a.entry.node.zIndex ?? 0;
@@ -625,6 +835,11 @@ export class FlowGraphNode extends Node {
625
835
  _viewport() {
626
836
  return this._prop('viewport') ?? this._vp;
627
837
  }
838
+ /** The viewport with the pane's window origin folded in — what every
839
+ * graph-to-window conversion shared with `./scene.ts` takes. */
840
+ _screenViewport() {
841
+ return screenViewport(this._viewport(), this._pane());
842
+ }
628
843
  /**
629
844
  * Device pixels per logical pixel — the display scale this pane's window
630
845
  * resolved to (react-x11's docs/scale.md): `1` on an ordinary display,
@@ -675,6 +890,46 @@ export class FlowGraphNode extends Node {
675
890
  _claim(rect, reason) {
676
891
  this.invalidate(false, this._device(rect), reason);
677
892
  }
893
+ /** Whether the pane draws the minimap itself. On a canvas of its own —
894
+ * under mounted bodies — it is `invalidate`'s to repaint. */
895
+ _drawsMiniMap() {
896
+ return this._panelCanvases.length === 0 && this._miniMapOptions() != null;
897
+ }
898
+ /**
899
+ * What moving or resizing `moves` changed in the minimap's corner, where
900
+ * the pane draws it: each node's place there before and after, while the
901
+ * fit is the one on screen — the graph's bounds with the viewport's,
902
+ * which put every node where it is there — and the whole corner when it
903
+ * is not, since every node moves in it then. A drag of one node among
904
+ * 2,000 claimed the corner on every step, and every such pass built and
905
+ * filled all 2,000 again, where it builds what it reaches now
906
+ * (`buildMiniMap`). Asked against the fit on screen rather than the one
907
+ * before the change, a step works the fit out once, and not at all when
908
+ * nothing moved on screen.
909
+ */
910
+ _miniMapMoves(moves) {
911
+ const corner = this._miniMapCorner();
912
+ if (!corner || moves.length === 0)
913
+ return null;
914
+ const fit = this._miniMap();
915
+ const shown = this._miniMapShown;
916
+ if (!fit && !shown)
917
+ return null;
918
+ if (!fit || !shown || !sameFit(fit, shown))
919
+ return corner;
920
+ let out = null;
921
+ for (const { entry, before } of moves) {
922
+ out = unionMaybe(out, miniMapPlace(shown, before));
923
+ out = unionMaybe(out, miniMapPlace(fit, this.rectOf(entry)));
924
+ }
925
+ return out && inflateRect(out, MINIMAP_BLEED);
926
+ }
927
+ /** A gesture's step, claimed in the minimap: `_miniMapMoves`. */
928
+ _claimMiniMapMoves(moves, reason) {
929
+ const map = this._miniMapMoves(moves);
930
+ if (map)
931
+ this._claim(map, reason);
932
+ }
678
933
  /**
679
934
  * The pane proper: `abs` inset by border and padding, off core's own
680
935
  * resolved layout (react-x11#254) — so a `<Flow style={{ borderWidth: 1,
@@ -749,9 +1004,85 @@ export class FlowGraphNode extends Node {
749
1004
  return !anchors.some((a) => Math.abs(a.x - at.x) <= near && Math.abs(a.y - at.y) <= near);
750
1005
  });
751
1006
  }
1007
+ /**
1008
+ * One entry as `./scene.ts` takes it: the live gesture state resolved
1009
+ * away, so everything past this point is pure. A drag's position and a
1010
+ * resize's box are already in `rect`, and `grips` is already filtered by
1011
+ * whatever the handles took.
1012
+ *
1013
+ * Built per call rather than cached: `rectOf` is what keeps a drag in step
1014
+ * with the commit that has not landed yet, and a cache here is a cache of
1015
+ * last frame's positions.
1016
+ */
1017
+ /** Whether a React body is mounted over this node — which is what makes
1018
+ * its card draw a title bar rather than a centred label, and what
1019
+ * `_emitBodies` reports a box for. */
1020
+ _mounted(entry) {
1021
+ return entry.type?.render != null && this._viewport().zoom >= RENDER_ZOOM;
1022
+ }
1023
+ /**
1024
+ * The frame being painted: each node's scene source, and the two lists a
1025
+ * 2D pass builds its scene from. A frame paints its damage as several
1026
+ * passes — a pan's strips, its pinned furniture, the corners of a rounded
1027
+ * pane — and the scene cache culls the graph to the screen once for all
1028
+ * of them (`SceneCache.edgesOnScreen`), keyed on these very lists, so
1029
+ * every pass has to be handed the same ones. The passes run one after
1030
+ * another in a single task, so a microtask queued by the first forgets
1031
+ * the frame after the last — the same reasoning as `_reportFrame`.
1032
+ */
1033
+ _frame = null;
1034
+ _paintedFrame() {
1035
+ let frame = this._frame;
1036
+ if (!frame) {
1037
+ frame = this._frame = { sources: new Map(), lists: null };
1038
+ void Promise.resolve().then(() => {
1039
+ this._frame = null;
1040
+ });
1041
+ }
1042
+ return frame;
1043
+ }
1044
+ _sourceOf(entry) {
1045
+ if (!this._painting)
1046
+ return this._source(entry);
1047
+ const { sources } = this._paintedFrame();
1048
+ let source = sources.get(entry);
1049
+ if (!source) {
1050
+ source = this._source(entry);
1051
+ sources.set(entry, source);
1052
+ }
1053
+ return source;
1054
+ }
1055
+ /** A 2D pass's node lists, the same arrays for every pass of a frame. */
1056
+ _frameLists(order) {
1057
+ const frame = this._paintedFrame();
1058
+ frame.lists ??= {
1059
+ all: this._entries.map((entry) => this._sourceOf(entry)),
1060
+ nodes: order
1061
+ .filter((entry) => !this._cardInLayer(entry.node.id))
1062
+ .map((entry) => this._sourceOf(entry)),
1063
+ };
1064
+ return frame.lists;
1065
+ }
1066
+ _source(entry) {
1067
+ return {
1068
+ node: entry.node,
1069
+ rect: this.rectOf(entry),
1070
+ specs: entry.specs,
1071
+ type: entry.type,
1072
+ header: this._headerHeight(entry),
1073
+ mounted: this._mounted(entry),
1074
+ connectable: this._connectable(entry),
1075
+ grips: this._grips(entry),
1076
+ };
1077
+ }
1078
+ /** Not through `_source`: a source carries its grips, and `_grips` asks
1079
+ * where the handles are — the pair would recur. */
752
1080
  _handlesOf(entry) {
753
- const rect = this.rectOf(entry);
754
- return entry.specs.map((spec) => handleAnchor(entry.node.id, rect, spec));
1081
+ return anchorsOf({
1082
+ node: entry.node,
1083
+ rect: this.rectOf(entry),
1084
+ specs: entry.specs,
1085
+ });
755
1086
  }
756
1087
  /**
757
1088
  * A node's box on screen, **on whole device pixels**.
@@ -768,22 +1099,12 @@ export class FlowGraphNode extends Node {
768
1099
  * testing reads the same rect, so what is drawn and what is clicked still
769
1100
  * agree to the pixel.
770
1101
  */
771
- _screenRect(entry) {
772
- const v = this._viewport();
773
- const rect = this.rectOf(entry);
774
- const p = this._toScreen(rect);
775
- const s = this._scale;
776
- const grid = (value) => Math.round(value * s) / s;
777
- const x = grid(p.x);
778
- const y = grid(p.y);
779
- return {
780
- x,
781
- y,
782
- // rounded as edges rather than as a size, so two nodes that share a
783
- // column still share it after rounding
784
- width: grid(p.x + rect.width * v.zoom) - x,
785
- height: grid(p.y + rect.height * v.zoom) - y,
786
- };
1102
+ _screenRect(entry,
1103
+ // A loop over the nodes passes it in: the viewport reads the pane's
1104
+ // content box off layout, and asked once per node it was most of what a
1105
+ // hit test cost on a large graph — every pointer move, 400 times.
1106
+ sv = this._screenViewport()) {
1107
+ return screenRect(sv, this.rectOf(entry), this._scale);
787
1108
  }
788
1109
  _visible() {
789
1110
  const pane = this._pane();
@@ -800,12 +1121,47 @@ export class FlowGraphNode extends Node {
800
1121
  const controlled = this.props.viewport !== undefined;
801
1122
  if (!controlled)
802
1123
  this._vp = v;
1124
+ if (previous.x !== v.x || previous.y !== v.y || previous.zoom !== v.zoom) {
1125
+ this._viewMovedAt = now();
1126
+ }
1127
+ if (previous.zoom !== v.zoom) {
1128
+ // A stream is steps close together — or a step that follows the frame
1129
+ // that drew the one before it: an animation zooms a step a frame, and
1130
+ // a frame whose bodies re-scale can take longer than the rest. Judged
1131
+ // on the steps alone, such an animation was a run of single jumps, so
1132
+ // its bodies were never held, every step re-scaled them all, and it
1133
+ // stayed at two frames a second where held it ran at twenty — on
1134
+ // whichever side of the line its first frames happened to fall.
1135
+ const t = now();
1136
+ this._zoomStream =
1137
+ t - this._zoomAt < GL_ZOOM_REST_MS ||
1138
+ t - this._zoomDrawnAt < GL_ZOOM_REST_MS;
1139
+ this._zoomAt = t;
1140
+ this._zoomFrameAt = -Infinity;
1141
+ this._zoomDrawnAt = -Infinity;
1142
+ }
1143
+ // Each claims its own box: a claim with no region is the whole window,
1144
+ // which made every pan step and dash tick a full frame. The minimap's
1145
+ // alone: its view box moves, and the controls show nothing of the
1146
+ // viewport.
1147
+ for (const canvas of this._miniMapCanvases())
1148
+ canvas.invalidate(false, canvas, 'props');
803
1149
  this._prop('onViewportChange')?.(v);
804
1150
  // The `fitView` that runs at the top of a paint has already changed what
805
1151
  // this frame will draw, so asking for another one would only draw the
806
1152
  // same picture twice.
807
1153
  if (!controlled && !this._painting) {
808
- if (!this._blitPan(previous, v))
1154
+ if (this._gl) {
1155
+ // Under GL a pan is the surface's offset uniform and nothing else,
1156
+ // and a zoom is the frame's to decide — the world scaled while the
1157
+ // gesture moves, rebuilt once it rests (`glFrame`). Neither is a
1158
+ // change to the world, and the 2D pane under the surface shows none
1159
+ // of the graph: claiming its box every step repainted it for
1160
+ // nothing (~4 ms a frame) — and, with bodies mounted, reached
1161
+ // core's overlay too.
1162
+ this._glRequest?.();
1163
+ }
1164
+ else if (!this._blitPan(previous, v))
809
1165
  this._repaint('scroll');
810
1166
  // same-frame compositing for mounted bodies — see `_dragStep`
811
1167
  this._emitBodies();
@@ -819,27 +1175,35 @@ export class FlowGraphNode extends Node {
819
1175
  * the shift exposed — which `paintDamage()` then hands to `paint`, so the
820
1176
  * existing culling draws the sliver and nothing else.
821
1177
  *
822
- * The furniture — minimap, zoom controls — is pinned to the pane while
823
- * its pixels would ride the blit, so its bands are carved out of the
824
- * region that shifts and claimed as ordinary damage. The blit gate tests
825
- * foreign claims against the *rect* (react-x11#309/#310), so a claim
826
- * sitting edge to edge with it leaves the frame a blit: the middle of
827
- * the pane is copied, the strips repaint with the new viewport, and the
828
- * panels repaint in place.
1178
+ * The furniture — minimap, zoom controls — stays put while the graph
1179
+ * moves, so it is handed over as `pinned` (react-x11#682, 2.22): the
1180
+ * whole pane is copied, and core repaints each panel and the image of
1181
+ * it the copy dragged along, with the strips the shift exposed.
1182
+ *
1183
+ * Mounted node bodies ride it. They are laid out in one box beside the
1184
+ * pane, which a pan moves by exactly the pan, so their pixels move with
1185
+ * the pane's: the box goes to `scrollContents` as a rider
1186
+ * (react-x11#671), its commit claims nothing, and what it leaves or
1187
+ * reaches outside the rect is all it costs. `<Flow>` clips it to the pane for that. A body entering or
1188
+ * leaving the pane, or changing as it goes, claims inside the rect and
1189
+ * declines that frame's blit; the next one blits again.
829
1190
  *
830
1191
  * Still a full repaint when:
831
1192
  * - the zoom moved (scaling is not a blit) or the shift is fractional on
832
1193
  * the device grid — every real pan gesture is whole device pixels;
833
- * - mounted node bodies exist: they are core-owned siblings whose
834
- * gesture-time commits claim *inside* the rect every step, which
835
- * declines the blit anyway — bailing early skips the churn;
836
- * - the furniture bands would eat the pane (a tiny pane, or panels on
837
- * both the top and the bottom of a short one).
1194
+ * - bodies are mounted and `<Flow>` has not handed their box over;
1195
+ * - the pane is too small to be worth it; and core falls back itself
1196
+ * when the repairs would repaint most of it.
838
1197
  */
839
1198
  _blitPan(previous, next) {
1199
+ // There is no backing store to scroll under GL: the surface redraws the
1200
+ // whole scene every frame, and a pan is its cheapest frame of all.
1201
+ if (this._gl)
1202
+ return false;
840
1203
  if (next.zoom !== previous.zoom)
841
1204
  return false;
842
- if (this._bodiesKey !== '')
1205
+ const riders = this._bodies.length > 0 ? this._bodiesLayer : null;
1206
+ if (this._bodies.length > 0 && !riders)
843
1207
  return false;
844
1208
  // Device pixels: the blit copies the backing store, and its grid is the
845
1209
  // panel's. A pointer step lands on it whatever the scale — it came off
@@ -853,59 +1217,57 @@ export class FlowGraphNode extends Node {
853
1217
  return true; // sub-pixel: nothing to show yet
854
1218
  if (shiftX !== dx || shiftY !== dy)
855
1219
  return false;
856
- const pane = this._pane();
857
- // The horizontal bands the furniture lives in. Full-width, because the
858
- // graph beside a panel must repaint too — it does not ride the blit.
859
- let topBand = 0;
860
- let bottomBand = 0;
861
- const claim = (panel, position) => {
862
- if ((position ?? 'bottom-right').startsWith('top')) {
863
- topBand = Math.max(topBand, panel.y + panel.height - pane.y);
864
- }
865
- else {
866
- bottomBand = Math.max(bottomBand, pane.y + pane.height - panel.y);
867
- }
868
- };
869
- const map = this._miniMapOptions();
870
- if (map) {
871
- claim(this._corner(map.position, map.width ?? MINIMAP_W, map.height ?? MINIMAP_H, 'bottom-right'), map.position);
872
- }
873
- const buttons = this._controlButtons();
874
- if (buttons.length > 0) {
875
- const first = buttons[0].rect;
876
- const last = buttons[buttons.length - 1].rect;
877
- claim(unionRects(first, last), this._controlsOptions()?.position ?? 'bottom-left');
878
- }
879
- // From here on, device pixels: `scrollContents` and the claims speak
880
- // core's units. The bands round *up* and the blit is what is left, so
881
- // the three sit edge to edge on whole pixels — a claim overlapping the
882
- // blit rect by a pixel is a foreign claim, and declines the blit.
1220
+ // The furniture — minimap, zoom controls — stays put while the graph
1221
+ // moves under it: pinned inside the region, which core repaints after
1222
+ // the copy, with the stale image the copy dragged along (react-x11
1223
+ // #682, 2.22). It used to be carved out of the region, and a region is
1224
+ // one rectangle, so controls in one bottom corner and the minimap in the
1225
+ // other carved a band the pane's full width — every card, label and edge
1226
+ // in it repainted on every pan frame: 47 fps over the stress example's
1227
+ // widgets on XQuartz, where a pane with no furniture pans at 80.
1228
+ //
1229
+ // Device pixels, rounded out, and a pixel over: a panel's own claims —
1230
+ // the minimap's view box moves with every pan — must land inside its
1231
+ // pinned rect, or they decline the blit.
883
1232
  const box = this.contentBox();
884
- const top = Math.ceil(toDevice(topBand, s));
885
- const bottom = Math.ceil(toDevice(bottomBand, s));
886
- const blit = {
887
- x: box.x,
888
- y: box.y + top,
889
- width: box.width,
890
- height: box.height - top - bottom,
891
- };
892
- if (blit.width < 64 * s || blit.height < 64 * s)
1233
+ if (box.width < 64 * s || box.height < 64 * s)
893
1234
  return false;
894
- if (Math.abs(dx) >= blit.width || Math.abs(dy) >= blit.height) {
1235
+ if (Math.abs(dx) >= box.width || Math.abs(dy) >= box.height)
895
1236
  return false;
1237
+ const pinned = [];
1238
+ const pin = (rect) => {
1239
+ const x0 = Math.max(box.x, Math.floor(rect.x) - 1);
1240
+ const y0 = Math.max(box.y, Math.floor(rect.y) - 1);
1241
+ const x1 = Math.min(box.x + box.width, Math.ceil(rect.x + rect.width) + 1);
1242
+ const y1 = Math.min(box.y + box.height, Math.ceil(rect.y + rect.height) + 1);
1243
+ if (x1 > x0 && y1 > y0) {
1244
+ pinned.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
1245
+ }
1246
+ };
1247
+ if (this._panelCanvases.length > 0) {
1248
+ // Painted on canvases of their own, over mounted bodies: a node laid
1249
+ // over the region is pinned by its paint bounds, which carry core's
1250
+ // slop round its box.
1251
+ for (const canvas of this._panelCanvases) {
1252
+ const reach = canvas.paintBounds?.();
1253
+ if (reach)
1254
+ pin(reach);
1255
+ }
896
1256
  }
897
- this.scrollContents(blit, dx, dy);
898
- if (top > 0) {
899
- this.invalidate(false, { x: box.x, y: box.y, width: box.width, height: top }, 'scroll');
900
- }
901
- if (bottom > 0) {
902
- this.invalidate(false, {
903
- x: box.x,
904
- y: blit.y + blit.height,
905
- width: box.width,
906
- height: bottom,
907
- }, 'scroll');
1257
+ else {
1258
+ const map = this._miniMapOptions();
1259
+ if (map) {
1260
+ pin(this._device(this._corner(map.position, map.width ?? MINIMAP_W, map.height ?? MINIMAP_H, 'bottom-right')));
1261
+ }
1262
+ const buttons = this._controlButtons();
1263
+ if (buttons.length > 0) {
1264
+ const first = buttons[0].rect;
1265
+ const last = buttons[buttons.length - 1].rect;
1266
+ pin(this._device(unionRects(first, last)));
1267
+ }
908
1268
  }
1269
+ this.scrollContents(box, dx, dy, riders ? [riders] : null, pinned);
1270
+ this._blittedPanAt = now();
909
1271
  return true;
910
1272
  }
911
1273
  /** Zoom about a point that must not move — the pointer under a wheel, the
@@ -948,6 +1310,27 @@ export class FlowGraphNode extends Node {
948
1310
  }));
949
1311
  }
950
1312
  // --- the imperative surface (`FlowInstance`) -----------------------------
1313
+ /**
1314
+ * `fn`, as a call through the handle. A programmatic pan — an animation
1315
+ * loop stepping `setViewport` — moves the pane at once and the bodies'
1316
+ * box through React, and on React's schedule the box caught up with the
1317
+ * pane in jumps: several steps' worth in one commit, landing in a frame
1318
+ * whose blit shifted by one. A rider that moves by more than the blit is
1319
+ * a layout move, so that frame repainted the pane whole — a fifth of the
1320
+ * frames of a pan over mounted bodies (`_blitPan`). Marked like a
1321
+ * gesture's, the emission commits before the call returns, and the box
1322
+ * moves by exactly the pan in the frame that blits it.
1323
+ */
1324
+ fromHandle(fn) {
1325
+ const was = this._handleSync;
1326
+ this._handleSync = true;
1327
+ try {
1328
+ return fn();
1329
+ }
1330
+ finally {
1331
+ this._handleSync = was;
1332
+ }
1333
+ }
951
1334
  getViewport() {
952
1335
  return { ...this._viewport() };
953
1336
  }
@@ -1069,11 +1452,12 @@ export class FlowGraphNode extends Node {
1069
1452
  /** The topmost node under a window point, or null. Walks paint order
1070
1453
  * backwards, which is what "topmost" means. */
1071
1454
  _nodeAt(x, y) {
1455
+ const sv = this._screenViewport();
1072
1456
  for (let i = this._order.length - 1; i >= 0; i--) {
1073
1457
  const entry = this._order[i];
1074
1458
  if (entry.node.hidden)
1075
1459
  continue;
1076
- if (rectContains(this._screenRect(entry), { x, y }))
1460
+ if (rectContains(this._screenRect(entry, sv), { x, y }))
1077
1461
  return entry;
1078
1462
  }
1079
1463
  return null;
@@ -1081,6 +1465,7 @@ export class FlowGraphNode extends Node {
1081
1465
  _handleAt(x, y, accept) {
1082
1466
  const zoom = this._viewport().zoom;
1083
1467
  const reach = Math.max(7, (HANDLE_RADIUS + HANDLE_SLOP) * zoom);
1468
+ const sv = this._screenViewport();
1084
1469
  for (let i = this._order.length - 1; i >= 0; i--) {
1085
1470
  const entry = this._order[i];
1086
1471
  if (entry.node.hidden)
@@ -1089,7 +1474,7 @@ export class FlowGraphNode extends Node {
1089
1474
  continue;
1090
1475
  // cheap reject: the handles are on the node's border, so nothing more
1091
1476
  // than `reach` outside its box can be one
1092
- const rect = this._screenRect(entry);
1477
+ const rect = this._screenRect(entry, sv);
1093
1478
  if (x < rect.x - reach ||
1094
1479
  x > rect.x + rect.width + reach ||
1095
1480
  y < rect.y - reach ||
@@ -1115,6 +1500,7 @@ export class FlowGraphNode extends Node {
1115
1500
  _gripAt(x, y) {
1116
1501
  const zoom = this._viewport().zoom;
1117
1502
  const reach = Math.max(6, (RESIZE_GRIP + RESIZE_SLOP) * zoom);
1503
+ const sv = this._screenViewport();
1118
1504
  for (let i = this._order.length - 1; i >= 0; i--) {
1119
1505
  const entry = this._order[i];
1120
1506
  if (entry.node.hidden)
@@ -1122,7 +1508,7 @@ export class FlowGraphNode extends Node {
1122
1508
  const grips = this._grips(entry);
1123
1509
  if (grips.length === 0)
1124
1510
  continue;
1125
- const rect = this._screenRect(entry);
1511
+ const rect = this._screenRect(entry, sv);
1126
1512
  if (x < rect.x - reach ||
1127
1513
  x > rect.x + rect.width + reach ||
1128
1514
  y < rect.y - reach ||
@@ -1168,6 +1554,13 @@ export class FlowGraphNode extends Node {
1168
1554
  * the square root of the gap (see `paths.ts`) — `8` where the shoulder
1169
1555
  * uses `6.25`, so the bound is generous rather than tight.
1170
1556
  */
1557
+ /** The coarse box with the label's plate in it: what an edge that
1558
+ * changes has to claim, where its route alone would leave a plate's
1559
+ * ends. */
1560
+ _edgeInkBox(edge) {
1561
+ const box = this._edgeCoarseBox(edge);
1562
+ return box && withLabelPlate(box, edge, this._viewport().zoom);
1563
+ }
1171
1564
  _edgeCoarseBox(edge) {
1172
1565
  const source = this._byId.get(edge.source);
1173
1566
  const target = this._byId.get(edge.target);
@@ -1175,27 +1568,11 @@ export class FlowGraphNode extends Node {
1175
1568
  return null;
1176
1569
  if (source.node.hidden || target.node.hidden)
1177
1570
  return null;
1178
- const zoom = this._viewport().zoom;
1179
- const a = this._screenRect(source);
1180
- const b = this._screenRect(target);
1181
- const span = Math.hypot(a.x + a.width / 2 - (b.x + b.width / 2), a.y + a.height / 2 - (b.y + b.height / 2));
1182
- const slack = Math.max(5, EDGE_SLOP * zoom) +
1183
- Math.max(EDGE_STEP_OFFSET * 3 * zoom, 8 * Math.sqrt(zoom * span));
1184
- const x = Math.min(a.x, b.x) - slack;
1185
- const y = Math.min(a.y, b.y) - slack;
1186
- return {
1187
- x,
1188
- y,
1189
- width: Math.max(a.x + a.width, b.x + b.width) + slack - x,
1190
- height: Math.max(a.y + a.height, b.y + b.height) + slack - y,
1191
- };
1571
+ return edgeCoarseBox(this._screenViewport(), this._source(source), this._source(target), this._scale);
1192
1572
  }
1193
1573
  /** The drawn handle, in screen pixels. Capped as well as floored: a dot
1194
1574
  * that scaled all the way up would be a saucer at 2.5×, and the thing it
1195
1575
  * marks — a point on the border — does not get bigger. */
1196
- _handleRadius() {
1197
- return clamp(HANDLE_RADIUS * this._viewport().zoom, 2.5, 6);
1198
- }
1199
1576
  _connectable(entry) {
1200
1577
  return ((entry.node.connectable ?? this._bool('nodesConnectable', true)) !== false);
1201
1578
  }
@@ -1206,59 +1583,19 @@ export class FlowGraphNode extends Node {
1206
1583
  /** Pick the handle an edge attaches to: the one it names, else the only
1207
1584
  * one of the right type, else the one facing the other end. */
1208
1585
  _endpoint(entry, handleId, type, towards) {
1209
- const anchors = this._handlesOf(entry);
1210
- if (handleId != null) {
1211
- const named = anchors.find((a) => a.id === handleId);
1212
- if (named)
1213
- return named;
1214
- }
1215
- const typed = anchors.filter((a) => a.type === type);
1216
- const candidates = typed.length > 0 ? typed : anchors;
1217
- if (candidates.length === 0)
1218
- return null;
1219
- if (candidates.length === 1 || !towards)
1220
- return candidates[0];
1221
- let best = candidates[0];
1222
- let bestDistance = Infinity;
1223
- for (const anchor of candidates) {
1224
- const d = Math.hypot(anchor.x - towards.x, anchor.y - towards.y);
1225
- if (d < bestDistance) {
1226
- bestDistance = d;
1227
- best = anchor;
1228
- }
1229
- }
1230
- return best;
1586
+ return endpoint({ node: entry.node, rect: this.rectOf(entry), specs: entry.specs }, handleId, type, towards);
1231
1587
  }
1232
1588
  /** An edge's polyline, in **screen** pixels, plus the two ends it joins.
1233
1589
  * Null when either end is missing — an edge to a node that was removed is
1234
1590
  * a normal state during an edit, not an error. */
1235
1591
  _edgeGeometry(edge) {
1236
- const sourceEntry = this._byId.get(edge.source);
1237
- const targetEntry = this._byId.get(edge.target);
1238
- if (!sourceEntry || !targetEntry)
1239
- return null;
1240
- if (sourceEntry.node.hidden || targetEntry.node.hidden)
1592
+ const source = this._byId.get(edge.source);
1593
+ const target = this._byId.get(edge.target);
1594
+ if (!source || !target)
1241
1595
  return null;
1242
- const sourceCentre = this._centre(sourceEntry);
1243
- const targetCentre = this._centre(targetEntry);
1244
- const from = this._endpoint(sourceEntry, edge.sourceHandle, 'source', targetCentre);
1245
- const to = this._endpoint(targetEntry, edge.targetHandle, 'target', sourceCentre);
1246
- if (!from || !to)
1596
+ if (source.node.hidden || target.node.hidden)
1247
1597
  return null;
1248
- const zoom = this._viewport().zoom;
1249
- const s = this._toScreen(from);
1250
- const t = this._toScreen(to);
1251
- const points = edgePath(edge.type, { x: s.x, y: s.y, position: from.position }, { x: t.x, y: t.y, position: to.position }, {
1252
- stepOffset: EDGE_STEP_OFFSET * zoom,
1253
- radius: 8 * zoom,
1254
- scale: zoom,
1255
- loop: edge.source === edge.target,
1256
- });
1257
- return { points, from, to };
1258
- }
1259
- _centre(entry) {
1260
- const r = this.rectOf(entry);
1261
- return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
1598
+ return edgeRoute(this._screenViewport(), edge, this._source(source), this._source(target));
1262
1599
  }
1263
1600
  // --- pane furniture geometry --------------------------------------------
1264
1601
  _corner(where, width, height, fallback) {
@@ -1326,23 +1663,49 @@ export class FlowGraphNode extends Node {
1326
1663
  // A minimap that covers the pane it summarises is worse than none.
1327
1664
  return null;
1328
1665
  }
1329
- const rects = [];
1330
- for (const entry of this._entries) {
1331
- if (!entry.node.hidden)
1332
- rects.push(this.rectOf(entry));
1333
- }
1334
1666
  // The viewport joins the bounds, so panning off the graph still shows
1335
- // where you are rather than pinning the box to an edge.
1667
+ // where you are rather than pinning the box to an edge. Worked out with
1668
+ // no box made per node: a drag asks this twice a step and again for
1669
+ // the pass (`_miniMapMoves`), over every node of the graph.
1336
1670
  const v = this._viewport();
1337
- rects.push({
1338
- x: -v.x / v.zoom,
1339
- y: -v.y / v.zoom,
1340
- width: pane.width / v.zoom,
1341
- height: pane.height / v.zoom,
1342
- });
1343
- const bounds = boundsOf(rects);
1344
- if (!bounds)
1345
- return null;
1671
+ let minX = -v.x / v.zoom;
1672
+ let minY = -v.y / v.zoom;
1673
+ let maxX = minX + pane.width / v.zoom;
1674
+ let maxY = minY + pane.height / v.zoom;
1675
+ const resizing = this._resizeTo;
1676
+ for (const entry of this._entries) {
1677
+ const node = entry.node;
1678
+ if (node.hidden)
1679
+ continue;
1680
+ let x;
1681
+ let y;
1682
+ let w;
1683
+ let h;
1684
+ if (resizing && resizing.id === node.id) {
1685
+ ({ x, y, width: w, height: h } = resizing.rect);
1686
+ }
1687
+ else {
1688
+ const p = this._positionOf(node);
1689
+ x = p.x;
1690
+ y = p.y;
1691
+ w = entry.width;
1692
+ h = entry.height;
1693
+ }
1694
+ if (x < minX)
1695
+ minX = x;
1696
+ if (y < minY)
1697
+ minY = y;
1698
+ if (x + w > maxX)
1699
+ maxX = x + w;
1700
+ if (y + h > maxY)
1701
+ maxY = y + h;
1702
+ }
1703
+ const bounds = {
1704
+ x: minX,
1705
+ y: minY,
1706
+ width: maxX - minX,
1707
+ height: maxY - minY,
1708
+ };
1346
1709
  const pad = Math.max(bounds.width, bounds.height) * 0.05 + 10;
1347
1710
  const padded = {
1348
1711
  x: bounds.x - pad,
@@ -1362,14 +1725,6 @@ export class FlowGraphNode extends Node {
1362
1725
  y: (y - oy) / map.scale + map.bounds.y,
1363
1726
  };
1364
1727
  }
1365
- _miniToScreen(map, p) {
1366
- const ox = map.panel.x + (map.panel.width - map.bounds.width * map.scale) / 2;
1367
- const oy = map.panel.y + (map.panel.height - map.bounds.height * map.scale) / 2;
1368
- return {
1369
- x: ox + (p.x - map.bounds.x) * map.scale,
1370
- y: oy + (p.y - map.bounds.y) * map.scale,
1371
- };
1372
- }
1373
1728
  // --- gestures ------------------------------------------------------------
1374
1729
  defaultMouseDown(ev) {
1375
1730
  if (this.props.disabled)
@@ -1561,10 +1916,10 @@ export class FlowGraphNode extends Node {
1561
1916
  gesture.x = ev.x;
1562
1917
  gesture.y = ev.y;
1563
1918
  this._selectBox(gesture);
1564
- // The box outline is all that moves this step; a node crossing the
1565
- // boundary changes `selected`, and that repaints through the
1566
- // controlled round trip like any other selection change.
1567
- this._claim(inflateRect(unionRects(oldBox, selectBoxRect(gesture)), CULL_MARGIN), 'style-state');
1919
+ // The box is all that moves this step; a node crossing the boundary
1920
+ // changes `selected`, and that repaints through the controlled round
1921
+ // trip like any other selection change.
1922
+ this._claimSelectStep(oldBox, selectBoxRect(gesture));
1568
1923
  return;
1569
1924
  }
1570
1925
  case 'minimap': {
@@ -1681,6 +2036,32 @@ export class FlowGraphNode extends Node {
1681
2036
  }
1682
2037
  this._prop('onNodeClick')?.(ev, entry.node);
1683
2038
  }
2039
+ /**
2040
+ * The nodes a drag is moving, once it has moved them: lifted out of the
2041
+ * rest of the graph for the rest of the gesture. Under GL the world is
2042
+ * built without them and they are a layer of their own (`glFrame`), so
2043
+ * each step packs them and their edges and leaves the world on the GPU;
2044
+ * in 2D the rest is two pictures a step copies from (`_liftShotFor`).
2045
+ * Null otherwise — and under GL the change back is a change of the
2046
+ * world's key, which rebuilds it with them in.
2047
+ */
2048
+ _lifted() {
2049
+ const g = this._gesture;
2050
+ if (g?.kind !== 'drag' || !g.moved || g.ids.length === 0) {
2051
+ if (this._liftSet) {
2052
+ this._liftSet = null;
2053
+ this._liftFor = null;
2054
+ this._liftGen++;
2055
+ }
2056
+ return null;
2057
+ }
2058
+ if (this._liftFor !== g) {
2059
+ this._liftSet = new Set(g.ids);
2060
+ this._liftFor = g;
2061
+ this._liftGen++;
2062
+ }
2063
+ return this._liftSet;
2064
+ }
1684
2065
  _dragStep(gesture, ev) {
1685
2066
  const zoom = this._viewport().zoom;
1686
2067
  const dx = (ev.x - gesture.startX) / zoom;
@@ -1721,25 +2102,43 @@ export class FlowGraphNode extends Node {
1721
2102
  }
1722
2103
  // Old places first, new places second: the union is everything this
1723
2104
  // step uncovers plus everything it covers, and nothing else — with the
1724
- // edges that follow the moved nodes, coarse, on both sides.
2105
+ // edges that follow the moved nodes, coarse, on both sides. Under GL
2106
+ // there is nothing to size: the step is a frame of the surface's, and
2107
+ // routing each moved node's edges for a rect nobody reads was a third
2108
+ // of a millisecond a step.
2109
+ const sized = !this._gl;
1725
2110
  let damage = null;
1726
- for (const id of gesture.ids) {
1727
- const entry = this._byId.get(id);
1728
- if (entry)
2111
+ // …and in the minimap, the boxes the step moves them from
2112
+ const maps = sized && this._drawsMiniMap();
2113
+ const moves = [];
2114
+ if (sized) {
2115
+ for (const id of gesture.ids) {
2116
+ const entry = this._byId.get(id);
2117
+ if (!entry)
2118
+ continue;
1729
2119
  damage = unionMaybe(damage, this._nodeDamage(entry));
2120
+ if (maps)
2121
+ moves.push({ entry, before: this.rectOf(entry) });
2122
+ }
1730
2123
  }
1731
2124
  this._dragTo = to;
1732
- for (const id of gesture.ids) {
1733
- const entry = this._byId.get(id);
1734
- if (entry)
1735
- damage = unionMaybe(damage, this._nodeDamage(entry));
2125
+ if (sized) {
2126
+ for (const id of gesture.ids) {
2127
+ const entry = this._byId.get(id);
2128
+ if (entry)
2129
+ damage = unionMaybe(damage, this._nodeDamage(entry));
2130
+ }
1736
2131
  }
1737
2132
  this._emitNodes(changes);
2133
+ const reason = this._lifted() ? 'lift' : 'content';
1738
2134
  if (damage) {
1739
- this._claim(inflateRect(damage, CULL_MARGIN), 'content');
2135
+ this._claim(inflateRect(damage, CULL_MARGIN), reason);
2136
+ if (maps)
2137
+ this._claimMiniMapMoves(moves, reason);
1740
2138
  }
1741
2139
  else {
1742
- this._repaint();
2140
+ // under GL: a frame, and — while the nodes are lifted — of them alone
2141
+ this._repaint(reason);
1743
2142
  }
1744
2143
  // Inside the gesture dispatch, deliberately: the body-rect setState this
1745
2144
  // triggers is a discrete-priority update, so React commits the moved
@@ -1765,6 +2164,8 @@ export class FlowGraphNode extends Node {
1765
2164
  minWidth: entry.node.minWidth ?? MIN_NODE_WIDTH,
1766
2165
  minHeight: entry.node.minHeight ?? MIN_NODE_HEIGHT,
1767
2166
  }, snap);
2167
+ const maps = this._drawsMiniMap();
2168
+ const before = this.rectOf(entry);
1768
2169
  let damage = this._nodeDamage(entry);
1769
2170
  this._resizeTo = { id: gesture.id, rect };
1770
2171
  damage = unionRects(damage, this._nodeDamage(entry));
@@ -1786,6 +2187,8 @@ export class FlowGraphNode extends Node {
1786
2187
  }
1787
2188
  this._emitNodes(changes);
1788
2189
  this._claim(inflateRect(damage, CULL_MARGIN), 'content');
2190
+ if (maps)
2191
+ this._claimMiniMapMoves([{ entry, before }], 'content');
1789
2192
  this._emitBodies();
1790
2193
  }
1791
2194
  _connectStep(gesture, ev) {
@@ -1836,28 +2239,79 @@ export class FlowGraphNode extends Node {
1836
2239
  /** The pending connection's polyline — one builder for the paint and for
1837
2240
  * the damage it has to claim, so the two cannot disagree. */
1838
2241
  _connectionPath(gesture) {
1839
- const v = this._viewport();
1840
- const from = this._toScreen(gesture.from);
1841
- const target = gesture.to ?? gesture.pointer;
1842
- const to = this._toScreen(target);
1843
- const toPosition = gesture.to?.position ??
1844
- (gesture.from.position === 'left'
1845
- ? 'right'
1846
- : gesture.from.position === 'right'
1847
- ? 'left'
1848
- : gesture.from.position === 'top'
1849
- ? 'bottom'
1850
- : 'top');
1851
- return edgePath('bezier', { x: from.x, y: from.y, position: gesture.from.position }, { x: to.x, y: to.y, position: toPosition }, {
1852
- stepOffset: EDGE_STEP_OFFSET * v.zoom,
1853
- radius: 8 * v.zoom,
1854
- scale: v.zoom,
1855
- });
2242
+ return connectionPath(this._screenViewport(), gesture);
1856
2243
  }
1857
2244
  _validConnection(connection) {
1858
2245
  const check = this._prop('isValidConnection');
1859
2246
  return check ? check(connection) !== false : true;
1860
2247
  }
2248
+ /**
2249
+ * What one step of a box selection changes on screen: a band round each
2250
+ * side of the box that moved, across the whole of both boxes. That covers
2251
+ * the outline where it was and where it is, and the tint where one box
2252
+ * lies over the graph and the other does not; a side that held still —
2253
+ * the two the gesture is anchored by — changed only where a moving side
2254
+ * shortened or lengthened it, which that side's band covers. Claiming the
2255
+ * box round both repainted everything under the selection, every node and
2256
+ * edge in it, on every step: 21 ms of a step on the stress example's
2257
+ * fan-out, which is what made a sweep lag the pointer.
2258
+ *
2259
+ * The bands are cut on the device grid so that none of them overlaps
2260
+ * another: core merges overlapping damage into the box round it, which is
2261
+ * the whole selection again — and a pass over a translucent tint must not
2262
+ * paint it twice. Claimed past this element's own `invalidate`, whose one
2263
+ * job here would be to repaint the minimap and the controls on their
2264
+ * canvases, and a box selection moves nothing on either.
2265
+ *
2266
+ * Under GL the box is the overlay's, drawn every frame: a step is a frame
2267
+ * and not a change to the world, which a claim would have rebuilt.
2268
+ */
2269
+ _claimSelectStep(from, to) {
2270
+ if (this._gl) {
2271
+ this._glRequest();
2272
+ return;
2273
+ }
2274
+ const s = this._scale;
2275
+ const lo = (v) => Math.floor(toDevice(v - SELECT_BAND, s));
2276
+ const hi = (v) => Math.ceil(toDevice(v + SELECT_BAND, s));
2277
+ /** The bands round the sides that moved along one axis, merged where
2278
+ * they meet, in device pixels. */
2279
+ const bands = (a0, a1, b0, b1) => {
2280
+ const out = [];
2281
+ if (a0 !== b0)
2282
+ out.push([lo(Math.min(a0, b0)), hi(Math.max(a0, b0))]);
2283
+ if (a1 !== b1)
2284
+ out.push([lo(Math.min(a1, b1)), hi(Math.max(a1, b1))]);
2285
+ out.sort((m, n) => m[0] - n[0]);
2286
+ if (out.length === 2 && out[1][0] <= out[0][1]) {
2287
+ return [[out[0][0], Math.max(out[0][1], out[1][1])]];
2288
+ }
2289
+ return out;
2290
+ };
2291
+ const outer = unionRects(from, to);
2292
+ const x0 = lo(outer.x);
2293
+ const x1 = hi(outer.x + outer.width);
2294
+ const y0 = lo(outer.y);
2295
+ const y1 = hi(outer.y + outer.height);
2296
+ const columns = bands(from.x, from.x + from.width, to.x, to.x + to.width);
2297
+ const rows = bands(from.y, from.y + from.height, to.y, to.y + to.height);
2298
+ const claim = (x, y, width, height) => {
2299
+ if (width > 0 && height > 0) {
2300
+ super.invalidate(false, { x, y, width, height }, 'style-state');
2301
+ }
2302
+ };
2303
+ for (const [c0, c1] of columns)
2304
+ claim(c0, y0, c1 - c0, y1 - y0);
2305
+ for (const [r0, r1] of rows) {
2306
+ // across both boxes, less the columns already claimed
2307
+ let x = x0;
2308
+ for (const [c0, c1] of columns) {
2309
+ claim(x, r0, c0 - x, r1 - r0);
2310
+ x = Math.max(x, c1);
2311
+ }
2312
+ claim(x, r0, x1 - x, r1 - r0);
2313
+ }
2314
+ }
1861
2315
  _selectBox(gesture) {
1862
2316
  const a = this._toGraph(gesture.startX, gesture.startY);
1863
2317
  const b = this._toGraph(gesture.x, gesture.y);
@@ -1984,11 +2438,12 @@ export class FlowGraphNode extends Node {
1984
2438
  a11yScene() {
1985
2439
  this._sync();
1986
2440
  const pane = this._pane();
2441
+ const sv = screenViewport(this._viewport(), pane);
1987
2442
  const items = [];
1988
2443
  for (const entry of this._order) {
1989
2444
  if (entry.node.hidden)
1990
2445
  continue;
1991
- const rect = this._screenRect(entry);
2446
+ const rect = this._screenRect(entry, sv);
1992
2447
  if (!rectsOverlap(rect, pane))
1993
2448
  continue;
1994
2449
  const data = entry.node.data;
@@ -2153,7 +2608,12 @@ export class FlowGraphNode extends Node {
2153
2608
  // changed — a moved node repaints the box it moved through, an edit to
2154
2609
  // anything else on the visual list repaints in full, and handler churn
2155
2610
  // repaints nothing.
2156
- let damage = null;
2611
+ // Boxes rather than the box around them: the nodes a box selection
2612
+ // takes in one step lie along both of its moving edges, and the box
2613
+ // round an L of nodes is the whole selection again.
2614
+ let damage = [];
2615
+ let edgesRedrawn = false;
2616
+ this._liftOnly = false;
2157
2617
  if (!shallowEqual(nextProps.nodeTypes, before.nodeTypes) ||
2158
2618
  !shallowEqual(nextProps.defaultEdgeOptions, before.defaultEdgeOptions)) {
2159
2619
  this._sync();
@@ -2161,16 +2621,20 @@ export class FlowGraphNode extends Node {
2161
2621
  }
2162
2622
  else {
2163
2623
  if (nextProps.edges !== before.edges) {
2164
- damage = this._applyEdges(this._rawEdges());
2624
+ const edgeDamage = this._applyEdges(this._rawEdges());
2625
+ if (edgeDamage === 'full')
2626
+ damage = 'full';
2627
+ else if (edgeDamage) {
2628
+ damage.push(edgeDamage);
2629
+ edgesRedrawn = true;
2630
+ }
2165
2631
  }
2166
2632
  if (damage !== 'full' && nextProps.nodes !== before.nodes) {
2167
2633
  const nodeDamage = this._applyNodes(this._nodes());
2168
- damage =
2169
- nodeDamage === 'full' || damage === null
2170
- ? nodeDamage
2171
- : nodeDamage === null
2172
- ? damage
2173
- : unionRects(damage, nodeDamage);
2634
+ if (nodeDamage === 'full')
2635
+ damage = 'full';
2636
+ else
2637
+ damage.push(...nodeDamage);
2174
2638
  }
2175
2639
  }
2176
2640
  // `fitView` is a one-shot: turning it on later refits, and it stays off
@@ -2187,19 +2651,38 @@ export class FlowGraphNode extends Node {
2187
2651
  }
2188
2652
  }
2189
2653
  }
2654
+ // What moved, restyled or reshaped is the graph's own content: under GL
2655
+ // it is a frame of the surface's, where a claim billed as `props` went
2656
+ // on to a window pass over the 2D pane under it — every step of a drag
2657
+ // an app stores, 1.2 ms of each, for pixels the surface covers.
2190
2658
  if (damage === 'full')
2191
2659
  this._repaint('props');
2192
- else if (damage)
2193
- this._claim(damage, 'props');
2660
+ else {
2661
+ // a commit of lifted nodes alone is a frame of the lifted layer
2662
+ const reason = this._liftOnly && !edgesRedrawn ? 'lift' : 'content';
2663
+ for (const box of damage)
2664
+ this._claim(box, reason);
2665
+ }
2666
+ this._liftOnly = false;
2194
2667
  // Core re-reads the scene for aria-prop commits; a `nodes`/`edges`
2195
2668
  // change is invisible to it, so the re-read is asked for by name.
2196
2669
  // Free when no assistive technology is listening.
2197
- if (damage)
2670
+ if (damage === 'full' || damage.length > 0)
2198
2671
  this.notifyA11ySceneChanged();
2199
2672
  }
2200
2673
  destroySubtree() {
2674
+ this._dropZoomShot();
2675
+ this._dropLiftShot();
2676
+ this._glRequest = null;
2677
+ if (this._bodiesRest != null)
2678
+ timers.clearTimeout?.(this._bodiesRest);
2679
+ this._bodiesRest = null;
2680
+ if (this._zoomRest != null)
2681
+ timers.clearTimeout?.(this._zoomRest);
2682
+ this._zoomRest = null;
2201
2683
  this._stopAnimation();
2202
2684
  this._dropGridTile();
2685
+ this._sceneCache.clear();
2203
2686
  super.destroySubtree();
2204
2687
  }
2205
2688
  _startAnimation() {
@@ -2207,10 +2690,62 @@ export class FlowGraphNode extends Node {
2207
2690
  return;
2208
2691
  this._animTimer =
2209
2692
  timers.setInterval?.(() => {
2210
- this._dashPhase += ANIMATION_SPEED;
2693
+ // A pan that blits copies the pane's pixels, dashes and all, and a
2694
+ // tick claims the dashes inside the band it copies — which declines
2695
+ // the copy: every frame a tick landed in repainted the whole pane,
2696
+ // a sixth of a pan's frames and all of its stutter. So the dashes
2697
+ // sit the pan out, phase and all, and what the pan copies and what
2698
+ // it draws agree; they march again once it has held still.
2699
+ //
2700
+ // And the view moving at all is what they wait out, not a blit that
2701
+ // happened: a tick declines the blit it lands beside, so a pan
2702
+ // whose frames ran past the wait had its ticks back, each one
2703
+ // cancelling the next blit and repainting the pane — over the
2704
+ // stress example's widgets in a large window, 2 frames a second,
2705
+ // ticks and pan steps taking turns. A step since the last tick, or
2706
+ // one within two ticks, holds the dashes still whatever a frame
2707
+ // costs. Under GL a tick is a uniform and blits nothing, and they
2708
+ // march through a pan as before.
2709
+ //
2710
+ // A 2D tick is a repaint of the box the dashes are in, which over a
2711
+ // dense graph in a large window is most of the pane: 75 ms a tick on
2712
+ // XQuartz, against a timer of 60. So the wait scales with what
2713
+ // frames cost: the view held still for two frames' worth, and one
2714
+ // tick every two frames at most — the dashes slow down before the
2715
+ // thread is theirs, and a pan whose frames come slowly still sees
2716
+ // them hold still.
2717
+ const t = now();
2718
+ const lastTick = this._tickAt;
2719
+ this._tickAt = t;
2720
+ if (t - this._blittedPanAt < ANIMATION_MS * 2)
2721
+ return;
2722
+ let steps = 1;
2723
+ if (!this._gl) {
2724
+ const cost = this._tickCost * 2;
2725
+ const still = Math.max(ANIMATION_MS * 2, cost);
2726
+ if (this._viewMovedAt > lastTick || t - this._viewMovedAt < still) {
2727
+ return;
2728
+ }
2729
+ // a tick under ANIMATION_MS apart, less a millisecond of timer
2730
+ // slop; further while ticks are dear — and then the dashes cover
2731
+ // the ground the skipped ticks would have, so they slow down in
2732
+ // steps and not in speed
2733
+ const since = t - this._marchedAt;
2734
+ if (since < Math.max(ANIMATION_MS, cost) - 1)
2735
+ return;
2736
+ steps = Math.min(4, Math.max(1, Math.round(since / ANIMATION_MS)));
2737
+ }
2738
+ this._marchedAt = t;
2739
+ this._dashPhase += ANIMATION_SPEED * steps;
2211
2740
  // the box the last paint saw animated edges in, not the pane: a
2212
- // marching dash should not cost a full grid repaint per tick
2213
- this.invalidate(false, this._animBox ? this._device(this._animBox) : this.abs, 'animation');
2741
+ // marching dash should not cost a full grid repaint per tick. And
2742
+ // only the part of it the pane shows — an edge on its way out of
2743
+ // the pane takes the box past it, and claimed whole, a tick
2744
+ // repainted everything the window has beside the graph.
2745
+ const box = this._animBox ? this._device(this._animBox) : this.abs;
2746
+ const shown = intersectRects(box, this.contentBox());
2747
+ if (shown)
2748
+ this.invalidate(false, shown, 'animation');
2214
2749
  }, ANIMATION_MS) ?? null;
2215
2750
  }
2216
2751
  _stopAnimation() {
@@ -2221,6 +2756,8 @@ export class FlowGraphNode extends Node {
2221
2756
  }
2222
2757
  // --- painting ------------------------------------------------------------
2223
2758
  paint(ctx) {
2759
+ this._frameTick();
2760
+ const paintStart = now();
2224
2761
  // What this pass is repainting. The renderer paints each damage rect as
2225
2762
  // its own clipped pass; content outside it survives on the window, so
2226
2763
  // everything we skip here is content the last frame already drew — the
@@ -2245,9 +2782,35 @@ export class FlowGraphNode extends Node {
2245
2782
  super.paint(ctx);
2246
2783
  if (!this._visible())
2247
2784
  return;
2248
- const painter = createPainter(ctx, this._textOptions());
2785
+ if (this._gl) {
2786
+ // The surface over this box draws the graph. What the 2D pass still
2787
+ // owes is the part of a paint that is not drawing — the derived graph
2788
+ // brought up to date, the bodies placed — and then the frame itself,
2789
+ // asked of the surface. Every claim this element makes arrives here,
2790
+ // so every change the 2D renderer would repaint is one GL frame.
2791
+ this._sync();
2792
+ this._emitBodies();
2793
+ this._emitPanels();
2794
+ this._glRequest?.();
2795
+ return;
2796
+ }
2797
+ // Mid-gesture, labels come from the layouts they already have, scaled:
2798
+ // shaping every one again at each step of a zoom was half of what a 2D
2799
+ // step cost, at sizes the next step moves off. Only where the context
2800
+ // scales text with its transform — X11's draws glyphs at the size they
2801
+ // were shaped at — and set exactly once the zoom rests (`_restZoom`).
2802
+ const approximateText = this._zoomMoving() &&
2803
+ ctx.scalesText === true;
2804
+ const painter = createPainter(ctx, {
2805
+ ...this._textOptions(),
2806
+ approximateText,
2807
+ });
2249
2808
  if (!painter)
2250
2809
  return; // a backend with no path API: geometry only
2810
+ if (approximateText) {
2811
+ this._textApproximated = true;
2812
+ this._restZoom();
2813
+ }
2251
2814
  this._sync();
2252
2815
  this._painting = true;
2253
2816
  if (this._fitPending) {
@@ -2298,31 +2861,116 @@ export class FlowGraphNode extends Node {
2298
2861
  { x: x + width - radius, y: y + height - radius },
2299
2862
  ].some((corner) => rectsOverlap({ x: corner.x, y: corner.y, width: radius, height: radius }, this._frameClip)));
2300
2863
  painter.clipRect(x, y, width, height, nearCorner ? radius : 0);
2301
- // The fill and the grid draw only the region this pass repaints — on a
2302
- // full pass that is the pane, and on a drag it is the sliver that moved.
2303
- const region = this._frameClip
2304
- ? intersectRects({ x, y, width, height }, this._frameClip)
2305
- : { x, y, width, height };
2306
- if (region) {
2307
- // The style's own colour if it set one — `super.paint` already filled
2308
- // it, and repeating it costs one rectangle and keeps the two agreeing.
2309
- painter.rect(region.x, region.y, region.width, region.height, 0, {
2310
- fill: this.style.backgroundColor ??
2311
- palette.background,
2312
- });
2313
- this._paintGrid(painter, palette, region);
2314
- }
2315
- const animated = this._paintEdges(painter, palette);
2316
- this._paintNodes(painter, palette);
2317
- this._paintConnection(painter, palette);
2318
- this._paintSelectionBox(painter, palette);
2319
- this._paintMiniMap(painter, palette);
2320
- this._paintControls(painter, palette);
2864
+ const started = now();
2865
+ // Not under a drag or a connection: the picture would hold the moving
2866
+ // node, or the line, where they were
2867
+ // …and only for a graph that costs more than a frame to paint live
2868
+ // (`ZOOM_SHOT_WORTH_MS`, `ZOOM_SHOT_DROP_MS`)
2869
+ const zooming = this._zoomMoving() && !this._gesture;
2870
+ const paintMs = zooming ? this._graphPaintMs() : 0;
2871
+ if (zooming && this._zoomShot !== null && paintMs < ZOOM_SHOT_DROP_MS) {
2872
+ this._dropZoomShot();
2873
+ }
2874
+ const shot = zooming && (this._zoomShot !== null || paintMs > ZOOM_SHOT_WORTH_MS)
2875
+ ? this._zoomShotFor(palette, approximateText)
2876
+ : null;
2877
+ // A drag's nodes over pictures of the rest (`_liftShotFor`), in 2D and
2878
+ // not mid-zoom.
2879
+ const lift = shot || approximateText ? null : this._lifted();
2880
+ const liftShot = lift ? this._liftShotFor(palette, lift) : null;
2881
+ if (!lift && this._liftShot)
2882
+ this._dropLiftShot();
2883
+ let scene;
2884
+ if (liftShot && lift) {
2885
+ // the pictures, with the moved nodes' edges between them and the
2886
+ // nodes over both; the pane's furniture over it all
2887
+ const lifted = buildScene(this._sceneInput(palette, { lift: { ids: lift, only: true } }));
2888
+ scene = buildScene(this._sceneInput(palette, 'overlay'));
2889
+ const built = now();
2890
+ this._compositeLift(ctx, liftShot, liftShot.under, null);
2891
+ paintGraphEdges(painter, lifted);
2892
+ this._compositeLift(ctx, liftShot, liftShot.over, liftShot.cards);
2893
+ paintGraphNodes(painter, lifted);
2894
+ paintFloat(painter, scene);
2895
+ this._reportFrame(built - started, now() - built);
2896
+ }
2897
+ else if (shot) {
2898
+ // the graph and the ground under it as the gesture drew them, scaled;
2899
+ // both live wherever the picture does not reach; and the pane's
2900
+ // furniture over them at the zoom of the moment
2901
+ scene = buildScene(this._sceneInput(palette, 'overlay'));
2902
+ const bands = this._uncovered(shot);
2903
+ const input = bands.length > 0 ? this._sceneInput(palette) : null;
2904
+ const live = input
2905
+ ? bands.map((band) => ({
2906
+ band,
2907
+ scene: buildScene({ ...input, clip: band }),
2908
+ }))
2909
+ : [];
2910
+ const built = now();
2911
+ this._compositeShot(ctx, shot);
2912
+ for (const { band, scene: part } of live) {
2913
+ painter.save();
2914
+ painter.clipRect(band.x, band.y, band.width, band.height, 0);
2915
+ paintGround(painter, part, this._grid);
2916
+ paintGraph(painter, part);
2917
+ painter.restore();
2918
+ }
2919
+ paintFloat(painter, scene);
2920
+ this._textApproximated = true;
2921
+ this._restZoom();
2922
+ this._reportFrame(built - started, now() - built);
2923
+ }
2924
+ else {
2925
+ // Only a pass over the whole pane may change how edges are stroked;
2926
+ // one over part of it follows what the rest was drawn with, and asks
2927
+ // for the whole when the edges on the pane have crossed over
2928
+ // (`EDGE_BATCH`) — so a pan's copy never meets a strip drawn the
2929
+ // other way.
2930
+ const input = this._sceneInput(palette);
2931
+ const whole = this._coversPane(clip);
2932
+ scene = buildScene(whole ? { ...input, batch: undefined, batchWas: this._batch } : input);
2933
+ if (whole)
2934
+ this._batch = scene.batch;
2935
+ else if (batchesEdges(scene.edgesOnPane, this._batch) !== this._batch) {
2936
+ this._repaint('content');
2937
+ }
2938
+ const built = now();
2939
+ paintScene(painter, scene, this._grid);
2940
+ const done = now();
2941
+ this._reportFrame(built - started, done - built);
2942
+ // a zoom's step, whole and live: what the next one would cost
2943
+ if (this._zoomMoving() && this._wholePane(clip)) {
2944
+ this._noteGraphPaint(done - started);
2945
+ }
2946
+ }
2947
+ // The box the dash ticks invalidate comes off the *drawn* geometry; a
2948
+ // pass that culled every animated edge keeps the one before it, because
2949
+ // the endpoints did not move, or that move's own damage would have
2950
+ // redrawn them here.
2951
+ if (scene.animBox)
2952
+ this._animBox = scene.animBox;
2321
2953
  painter.restore();
2322
2954
  this._painting = false;
2323
2955
  this._frameClip = null;
2956
+ {
2957
+ // What a frame costs, for the dash timer (`_startAnimation`): this
2958
+ // paint, and how long the server took to answer the frame before it
2959
+ // — on XQuartz most of it.
2960
+ const latency = this.root?.window?.frameLatency;
2961
+ // A reply can take seconds on a server that is waiting for a
2962
+ // display — a Mac's asleep — and that is when ticks most need to
2963
+ // hold; clamped rather than dropped, so one outlier does not stand
2964
+ // for long in the mean.
2965
+ const cost = now() -
2966
+ paintStart +
2967
+ (typeof latency === 'number' && latency > 0
2968
+ ? Math.min(latency, 2000)
2969
+ : 0);
2970
+ this._tickCost = this._tickCost * 0.7 + cost * 0.3;
2971
+ }
2324
2972
  // The timer exists only while something on screen needs it.
2325
- if (animated)
2973
+ if (scene.animated)
2326
2974
  this._startAnimation();
2327
2975
  else
2328
2976
  this._stopAnimation();
@@ -2336,147 +2984,1264 @@ export class FlowGraphNode extends Node {
2336
2984
  // to put the node bodies it mounts, and it is answered from the geometry
2337
2985
  // this frame just used — the same numbers, never a second derivation.
2338
2986
  this._emitBodies();
2987
+ this._emitPanels();
2988
+ // a pass of the frame that drew the latest zoom step ends here
2989
+ if (this._zoomFrameAt === this._lastFrameAt)
2990
+ this._zoomDrawnAt = now();
2339
2991
  }
2340
2992
  /**
2341
- * Where every mounted node body belongs, relative to the pane's own
2342
- * top-left. Sent only when the list changed, so a repaint that moved
2343
- * nothing does not re-render React — but a pan does, once per frame,
2344
- * which is the cost `render` is documented to carry.
2993
+ * Everything `buildScene` reads, gathered from the element once.
2994
+ *
2995
+ * The two node lists differ on purpose: `nodes` is the paint order with a
2996
+ * dragged node lifted to the top, and `all` is every node, because the
2997
+ * minimap summarises the graph rather than the pass.
2345
2998
  */
2346
- _emitBodies() {
2347
- const notify = this._prop('onNodeBodies');
2348
- if (!notify)
2349
- return;
2350
- const v = this._viewport();
2351
- const pane = this._pane();
2352
- const bodies = [];
2353
- for (const entry of this._order) {
2354
- if (entry.node.hidden || !this._mounted(entry))
2355
- continue;
2356
- const rect = this._screenRect(entry);
2357
- if (!rectsOverlap(rect, pane))
2358
- continue;
2359
- const header = this._headerHeight(entry) * v.zoom;
2360
- const inset = NODE_BODY_INSET * v.zoom;
2361
- const width = rect.width - inset * 2;
2362
- const height = rect.height - header - inset;
2363
- if (width <= 1 || height <= 1)
2364
- continue;
2365
- bodies.push({
2366
- id: entry.node.id,
2367
- // pane-relative and logical, because that is what an absolutely
2368
- // positioned box beside the pane is laid out against, and in
2369
- x: Math.round(rect.x + inset - pane.x),
2370
- y: Math.round(rect.y + header - pane.y),
2371
- width: Math.round(width),
2372
- height: Math.round(height),
2373
- zoom: v.zoom,
2374
- selected: entry.node.selected ?? false,
2375
- });
2999
+ invalidate(layout, damage, reason) {
3000
+ // Whatever changed the graph may have changed the minimap, which
3001
+ // `<Flow>` may be painting on a canvas of its own; a change to this
3002
+ // element's props may have changed the controls too. The controls draw
3003
+ // nothing of the graph, and repainting them with every step of a drag
3004
+ // repainted the minimap with them — each canvas builds the panels its
3005
+ // box reaches, and theirs reached both. Each claims its own box: a
3006
+ // claim with no region is the whole window, which made every pan step
3007
+ // and dash tick a full frame. A dash tick changes neither — the minimap
3008
+ // draws no dashes — and repainting both canvases on every one kept an
3009
+ // idle pane painting them 17 times a second, and put a claim in the
3010
+ // middle of any pan frame it landed in.
3011
+ if (reason !== 'animation') {
3012
+ const canvases = reason === 'props' ? this._panelCanvases : this._miniMapCanvases();
3013
+ for (const canvas of canvases)
3014
+ canvas.invalidate(false, canvas, 'props');
2376
3015
  }
2377
- // The zoom is in the key as well as the rect it produced: it is the
2378
- // body's subtree scale now, and two zooms a rounding apart can leave a
2379
- // box on the same integers while the text inside it should have moved.
2380
- const key = bodies
2381
- .map((b) => `${b.id}:${b.x},${b.y},${b.width},${b.height},${b.zoom},${b.selected}`)
2382
- .join('|');
2383
- if (key === this._bodiesKey)
3016
+ // A dash tick moves the phase, a uniform on the GPU, so it is no change
3017
+ // to the world. A pan or a zoom never gets here under GL.
3018
+ if (reason === 'lift')
3019
+ this._liftVersion++;
3020
+ else if (reason !== 'animation')
3021
+ this._worldVersion++;
3022
+ // …and what a 2D zoom gesture composites is the graph as it was: a
3023
+ // change to it is a picture that no longer is
3024
+ if (reason !== 'animation' && reason !== 'scroll')
3025
+ this._dropZoomShot();
3026
+ // Under GL the surface over this box draws the graph and the 2D pane
3027
+ // under it shows nothing: a claim of its pixels was a window pass over
3028
+ // them, unseen, whose one job was to reach `paint` and ask the surface
3029
+ // for a frame — a BeginDraw, a walk and a commit on every step of a
3030
+ // drag, 2.6 ms of each. The graph's own claims ask for the frame
3031
+ // directly; the frame does what `paint` did (`glFrame`). A layout change
3032
+ // and a change of props — which may move the panels — still go through.
3033
+ if (!layout &&
3034
+ damage != null &&
3035
+ (reason === 'content' ||
3036
+ reason === 'style-state' ||
3037
+ reason === 'animation' ||
3038
+ reason === 'lift') &&
3039
+ this._gl) {
3040
+ this._glRequest();
2384
3041
  return;
2385
- this._bodiesKey = key;
2386
- notify(bodies, this._gestureSync);
3042
+ }
3043
+ // a lift is this element's own bookkeeping; core knows it as content
3044
+ super.invalidate(layout, damage, reason === 'lift' ? 'content' : reason);
2387
3045
  }
2388
- /** The grid, drawn only inside `region` — the pass's damage on a partial
2389
- * repaint, the pane on a full one. Alignment stays anchored to the
2390
- * viewport origin, so the region never changes where a dot falls. */
2391
- _paintGrid(painter, palette, region) {
2392
- const options = normalizeBackground(this.props.background);
2393
- if (options.variant === 'none')
3046
+ /** A 2D frame's cost so far, while its flush is still painting passes. */
3047
+ _frameCost = null;
3048
+ /**
3049
+ * One `onFrame` per window flush, however many damage passes it painted.
3050
+ * The window paints its passes one after another in a single task, so a
3051
+ * microtask queued by the first runs after the last — and reports the
3052
+ * frame whole, which is what a frame rate counts.
3053
+ */
3054
+ _reportFrame(sceneMs, drawMs) {
3055
+ if (!this.props.onFrame)
2394
3056
  return;
2395
- const v = this._viewport();
2396
- let step = options.gap * v.zoom;
2397
- if (!(step > 0))
2398
- return;
2399
- // Doubling rather than clamping keeps the grid *aligned* to the graph
2400
- // while zooming out: every visible line is still a real one.
2401
- while (step < MIN_GRID_PX)
2402
- step *= 2;
2403
- const color = options.color ?? palette.grid;
2404
- const pane = this._pane();
2405
- const { x, y, width, height } = region;
2406
- const originX = pane.x + v.x;
2407
- const originY = pane.y + v.y;
2408
- if (this._paintGridPattern(painter, options, step, color, region))
2409
- return;
2410
- const firstX = originX + Math.ceil((x - originX) / step) * step;
2411
- const firstY = originY + Math.ceil((y - originY) / step) * step;
2412
- if (options.variant === 'lines') {
2413
- const runs = [];
2414
- for (let gx = firstX; gx <= x + width; gx += step) {
2415
- runs.push([
2416
- { x: gx, y },
2417
- { x: gx, y: y + height },
2418
- ]);
2419
- }
2420
- for (let gy = firstY; gy <= y + height; gy += step) {
2421
- runs.push([
2422
- { x, y: gy },
2423
- { x: x + width, y: gy },
2424
- ]);
2425
- }
2426
- painter.strokeRuns(runs, { stroke: color, lineWidth: 1 });
2427
- return;
2428
- }
2429
- if (options.variant === 'cross') {
2430
- const arm = Math.max(2, options.size * 3 * v.zoom);
2431
- const runs = [];
2432
- for (let gx = firstX; gx <= x + width; gx += step) {
2433
- for (let gy = firstY; gy <= y + height; gy += step) {
2434
- runs.push([
2435
- { x: gx - arm, y: gy },
2436
- { x: gx + arm, y: gy },
2437
- ]);
2438
- runs.push([
2439
- { x: gx, y: gy - arm },
2440
- { x: gx, y: gy + arm },
2441
- ]);
2442
- }
2443
- }
2444
- painter.strokeRuns(runs, { stroke: color, lineWidth: 1 });
3057
+ if (this._frameCost) {
3058
+ this._frameCost.sceneMs += sceneMs;
3059
+ this._frameCost.drawMs += drawMs;
2445
3060
  return;
2446
3061
  }
2447
- const centres = [];
2448
- for (let gx = firstX; gx <= x + width; gx += step) {
2449
- for (let gy = firstY; gy <= y + height; gy += step) {
2450
- centres.push({ x: gx, y: gy });
2451
- }
2452
- }
2453
- painter.dots(centres, Math.max(1, Math.round(options.size * 2 * v.zoom)), color);
3062
+ this._frameCost = { sceneMs, drawMs };
3063
+ void Promise.resolve().then(() => {
3064
+ const cost = this._frameCost;
3065
+ this._frameCost = null;
3066
+ if (!cost || this.destroyed)
3067
+ return;
3068
+ this._prop('onFrame')?.({
3069
+ renderer: 'retained',
3070
+ sceneMs: cost.sceneMs,
3071
+ packMs: 0,
3072
+ drawMs: cost.drawMs,
3073
+ drawCalls: 0,
3074
+ lines: 0,
3075
+ boxes: 0,
3076
+ triangles: 0,
3077
+ uploadBytes: 0,
3078
+ worldRebuilt: false,
3079
+ gaps: { text: 0, custom: 0 },
3080
+ bodies: this.bodyBudget(),
3081
+ });
3082
+ });
3083
+ }
3084
+ get _gl() {
3085
+ return this.props.renderer === 'gl' && this._glRequest != null;
3086
+ }
3087
+ /** What the GL renderer's label atlas sets strings with: this pane's app,
3088
+ * for its staging surface, and the options its 2D painter measures with
3089
+ * — so a label is cut and placed the same on both renderers. Null where
3090
+ * there is no text engine to ask. */
3091
+ glText() {
3092
+ const options = this._textOptions();
3093
+ return options.fonts ? { app: this.app, options } : null;
2454
3094
  }
2455
3095
  /**
2456
- * The grid as one repeating fill (ntk#263): a step-sized tile drawn once,
2457
- * a `createPattern('repeat')` over it, and one composite for the whole
2458
- * region — where the runs path pays a region-sized coverage mask.
3096
+ * The GL surface's hook: how it asks for frames. Set while a surface draws
3097
+ * this pane, cleared when it goes — and until it is set the pane draws
3098
+ * itself, so a surface that is still loading shows the 2D graph rather
3099
+ * than an empty box.
3100
+ */
3101
+ setGlRequest(request) {
3102
+ if (this._glRequest === request)
3103
+ return;
3104
+ this._glRequest = request;
3105
+ // whichever renderer now draws, it draws the whole pane
3106
+ this._repaint('props');
3107
+ }
3108
+ /**
3109
+ * One GL frame's worth of scene, in the two layers the renderer keeps
3110
+ * apart (`./gl/renderer.ts`): the **world**, rebuilt only when it changed
3111
+ * since the key the surface last drew, and the **overlay**, every frame.
2459
3112
  *
2460
- * Two decisions with reasons:
3113
+ * The world is built at a pinned origin and culled to an *overscan* — the
3114
+ * view grown by a pane in every direction — so a pan inside it changes
3115
+ * nothing but `offset`. When the view leaves the overscan, or the zoom
3116
+ * moves, the overscan is re-centred and the world rebuilt; at every other
3117
+ * frame of a pan the world comes back `null` and the renderer draws the
3118
+ * buffers already on the GPU.
2461
3119
  *
2462
- * - **Integral device steps only.** The tile is a pixmap, so its size is
2463
- * whole pixels; at a fractional step the pattern would drift against
2464
- * the graph's own coordinates — against `snapToGrid`, against node
2465
- * positions — by a fraction per tile, and a grid that slides under the
2466
- * content it grids is worse than a slower exact one.
2467
- * - **The phase lives in the tile, not in a picture transform.** An
2468
- * untransformed repeat is anchored to the window origin, so the grid's
2469
- * alignment is baked in by drawing the mark at `origin mod tile` — and
2470
- * re-baking when the origin moves. A `setTransform` translate says the
2471
- * same thing in one request, but a *transformed* repeat forfeits the
2472
- * server's untransformed fast path — on the in-process test server
2473
- * that was ~180 ms a frame of per-pixel arithmetic, and re-rendering a
2474
- * 24px tile is a handful of requests on any server.
3120
+ * A zoom *gesture* is drawn the same way: while the zoom keeps moving,
3121
+ * each step draws the world on the GPU magnified by `zoom` — the view's
3122
+ * zoom over the one it was built at — and the world is rebuilt at the
3123
+ * zoom the gesture stopped at, `GL_ZOOM_REST_MS` after its last step.
3124
+ * Rebuilding every step was the whole of a zoom's cost: a scene built,
3125
+ * packed and uploaded per step, 19 ms of it on a 300-node graph, where a
3126
+ * pan step is a uniform. What a magnified world gets wrong until then is
3127
+ * what a zoom does not scale linearly — a label's raster, the hairlines
3128
+ * with a floor of a pixel, the detail that appears past a zoom — which is
3129
+ * what every map does during a pinch. A single jump — a control's button,
3130
+ * `fitView`, `setViewport` — is not a stream and rebuilds at once, and so
3131
+ * does a step that would magnify one build by more than `GL_ZOOM_SPAN`
3132
+ * or show more of the graph than the overscan holds.
3133
+ *
3134
+ * The key is everything the world is a function of that this element can
3135
+ * name: its version (every repaint but a view change moves it), the
3136
+ * overscan, the zoom it was built at, and the palette and face — the two
3137
+ * a theme change moves, which core announces to the window rather than
3138
+ * to this element.
3139
+ *
3140
+ * Everything a 2D paint does around its drawing happens here too: the
3141
+ * pending fit, the dash timer, the first scene announcement, the bodies.
2475
3142
  */
2476
- _paintGridPattern(painter, options, step, color, region) {
2477
- // The tile is a pixmap on the panel's grid, so the pitch has to be a
2478
- // whole number of *device* pixels, and the phase is where the viewport
2479
- // origin lands on that grid.
3143
+ glFrame(lastKey, lastLifted = null) {
3144
+ if (!this._visible())
3145
+ return null;
3146
+ this._frameTick();
3147
+ this._sync();
3148
+ this._painting = true;
3149
+ this._frameClip = null;
3150
+ if (this._fitPending) {
3151
+ this._fitPending = false;
3152
+ this._fit(this._prop('fitViewOptions'));
3153
+ }
3154
+ const v = this._viewport();
3155
+ const pane = this._pane();
3156
+ const palette = this._palette();
3157
+ const lift = this._lifted();
3158
+ const keyOf = () => `${this._worldVersion}|${this._worldCullId}|${this._worldZoom}|` +
3159
+ `${this._fontSeen}|${JSON.stringify(palette)}|${this._liftGen}`;
3160
+ let key = keyOf();
3161
+ // The view in the world's own coordinates — graph × the zoom it was
3162
+ // built at, the pan left out: `offset` puts the pan back, and `zoom`
3163
+ // the rest of the zoom.
3164
+ const zoom = v.zoom / this._worldZoom;
3165
+ const view = {
3166
+ x: -v.x / zoom,
3167
+ y: -v.y / zoom,
3168
+ width: pane.width / zoom,
3169
+ height: pane.height / zoom,
3170
+ };
3171
+ const cull = this._worldCull;
3172
+ const inside = cull != null &&
3173
+ view.x >= cull.x &&
3174
+ view.y >= cull.y &&
3175
+ view.x + view.width <= cull.x + cull.width &&
3176
+ view.y + view.height <= cull.y + cull.height;
3177
+ const scaled = inside &&
3178
+ zoom !== 1 &&
3179
+ key === lastKey &&
3180
+ this._zoomStream &&
3181
+ now() - this._zoomAt < GL_ZOOM_REST_MS &&
3182
+ zoom <= GL_ZOOM_SPAN &&
3183
+ zoom >= 1 / GL_ZOOM_SPAN;
3184
+ if (scaled) {
3185
+ this._restZoom();
3186
+ }
3187
+ else if (!inside || zoom !== 1) {
3188
+ this._worldCull = {
3189
+ x: -v.x - pane.width,
3190
+ y: -v.y - pane.height,
3191
+ width: pane.width * 3,
3192
+ height: pane.height * 3,
3193
+ };
3194
+ this._worldZoom = v.zoom;
3195
+ this._worldCullId++;
3196
+ key = keyOf();
3197
+ }
3198
+ let world = null;
3199
+ if (key !== lastKey) {
3200
+ world = buildScene(this._sceneInput(palette, {
3201
+ world: this._worldCull,
3202
+ lift: lift ? { ids: lift, only: false } : undefined,
3203
+ }));
3204
+ this._worldAnimated = world.animated;
3205
+ }
3206
+ // The nodes a drag moves, and their edges, on their own: a step
3207
+ // rebuilds them and not the world (`_liftVersion`).
3208
+ let lifted;
3209
+ const liftedKey = lift ? `${this._liftVersion}|${key}` : null;
3210
+ if (lift && liftedKey !== lastLifted) {
3211
+ lifted = buildScene(this._sceneInput(palette, {
3212
+ world: this._worldCull,
3213
+ lift: { ids: lift, only: true },
3214
+ }));
3215
+ this._liftedAnimated = lifted.animated;
3216
+ }
3217
+ else if (!lift && lastLifted != null) {
3218
+ lifted = null;
3219
+ this._liftedAnimated = false;
3220
+ }
3221
+ if (world || lifted !== undefined) {
3222
+ // The dash timer lives as long as an animated edge is drawn; its
3223
+ // ticks move the phase, a uniform.
3224
+ if (this._worldAnimated || this._liftedAnimated)
3225
+ this._startAnimation();
3226
+ else
3227
+ this._stopAnimation();
3228
+ }
3229
+ const overlay = buildScene(this._sceneInput(palette, 'overlay'));
3230
+ this._painting = false;
3231
+ if (!this._sceneAnnounced) {
3232
+ this._sceneAnnounced = true;
3233
+ this.notifyA11ySceneChanged();
3234
+ }
3235
+ this._emitBodies();
3236
+ return {
3237
+ world,
3238
+ key,
3239
+ lifted,
3240
+ liftedKey,
3241
+ offset: { x: pane.x + v.x, y: pane.y + v.y },
3242
+ zoom: scaled ? zoom : 1,
3243
+ overlay,
3244
+ // the scene bakes `-phase * zoom` into a marching edge; the GPU is
3245
+ // handed the same number as a uniform
3246
+ phase: -this._dashPhase * v.zoom,
3247
+ moving: this._zoomMoving(),
3248
+ };
3249
+ }
3250
+ /** The frame that rebuilds the GL world once a zoom gesture stops — asked
3251
+ * for `GL_ZOOM_REST_MS` after its last step, however many steps the
3252
+ * timer outlived. */
3253
+ _restZoom() {
3254
+ if (this._zoomRest != null)
3255
+ return;
3256
+ const wait = this._zoomAt + GL_ZOOM_REST_MS - now();
3257
+ this._zoomRest = timers.setTimeout?.(() => {
3258
+ this._zoomRest = null;
3259
+ if (now() - this._zoomAt < GL_ZOOM_REST_MS)
3260
+ this._restZoom();
3261
+ else if (this._gl)
3262
+ this._glRequest?.();
3263
+ else if (this._textApproximated) {
3264
+ this._dropZoomShot();
3265
+ // …and in 2D, the labels the gesture drew scaled, set at their
3266
+ // own sizes
3267
+ this._textApproximated = false;
3268
+ this._repaint('content');
3269
+ }
3270
+ }, Math.max(0, wait) + 1);
3271
+ }
3272
+ /** A whole paint of the graph took `ms` (`_graphPaints`). */
3273
+ _noteGraphPaint(ms) {
3274
+ this._graphPaints.push(ms);
3275
+ if (this._graphPaints.length > GRAPH_PAINTS_KEPT)
3276
+ this._graphPaints.shift();
3277
+ }
3278
+ /** The median of the graph's last whole paints — the lower of the two
3279
+ * middle ones for an even count — or zero before the first. */
3280
+ _graphPaintMs() {
3281
+ const paints = this._graphPaints;
3282
+ if (paints.length === 0)
3283
+ return 0;
3284
+ const sorted = [...paints].sort((a, b) => a - b);
3285
+ return sorted[(sorted.length - 1) >> 1];
3286
+ }
3287
+ /** Whether a pass over `clip` painted the whole pane — what a zoom step
3288
+ * paints, and the only pass whose cost says what one would. */
3289
+ /** Whether a pass (`paintDamage()`, device pixels) repaints every pixel
3290
+ * of the pane, unlike {@link _wholePane}'s nine tenths. */
3291
+ _coversPane(clip) {
3292
+ if (clip === null)
3293
+ return true;
3294
+ const pane = this._pane();
3295
+ const c = this._logical(clip);
3296
+ const slack = 0.5 / this._scale;
3297
+ return (c.x <= pane.x + slack &&
3298
+ c.y <= pane.y + slack &&
3299
+ c.x + c.width >= pane.x + pane.width - slack &&
3300
+ c.y + c.height >= pane.y + pane.height - slack);
3301
+ }
3302
+ _wholePane(clip) {
3303
+ if (clip === null)
3304
+ return true;
3305
+ const pane = this._pane();
3306
+ const s = this._scale;
3307
+ return clip.width * clip.height >= 0.9 * pane.width * s * pane.height * s;
3308
+ }
3309
+ /**
3310
+ * The graph painted once onto a surface of its own for the zoom gesture
3311
+ * under way — made at the gesture's first step that finds none — or null
3312
+ * where there is no surface to paint on.
3313
+ */
3314
+ _zoomShotFor(palette, approximateText) {
3315
+ const held = this._zoomShot;
3316
+ if (held) {
3317
+ // Kept while it is sharp enough and covers enough: magnified past
3318
+ // `GL_ZOOM_SPAN` it is soft, and a zoom out uncovers a ring the step
3319
+ // paints live, which past `ZOOM_SHOT_LIVE` of the pane costs what a
3320
+ // new picture does.
3321
+ const k = this._viewport().zoom / held.viewport.zoom;
3322
+ const pane = this._pane();
3323
+ const live = this._uncovered(held).reduce((sum, r) => sum + r.width * r.height, 0) /
3324
+ Math.max(1, pane.width * pane.height);
3325
+ if (k <= GL_ZOOM_SPAN && live <= ZOOM_SHOT_LIVE)
3326
+ return held;
3327
+ this._dropZoomShot();
3328
+ }
3329
+ const ctor = ntk.Surface;
3330
+ if (typeof ctor !== 'function')
3331
+ return null;
3332
+ const pane = this._pane();
3333
+ const scale = this._scale;
3334
+ const width = Math.max(1, Math.ceil(pane.width * scale));
3335
+ const height = Math.max(1, Math.ceil(pane.height * scale));
3336
+ let surface;
3337
+ try {
3338
+ surface = new ctor(this.app, { width, height, format: 'argb32' });
3339
+ }
3340
+ catch {
3341
+ return null;
3342
+ }
3343
+ const sctx = surface.getContext('2d');
3344
+ // its labels drawn as a step inside the gesture draws them — from the
3345
+ // sizes they have, where the window's context scales text — so the one
3346
+ // paint of the gesture shapes nothing the gesture would not
3347
+ const painter = sctx
3348
+ ? createPainter(sctx, { ...this._textOptions(), approximateText })
3349
+ : null;
3350
+ if (!sctx || !painter || typeof sctx.translate !== 'function') {
3351
+ surface.destroy?.();
3352
+ return null;
3353
+ }
3354
+ // The surface's corner is the pane's, and the whole of the pane is in
3355
+ // it whatever this pass's damage, since every step after composites it.
3356
+ // The ground too: a grid at the fractional pitch a zoom passes through
3357
+ // is thousands of runs, 7 ms of Direct2D a step over the stress
3358
+ // lattice's pane once the graph was a picture. Drawn as runs here —
3359
+ // the tile is a pattern on the window's context, phased in its pixels,
3360
+ // and a zoom's pitch is a fraction of a pixel almost always.
3361
+ sctx.translate(-pane.x * scale, -pane.y * scale);
3362
+ const building = now();
3363
+ const whole = buildScene({ ...this._sceneInput(palette), clip: null });
3364
+ const built = now();
3365
+ paintGround(painter, whole);
3366
+ const grounded = now();
3367
+ paintGraph(painter, whole);
3368
+ // What a step would cost live: the scene and the graph. Not the ground,
3369
+ // which is runs here and a tile on the window — on X11 it made a
3370
+ // picture's paint half as dear again as a step, and kept the picture of
3371
+ // a graph that did not need one.
3372
+ this._noteGraphPaint(built - building + (now() - grounded));
3373
+ this._zoomShot = { surface, viewport: this._viewport(), width, height };
3374
+ return this._zoomShot;
3375
+ }
3376
+ /** Where the gesture's picture lands at the viewport of the moment, in
3377
+ * the pane's logical pixels: a world point drawn at `w·z₀ + v₀` is at
3378
+ * `w·z + v`, so the picture is scaled by `z / z₀` about the pane's corner
3379
+ * and moved by `v − v₀·z/z₀`. */
3380
+ _shotRect(shot) {
3381
+ const v = this._viewport();
3382
+ const v0 = shot.viewport;
3383
+ const k = v.zoom / v0.zoom;
3384
+ const pane = this._pane();
3385
+ const s = this._scale;
3386
+ return {
3387
+ x: pane.x + v.x - v0.x * k,
3388
+ y: pane.y + v.y - v0.y * k,
3389
+ width: (shot.width / s) * k,
3390
+ height: (shot.height / s) * k,
3391
+ };
3392
+ }
3393
+ _compositeShot(ctx, shot) {
3394
+ const draw = ctx.drawImage;
3395
+ if (typeof draw !== 'function')
3396
+ return;
3397
+ const to = this._shotRect(shot);
3398
+ const s = this._scale;
3399
+ draw.call(ctx, shot.surface, 0, 0, shot.width, shot.height, to.x * s, to.y * s, to.width * s, to.height * s);
3400
+ }
3401
+ /**
3402
+ * The pane this pass reaches that the gesture's picture does not: nothing
3403
+ * while zooming in about a point in the pane, a ring round it zooming out,
3404
+ * a side after a pan. Up to four bands — above, below, and either side
3405
+ * between them — each reaching a pixel under the picture's edge, which
3406
+ * its filtering leaves half there.
3407
+ */
3408
+ _uncovered(shot) {
3409
+ const pane = this._frameClip
3410
+ ? intersectRects(this._pane(), this._frameClip)
3411
+ : this._pane();
3412
+ if (!pane)
3413
+ return [];
3414
+ const inner = inflateRect(this._shotRect(shot), -1);
3415
+ const x0 = Math.max(pane.x, inner.x);
3416
+ const y0 = Math.max(pane.y, inner.y);
3417
+ const x1 = Math.min(pane.x + pane.width, inner.x + inner.width);
3418
+ const y1 = Math.min(pane.y + pane.height, inner.y + inner.height);
3419
+ if (x1 <= x0 || y1 <= y0)
3420
+ return [pane];
3421
+ const right = pane.x + pane.width;
3422
+ const bottom = pane.y + pane.height;
3423
+ const bands = [];
3424
+ if (y0 > pane.y) {
3425
+ bands.push({
3426
+ x: pane.x,
3427
+ y: pane.y,
3428
+ width: pane.width,
3429
+ height: y0 - pane.y,
3430
+ });
3431
+ }
3432
+ if (bottom > y1) {
3433
+ bands.push({ x: pane.x, y: y1, width: pane.width, height: bottom - y1 });
3434
+ }
3435
+ if (x0 > pane.x) {
3436
+ bands.push({ x: pane.x, y: y0, width: x0 - pane.x, height: y1 - y0 });
3437
+ }
3438
+ if (right > x1) {
3439
+ bands.push({ x: x1, y: y0, width: right - x1, height: y1 - y0 });
3440
+ }
3441
+ return bands;
3442
+ }
3443
+ _dropZoomShot() {
3444
+ this._zoomShot?.surface.destroy?.();
3445
+ this._zoomShot = null;
3446
+ }
3447
+ /**
3448
+ * The pictures a 2D drag copies from — made on the first pass of the
3449
+ * gesture that has its nodes lifted, and kept while the graph under them
3450
+ * and the view hold still. A drag step repainted the box round the moved
3451
+ * node and every edge on it, and an edge is as long as it is: the stress
3452
+ * lattice sends its last rows' edges back to its first nodes, so dragging
3453
+ * one repainted the window a step, every card and edge in it, at 46-57
3454
+ * fps on Cocoa. Cutting the damage finer did not help — core merges a
3455
+ * frame's rects down to four, and a long diagonal's pieces merge back to
3456
+ * most of the pane. Copying what did not move is what does.
3457
+ *
3458
+ * Null where there is no offscreen surface, and for a gesture whose view
3459
+ * moved while it held pictures: it paints live from then on, rather than
3460
+ * paint the graph twice a step.
3461
+ */
3462
+ _liftShotFor(palette, lift) {
3463
+ const gesture = this._liftFor;
3464
+ if (!gesture || this._liftShotRefused === gesture)
3465
+ return null;
3466
+ const v = this._viewport();
3467
+ const pane = this._pane();
3468
+ const scale = this._scale;
3469
+ const width = Math.max(1, Math.ceil(pane.width * scale));
3470
+ const height = Math.max(1, Math.ceil(pane.height * scale));
3471
+ const held = this._liftShot;
3472
+ if (held) {
3473
+ const still = held.viewport.x === v.x &&
3474
+ held.viewport.y === v.y &&
3475
+ held.viewport.zoom === v.zoom;
3476
+ if (held.gesture === gesture &&
3477
+ still &&
3478
+ held.version === this._worldVersion &&
3479
+ held.width === width &&
3480
+ held.height === height) {
3481
+ return held;
3482
+ }
3483
+ this._dropLiftShot();
3484
+ if (held.gesture === gesture && !still) {
3485
+ this._liftShotRefused = gesture;
3486
+ return null;
3487
+ }
3488
+ }
3489
+ const ctor = ntk.Surface;
3490
+ if (typeof ctor !== 'function')
3491
+ return null;
3492
+ const make = () => {
3493
+ let surface;
3494
+ try {
3495
+ surface = new ctor(this.app, { width, height, format: 'argb32' });
3496
+ }
3497
+ catch {
3498
+ return null;
3499
+ }
3500
+ const sctx = surface.getContext('2d');
3501
+ const painter = sctx ? createPainter(sctx, this._textOptions()) : null;
3502
+ if (!sctx || !painter || typeof sctx.translate !== 'function') {
3503
+ surface.destroy?.();
3504
+ return null;
3505
+ }
3506
+ // a clear ground: the cards' picture goes over the edges
3507
+ sctx.clearRect?.(0, 0, width, height);
3508
+ sctx.translate(-pane.x * scale, -pane.y * scale);
3509
+ return { surface, painter };
3510
+ };
3511
+ const under = make();
3512
+ const over = under ? make() : null;
3513
+ if (!under || !over) {
3514
+ under?.surface.destroy?.();
3515
+ return null;
3516
+ }
3517
+ const rest = buildScene({
3518
+ ...this._sceneInput(palette, { lift: { ids: lift, only: false } }),
3519
+ clip: null,
3520
+ });
3521
+ paintGround(under.painter, rest);
3522
+ paintGraphEdges(under.painter, rest);
3523
+ paintGraphNodes(over.painter, rest);
3524
+ this._liftShot = {
3525
+ under: under.surface,
3526
+ over: over.surface,
3527
+ cards: rest.nodes.map((item) => inflateRect(item.rect, CULL_MARGIN)),
3528
+ gesture,
3529
+ version: this._worldVersion,
3530
+ viewport: { ...v },
3531
+ width,
3532
+ height,
3533
+ };
3534
+ return this._liftShot;
3535
+ }
3536
+ /**
3537
+ * One of a drag's pictures, copied into this pass's rect of the pane —
3538
+ * the whole pane on a full pass. The ground's picture is opaque and is
3539
+ * *copied*, which the server does as a copy rather than a blend (`copy`
3540
+ * is XRender's `Src`, and a memcpy on Cocoa); the cards' picture is
3541
+ * blended in only where a card is (`only`). On XQuartz the two blends of
3542
+ * the step's box — as big as the moved node's longest edge — were most
3543
+ * of what a drag cost the server.
3544
+ */
3545
+ _compositeLift(ctx, shot, surface, only) {
3546
+ const c = ctx;
3547
+ if (typeof c.drawImage !== 'function')
3548
+ return;
3549
+ const pane = this._pane();
3550
+ const rect = this._frameClip ? intersectRects(pane, this._frameClip) : pane;
3551
+ if (!rect)
3552
+ return;
3553
+ if (only) {
3554
+ for (const card of only) {
3555
+ const part = intersectRects(rect, card);
3556
+ if (part)
3557
+ this._copyShotRect(c, shot, surface, part);
3558
+ }
3559
+ return;
3560
+ }
3561
+ const before = c.globalCompositeOperation;
3562
+ c.globalCompositeOperation = 'copy';
3563
+ try {
3564
+ this._copyShotRect(c, shot, surface, rect);
3565
+ }
3566
+ finally {
3567
+ c.globalCompositeOperation = before ?? 'source-over';
3568
+ }
3569
+ }
3570
+ /** A rect of the pane, logical pixels, drawn from the same rect of a
3571
+ * picture of it — on whole device pixels, so nothing is resampled. */
3572
+ _copyShotRect(ctx, shot, surface, rect) {
3573
+ const pane = this._pane();
3574
+ const s = this._scale;
3575
+ const ox = pane.x * s;
3576
+ const oy = pane.y * s;
3577
+ const x0 = Math.max(0, Math.floor(rect.x * s - ox));
3578
+ const y0 = Math.max(0, Math.floor(rect.y * s - oy));
3579
+ const x1 = Math.min(shot.width, Math.ceil((rect.x + rect.width) * s - ox));
3580
+ const y1 = Math.min(shot.height, Math.ceil((rect.y + rect.height) * s - oy));
3581
+ if (x1 <= x0 || y1 <= y0)
3582
+ return;
3583
+ const w = x1 - x0;
3584
+ const h = y1 - y0;
3585
+ ctx.drawImage(surface, x0, y0, w, h, ox + x0, oy + y0, w, h);
3586
+ }
3587
+ _dropLiftShot() {
3588
+ this._liftShot?.under.destroy?.();
3589
+ this._liftShot?.over.destroy?.();
3590
+ this._liftShot = null;
3591
+ }
3592
+ /** Whether a zoom gesture is moving: a stream of zoom steps, the last of
3593
+ * them under `GL_ZOOM_REST_MS` ago. A single step — a button, `fitView`,
3594
+ * an app's `setViewport` — is not one. */
3595
+ _zoomMoving() {
3596
+ return this._zoomStream && now() - this._zoomAt < GL_ZOOM_REST_MS;
3597
+ }
3598
+ /** Whether this pass's damage reaches the minimap's corner at all —
3599
+ * answered from the panel's box alone, without resolving the map. */
3600
+ _miniMapReached() {
3601
+ const corner = this._miniMapCorner();
3602
+ if (!corner)
3603
+ return false;
3604
+ const clip = this._frameClip;
3605
+ return !clip || rectsOverlap(corner, clip);
3606
+ }
3607
+ /** The minimap's panel, in logical window pixels, or null without one. */
3608
+ _miniMapCorner() {
3609
+ const options = this._miniMapOptions();
3610
+ if (!options)
3611
+ return null;
3612
+ return this._corner(options.position, options.width ?? MINIMAP_W, options.height ?? MINIMAP_H, 'bottom-right');
3613
+ }
3614
+ /**
3615
+ * Everything `buildScene` reads, for one of three uses:
3616
+ *
3617
+ * - `'all'` — the 2D renderer's pass: the pane as it is, culled to this
3618
+ * pass's damage.
3619
+ * - `{ world }` — the GL graph: built at a **pinned origin** (the viewport's
3620
+ * translation and the pane's origin both left out, so a pan does not
3621
+ * change a vertex) and culled to the overscan `world` rather than the
3622
+ * pane, with none of the pane's furniture.
3623
+ * - `'overlay'` — the GL furniture: the pane's background, grid, selection,
3624
+ * minimap and controls, and no graph at all.
3625
+ */
3626
+ /**
3627
+ * The order nodes are drawn in, bottom to top: `zIndex`, then selection,
3628
+ * then declaration (`_order`), with a node being dragged lifted over all
3629
+ * of them. The one order both halves of a node follow — its card in the
3630
+ * scene, its mounted body in `_emitBodies` — so a body is stacked over
3631
+ * the bodies of the cards under its own, and under the ones above.
3632
+ */
3633
+ _paintOrder() {
3634
+ const dragging = this._dragTo;
3635
+ if (!dragging || dragging.size === 0)
3636
+ return this._order;
3637
+ const order = [];
3638
+ const lifted = [];
3639
+ for (const entry of this._order) {
3640
+ if (dragging.has(entry.node.id))
3641
+ lifted.push(entry);
3642
+ else
3643
+ order.push(entry);
3644
+ }
3645
+ return order.concat(lifted);
3646
+ }
3647
+ _sceneInput(palette, layer = 'all',
3648
+ /** The panels even while `<Flow>` paints them itself — for that paint. */
3649
+ panels = false) {
3650
+ // One card, for `paintCard`, which hands in the node itself: nothing
3651
+ // about the rest of the graph is read, where the whole input — every
3652
+ // node's source twice over, the minimap's walk for its bounds — was most
3653
+ // of what a card cost to paint, and a zoom repaints every one of them.
3654
+ if (layer === 'card') {
3655
+ const gesture = this._gesture;
3656
+ return {
3657
+ viewport: this._viewport(),
3658
+ pane: this._pane(),
3659
+ clip: null,
3660
+ palette,
3661
+ background: normalizeBackground(undefined),
3662
+ nodes: [],
3663
+ all: [],
3664
+ edges: [],
3665
+ dashPhase: this._dashPhase,
3666
+ hover: this._hover,
3667
+ connection: gesture?.kind === 'connect' ? gesture : null,
3668
+ selection: null,
3669
+ miniMap: null,
3670
+ controls: [],
3671
+ scale: this._scale,
3672
+ measure: this._measureBox,
3673
+ cache: this._sceneCache,
3674
+ batch: this._batch,
3675
+ };
3676
+ }
3677
+ const order = this._paintOrder();
3678
+ const gesture = this._gesture;
3679
+ // The panel cull comes before `_miniMap()`, which walks every node to
3680
+ // find the graph's bounds — during a drag that walk per pass would cost
3681
+ // more than the panel it skips. The dot for a mid-drag node goes stale
3682
+ // until the release repaints in full; that is the trade, and it is
3683
+ // deliberate.
3684
+ const world = typeof layer === 'object' ? (layer.world ?? null) : null;
3685
+ const lift = typeof layer === 'object' ? layer.lift : undefined;
3686
+ const overlay = layer === 'overlay';
3687
+ // the furniture is the overlay's: a lifted layer or the graph under it
3688
+ // has none of its own
3689
+ const map = !world && !lift && this._miniMapReached() ? this._miniMap() : null;
3690
+ // the fit the minimap on screen is painted with, from this pass on
3691
+ if (map && !panels && this._painting && this._panelCanvases.length === 0) {
3692
+ this._miniMapShown = map;
3693
+ }
3694
+ const viewport = this._viewport();
3695
+ const pane = this._pane();
3696
+ // An edge is the lifted layer's when either end is lifted.
3697
+ const edges = lift
3698
+ ? this._edges.filter((e) => (lift.ids.has(e.source) || lift.ids.has(e.target)) === lift.only)
3699
+ : this._edges;
3700
+ let all;
3701
+ if (lift?.only) {
3702
+ // the lifted nodes and the far ends of their edges: all the lifted
3703
+ // layer routes to, where every node's source was most of a step
3704
+ const ids = new Set(lift.ids);
3705
+ for (const e of edges) {
3706
+ ids.add(e.source);
3707
+ ids.add(e.target);
3708
+ }
3709
+ all = [];
3710
+ for (const id of ids) {
3711
+ const entry = this._byId.get(id);
3712
+ if (entry)
3713
+ all.push(this._sourceOf(entry));
3714
+ }
3715
+ }
3716
+ else if (overlay && !map) {
3717
+ all = [];
3718
+ }
3719
+ else if (layer === 'all' && this._painting) {
3720
+ all = this._frameLists(order).all;
3721
+ }
3722
+ else {
3723
+ all = this._entries.map((entry) => this._sourceOf(entry));
3724
+ }
3725
+ return {
3726
+ viewport: world ? { x: 0, y: 0, zoom: viewport.zoom } : viewport,
3727
+ pane: world
3728
+ ? { x: 0, y: 0, width: pane.width, height: pane.height }
3729
+ : pane,
3730
+ clip: world || overlay ? null : this._frameClip,
3731
+ cull: world,
3732
+ palette,
3733
+ paneBackground: this.style.backgroundColor,
3734
+ background: normalizeBackground(this.props.background),
3735
+ // a card `<Flow>` paints in the bodies' layer is not painted twice
3736
+ nodes: overlay
3737
+ ? []
3738
+ : layer === 'all' && this._painting
3739
+ ? this._frameLists(order).nodes
3740
+ : order
3741
+ .filter((entry) => !this._cardInLayer(entry.node.id) &&
3742
+ (!lift || lift.ids.has(entry.node.id) === lift.only))
3743
+ .map((entry) => this._sourceOf(entry)),
3744
+ all,
3745
+ edges: overlay ? [] : edges,
3746
+ dashPhase: this._dashPhase,
3747
+ hover: this._hover,
3748
+ connection: !overlay && gesture?.kind === 'connect' ? gesture : null,
3749
+ selection: !world && gesture?.kind === 'select' ? selectBoxRect(gesture) : null,
3750
+ miniMap: map && (panels || this._panelCanvases.length === 0)
3751
+ ? {
3752
+ panel: map.panel,
3753
+ bounds: map.bounds,
3754
+ scale: map.scale,
3755
+ nodeColor: map.options.nodeColor,
3756
+ maskColor: map.options.maskColor,
3757
+ // the pane's own pass: the part of the minimap it repaints
3758
+ reach: panels ? null : this._frameClip,
3759
+ }
3760
+ : null,
3761
+ controls: world || lift || (!panels && this._panelCanvases.length > 0)
3762
+ ? []
3763
+ : this._controlButtons(),
3764
+ scale: this._scale,
3765
+ measure: this._measureBox,
3766
+ cache: this._sceneCache,
3767
+ batch: this._batch,
3768
+ };
3769
+ }
3770
+ /**
3771
+ * Where every mounted node body belongs, and where the graph's origin is.
3772
+ *
3773
+ * The two are sent apart because they change apart. A body's rect is
3774
+ * relative to the **graph's origin on screen** — its position times the
3775
+ * zoom — which a pan does not move; the origin is the viewport's
3776
+ * translation, which is all a pan moves. So a pan re-sends the *same*
3777
+ * bodies array with a new origin, `<Flow>` moves the one box they are
3778
+ * laid out in, and React re-renders none of them: the per-step commit of
3779
+ * every mounted body's position is what held a pan over 48 of them to 57
3780
+ * frames a second. A drag, a zoom, a selection or a body entering the pane
3781
+ * sends a new array, as before.
3782
+ */
3783
+ _emitBodies() {
3784
+ const notify = this._prop('onNodeBodies');
3785
+ if (!notify)
3786
+ return;
3787
+ const v = this._viewport();
3788
+ if (this._holdBodies(v.zoom)) {
3789
+ if (!this._bodiesHeld) {
3790
+ // The 2D pane draws the cards again from this frame, not from the
3791
+ // commit that hides the bodies. The GL world never stopped drawing
3792
+ // them (`_cardInLayer`).
3793
+ this._bodiesHeld = true;
3794
+ notify(this._bodies, this._gestureSync || this._handleSync, this._bodiesOrigin, true);
3795
+ }
3796
+ return;
3797
+ }
3798
+ const pane = this._pane();
3799
+ // Exact, and every rect below relative to it and free of the pan: a
3800
+ // card's offset from the graph's origin is its graph position times the
3801
+ // zoom, so a pan re-sends the same array and moves only the origin.
3802
+ // Nothing here is rounded — `<Flow>` puts the boxes on the device-pixel
3803
+ // grid, once. Rounded here as well, to logical pixels against a rounded
3804
+ // origin, a card's offset flipped by a pixel as the pan's fraction
3805
+ // cycled against the device grid `_screenRect` snaps to: the array was
3806
+ // new on most steps of a pan, and at 1.25× the layer changed size with
3807
+ // it — every body repainted, where core would have moved them.
3808
+ const origin = { x: v.x, y: v.y };
3809
+ const sv = screenViewport(v, pane);
3810
+ const was = this._bodies;
3811
+ const bodies = [];
3812
+ let changed = false;
3813
+ for (const entry of this._paintOrder()) {
3814
+ if (entry.node.hidden || !this._mounted(entry))
3815
+ continue;
3816
+ if (!rectsOverlap(this._screenRect(entry, sv), pane))
3817
+ continue;
3818
+ const graph = this.rectOf(entry);
3819
+ const x = graph.x * v.zoom;
3820
+ const y = graph.y * v.zoom;
3821
+ const cardWidth = graph.width * v.zoom;
3822
+ const cardHeight = graph.height * v.zoom;
3823
+ const header = this._headerHeight(entry) * v.zoom;
3824
+ const inset = NODE_BODY_INSET * v.zoom;
3825
+ const width = cardWidth - inset * 2;
3826
+ const height = cardHeight - header - inset;
3827
+ if (width <= 1 || height <= 1)
3828
+ continue;
3829
+ const id = entry.node.id;
3830
+ const hovered = this._hover.nodeId === id;
3831
+ const selected = entry.node.selected ?? false;
3832
+ // The zoom counts as well as the rect it produced: it is the body's
3833
+ // subtree scale, and two zooms a hair apart can leave a box where it
3834
+ // was while the text inside it should have moved.
3835
+ const old = this._bodiesById.get(id);
3836
+ const body = old &&
3837
+ old.zoom === v.zoom &&
3838
+ old.selected === selected &&
3839
+ old.hovered === hovered &&
3840
+ old.card.x === x &&
3841
+ old.card.y === y &&
3842
+ old.card.width === cardWidth &&
3843
+ old.card.height === cardHeight &&
3844
+ old.height === height
3845
+ ? old
3846
+ : {
3847
+ id,
3848
+ card: { x, y, width: cardWidth, height: cardHeight },
3849
+ hovered,
3850
+ // logical, and relative to the graph's origin on screen — the
3851
+ // box they are laid out in sits at `origin` in the pane
3852
+ x: x + inset,
3853
+ y: y + header,
3854
+ width,
3855
+ height,
3856
+ zoom: v.zoom,
3857
+ selected,
3858
+ };
3859
+ if (body !== was[bodies.length])
3860
+ changed = true;
3861
+ bodies.push(body);
3862
+ }
3863
+ if (bodies.length !== was.length)
3864
+ changed = true;
3865
+ // An origin with no bodies laid out at it is nobody's news: a pan over a
3866
+ // graph whose node types mount bodies, with none on screen, sent one per
3867
+ // step, and `<Flow>` re-rendered for it — its panels' canvases with it.
3868
+ // The origin is still kept, and goes out with the first body to arrive.
3869
+ const moved = was.length > 0 &&
3870
+ (origin.x !== this._bodiesOrigin.x || origin.y !== this._bodiesOrigin.y);
3871
+ const released = this._bodiesHeld;
3872
+ if (!changed && !moved && !released)
3873
+ return;
3874
+ if (changed) {
3875
+ this._bodies = bodies;
3876
+ this._bodiesById = new Map(bodies.map((body) => [body.id, body]));
3877
+ }
3878
+ this._bodiesOrigin = origin;
3879
+ this._bodiesHeld = false;
3880
+ notify(this._bodies, this._gestureSync || this._handleSync, origin, false);
3881
+ }
3882
+ /**
3883
+ * Whether mounted bodies sit this zoom out.
3884
+ *
3885
+ * A zoom changes every body's scale, and a scale is not a transform: each
3886
+ * body is styled, laid out, its text shaped and its pixels painted again,
3887
+ * at the new size, per step. That is about a millisecond a body on this
3888
+ * class of machine — 42 bodies held a zoom to 15 frames a second where
3889
+ * the graph alone costs 13 ms a step. So each step of a zoom *gesture*
3890
+ * predicts what re-scaling the bodies on screen would add, and when that
3891
+ * is over `BODY_BUDGET_MS` they sit the gesture out: still mounted — no
3892
+ * state is lost — but hidden (`display: 'none'`) and untouched, while the
3893
+ * cards, their labels and the edges carry it. Once the zoom has held still
3894
+ * for
3895
+ * `BODY_ZOOM_REST_MS` they come back, laid out once at the new scale. A
3896
+ * handful of bodies fits the budget and zooms live.
3897
+ *
3898
+ * The prediction is bodies × what one costs, measured from the pane's own
3899
+ * frames (`_frameTick`) — so a slower machine holds sooner. Once held, a
3900
+ * gesture stays held until it rests: bodies blinking in and out as the
3901
+ * count crosses the line would be worse than either.
3902
+ *
3903
+ * Only a stream counts: a gesture's steps — the wheel, a pinch, the keys
3904
+ * (`_gestureSync`) — or an animation's, `setViewport` a frame at a time,
3905
+ * each zoom under `GL_ZOOM_REST_MS` after the one before (`_zoomStream`,
3906
+ * the rule the zoom's picture and its scaled labels already follow). A
3907
+ * single programmatic jump — `fitView`, `setViewport`, a control's button
3908
+ * — applies at once, so an app that sets the viewport and reads the result
3909
+ * sees it; the pane never animates a viewport of its own. An animation
3910
+ * that stepped every body at every frame held a zoom over the stress
3911
+ * example's charts to 10 frames a second.
3912
+ */
3913
+ _holdBodies(zoom) {
3914
+ if (zoom !== this._seenZoom) {
3915
+ const first = Number.isNaN(this._seenZoom);
3916
+ this._seenZoom = zoom;
3917
+ if (first ||
3918
+ (!this._gestureSync && !this._zoomStream && !this._bodiesHeld)) {
3919
+ return false;
3920
+ }
3921
+ const bodies = this._bodies.length;
3922
+ if (!this._bodiesHeld && bodies * this._bodyStepMs <= this._budgetMs()) {
3923
+ this._zoomStep = { live: true, bodies };
3924
+ return false;
3925
+ }
3926
+ this._zoomStep = { live: false, bodies: 0 };
3927
+ this._bodiesAt = now();
3928
+ this._restBodies();
3929
+ return true;
3930
+ }
3931
+ return this._bodiesHeld && this._bodiesRest != null;
3932
+ }
3933
+ /** Bring the held bodies back once the zoom has held still for
3934
+ * `BODY_ZOOM_REST_MS` — judged on the pane's clock, like the zoom's own
3935
+ * rest (`_restZoom`): a timer that comes due while steps are still
3936
+ * arriving waits out what is left of the rest instead. */
3937
+ _restBodies() {
3938
+ if (this._bodiesRest != null)
3939
+ return;
3940
+ const wait = this._restFrom() + BODY_ZOOM_REST_MS - now();
3941
+ this._bodiesRest = timers.setTimeout?.(() => {
3942
+ this._bodiesRest = null;
3943
+ if (now() - this._restFrom() < BODY_ZOOM_REST_MS)
3944
+ this._restBodies();
3945
+ else
3946
+ this._emitBodies();
3947
+ }, Math.max(0, wait) + 1);
3948
+ }
3949
+ /**
3950
+ * What the bodies' rest is counted from: the last step of the zoom, or
3951
+ * the frame that drew it where that came later. A step the window has not
3952
+ * drawn yet is a zoom still moving — the frame is late, not the gesture
3953
+ * over — and counted from the step alone, a frame a server took half a
3954
+ * second to answer brought the bodies back between two steps of one
3955
+ * animation, whose next frame re-scaled all of them and was late in turn.
3956
+ * Past `BODY_REST_CAP_MS` without that frame they come back anyway: a
3957
+ * surface that stopped drawing must not keep them out for good.
3958
+ */
3959
+ _restFrom() {
3960
+ const drawn = this._zoomDrawnAt;
3961
+ if (drawn !== -Infinity)
3962
+ return Math.max(this._bodiesAt, drawn);
3963
+ return now() - this._bodiesAt < BODY_REST_CAP_MS ? now() : this._bodiesAt;
3964
+ }
3965
+ /**
3966
+ * Paint one mounted node's card — fill, border, header, handles, grips —
3967
+ * into `ctx`, a `<canvas>` in the bodies' layer (`<Flow>`), exactly as the
3968
+ * scene paints it. `ctx` is at the canvas's origin, `abs` its place in
3969
+ * the window in device pixels.
3970
+ *
3971
+ * Why the card is painted twice: the bodies share one layer over every
3972
+ * card — over the GL surface, or beside the 2D pane — so a body could not
3973
+ * go under a card that was over its own. It painted over that card's
3974
+ * header, border and handles: a selected node's outline hidden behind the
3975
+ * body of the node under it. A card painted in the bodies' layer, just
3976
+ * under its own body, is stacked with it in the cards' paint order, and
3977
+ * covers the card the graph drew for it.
3978
+ */
3979
+ paintCard(id, ctx, abs) {
3980
+ const entry = this._byId.get(id);
3981
+ if (!entry)
3982
+ return;
3983
+ const painter = createPainter(ctx, this._textOptions());
3984
+ if (!painter)
3985
+ return;
3986
+ const palette = this._palette();
3987
+ const input = this._sceneInput(palette, 'card');
3988
+ const [item] = buildNodeItems({
3989
+ ...input,
3990
+ nodes: [this._source(entry)],
3991
+ clip: null,
3992
+ cull: undefined,
3993
+ });
3994
+ if (!item)
3995
+ return;
3996
+ const c = ctx;
3997
+ c.save();
3998
+ try {
3999
+ c.translate(-abs.x, -abs.y);
4000
+ paintNodeItem(painter, item, { viewport: input.viewport, palette });
4001
+ }
4002
+ finally {
4003
+ c.restore();
4004
+ }
4005
+ }
4006
+ /**
4007
+ * Whether a card is the bodies' layer's to draw and not the pane's: on
4008
+ * the 2D renderer, shown there and the bodies not held out of a zoom.
4009
+ *
4010
+ * **Never under GL.** The layer and the GL frame reach the screen by two
4011
+ * paths — the layer is the surface's overlay, painted and presented with
4012
+ * the window's frame, and a GL frame is swapped on the surface's own
4013
+ * turn — and this element cannot order them. So a card left to the layer
4014
+ * was, on some path, a card nobody drew: edges crossing the empty place
4015
+ * where it should be. The first paint of a scene of charts did that until
4016
+ * the layer had painted; a GL frame landing between a zoom's hold and the
4017
+ * commit that hid the bodies did it; and on macOS a wheel notch did it,
4018
+ * painting the overlay inside its own dispatch 20–25 ms before the GL
4019
+ * frame it asked for — a frame of bare edges at the start of every zoom
4020
+ * that held the bodies. Each was fixed by ordering one more path.
4021
+ *
4022
+ * The world draws every card instead, mounted or not, and the layer only
4023
+ * ever adds to it: its card and body land on the world's copy of that
4024
+ * card. Whatever order the two paths take, the worst the screen can show
4025
+ * is a body a frame early or late, over a card that is there. What it
4026
+ * costs is a few dozen cards more in a world build, drawn under their
4027
+ * copies. The 2D pane is painted in the same pass as the layer, in one
4028
+ * frame, and it leaves them out.
4029
+ */
4030
+ _cardInLayer(id) {
4031
+ // held: hidden, or on the way to it, whatever `<Flow>` last reported
4032
+ return !this._gl && !this._bodiesHeld && this._shownBodies.has(id);
4033
+ }
4034
+ /**
4035
+ * The cards `<Flow>` has on screen in the bodies' layer (`paintCard`),
4036
+ * which the 2D pane then leaves out. Told after the commit that shows
4037
+ * them, so no 2D frame lacks both; told the empty set while bodies are
4038
+ * held out of a zoom, when their cards are the pane's again. Under GL it
4039
+ * changes nothing drawn: the world draws every card (`_cardInLayer`).
4040
+ */
4041
+ setShownBodies(ids) {
4042
+ const was = this._shownBodies;
4043
+ if (was.size === ids.size && [...ids].every((id) => was.has(id)))
4044
+ return;
4045
+ this._shownBodies = ids;
4046
+ // the pane, not the window
4047
+ if (!this._gl)
4048
+ this._repaint('content');
4049
+ }
4050
+ /**
4051
+ * The canvases `<Flow>` paints the minimap and controls on, over its
4052
+ * bodies (`paintPanels`): the bodies' layer is over the graph, so panels
4053
+ * the graph drew went under any card that reached them. While there are
4054
+ * any the graph leaves the panels out, and every change to the pane asks
4055
+ * them to repaint. Presses still land on the pane: the canvases take
4056
+ * none.
4057
+ */
4058
+ /** The panel canvases the minimap is painted on — the ones whose box
4059
+ * reaches its corner. */
4060
+ _miniMapCanvases() {
4061
+ const canvases = this._panelCanvases;
4062
+ if (canvases.length === 0)
4063
+ return canvases;
4064
+ const corner = this._miniMapCorner();
4065
+ if (!corner)
4066
+ return [];
4067
+ const box = this._device(corner);
4068
+ return canvases.filter((c) => !c.abs || rectsOverlap(c.abs, box));
4069
+ }
4070
+ /**
4071
+ * The box `<Flow>` lays the node bodies out in, beside this pane, under
4072
+ * the 2D renderer — null under GL, and with no bodies to lay out. A pan
4073
+ * hands it to `scrollContents` as a rider: its pixels move with the
4074
+ * pane's.
4075
+ */
4076
+ setBodiesLayer(layer) {
4077
+ this._bodiesLayer = layer ?? null;
4078
+ }
4079
+ setPanelCanvases(canvases) {
4080
+ const had = this._panelCanvases.length > 0;
4081
+ this._panelCanvases = canvases;
4082
+ if (had !== canvases.length > 0)
4083
+ this._repaint('props');
4084
+ }
4085
+ /** Where the minimap and the controls are, relative to the pane's
4086
+ * top-left, logical — with a pixel round for their borders. */
4087
+ panelRects() {
4088
+ const pane = this._pane();
4089
+ const rects = [];
4090
+ const map = this._miniMapOptions();
4091
+ if (map) {
4092
+ const panel = this._corner(map.position, map.width ?? MINIMAP_W, map.height ?? MINIMAP_H, 'bottom-right');
4093
+ if (pane.width >= panel.width * 1.6 && pane.height >= panel.height * 1.6)
4094
+ rects.push(panel);
4095
+ }
4096
+ const buttons = this._controlButtons();
4097
+ if (buttons.length > 0) {
4098
+ rects.push(unionRects(buttons[0].rect, buttons[buttons.length - 1].rect));
4099
+ }
4100
+ return rects.map((r) => ({
4101
+ x: Math.floor(r.x - pane.x) - 1,
4102
+ y: Math.floor(r.y - pane.y) - 1,
4103
+ width: Math.ceil(r.width) + 2,
4104
+ height: Math.ceil(r.height) + 2,
4105
+ }));
4106
+ }
4107
+ /** Paint the minimap and the controls into `ctx`, a canvas whose place in
4108
+ * the window, in device pixels, is `abs` — as the scene paints them. */
4109
+ paintPanels(ctx, abs) {
4110
+ const painter = createPainter(ctx, this._textOptions());
4111
+ if (!painter)
4112
+ return;
4113
+ // Only the panels this canvas's box reaches: the minimap's canvas and
4114
+ // the controls' each built and painted both, which for the controls
4115
+ // was every node in the graph, clipped away. The frame clip is set to
4116
+ // the box too, for the minimap's own reach test — a canvas paints
4117
+ // outside this element's paint, where the clip is the last pass's.
4118
+ const bounds = abs.width > 0 && abs.height > 0 ? this._logical(abs) : null;
4119
+ const clip = this._frameClip;
4120
+ this._frameClip = bounds;
4121
+ let scene;
4122
+ try {
4123
+ scene = buildScene({
4124
+ ...this._sceneInput(this._palette(), 'overlay', true),
4125
+ clip: bounds,
4126
+ });
4127
+ }
4128
+ finally {
4129
+ this._frameClip = clip;
4130
+ }
4131
+ const c = ctx;
4132
+ c.save();
4133
+ try {
4134
+ c.translate(-abs.x, -abs.y);
4135
+ paintPanels(painter, scene);
4136
+ }
4137
+ finally {
4138
+ c.restore();
4139
+ }
4140
+ }
4141
+ /** Tell `<Flow>` where the panels are when that changed (`onPanels`). */
4142
+ _emitPanels() {
4143
+ const notify = this._prop('onPanels');
4144
+ if (!notify)
4145
+ return;
4146
+ const rects = this.panelRects();
4147
+ const key = rects
4148
+ .map((r) => `${r.x},${r.y},${r.width},${r.height}`)
4149
+ .join('|');
4150
+ if (key === this._panelsKey)
4151
+ return;
4152
+ this._panelsKey = key;
4153
+ notify(rects);
4154
+ }
4155
+ /** `adaptive` as a number of milliseconds: `Infinity` never holds. */
4156
+ _budgetMs() {
4157
+ const adaptive = this._prop('adaptive');
4158
+ if (adaptive === false)
4159
+ return Infinity;
4160
+ if (typeof adaptive === 'object' && adaptive?.budgetMs != null) {
4161
+ return Math.max(0, adaptive.budgetMs);
4162
+ }
4163
+ return BODY_BUDGET_MS;
4164
+ }
4165
+ /** Where the mounted bodies stand against the budget — reported with
4166
+ * every frame (`FlowFrameStats.bodies`). */
4167
+ bodyBudget() {
4168
+ const count = this._bodies.length;
4169
+ return {
4170
+ count,
4171
+ held: this._bodiesHeld,
4172
+ perBodyMs: this._bodyStepMs,
4173
+ predictedMs: count * this._bodyStepMs,
4174
+ budgetMs: this._budgetMs(),
4175
+ };
4176
+ }
4177
+ /**
4178
+ * A frame was drawn: time the zoom step it carried, into the model
4179
+ * `_holdBodies` predicts from. A step with no body re-scaled is the
4180
+ * baseline; one that re-scaled some says what each cost over it. Passes
4181
+ * a millisecond or two apart are one frame (the 2D renderer paints a
4182
+ * frame's damage as several), and a gap long enough to be the user
4183
+ * pausing is not a step at all.
4184
+ */
4185
+ _frameTick() {
4186
+ const t = now();
4187
+ // the first frame to draw the latest zoom step, and when: the GL
4188
+ // renderer's is over as it starts — the bodies' cost is the window's
4189
+ // flush before it — and the 2D one's ends with its last pass (`paint`)
4190
+ if (this._zoomFrameAt === -Infinity && this._zoomAt !== -Infinity) {
4191
+ this._zoomFrameAt = t;
4192
+ this._zoomDrawnAt = t;
4193
+ }
4194
+ const dt = t - this._lastFrameAt;
4195
+ if (dt < 2)
4196
+ return;
4197
+ this._lastFrameAt = t;
4198
+ const step = this._zoomStep;
4199
+ this._zoomStep = null;
4200
+ if (!step || dt > 250)
4201
+ return;
4202
+ if (!step.live || step.bodies === 0) {
4203
+ this._baseStepMs = Number.isNaN(this._baseStepMs)
4204
+ ? dt
4205
+ : this._baseStepMs * 0.7 + dt * 0.3;
4206
+ }
4207
+ else if (!Number.isNaN(this._baseStepMs)) {
4208
+ const each = Math.max(0, dt - this._baseStepMs) / step.bodies;
4209
+ this._bodyStepMs = this._bodyStepMs * 0.7 + each * 0.3;
4210
+ }
4211
+ }
4212
+ /**
4213
+ * The grid as one repeating fill (ntk#263): a step-sized tile drawn once,
4214
+ * a `createPattern('repeat')` over it, and one composite for the whole
4215
+ * region — where the runs path in `./paint.ts` pays a region-sized
4216
+ * coverage mask.
4217
+ *
4218
+ * Handed to `paintScene` as its grid painter, and answering **false** when
4219
+ * it does not apply, so the universal path takes over. Two decisions with
4220
+ * reasons:
4221
+ *
4222
+ * - **Integral device steps only.** The tile is a pixmap, so its size is
4223
+ * whole pixels; at a fractional step the pattern would drift against the
4224
+ * graph's own coordinates — against `snapToGrid`, against node positions
4225
+ * — by a fraction per tile, and a grid that slides under the content it
4226
+ * grids is worse than a slower exact one.
4227
+ * - **The phase lives in the tile, not in a picture transform.** An
4228
+ * untransformed repeat is anchored to the window origin, so the grid's
4229
+ * alignment is baked in by drawing the mark at `origin mod tile` — and
4230
+ * re-baking when the origin moves. A `setTransform` translate says the
4231
+ * same thing in one request, but a *transformed* repeat forfeits the
4232
+ * server's untransformed fast path — on the in-process test server that
4233
+ * was ~180 ms a frame of per-pixel arithmetic, and re-rendering a 24px
4234
+ * tile is a handful of requests on any server.
4235
+ *
4236
+ * An arrow property rather than a method: it is passed as a value, and a
4237
+ * method would arrive with no `this`.
4238
+ */
4239
+ _grid = (painter, grid) => {
4240
+ const options = { variant: grid.variant, size: grid.size };
4241
+ const { step, color, region } = grid;
4242
+ // The tile is a pixmap on the panel's grid, so the pitch has to be a
4243
+ // whole number of *device* pixels, and the phase is where the viewport
4244
+ // origin lands on that grid.
2480
4245
  const s = this._scale;
2481
4246
  const pitch = toDevice(step, s);
2482
4247
  const tileSize = Math.round(pitch);
@@ -2488,8 +4253,8 @@ export class FlowGraphNode extends Node {
2488
4253
  const v = this._viewport();
2489
4254
  const pane = this._pane();
2490
4255
  const mod = (a, m) => ((a % m) + m) % m;
2491
- const px = Math.round(mod(toDevice(pane.x + v.x, s), tileSize)) % tileSize;
2492
- const py = Math.round(mod(toDevice(pane.y + v.y, s), tileSize)) % tileSize;
4256
+ const px = snapToGrid(mod(toDevice(pane.x + v.x, s), tileSize)) % tileSize;
4257
+ const py = snapToGrid(mod(toDevice(pane.y + v.y, s), tileSize)) % tileSize;
2493
4258
  // device pixels per graph unit — what a mark's size is drawn in
2494
4259
  const unit = v.zoom * s;
2495
4260
  const key = `${options.variant}|${Math.round(options.size * unit * 4)}|${color}|${px},${py}`;
@@ -2515,7 +4280,7 @@ export class FlowGraphNode extends Node {
2515
4280
  const fill = this._device(region);
2516
4281
  raw.fillRect?.(fill.x, fill.y, fill.width, fill.height);
2517
4282
  return true;
2518
- }
4283
+ };
2519
4284
  _makeGridSurface(tileSize) {
2520
4285
  const ctor = ntk.Surface;
2521
4286
  if (typeof ctor !== 'function')
@@ -2536,6 +4301,16 @@ export class FlowGraphNode extends Node {
2536
4301
  * Device pixels throughout: `unit` is how many of them a graph unit is. */
2537
4302
  _renderGridTile(tile, options, unit, color, px, py) {
2538
4303
  const t = tile.size;
4304
+ // Every mark on whole pixels. A backend cuts a fractional edge its own
4305
+ // way, and X11 cut a mark's position and its size separately, so a mark
4306
+ // that straddled the tile's edge lost a column: at phase 0 a 3px dot
4307
+ // came out as two pixels a column apart and a 1px line not at all. The
4308
+ // grid changed shape as the view panned, and a pan's blit kept the old
4309
+ // shape where a repaint drew the new one. The phase point `(px, py)` is
4310
+ // a pixel edge; a line and a cross are centred on the pixel before it,
4311
+ // and a dot is `size` pixels ending on it, where the cut had put them.
4312
+ const arm = Math.max(1, Math.floor(Math.max(2, options.size * 3 * unit)));
4313
+ const dot = Math.max(1, Math.round(options.size * 2 * unit));
2539
4314
  tile.surface.render((ctx) => {
2540
4315
  ctx.clearRect?.(0, 0, t, t);
2541
4316
  ctx.fillStyle = color;
@@ -2545,18 +4320,18 @@ export class FlowGraphNode extends Node {
2545
4320
  const cy = py + j * t;
2546
4321
  if (options.variant === 'lines') {
2547
4322
  if (j === 0)
2548
- ctx.fillRect(cx - 0.5, 0, 1, t);
4323
+ ctx.fillRect(cx - 1, 0, 1, t);
2549
4324
  if (i === 0)
2550
- ctx.fillRect(0, cy - 0.5, t, 1);
4325
+ ctx.fillRect(0, cy - 1, t, 1);
2551
4326
  }
2552
4327
  else if (options.variant === 'cross') {
2553
- const arm = Math.max(2, options.size * 3 * unit);
2554
- ctx.fillRect(cx - arm, cy - 0.5, arm * 2, 1);
2555
- ctx.fillRect(cx - 0.5, cy - arm, 1, arm * 2);
4328
+ ctx.fillRect(cx - 1 - arm, cy - 1, arm * 2 + 1, 1);
4329
+ ctx.fillRect(cx - 1, cy - 1 - arm, 1, arm * 2 + 1);
2556
4330
  }
2557
4331
  else {
2558
- const dot = Math.max(1, Math.round(options.size * 2 * unit));
2559
- ctx.fillRect(cx - dot / 2, cy - dot / 2, dot, dot);
4332
+ const x0 = Math.floor(cx - dot / 2);
4333
+ const y0 = Math.floor(cy - dot / 2);
4334
+ ctx.fillRect(x0, y0, dot, dot);
2560
4335
  }
2561
4336
  }
2562
4337
  }
@@ -2570,545 +4345,5 @@ export class FlowGraphNode extends Node {
2570
4345
  tile.surface.destroy?.();
2571
4346
  this._gridTile = null;
2572
4347
  }
2573
- /** Draws every visible edge; answers whether any of them is animated, so
2574
- * the caller knows whether to keep a timer alive. */
2575
- _paintEdges(painter, palette) {
2576
- const v = this._viewport();
2577
- const pane = this._pane();
2578
- const labels = v.zoom >= LABEL_ZOOM;
2579
- let animated = false;
2580
- // Collect, then draw. A stroke has no fast path in ntk — every one is a
2581
- // mask rasterized in JS, uploaded with `PutImage` and composited — so a
2582
- // graph of seven hundred edges was seven hundred round trips and about
2583
- // four megabytes a frame. Bucketed by the style they will be stroked
2584
- // with, the same graph is one path per distinct pen, which for almost
2585
- // every graph is one.
2586
- const strokes = new StrokeBuckets();
2587
- const markers = new Map();
2588
- const labelChips = [];
2589
- const pendingLabels = [];
2590
- const clip = this._frameClip;
2591
- let animBox = null;
2592
- for (const edge of this._edges) {
2593
- if (edge.hidden)
2594
- continue;
2595
- // two rejects: one from the nodes alone, one from the route it took
2596
- const coarse = this._edgeCoarseBox(edge);
2597
- if (!coarse || !rectsOverlap(coarse, pane))
2598
- continue;
2599
- // Tracked before the damage skip, deliberately: whether the dash
2600
- // timer runs is a question about the viewport, not about what this
2601
- // particular pass repaints — deciding it after the skip is how a drag
2602
- // in one corner would stop the dash marching in the other. The box
2603
- // the ticks invalidate is collected below from the *drawn* geometry:
2604
- // the coarse box carries the bezier slack, and a tick that repaints
2605
- // slack repaints a card-sized halo of neighbours sixteen times a
2606
- // second.
2607
- if (edge.animated)
2608
- animated = true;
2609
- if (clip && !rectsOverlap(coarse, clip))
2610
- continue;
2611
- const geometry = this._edgeGeometry(edge);
2612
- if (!geometry)
2613
- continue;
2614
- if (!rectsOverlap(pathBounds(geometry.points), pane))
2615
- continue;
2616
- const selected = edge.selected ?? false;
2617
- const hovered = this._hover.edgeId === edge.id;
2618
- const stroke = edge.style?.stroke ??
2619
- (selected
2620
- ? palette.edgeSelected
2621
- : hovered
2622
- ? palette.text
2623
- : palette.edge);
2624
- const lineWidth = Math.max(1, (edge.style?.strokeWidth ?? (selected ? 2 : 1.5)) * v.zoom);
2625
- // `markerEnd` left out means an arrow: a directed graph whose edges do
2626
- // not say which way they point is a set of lines. `null` opts out.
2627
- const markerEnd = normalizeMarker(edge.markerEnd === undefined ? 'arrowclosed' : edge.markerEnd);
2628
- const markerStart = normalizeMarker(edge.markerStart);
2629
- // The tip stops short of the handle rather than at it: the handle dot
2630
- // is drawn *over* the edges, with the nodes, so an arrow aimed at the
2631
- // handle's centre is an arrow mostly hidden under a white circle.
2632
- const inset = v.zoom >= HANDLE_ZOOM ? this._handleRadius() + 1 : 1;
2633
- const last = geometry.points[geometry.points.length - 1];
2634
- const outAngle = endAngle(geometry.points);
2635
- const endTip = {
2636
- x: last.x - Math.cos(outAngle) * inset,
2637
- y: last.y - Math.sin(outAngle) * inset,
2638
- };
2639
- let points = geometry.points;
2640
- if (markerEnd) {
2641
- // and the stroke stops behind the head, so a filled triangle is a
2642
- // triangle rather than a triangle with a line through it
2643
- points = trimEnd(points, (markerEnd.size ?? DEFAULT_MARKER_SIZE) * v.zoom * 0.8 + inset);
2644
- }
2645
- const dash = edge.style?.dash ?? (edge.animated ? DEFAULT_DASH : undefined);
2646
- if (edge.animated) {
2647
- const tight = inflateRect(pathBounds(geometry.points), CULL_MARGIN);
2648
- animBox = animBox ? unionRects(animBox, tight) : tight;
2649
- }
2650
- strokes
2651
- .bucket(stroke, lineWidth, dash, edge.animated ? this._dashPhase : 0, v.zoom)
2652
- .push(points);
2653
- if (markerEnd) {
2654
- this._collectMarker(markers, endTip, outAngle, markerEnd.type, markerEnd.color ?? stroke, (markerEnd.size ?? DEFAULT_MARKER_SIZE) * v.zoom, lineWidth);
2655
- }
2656
- if (markerStart) {
2657
- const inAngle = startAngle(geometry.points);
2658
- this._collectMarker(markers, {
2659
- x: geometry.points[0].x - Math.cos(inAngle) * inset,
2660
- y: geometry.points[0].y - Math.sin(inAngle) * inset,
2661
- }, inAngle, markerStart.type, markerStart.color ?? stroke, (markerStart.size ?? DEFAULT_MARKER_SIZE) * v.zoom, lineWidth);
2662
- }
2663
- if (labels && edge.label) {
2664
- const at = pointAtFraction(geometry.points, 0.5);
2665
- const size = Math.max(8, 11 * v.zoom);
2666
- const metrics = painter.measureText(edge.label, { size });
2667
- const padX = 5 * v.zoom;
2668
- const padY = 2 * v.zoom;
2669
- // The chip is collected with the rest; the text is not, because a
2670
- // glyph run is already one request and nothing is gained by holding
2671
- // it. Both still land above every edge, which is the point of the
2672
- // chip.
2673
- labelChips.push({
2674
- rect: {
2675
- x: Math.round(at.x - metrics.width / 2 - padX),
2676
- y: Math.round(at.y - metrics.height / 2 - padY),
2677
- width: Math.round(metrics.width + padX * 2),
2678
- height: Math.round(metrics.height + padY * 2),
2679
- },
2680
- radius: Math.max(2, Math.round(3 * v.zoom)),
2681
- fill: edge.style?.labelBackground ?? tint(palette.background, 0.92),
2682
- });
2683
- pendingLabels.push({
2684
- text: edge.label,
2685
- x: at.x,
2686
- y: at.y,
2687
- size,
2688
- color: edge.style?.labelColor ?? palette.text,
2689
- });
2690
- }
2691
- }
2692
- strokes.paint(painter);
2693
- for (const bucket of markers.values()) {
2694
- // the same threshold, for the same reason: a handful of arrowheads
2695
- // scattered over the pane is cheaper drawn as a handful
2696
- if (bucket.filled.length >= MARKER_BATCH_MIN) {
2697
- painter.polygons(bucket.filled, { fill: bucket.color });
2698
- }
2699
- else {
2700
- for (const head of bucket.filled) {
2701
- painter.polygon(head, { fill: bucket.color });
2702
- }
2703
- }
2704
- if (bucket.open.length >= MARKER_BATCH_MIN) {
2705
- painter.strokeRuns(bucket.open, {
2706
- stroke: bucket.color,
2707
- lineWidth: bucket.lineWidth,
2708
- });
2709
- }
2710
- else {
2711
- for (const head of bucket.open) {
2712
- painter.strokeRuns([head], {
2713
- stroke: bucket.color,
2714
- lineWidth: bucket.lineWidth,
2715
- });
2716
- }
2717
- }
2718
- }
2719
- for (const chip of labelChips) {
2720
- painter.rect(chip.rect.x, chip.rect.y, chip.rect.width, chip.rect.height, chip.radius, { fill: chip.fill });
2721
- }
2722
- for (const l of pendingLabels) {
2723
- painter.text(l.text, l.x, l.y, {
2724
- size: l.size,
2725
- color: l.color,
2726
- align: 'center',
2727
- baseline: 'middle',
2728
- });
2729
- }
2730
- // A pass that drew an animated edge knows exactly where its dash is; a
2731
- // pass that culled them all keeps the previous box — the endpoints did
2732
- // not move, or the move's own damage would have redrawn them here.
2733
- if (animBox)
2734
- this._animBox = animBox;
2735
- return animated;
2736
- }
2737
- /** An arrowhead's three or four points, added to the pile for its colour
2738
- * rather than drawn — see {@link StrokeBuckets} for why. */
2739
- _collectMarker(markers, at, angle, type, color, size, lineWidth) {
2740
- const spread = 0.42; // radians off the shaft — a ~24° half-angle
2741
- const back = {
2742
- x: at.x - Math.cos(angle) * size,
2743
- y: at.y - Math.sin(angle) * size,
2744
- };
2745
- const left = {
2746
- x: at.x - Math.cos(angle - spread) * size,
2747
- y: at.y - Math.sin(angle - spread) * size,
2748
- };
2749
- const right = {
2750
- x: at.x - Math.cos(angle + spread) * size,
2751
- y: at.y - Math.sin(angle + spread) * size,
2752
- };
2753
- const key = `${color}|${lineWidth}`;
2754
- let bucket = markers.get(key);
2755
- if (!bucket) {
2756
- bucket = { color, lineWidth, filled: [], open: [] };
2757
- markers.set(key, bucket);
2758
- }
2759
- if (type === 'arrowclosed')
2760
- bucket.filled.push([at, left, back, right]);
2761
- else
2762
- bucket.open.push([left, at, right]);
2763
- }
2764
- _paintNodes(painter, palette) {
2765
- const dragging = this._dragTo;
2766
- const order = [];
2767
- const deferred = [];
2768
- for (const entry of this._order) {
2769
- // a dragged node comes to the top
2770
- if (dragging?.has(entry.node.id))
2771
- deferred.push(entry);
2772
- else
2773
- order.push(entry);
2774
- }
2775
- order.push(...deferred);
2776
- // Nodes are drawn one at a time, in z-order, and both halves of that
2777
- // were measured rather than assumed — see `_paintHandles` for why
2778
- // batching a card or a handle costs more than it saves.
2779
- for (const entry of order)
2780
- this._paintNode(painter, palette, entry);
2781
- }
2782
- _paintNode(painter, palette, entry) {
2783
- if (entry.node.hidden)
2784
- return;
2785
- const rect = this._screenRect(entry);
2786
- if (!rectsOverlap(rect, this._pane()))
2787
- return;
2788
- // inflated by the margin its handles and grips can ink outside the box —
2789
- // the same margin every invalidate grew by, so the two agree
2790
- const clip = this._frameClip;
2791
- if (clip && !rectsOverlap(inflateRect(rect, CULL_MARGIN), clip))
2792
- return;
2793
- const v = this._viewport();
2794
- const selected = entry.node.selected ?? false;
2795
- const hovered = this._hover.nodeId === entry.node.id;
2796
- const handles = this._handlesOf(entry).map((anchor) => {
2797
- const s = this._toScreen(anchor);
2798
- return { ...anchor, x: s.x, y: s.y };
2799
- });
2800
- if (entry.type?.paint) {
2801
- // Never batched: a type that draws its own body is drawing whatever it
2802
- // likes, in an order only it knows.
2803
- entry.type.paint({
2804
- node: entry.node,
2805
- rect,
2806
- zoom: v.zoom,
2807
- selected,
2808
- hovered,
2809
- palette,
2810
- painter,
2811
- handles,
2812
- });
2813
- }
2814
- else {
2815
- this._paintCardShape(painter, palette, entry, rect, selected, hovered);
2816
- this._paintCardInk(painter, palette, entry, rect);
2817
- }
2818
- if (this._connectable(entry) && (v.zoom >= HANDLE_ZOOM || hovered)) {
2819
- this._paintHandles(painter, palette, handles);
2820
- }
2821
- this._paintGrips(painter, palette, entry, rect);
2822
- }
2823
- _mounted(entry) {
2824
- return entry.type?.render != null && this._viewport().zoom >= RENDER_ZOOM;
2825
- }
2826
- _paintGrips(painter, palette, entry, rect) {
2827
- const grips = this._grips(entry);
2828
- if (grips.length === 0)
2829
- return;
2830
- const size = Math.max(4, RESIZE_GRIP * 2 * this._viewport().zoom);
2831
- for (const dir of grips) {
2832
- const at = gripPoint(rect, dir);
2833
- painter.rect(at.x - size / 2, at.y - size / 2, size, size, 1, {
2834
- fill: palette.nodeBackground,
2835
- stroke: palette.accent,
2836
- lineWidth: 1.5,
2837
- });
2838
- }
2839
- }
2840
- /** The built-in node: a card with a label, an optional second line and an
2841
- * optional accent stripe down its leading edge. */
2842
- /** The card itself: one rounded box, and the accent stripe if it has one.
2843
- * With a `batch` the box joins the pile for its appearance instead of
2844
- * being drawn on its own. */
2845
- _paintCardShape(painter, palette, entry, rect, selected, hovered) {
2846
- const v = this._viewport();
2847
- const style = entry.node.style;
2848
- const radius = Math.round((style?.borderRadius ?? 6) * v.zoom);
2849
- const border = selected
2850
- ? palette.accent
2851
- : hovered
2852
- ? tint(palette.accent, 0.55)
2853
- : (style?.borderColor ?? palette.nodeBorder);
2854
- const options = {
2855
- fill: style?.background ?? palette.nodeBackground,
2856
- stroke: border,
2857
- lineWidth: Math.max(1, Math.round((style?.borderWidth ?? (selected ? 2 : 1)) * v.zoom)),
2858
- };
2859
- painter.rect(rect.x, rect.y, rect.width, rect.height, radius, options);
2860
- if (style?.accent) {
2861
- // Inset past the rounded corners instead of clipped to them: a
2862
- // non-rectangular clip forfeits ntk's rounded-box fast path for every
2863
- // fill under it — measured as a pixmap create/free and a trapezoid
2864
- // pass per card per repaint.
2865
- painter.rect(rect.x + Math.max(1, v.zoom), rect.y + radius, Math.max(2, 3 * v.zoom), rect.height - radius * 2, 0, { fill: style.accent });
2866
- }
2867
- }
2868
- /** The label and its second line — the half of a card that has to come
2869
- * after every card when they are batched. */
2870
- _paintCardInk(painter, palette, entry, rect) {
2871
- const v = this._viewport();
2872
- if (v.zoom < LABEL_ZOOM)
2873
- return;
2874
- const style = entry.node.style;
2875
- const header = this._mounted(entry) ? this._headerHeight(entry) : 0;
2876
- const data = entry.node.data;
2877
- const label = data?.label ?? entry.node.id;
2878
- const description = data?.description;
2879
- const color = style?.color ?? palette.text;
2880
- const padX = NODE_PAD_X * v.zoom;
2881
- const centre = rect.x + rect.width / 2;
2882
- const maxWidth = Math.max(8, rect.width - padX * 2);
2883
- const showDescription = Boolean(description) && v.zoom >= DESC_ZOOM;
2884
- if (header > 0) {
2885
- // The title bar of a node whose body is somebody else's: left-aligned,
2886
- // with a hairline under it so the strip reads as the thing to grab.
2887
- const band = header * v.zoom;
2888
- painter.text(label, rect.x + padX, rect.y + band / 2, {
2889
- size: NODE_LABEL_SIZE * v.zoom,
2890
- weight: 'bold',
2891
- color,
2892
- baseline: 'middle',
2893
- maxWidth,
2894
- });
2895
- painter.strokeRuns([
2896
- [
2897
- { x: rect.x + 1, y: rect.y + band },
2898
- { x: rect.x + rect.width - 1, y: rect.y + band },
2899
- ],
2900
- ], { stroke: palette.nodeBorder, lineWidth: 1 });
2901
- return;
2902
- }
2903
- if (!showDescription) {
2904
- painter.text(label, centre, rect.y + rect.height / 2, {
2905
- size: NODE_LABEL_SIZE * v.zoom,
2906
- weight: 'bold',
2907
- color,
2908
- align: 'center',
2909
- baseline: 'middle',
2910
- maxWidth,
2911
- });
2912
- return;
2913
- }
2914
- const top = rect.y + NODE_PAD_Y * v.zoom;
2915
- painter.text(label, centre, top, {
2916
- size: NODE_LABEL_SIZE * v.zoom,
2917
- weight: 'bold',
2918
- color,
2919
- align: 'center',
2920
- maxWidth,
2921
- });
2922
- painter.text(description, centre, top + NODE_LABEL_SIZE * 1.35 * v.zoom, {
2923
- size: NODE_DESC_SIZE * v.zoom,
2924
- color: palette.dim,
2925
- align: 'center',
2926
- maxWidth,
2927
- });
2928
- }
2929
- /**
2930
- * Handles are drawn one disc at a time, and that is the measured answer
2931
- * rather than the obvious one.
2932
- *
2933
- * Batching them into a single path halved the requests and made the frame
2934
- * *slower*: a path's mask is its bounding box, so forty dots scattered
2935
- * across the pane rasterize to a paneful of mask — three quarters of a
2936
- * megabyte — where forty small ones cost a kilobyte each. Batching pays
2937
- * for the edges because an edge already spans that box; it does not pay
2938
- * for anything small and scattered.
2939
- */
2940
- _paintHandles(painter, palette, handles) {
2941
- const v = this._viewport();
2942
- const radius = this._handleRadius();
2943
- const gesture = this._gesture;
2944
- const connecting = gesture?.kind === 'connect' ? gesture : null;
2945
- const lineWidth = Math.max(1, 1.5 * v.zoom);
2946
- for (const anchor of handles) {
2947
- const active = sameHandle(this._hover.handle, anchor) ||
2948
- sameHandle(connecting?.to ?? null, anchor) ||
2949
- sameHandle(connecting?.from ?? null, anchor);
2950
- const options = {
2951
- fill: active ? palette.accent : palette.nodeBackground,
2952
- stroke: active ? palette.accent : palette.handle,
2953
- lineWidth,
2954
- };
2955
- painter.circle(anchor.x, anchor.y, active ? radius * 1.4 : radius, options);
2956
- if (anchor.label && v.zoom >= DESC_ZOOM) {
2957
- const outside = anchor.position === 'left' || anchor.position === 'top';
2958
- painter.text(anchor.label, anchor.x +
2959
- (anchor.position === 'left'
2960
- ? -radius - 4 * v.zoom
2961
- : radius + 4 * v.zoom), anchor.y, {
2962
- size: 10 * v.zoom,
2963
- color: palette.dim,
2964
- align: outside ? 'right' : 'left',
2965
- baseline: 'middle',
2966
- });
2967
- }
2968
- }
2969
- }
2970
- _paintConnection(painter, palette) {
2971
- const gesture = this._gesture;
2972
- if (gesture?.kind !== 'connect')
2973
- return;
2974
- const v = this._viewport();
2975
- const target = gesture.to ?? gesture.pointer;
2976
- const to = this._toScreen(target);
2977
- const points = this._connectionPath(gesture);
2978
- const invalid = gesture.to != null && !gesture.valid;
2979
- painter.polyline(points, {
2980
- stroke: invalid ? palette.dim : palette.accent,
2981
- lineWidth: Math.max(1.5, 2 * v.zoom),
2982
- dash: gesture.to && gesture.valid ? undefined : [5 * v.zoom, 4 * v.zoom],
2983
- });
2984
- painter.circle(to.x, to.y, this._handleRadius(), {
2985
- fill: invalid ? palette.dim : palette.accent,
2986
- });
2987
- }
2988
- _paintSelectionBox(painter, palette) {
2989
- const gesture = this._gesture;
2990
- if (gesture?.kind !== 'select')
2991
- return;
2992
- const x = Math.min(gesture.startX, gesture.x);
2993
- const y = Math.min(gesture.startY, gesture.y);
2994
- painter.rect(x, y, Math.abs(gesture.x - gesture.startX), Math.abs(gesture.y - gesture.startY), 2, { fill: palette.selection, stroke: palette.accent, lineWidth: 1 });
2995
- }
2996
- _paintMiniMap(painter, palette) {
2997
- const opts = this._miniMapOptions();
2998
- if (!opts)
2999
- return;
3000
- // The panel cull comes before `_miniMap()`, which walks every node to
3001
- // find the graph's bounds — during a drag that walk per pass would cost
3002
- // more than the panel it skips. The dot for a mid-drag node goes stale
3003
- // until the release repaints in full; that is the trade, and it is
3004
- // deliberate.
3005
- const clip = this._frameClip;
3006
- if (clip) {
3007
- const quick = this._corner(opts.position, opts.width ?? MINIMAP_W, opts.height ?? MINIMAP_H, 'bottom-right');
3008
- if (!rectsOverlap(quick, clip))
3009
- return;
3010
- }
3011
- const map = this._miniMap();
3012
- if (!map)
3013
- return;
3014
- const { panel, options } = map;
3015
- painter.rect(panel.x, panel.y, panel.width, panel.height, 4, {
3016
- fill: tint(palette.surface, 0.92),
3017
- stroke: palette.surfaceBorder,
3018
- lineWidth: 1,
3019
- });
3020
- painter.save();
3021
- painter.clipRect(panel.x, panel.y, panel.width, panel.height, 4);
3022
- const nodeColor = options.nodeColor;
3023
- for (const entry of this._entries) {
3024
- if (entry.node.hidden)
3025
- continue;
3026
- const rect = this.rectOf(entry);
3027
- const at = this._miniToScreen(map, rect);
3028
- const fill = typeof nodeColor === 'function'
3029
- ? nodeColor(entry.node)
3030
- : (nodeColor ??
3031
- (entry.node.selected ? palette.accent : palette.nodeBorder));
3032
- painter.rect(at.x, at.y, Math.max(1, rect.width * map.scale), Math.max(1, rect.height * map.scale), 0, { fill });
3033
- }
3034
- // The viewport, as an outline over a wash on everything outside it —
3035
- // cheaper than four mask rectangles and reads the same.
3036
- const v = this._viewport();
3037
- const paneRect = this._pane();
3038
- const view = this._miniToScreen(map, {
3039
- x: -v.x / v.zoom,
3040
- y: -v.y / v.zoom,
3041
- });
3042
- painter.rect(view.x, view.y, (paneRect.width / v.zoom) * map.scale, (paneRect.height / v.zoom) * map.scale, 2, {
3043
- fill: options.maskColor ?? tint(palette.accent, 0.1),
3044
- stroke: palette.accent,
3045
- lineWidth: 1,
3046
- });
3047
- painter.restore();
3048
- }
3049
- _paintControls(painter, palette) {
3050
- const buttons = this._controlButtons();
3051
- if (buttons.length === 0)
3052
- return;
3053
- const clip = this._frameClip;
3054
- if (clip && !buttons.some((b) => rectsOverlap(b.rect, clip)))
3055
- return;
3056
- const first = buttons[0].rect;
3057
- const last = buttons[buttons.length - 1].rect;
3058
- painter.rect(first.x, first.y, first.width, last.y + last.height - first.y, 4, {
3059
- fill: tint(palette.surface, 0.94),
3060
- stroke: palette.surfaceBorder,
3061
- lineWidth: 1,
3062
- });
3063
- for (let i = 0; i < buttons.length; i++) {
3064
- const { rect, action } = buttons[i];
3065
- if (i > 0) {
3066
- painter.strokeRuns([
3067
- [
3068
- { x: rect.x + 4, y: rect.y },
3069
- { x: rect.x + rect.width - 4, y: rect.y },
3070
- ],
3071
- ], { stroke: palette.surfaceBorder, lineWidth: 1 });
3072
- }
3073
- const cx = rect.x + rect.width / 2;
3074
- const cy = rect.y + rect.height / 2;
3075
- const arm = 5;
3076
- const runs = [];
3077
- if (action === 'in' || action === 'out') {
3078
- runs.push([
3079
- { x: cx - arm, y: cy },
3080
- { x: cx + arm, y: cy },
3081
- ]);
3082
- if (action === 'in') {
3083
- runs.push([
3084
- { x: cx, y: cy - arm },
3085
- { x: cx, y: cy + arm },
3086
- ]);
3087
- }
3088
- }
3089
- else {
3090
- // a frame with its sides opened out: "fit what is there into this"
3091
- const a = 5;
3092
- runs.push([
3093
- { x: cx - a, y: cy - a + 3 },
3094
- { x: cx - a, y: cy - a },
3095
- { x: cx - a + 3, y: cy - a },
3096
- ], [
3097
- { x: cx + a - 3, y: cy - a },
3098
- { x: cx + a, y: cy - a },
3099
- { x: cx + a, y: cy - a + 3 },
3100
- ], [
3101
- { x: cx + a, y: cy + a - 3 },
3102
- { x: cx + a, y: cy + a },
3103
- { x: cx + a - 3, y: cy + a },
3104
- ], [
3105
- { x: cx - a + 3, y: cy + a },
3106
- { x: cx - a, y: cy + a },
3107
- { x: cx - a, y: cy + a - 3 },
3108
- ]);
3109
- }
3110
- painter.strokeRuns(runs, { stroke: palette.text, lineWidth: 1.5 });
3111
- }
3112
- }
3113
4348
  }
3114
4349
  //# sourceMappingURL=node.js.map