| … | … | @@ -99,6 +99,8 @@ var NV = { |
| 99 | 99 | panning: false, panSX: 0, panSY: 0, |
| 100 | 100 | initialized: false, |
| 101 | 101 | sourceCache: {}, |
| 102 | frames: [], |
| 103 | layoutPending: false, |
| 102 | 104 | selected: null |
| 103 | 105 | } |
| 104 | 106 | |
| … | … | @@ -439,12 +441,64 @@ window.nvToggleFrames = function (on) { |
| 439 | 441 | // ---- layout: columns by dependency depth, left = supplies, right = consumers |
| 440 | 442 | |
| 441 | 443 | function nvLayout (nodes, links) { |
| 444 | NV.frames = [] |
| 445 | // The <script type="module"> that supplies the shared layout is DEFERRED, so |
| 446 | // it has not necessarily run when this does — a human clicking the tab gets |
| 447 | // it, an immediate deep-link does not, which is the worst kind of bug. Pull it |
| 448 | // in on demand and redraw once it lands. |
| 449 | if (!window.__ctLayout) { |
| 450 | if (!NV.layoutPending) { |
| 451 | NV.layoutPending = true |
| 452 | import(API + '/floor/layout.js').then(function (m) { |
| 453 | window.__ctLayout = m.layout |
| 454 | NV.layoutPending = false |
| 455 | nvBuild() |
| 456 | }).catch(function () { NV.layoutPending = false }) |
| 457 | } |
| 458 | return nvLayoutFallback(nodes, links) |
| 459 | } |
| 460 | |
| 461 | // A group is a CONTAINER in the layout, not a rectangle drawn around whatever |
| 462 | // positions fell out afterwards. That is the whole difference: containers are |
| 463 | // placed as whole boxes in one pass, so sibling groups cannot interleave. |
| 464 | var containers = [] |
| 465 | var seen = {} |
| 466 | var items = [] |
| 467 | for (var i = 0; i < nodes.length; i++) { |
| 468 | var n = nodes[i] |
| 469 | var frame = n.isInterface ? null : nvFrameFor(n) |
| 470 | if (frame && !seen[frame]) { |
| 471 | seen[frame] = true |
| 472 | containers.push({ id: 'frame:' + frame, parent: null, label: frame }) |
| 473 | } |
| 474 | items.push({ |
| 475 | id: n.id, |
| 476 | container: frame ? 'frame:' + frame : null, |
| 477 | w: NV_W, |
| 478 | h: nvNodeHeight(n) |
| 479 | }) |
| 480 | } |
| 481 | |
| 482 | var out = window.__ctLayout({ nodes: items, links: links, containers: containers }) |
| 483 | |
| 484 | var pos = {} |
| 485 | for (var j = 0; j < out.nodes.length; j++) pos[out.nodes[j].id] = out.nodes[j] |
| 486 | for (var k = 0; k < nodes.length; k++) { |
| 487 | var p = pos[nodes[k].id] |
| 488 | if (!p) continue |
| 489 | nodes[k].x = p.x |
| 490 | nodes[k].y = p.y |
| 491 | } |
| 492 | NV.frames = out.containers |
| 493 | } |
| 494 | |
| 495 | // Only if the shared module failed to load: the original single-pass layering, |
| 496 | // kept so the tab still draws something rather than nothing. |
| 497 | function nvLayoutFallback (nodes, links) { |
| 442 | 498 | var byId = {} |
| 443 | 499 | for (var i = 0; i < nodes.length; i++) byId[nodes[i].id] = nodes[i] |
| 444 | | |
| 445 | 500 | var depth = {} |
| 446 | 501 | for (var i = 0; i < nodes.length; i++) depth[nodes[i].id] = 0 |
| 447 | | // Longest-path layering. Bounded iterations so a dependency cycle cannot spin. |
| 448 | 502 | for (var pass = 0; pass < nodes.length + 2; pass++) { |
| 449 | 503 | var moved = false |
| 450 | 504 | for (var l = 0; l < links.length; l++) { |
| … | … | @@ -454,20 +508,6 @@ function nvLayout (nodes, links) { |
| 454 | 508 | } |
| 455 | 509 | if (!moved) break |
| 456 | 510 | } |
| 457 | | |
| 458 | | // Interface nodes are pinned to the far edges. |
| 459 | | for (var i = 0; i < nodes.length; i++) { |
| 460 | | if (nodes[i].id === '__group_in__') depth[nodes[i].id] = -1 |
| 461 | | } |
| 462 | | var maxD = 0 |
| 463 | | for (var i = 0; i < nodes.length; i++) { |
| 464 | | if (nodes[i].id === '__group_out__') continue |
| 465 | | if (depth[nodes[i].id] > maxD) maxD = depth[nodes[i].id] |
| 466 | | } |
| 467 | | for (var i = 0; i < nodes.length; i++) { |
| 468 | | if (nodes[i].id === '__group_out__') depth[nodes[i].id] = maxD + 1 |
| 469 | | } |
| 470 | | |
| 471 | 511 | var cols = {} |
| 472 | 512 | for (var i = 0; i < nodes.length; i++) { |
| 473 | 513 | var d = depth[nodes[i].id] |
| … | … | @@ -478,13 +518,6 @@ function nvLayout (nodes, links) { |
| 478 | 518 | var x = 0 |
| 479 | 519 | for (var c = 0; c < keys.length; c++) { |
| 480 | 520 | var col = cols[keys[c]] |
| 481 | | // Order rows within a column by frame, so a frame's members end up adjacent |
| 482 | | // and its box encloses them instead of straddling unrelated nodes. |
| 483 | | col.sort(function (a, b) { |
| 484 | | var fa = nvFrameFor(a) || '~', fb = nvFrameFor(b) || '~' |
| 485 | | if (fa !== fb) return fa < fb ? -1 : 1 |
| 486 | | return a.title < b.title ? -1 : a.title > b.title ? 1 : 0 |
| 487 | | }) |
| 488 | 521 | var y = 0 |
| 489 | 522 | for (var r = 0; r < col.length; r++) { |
| 490 | 523 | col[r].x = x |
| … | … | @@ -540,36 +573,17 @@ function nvFrameFor (n) { |
| 540 | 573 | |
| 541 | 574 | function nvDrawFrames (parent) { |
| 542 | 575 | if (!NV.showFrames) return |
| 543 | | var groups = {} |
| 544 | | for (var i = 0; i < NV.nodes.length; i++) { |
| 545 | | var n = NV.nodes[i] |
| 546 | | if (n.isInterface) continue |
| 547 | | var f = nvFrameFor(n) |
| 548 | | if (!f) continue |
| 549 | | if (!groups[f]) groups[f] = [] |
| 550 | | groups[f].push(n) |
| 551 | | } |
| 552 | | var names = Object.keys(groups).sort() |
| 553 | | for (var g = 0; g < names.length; g++) { |
| 554 | | var members = groups[names[g]] |
| 555 | | if (members.length < 2) continue |
| 556 | | var x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity |
| 557 | | for (var i = 0; i < members.length; i++) { |
| 558 | | var m = members[i] |
| 559 | | if (m.x < x0) x0 = m.x |
| 560 | | if (m.y < y0) y0 = m.y |
| 561 | | if (m.x + NV_W > x1) x1 = m.x + NV_W |
| 562 | | var h = nvNodeHeight(m) |
| 563 | | if (m.y + h > y1) y1 = m.y + h |
| 564 | | } |
| 565 | | var pad = 16 |
| 576 | var frames = NV.frames || [] |
| 577 | for (var g = 0; g < frames.length; g++) { |
| 578 | var box = frames[g] |
| 566 | 579 | var color = NV_FRAME_COLORS[g % NV_FRAME_COLORS.length] |
| 580 | |
| 567 | 581 | var r = document.createElementNS('http://www.w3.org/2000/svg', 'rect') |
| 568 | 582 | r.setAttribute('class', 'nv-frame') |
| 569 | | r.setAttribute('x', x0 - pad) |
| 570 | | r.setAttribute('y', y0 - pad - 16) |
| 571 | | r.setAttribute('width', (x1 - x0) + pad * 2) |
| 572 | | r.setAttribute('height', (y1 - y0) + pad * 2 + 16) |
| 583 | r.setAttribute('x', box.x) |
| 584 | r.setAttribute('y', box.y) |
| 585 | r.setAttribute('width', box.w) |
| 586 | r.setAttribute('height', box.h) |
| 573 | 587 | r.setAttribute('rx', 8) |
| 574 | 588 | r.setAttribute('fill', color) |
| 575 | 589 | r.setAttribute('stroke', color) |
| … | … | @@ -577,10 +591,10 @@ function nvDrawFrames (parent) { |
| 577 | 591 | |
| 578 | 592 | var t = document.createElementNS('http://www.w3.org/2000/svg', 'text') |
| 579 | 593 | t.setAttribute('class', 'nv-frame-label') |
| 580 | | t.setAttribute('x', x0 - pad + 8) |
| 581 | | t.setAttribute('y', y0 - pad - 3) |
| 594 | t.setAttribute('x', box.x + 8) |
| 595 | t.setAttribute('y', box.y + 13) |
| 582 | 596 | t.setAttribute('fill', color) |
| 583 | | t.textContent = names[g] |
| 597 | t.textContent = box.label |
| 584 | 598 | parent.appendChild(t) |
| 585 | 599 | } |
| 586 | 600 | } |