Home Reference Source

cables_dev/cables_ui/src/ui/components/patchlayout.js

  1. import uiconfig from "../uiconfig.js";
  2. import gluiconfig from "../glpatch/gluiconfig.js";
  3. import { UiOp } from "../core_extend_op.js";
  4. const GRID_X = uiconfig.snapX;
  5. const GRID_Y = uiconfig.snapY;
  6. const GAP_X = 2 * GRID_X;
  7. const GAP_Y = GRID_Y;
  8. const FANOUT_GAP_X = 4 * GRID_X;
  9. const FANOUT_MIN_CHILDREN = 3;
  10. const SMALL_BRANCH_FACTOR = 2;
  11. const MAX_SEARCH_STEPS = 400;
  12. const LINK_GAP_MIN_LINKS = 2;
  13. const LINK_GAP_MAX_LINKS = 10;
  14. const LINK_GAP_MIN_OPS = 2;
  15. const LINK_GAP_MAX_OPS = 5;
  16. /**
  17. * @typedef {{x:number,y:number,w:number,h:number}} LayoutRect
  18. * @typedef {{chain:UiOp[],anchor:UiOp,link:{parent:UiOp,portOut:any,portIn:any},above:UiOp}} SideChain
  19. * @typedef {{portOut:any,children:UiOp[]}} PortChildren
  20. */
  21. /**
  22. * @param {number} v
  23. * @param {number} grid
  24. */
  25. function snapUp(v, grid)
  26. {
  27. return Math.ceil(v / grid) * grid;
  28. }
  29. /**
  30. * @param {number} v
  31. * @param {number} grid
  32. */
  33. function snapDown(v, grid)
  34. {
  35. return Math.floor(v / grid) * grid;
  36. }
  37. /**
  38. * @param {number} v
  39. * @param {number} grid
  40. */
  41. function snapNearest(v, grid)
  42. {
  43. return Math.round(v / grid) * grid;
  44. }
  45. /**
  46. * @param {LayoutRect} a
  47. * @param {LayoutRect} b
  48. */
  49. function rectsTooClose(a, b)
  50. {
  51. return a.x < b.x + b.w + GAP_X && b.x < a.x + a.w + GAP_X && a.y < b.y + b.h + GAP_Y && b.y < a.y + a.h + GAP_Y;
  52. }
  53. /**
  54. * @param {{left:number,right:number}} ext
  55. */
  56. function extentWidth(ext)
  57. {
  58. return ext.right - ext.left;
  59. }
  60. export default class PatchLayout
  61. {
  62. #glPatch = null;
  63. /** @type {Set<UiOp>} */
  64. #set = new Set();
  65. /** @type {Map<UiOp,SideChain>} */
  66. #sideChains = new Map();
  67. #extents = new Map();
  68. /** @type {Map<UiOp,{x:number,index:number,parent:UiOp}>} */
  69. #fanSlots = new Map();
  70. /** @type {LayoutRect[]} */
  71. #fixed = [];
  72. /** @type {Set<UiOp>} */
  73. #selected = new Set();
  74. /**
  75. * @param {import("../glpatch/glpatch.js").default} glPatch
  76. */
  77. constructor(glPatch)
  78. {
  79. this.#glPatch = glPatch;
  80. }
  81. /**
  82. * @param {UiOp[]} ops
  83. * @param {UiOp[]} allOps
  84. * @returns {Map<UiOp,LayoutRect>|string}
  85. */
  86. layout(ops, allOps = [])
  87. {
  88. this.#selected = new Set(ops);
  89. const all = [];
  90. for (const o of ops) all.push(o);
  91. for (const o of allOps)
  92. if (!this.#selected.has(o) && o.uiAttribs.translate && !o.uiAttribs.hidden && this.#glPatch.getGlOp(o)) all.push(o);
  93. this.#fixed = [];
  94. for (const o of all)
  95. if (!this.#selected.has(o)) this.#fixed.push(this.#opRect(o));
  96. const result = new Map();
  97. for (const group of this.#connectedGroups(all))
  98. {
  99. let hasSelected = false;
  100. for (const o of group) if (this.#selected.has(o)) hasSelected = true;
  101. if (!hasSelected) continue;
  102. const pos = this.#layoutGroup(group);
  103. if (typeof pos == "string") return pos;
  104. for (const o of pos.keys())
  105. {
  106. if (!this.#selected.has(o)) continue;
  107. result.set(o, pos.get(o));
  108. this.#fixed.push(pos.get(o));
  109. }
  110. }
  111. return result;
  112. }
  113. /**
  114. * @param {UiOp[]} ops
  115. * @returns {UiOp[][]}
  116. */
  117. #connectedGroups(ops)
  118. {
  119. this.#set = new Set(ops);
  120. const groupOf = new Map();
  121. const groups = [];
  122. for (const start of ops)
  123. {
  124. if (groupOf.has(start)) continue;
  125. const group = [];
  126. const todo = [start];
  127. groupOf.set(start, group);
  128. while (todo.length)
  129. {
  130. const o = todo.pop();
  131. group.push(o);
  132. const linked = [];
  133. for (const link of this.#linksFromSet(o)) linked.push(link.parent);
  134. for (const child of this.#childOpsInSet(o)) linked.push(child);
  135. for (const other of linked)
  136. if (!groupOf.has(other))
  137. {
  138. groupOf.set(other, group);
  139. todo.push(other);
  140. }
  141. }
  142. groups.push(group);
  143. }
  144. return groups.sort((a, b) => b.length - a.length);
  145. }
  146. /**
  147. * @param {UiOp[]} ops
  148. * @returns {Map<UiOp,LayoutRect>|string}
  149. */
  150. #layoutGroup(ops)
  151. {
  152. this.#set = new Set(ops);
  153. this.#extents = new Map();
  154. this.#sideChains = this.#findSideChains(ops);
  155. let layers = this.#layerOps(ops);
  156. if (!layers)
  157. {
  158. this.#sideChains = new Map();
  159. layers = this.#layerOps(ops);
  160. }
  161. if (!layers) return "the links between the ops form a cycle";
  162. let top = Infinity;
  163. for (const o of ops) top = Math.min(top, o.uiAttribs.translate.y);
  164. const startY = snapNearest(top, GRID_Y);
  165. const firstPos = this.#placeOps(layers, startY, new Map(), new Map());
  166. if (typeof firstPos == "string") return firstPos;
  167. return this.#placeOps(layers, startY, this.#sourceHints(ops, firstPos), this.#fanRows(firstPos));
  168. }
  169. /**
  170. * @param {UiOp} op
  171. */
  172. #linksFromSet(op)
  173. {
  174. const result = [];
  175. for (const portIn of op.portsIn)
  176. for (const link of portIn.links)
  177. {
  178. const parent = /** @type {UiOp} */ (link.portOut.op);
  179. if (this.#set.has(parent)) result.push({ "parent": parent, "portOut": link.portOut, "portIn": portIn });
  180. }
  181. return result;
  182. }
  183. /**
  184. * @param {UiOp} op
  185. * @returns {UiOp}
  186. */
  187. #primaryParent(op)
  188. {
  189. const links = this.#linksFromSet(op);
  190. return links.length ? links[0].parent : null;
  191. }
  192. /**
  193. * @param {UiOp} op
  194. * @returns {UiOp[]}
  195. */
  196. #childOpsInSet(op)
  197. {
  198. const children = [];
  199. for (const portOut of op.portsOut)
  200. for (const link of portOut.links)
  201. {
  202. const child = /** @type {UiOp} */ (link.portIn.op);
  203. if (this.#set.has(child) && !children.includes(child)) children.push(child);
  204. }
  205. return children;
  206. }
  207. /**
  208. * @param {UiOp} o
  209. */
  210. #isUnconnected(o)
  211. {
  212. return !this.#linksFromSet(o).length && !this.#childOpsInSet(o).length;
  213. }
  214. /**
  215. * @param {UiOp} o
  216. * @param {number} [x]
  217. * @param {number} [y]
  218. * @returns {LayoutRect}
  219. */
  220. #opRect(o, x = o.uiAttribs.translate.x, y = o.uiAttribs.translate.y)
  221. {
  222. const glOp = this.#glPatch.getGlOp(o);
  223. return { "x": x, "y": y, "w": glOp.w, "h": glOp.h };
  224. }
  225. /**
  226. * @param {UiOp} op
  227. * @param {any} port
  228. */
  229. #portCenterX(op, port)
  230. {
  231. return op.getPortPosX(port.name, null, true) || 0;
  232. }
  233. /**
  234. * @param {UiOp} parent
  235. * @param {UiOp} child
  236. */
  237. #outLinkIndex(parent, child)
  238. {
  239. let index = 0;
  240. for (const portOut of parent.portsOut)
  241. for (const link of portOut.links)
  242. {
  243. if (link.portIn.op == child) return index;
  244. index++;
  245. }
  246. return index;
  247. }
  248. /**
  249. * @param {UiOp} o
  250. * @returns {UiOp[]}
  251. */
  252. #parentsForLayers(o)
  253. {
  254. const parents = [];
  255. for (const link of this.#linksFromSet(o)) parents.push(link.parent);
  256. if (this.#sideChains.has(o)) parents.push(this.#sideChains.get(o).above);
  257. return parents;
  258. }
  259. /**
  260. * @param {UiOp[]} ops
  261. * @returns {UiOp[][]}
  262. */
  263. #layerOps(ops)
  264. {
  265. const layer = new Map();
  266. for (const o of ops) layer.set(o, 0);
  267. for (let pass = 0; pass <= ops.length; pass++)
  268. {
  269. let changed = false;
  270. for (const o of ops)
  271. for (const parent of this.#parentsForLayers(o))
  272. if (layer.get(o) < layer.get(parent) + 1)
  273. {
  274. layer.set(o, layer.get(parent) + 1);
  275. changed = true;
  276. }
  277. if (!changed)
  278. {
  279. this.#pullDownToChildren(ops, layer);
  280. const layers = [];
  281. for (const o of ops)
  282. {
  283. const l = layer.get(o);
  284. if (!layers[l]) layers[l] = [];
  285. layers[l].push(o);
  286. }
  287. const result = [];
  288. for (const row of layers) if (row) result.push(row);
  289. return result;
  290. }
  291. }
  292. return null;
  293. }
  294. /**
  295. * @param {UiOp[]} ops
  296. * @param {Map<UiOp,number>} layer
  297. */
  298. #pullDownToChildren(ops, layer)
  299. {
  300. const children = new Map();
  301. for (const o of ops) children.set(o, this.#childOpsInSet(o));
  302. for (const source of this.#sideChains.keys()) children.get(this.#sideChains.get(source).above).push(source);
  303. for (let pass = 0; pass <= ops.length; pass++)
  304. {
  305. let changed = false;
  306. for (const o of ops)
  307. {
  308. if (!children.get(o).length) continue;
  309. let above = Infinity;
  310. for (const c of children.get(o)) above = Math.min(above, layer.get(c) - 1);
  311. if (layer.get(o) < above)
  312. {
  313. layer.set(o, above);
  314. changed = true;
  315. }
  316. }
  317. if (!changed) return;
  318. }
  319. }
  320. /**
  321. * @param {LayoutRect} rect
  322. * @param {LayoutRect[]} placed
  323. */
  324. #isFree(rect, placed)
  325. {
  326. for (const p of placed) if (rectsTooClose(rect, p)) return false;
  327. return true;
  328. }
  329. /**
  330. * @param {LayoutRect} rect
  331. * @param {LayoutRect[]} placed
  332. * @param {boolean} allowLeft
  333. * @param {boolean} allowUp
  334. */
  335. #moveToNearestFreeSpot(rect, placed, allowLeft, allowUp)
  336. {
  337. const startX = rect.x;
  338. const startY = rect.y;
  339. for (let distance = 0; distance <= MAX_SEARCH_STEPS; distance++)
  340. for (let stepsY = 0; stepsY <= distance; stepsY++)
  341. {
  342. const stepsX = distance - stepsY;
  343. for (const dirX of stepsX ? [1, -1] : [1])
  344. for (const dirY of stepsY ? [1, -1] : [1])
  345. {
  346. if ((dirX < 0 && !allowLeft) || (dirY < 0 && !allowUp)) continue;
  347. rect.x = startX + dirX * stepsX * GRID_X;
  348. rect.y = startY + dirY * stepsY * GRID_Y;
  349. if (this.#isFree(rect, placed)) return true;
  350. }
  351. }
  352. return false;
  353. }
  354. /**
  355. * @param {UiOp[]} ops
  356. * @returns {Map<UiOp,SideChain>}
  357. */
  358. #findSideChains(ops)
  359. {
  360. const sideChains = new Map();
  361. const inChain = new Set();
  362. for (const anchor of ops)
  363. {
  364. const above = this.#primaryParent(anchor);
  365. if (!above) continue;
  366. for (const link of this.#linksFromSet(anchor))
  367. {
  368. const last = link.parent;
  369. if (last == above || inChain.has(last)) continue;
  370. if (this.#childOpsInSet(last).length != 1 && this.#linksFromSet(last).length) continue;
  371. const chain = [last];
  372. for (;;)
  373. {
  374. const parents = [];
  375. for (const l of this.#linksFromSet(chain[0])) if (!parents.includes(l.parent)) parents.push(l.parent);
  376. if (parents.length != 1 || parents[0] == above || chain.includes(parents[0]) || this.#childOpsInSet(parents[0]).length != 1) break;
  377. chain.unshift(parents[0]);
  378. }
  379. for (const o of chain) inChain.add(o);
  380. sideChains.set(chain[0], { "chain": chain, "anchor": anchor, "link": link, "above": above });
  381. }
  382. }
  383. return sideChains;
  384. }
  385. /**
  386. * @param {UiOp} source
  387. * @param {SideChain} sideChain
  388. * @param {Map<UiOp,LayoutRect>} pos
  389. * @param {Map} fanSlots
  390. * @returns {LayoutRect}
  391. */
  392. #sideChainRect(source, sideChain, pos, fanSlots)
  393. {
  394. const above = pos.get(sideChain.above);
  395. const last = sideChain.chain[sideChain.chain.length - 1];
  396. let chainOffset = 0;
  397. for (let i = 1; i < sideChain.chain.length; i++) chainOffset += this.#childOffsetX(sideChain.chain[i]);
  398. const x = this.#childX(sideChain.anchor, pos, fanSlots) + this.#portCenterX(sideChain.anchor, sideChain.link.portIn) - this.#portCenterX(last, sideChain.link.portOut) - chainOffset;
  399. let y = above.y + above.h + GAP_Y;
  400. for (const link of this.#linksFromSet(source))
  401. {
  402. const p = pos.get(link.parent);
  403. y = Math.max(y, p.y + p.h + this.#childGap(link.parent));
  404. }
  405. return this.#opRect(source, snapNearest(x, GRID_X), snapUp(y, GRID_Y));
  406. }
  407. /**
  408. * @param {Map<UiOp,LayoutRect>} firstPos
  409. * @returns {Map<UiOp,number>}
  410. */
  411. #fanRows(firstPos)
  412. {
  413. const rows = new Map();
  414. for (const child of this.#fanSlots.keys())
  415. {
  416. const parent = this.#fanSlots.get(child).parent;
  417. const dy = firstPos.get(child).y - firstPos.get(parent).y;
  418. if (!rows.has(parent) || rows.get(parent) < dy) rows.set(parent, dy);
  419. }
  420. return rows;
  421. }
  422. /**
  423. * @param {UiOp[]} ops
  424. * @param {Map<UiOp,LayoutRect>} firstPos
  425. */
  426. #sourceHints(ops, firstPos)
  427. {
  428. const hints = new Map();
  429. for (const source of ops)
  430. {
  431. if (!this.#selected.has(source) || this.#primaryParent(source) || this.#sideChains.has(source)) continue;
  432. const sourceChildren = this.#childOpsInSet(source);
  433. if (!sourceChildren.length) continue;
  434. let highestChild = Infinity;
  435. for (const c of sourceChildren) highestChild = Math.min(highestChild, firstPos.get(c).y);
  436. const first = firstPos.get(source);
  437. hints.set(source, { "x": first.x, "y": snapDown(highestChild - first.h - this.#childGap(source), GRID_Y) });
  438. }
  439. return hints;
  440. }
  441. /**
  442. * @param {UiOp} parent
  443. * @returns {number}
  444. */
  445. #childGap(parent)
  446. {
  447. let links = 0;
  448. for (const portOut of parent.portsOut)
  449. for (const link of portOut.links)
  450. if (this.#set.has(/** @type {UiOp} */ (link.portIn.op))) links++;
  451. if (links < LINK_GAP_MIN_LINKS) return GAP_Y;
  452. const t = (Math.min(links, LINK_GAP_MAX_LINKS) - LINK_GAP_MIN_LINKS) / (LINK_GAP_MAX_LINKS - LINK_GAP_MIN_LINKS);
  453. return (LINK_GAP_MIN_OPS + t * (LINK_GAP_MAX_OPS - LINK_GAP_MIN_OPS)) * gluiconfig.opHeight;
  454. }
  455. /**
  456. * @param {UiOp} o
  457. * @param {Map<UiOp,LayoutRect>} pos
  458. * @param {number} startY
  459. * @param {Map} hints
  460. */
  461. #opY(o, pos, startY, hints)
  462. {
  463. const links = this.#linksFromSet(o);
  464. if (links.length)
  465. {
  466. let y = -Infinity;
  467. for (const link of links)
  468. {
  469. const p = pos.get(link.parent);
  470. y = Math.max(y, p.y + p.h + this.#childGap(link.parent));
  471. }
  472. return snapUp(y, GRID_Y);
  473. }
  474. if (hints.has(o)) return hints.get(o).y;
  475. if (!this.#childOpsInSet(o).length) return snapNearest(o.uiAttribs.translate.y, GRID_Y);
  476. return startY;
  477. }
  478. /**
  479. * @param {UiOp[][]} layers
  480. * @param {number} startY
  481. * @param {Map} hints
  482. * @param {Map<UiOp,number>} fanRows
  483. * @returns {Map<UiOp,LayoutRect>|string}
  484. */
  485. #placeOps(layers, startY, hints, fanRows)
  486. {
  487. const placed = [];
  488. for (const rect of this.#fixed) placed.push(rect);
  489. const pos = new Map();
  490. const fanSlots = new Map();
  491. this.#fanSlots = fanSlots;
  492. for (const row of layers)
  493. {
  494. const desired = new Map();
  495. const order = new Map();
  496. for (const o of row)
  497. {
  498. const parent = this.#primaryParent(o);
  499. if (!this.#selected.has(o)) desired.set(o, o.uiAttribs.translate.x);
  500. else if (this.#sideChains.has(o)) desired.set(o, this.#sideChainRect(o, this.#sideChains.get(o), pos, fanSlots).x);
  501. else if (parent) desired.set(o, this.#childX(o, pos, fanSlots));
  502. else desired.set(o, hints.has(o) ? hints.get(o).x : snapNearest(o.uiAttribs.translate.x, GRID_X));
  503. order.set(o, parent ? this.#outLinkIndex(parent, o) : 0);
  504. }
  505. row.sort((a, b) => (desired.get(a) - desired.get(b)) || (order.get(a) - order.get(b)) || (a.uiAttribs.translate.x - b.uiAttribs.translate.x));
  506. const lastChild = new Map();
  507. for (const o of row)
  508. {
  509. if (this.#isUnconnected(o)) continue;
  510. if (!this.#selected.has(o))
  511. {
  512. pos.set(o, this.#opRect(o));
  513. this.#addFanSlots(o, pos.get(o), fanSlots);
  514. continue;
  515. }
  516. const parent = this.#primaryParent(o);
  517. const fanSlot = fanSlots.get(o);
  518. const sideChain = this.#sideChains.get(o);
  519. const sibling = parent && !fanSlot && !sideChain ? lastChild.get(parent) : null;
  520. const rect = sideChain ? this.#sideChainRect(o, sideChain, pos, fanSlots) : this.#opRect(o, desired.get(o), this.#opY(o, pos, startY, hints));
  521. if (sibling) rect.x = Math.max(rect.x, snapUp(this.#siblingMinX(sibling, o), GRID_X));
  522. if (fanSlot && fanRows.has(fanSlot.parent)) rect.y = Math.max(rect.y, snapUp(pos.get(fanSlot.parent).y + fanRows.get(fanSlot.parent), GRID_Y));
  523. const allowLeft = !sibling && !(fanSlot && fanSlot.index > 0);
  524. if (!this.#moveToNearestFreeSpot(rect, placed, allowLeft, !parent && !sideChain)) return "no free place found for op " + o.getTitle();
  525. placed.push(rect);
  526. pos.set(o, rect);
  527. if (parent && !fanSlot && !sideChain) lastChild.set(parent, { "x": rect.x, "op": o });
  528. this.#addFanSlots(o, rect, fanSlots);
  529. }
  530. }
  531. for (const row of layers)
  532. for (const o of row)
  533. {
  534. if (!this.#isUnconnected(o)) continue;
  535. if (!this.#selected.has(o))
  536. {
  537. pos.set(o, this.#opRect(o));
  538. continue;
  539. }
  540. const rect = this.#opRect(o, snapNearest(o.uiAttribs.translate.x, GRID_X), snapNearest(o.uiAttribs.translate.y, GRID_Y));
  541. if (!this.#moveToNearestFreeSpot(rect, placed, true, true)) return "no free place found for op " + o.getTitle();
  542. placed.push(rect);
  543. pos.set(o, rect);
  544. }
  545. return pos;
  546. }
  547. /**
  548. * @param {UiOp} o
  549. * @param {Map<UiOp,LayoutRect>} pos
  550. * @param {Map} fanSlots
  551. */
  552. #childX(o, pos, fanSlots)
  553. {
  554. if (fanSlots.has(o)) return fanSlots.get(o).x;
  555. return snapNearest(pos.get(this.#primaryParent(o)).x + this.#childOffsetX(o), GRID_X);
  556. }
  557. /**
  558. * @param {UiOp} op
  559. * @returns {PortChildren[]}
  560. */
  561. #childrenByPort(op)
  562. {
  563. const seen = new Set();
  564. const result = [];
  565. for (const portOut of op.portsOut)
  566. {
  567. const children = [];
  568. for (const link of portOut.links)
  569. {
  570. const c = /** @type {UiOp} */ (link.portIn.op);
  571. if (seen.has(c) || !this.#set.has(c) || this.#primaryParent(c) != op || this.#sideChains.has(c)) continue;
  572. seen.add(c);
  573. children.push(c);
  574. }
  575. result.push({ "portOut": portOut, "children": children });
  576. }
  577. return result;
  578. }
  579. /**
  580. * @param {UiOp} child
  581. */
  582. #childOffsetX(child)
  583. {
  584. const link = this.#linksFromSet(child)[0];
  585. return this.#portCenterX(link.parent, link.portOut) - this.#portCenterX(child, link.portIn);
  586. }
  587. /**
  588. * @param {UiOp[]} children
  589. */
  590. #fanWidth(children)
  591. {
  592. let total = (children.length - 1) * FANOUT_GAP_X;
  593. for (const c of children) total += extentWidth(this.#subtreeExtent(c));
  594. return total;
  595. }
  596. /**
  597. * @param {UiOp} o
  598. * @returns {{left:number,right:number}}
  599. */
  600. #subtreeExtent(o)
  601. {
  602. if (this.#extents.has(o)) return this.#extents.get(o);
  603. const ext = { "left": 0, "right": this.#opRect(o).w };
  604. this.#extents.set(o, ext);
  605. for (const sideChain of this.#sideChains.values())
  606. {
  607. if (sideChain.anchor != o) continue;
  608. const last = sideChain.chain[sideChain.chain.length - 1];
  609. const x = this.#portCenterX(o, sideChain.link.portIn) - this.#portCenterX(last, sideChain.link.portOut);
  610. let chainWidth = 0;
  611. for (const c of sideChain.chain) chainWidth = Math.max(chainWidth, this.#opRect(c).w);
  612. ext.left = Math.min(ext.left, x);
  613. ext.right = Math.max(ext.right, x + chainWidth);
  614. }
  615. let prev = null;
  616. for (const byPort of this.#childrenByPort(o))
  617. {
  618. if (byPort.children.length >= FANOUT_MIN_CHILDREN)
  619. {
  620. const total = this.#fanWidth(byPort.children);
  621. const start = this.#portCenterX(o, byPort.portOut) - total / 2;
  622. ext.left = Math.min(ext.left, start);
  623. ext.right = Math.max(ext.right, start + total);
  624. continue;
  625. }
  626. for (const child of byPort.children)
  627. {
  628. const childExt = this.#subtreeExtent(child);
  629. let x = this.#childOffsetX(child);
  630. if (prev) x = Math.max(x, this.#siblingMinX(prev, child));
  631. ext.left = Math.min(ext.left, x + childExt.left);
  632. ext.right = Math.max(ext.right, x + childExt.right);
  633. prev = { "x": x, "op": child };
  634. }
  635. }
  636. return ext;
  637. }
  638. /**
  639. * @param {UiOp} o
  640. * @returns {number}
  641. */
  642. #subtreeSize(o)
  643. {
  644. let size = 1;
  645. for (const sideChain of this.#sideChains.values()) if (sideChain.anchor == o) size += sideChain.chain.length;
  646. for (const byPort of this.#childrenByPort(o))
  647. for (const c of byPort.children) size += this.#subtreeSize(c);
  648. return size;
  649. }
  650. /**
  651. * @param {{x:number,op:UiOp}} prev
  652. * @param {UiOp} o
  653. */
  654. #siblingMinX(prev, o)
  655. {
  656. const small = this.#subtreeSize(o) * SMALL_BRANCH_FACTOR <= this.#subtreeSize(prev.op);
  657. const prevRight = small ? prev.x + this.#opRect(prev.op).w : prev.x + this.#subtreeExtent(prev.op).right;
  658. return prevRight + GAP_X - this.#subtreeExtent(o).left;
  659. }
  660. /**
  661. * @param {UiOp} op
  662. * @param {LayoutRect} rect
  663. * @param {Map} fanSlots
  664. */
  665. #addFanSlots(op, rect, fanSlots)
  666. {
  667. for (const byPort of this.#childrenByPort(op))
  668. {
  669. if (byPort.children.length < FANOUT_MIN_CHILDREN) continue;
  670. let x = rect.x + this.#portCenterX(op, byPort.portOut) - this.#fanWidth(byPort.children) / 2;
  671. for (let i = 0; i < byPort.children.length; i++)
  672. {
  673. const ext = this.#subtreeExtent(byPort.children[i]);
  674. fanSlots.set(byPort.children[i], { "x": snapNearest(x - ext.left, GRID_X), "index": i, "parent": op });
  675. x += extentWidth(ext) + FANOUT_GAP_X;
  676. }
  677. }
  678. }
  679. }