function activeSnap() {
return window.viewSide === 'baseline'
? (window.BASELINE ?? window.CURRENT)
: (window.CURRENT ?? window.BASELINE);
}
function viewMode() {
if (!window.BASELINE || !window.CURRENT) return 'review'; return window.viewSide === 'current' ? 'current' : 'baseline';
}
function viewModeSuffix() {
const m = viewMode();
return m === 'current' ? ' Current' : m === 'baseline' ? ' Baseline' : '';
}
function activeGraph(level) {
return activeSnap()?.graphs?.[level] || { nodes: [], edges: [] };
}
function activeLocalGraph(level) {
const g = activeGraph(level);
const nodes = g.nodes.filter(n => !isExternalNode(n, level));
const ids = new Set(nodes.map(n => n.id));
const edges = g.edges.filter(e => ids.has(e.source) && ids.has(e.target));
return { nodes, edges };
}
function unionGraph(level) {
return window.DIFF?.[level] || { nodes: [], edges: [] };
}
function applySideVisibility(frame) {
if (!frame) return;
frame.classList.remove('hide-nodes-added', 'hide-edges-added',
'hide-nodes-removed', 'hide-edges-removed',
'side-baseline', 'side-current');
const m = viewMode();
if (m === 'baseline') frame.classList.add('hide-nodes-added', 'hide-edges-added');
else if (m === 'current') frame.classList.add('hide-nodes-removed', 'hide-edges-removed');
frame.classList.add(window.viewSide === 'current' ? 'side-current' : 'side-baseline');
}
function applySideSizing(frame, level) {
if (!frame) return;
const sizeMode = window.nodeSizeMode || null;
if (!sizeMode || (window.drillGroup || null) === null) return;
const mode = sizeMode;
const byId = new Map((activeSnap()?.graphs?.[level]?.nodes || []).map(n => [n.id, n]));
frame.querySelectorAll('g.node').forEach(g => {
const n = byId.get(g.dataset.nodeId);
const ell = g.querySelector('ellipse');
if (!n || !ell) return; const cx = parseFloat(ell.getAttribute('cx'));
const cy = parseFloat(ell.getAttribute('cy'));
const d = metricNodeDiam(n, mode);
const r = (d * 36).toFixed(2); ell.setAttribute('rx', r);
ell.setAttribute('ry', r);
const txt = g.querySelector('text');
if (txt) {
const fs = metricFontSize(d);
const v = metricNodeVal(n, mode);
txt.setAttribute('font-size', fs.toFixed(2));
txt.setAttribute('x', cx);
txt.setAttribute('y', (cy + fs * 0.3).toFixed(2)); txt.textContent = v > 0 ? fmtMetricShort(v) : '';
}
});
}
function setViewSide(side) {
if (side === window.viewSide
|| (side === 'current' && !window.CURRENT)
|| (side === 'baseline' && !window.BASELINE)) return;
window.viewSide = side;
window.navSetSide?.();
document.querySelectorAll('g.node.node-hl').forEach(n => n.classList.remove('node-hl'));
document.querySelectorAll('.row-hl').forEach(r => r.classList.remove('row-hl'));
document.querySelectorAll('.view').forEach(sec => {
const frame = sec.querySelector('.svg-frame');
applySideVisibility(frame);
applySideSizing(frame, sec.dataset.view);
sec._refreshNodeTable?.();
});
updateWarnCount();
updateActiveSnapGroup();
const m = window._modalNode;
if (m && document.getElementById('node-modal-overlay')?.style.display === 'flex')
window.openModalForNode?.(m.id, m.level);
}
function toggleViewSide() {
if (!window.BASELINE || !window.CURRENT) return;
setViewSide(window.viewSide === 'current' ? 'baseline' : 'current');
}
function recomputeAll() {
const EMPTY = { graphs: {} };
const baseline = window.BASELINE;
const current = window.CURRENT;
window.DIFF = computeDiff(baseline ?? current ?? EMPTY, current ?? baseline ?? EMPTY);
window.CYCLES = computeCycles(baseline ?? current ?? EMPTY, current ?? baseline ?? EMPTY);
window.META = computeMeta(baseline, current);
buildSummary();
updateFilesTab();
window.drillGroup = null;
window.dig = 0;
window.drillDig = 0;
window.clearGroupingCache?.();
document.querySelectorAll('.drill-breadcrumb').forEach(bc => { bc.style.display = 'none'; });
document.querySelectorAll('.dig-lod').forEach(el => el.style.removeProperty('display'));
document.querySelectorAll('.svg-frame').forEach(f => { delete f.dataset.bigConfirmed; });
document.querySelectorAll('.view').forEach(sec => { sec.dataset.rendered = 'false'; });
updateHeader();
window.updateDigLabel?.();
const active = document.querySelector('.view.active');
if (active && window.gv) renderView(active);
}
function renderView(section, opts = {}) {
const level = section.dataset.view;
const frame = section.querySelector('.svg-frame');
const loading = section.querySelector('.loading-indicator');
updateWarnCount();
let viewSpec = null, wasZoomed = false;
if (opts.preserve) {
const cur = frame.querySelector('svg')?.getAttribute('viewBox');
const nat = frame.dataset.naturalVB;
if (cur && nat && cur !== nat) {
const [cx, cy, cw, ch] = cur.split(/[ ,]+/).map(Number);
const [ox, oy, ow, oh] = nat.split(/[ ,]+/).map(Number);
if (ow > 0 && oh > 0) {
viewSpec = {
zw: cw / ow, zh: ch / oh,
fx: (cx + cw / 2 - ox) / ow,
fy: (cy + ch / 2 - oy) / oh,
};
wasZoomed = frame.classList.contains('zoomed');
}
}
}
if (loading) { loading.textContent = 'Computing layout…'; loading.classList.add('on'); }
setTimeout(() => {
const g = unionGraph(level);
drawSVG(frame, g.nodes, g.edges, level);
applySideVisibility(frame);
applySideSizing(frame, level);
window._ntSelected?.[level]?.forEach(id => section._gNodeMap?.get(id)?.classList.add('node-selected'));
if (viewSpec) {
const svg = frame.querySelector('svg');
const [ox, oy, ow, oh] = (frame.dataset.naturalVB || '0 0 0 0').split(/[ ,]+/).map(Number);
if (svg && ow > 0 && oh > 0) {
const nw = viewSpec.zw * ow, nh = viewSpec.zh * oh;
const cx = ox + viewSpec.fx * ow, cy = oy + viewSpec.fy * oh;
svg.setAttribute('viewBox', `${cx - nw / 2} ${cy - nh / 2} ${nw} ${nh}`);
if (wasZoomed) frame.classList.add('zoomed');
}
}
section.dataset.rendered = 'true';
section._refreshNodeTable?.();
window.updateDigLabel?.(level);
if (loading) loading.classList.remove('on');
}, 30);
}
function applyViewState(st, { rerender = false } = {}) {
const grp = st.group || null;
const mode = st.mode || null;
const dig = st.dig != null ? (Number(st.dig) | 0) : 0;
let changed = false;
if ((window.dig || 0) !== dig) { window.dig = dig; changed = true; }
if (window.drillGroup !== grp) { window.drillGroup = grp; window.drillDig = grp ? dig : 0; changed = true; }
if (window.nodeSizeMode !== mode) { window.nodeSizeMode = mode; changed = true; }
const lvl = st.level ?? currentLevel();
window.renderBreadcrumb?.(lvl);
document.querySelectorAll('.dig-lod').forEach(el => { el.style.display = grp ? 'none' : ''; });
window.updateDigLabel?.(lvl);
document.querySelectorAll('.size-row[data-row="metric"] .size-mode-btn').forEach(b => {
const bMode = b.dataset.size === 'dot' ? null : b.dataset.size;
b.classList.toggle('active', bMode === mode);
});
if ((changed || rerender) && window.gv) {
document.querySelectorAll('.view').forEach(sec => { sec.dataset.rendered = 'false'; });
const active = document.querySelector('.view.active');
if (active) renderView(active, { preserve: false });
}
}