const DIG_MIN = -12, DIG_MAX = 6;
function clampDig(z) { return Math.max(DIG_MIN, Math.min(DIG_MAX, (z | 0))); }
window.clampDig = clampDig;
function viewTier(level) {
if (window.tier === 'crate' || window.tier === 'file') return window.tier;
return levelUi(level).grouping?.key ? 'crate' : 'file';
}
window.viewTier = viewTier;
function crateIdOf(level, n) {
const gk = levelUi(level).grouping?.key;
const c = gk ? n[gk] : null;
return (c != null && c !== '') ? String(c) : null;
}
window.crateIdOf = crateIdOf;
function nodeDirSegs(id) { return relPathOf(id).split('/').slice(0, -1); }
window.nodeDirSegs = nodeDirSegs;
function autoFocusSegs(level) {
const dirs = (unionGraph(level).nodes || [])
.filter(n => !isExternalNode(n, level))
.map(n => nodeDirSegs(n.id));
const prefix = [];
for (;;) {
let hasDirectFile = false;
const subdirs = new Set();
for (const segs of dirs) {
if (segs.length < prefix.length) continue;
let under = true;
for (let i = 0; i < prefix.length; i++) if (segs[i] !== prefix[i]) { under = false; break; }
if (!under) continue;
if (segs.length === prefix.length) hasDirectFile = true;
else subdirs.add(segs[prefix.length]);
}
if (!hasDirectFile && subdirs.size === 1) { prefix.push([...subdirs][0]); continue; }
break;
}
return prefix;
}
window.autoFocusSegs = autoFocusSegs;
function relPathOf(id) { return String(id || '').replace(/^\{[^}]+\}\//, ''); }
const _crateRootCache = new Map(); function crateRoots(level) {
if (_crateRootCache.has(level)) return _crateRootCache.get(level);
const gk = levelUi(level).grouping?.key;
const byCrate = new Map();
if (gk) {
for (const n of (unionGraph(level).nodes || [])) {
if (isExternalNode(n, level)) continue;
const crate = n[gk];
if (crate == null || crate === '') continue;
const dirs = nodeDirSegs(n.id); const arr = byCrate.get(String(crate));
if (arr) arr.push(dirs); else byCrate.set(String(crate), [dirs]);
}
}
const roots = new Map();
for (const [crate, list] of byCrate) {
let prefix = list[0].slice();
for (let k = 1; k < list.length; k++) {
const segs = list[k];
let i = 0;
while (i < prefix.length && i < segs.length && prefix[i] === segs[i]) i++;
prefix = prefix.slice(0, i);
}
roots.set(crate, prefix);
}
_crateRootCache.set(level, roots);
return roots;
}
const _crateDirsCache = new Map(); const SRC_DIRS = new Set(['src', 'tests', 'benches', 'lib', 'bin']);
function crateDirs(level) {
if (_crateDirsCache.has(level)) return _crateDirsCache.get(level);
const dirOf = new Map();
let maxDepth = 0;
for (const [crate, segs0] of crateRoots(level)) {
const segs = segs0.slice();
while (segs.length && SRC_DIRS.has(segs[segs.length - 1])) segs.pop();
dirOf.set(crate, segs);
if (segs.length > maxDepth) maxDepth = segs.length;
}
const res = { dirOf, maxDepth };
_crateDirsCache.set(level, res);
return res;
}
function maxCrateDepth(level) { return crateDirs(level).maxDepth; }
window.maxCrateDepth = maxCrateDepth;
const _fileDepthCache = new Map();
function maxFileDepth(level) {
if (_fileDepthCache.has(level)) return _fileDepthCache.get(level);
let m = 0;
for (const n of (unionGraph(level).nodes || [])) {
if (isExternalNode(n, level)) continue;
const d = relPathOf(n.id).split('/').length - 1; if (d > m) m = d;
}
_fileDepthCache.set(level, m);
return m;
}
window.maxFileDepth = maxFileDepth;
function digFloor(level) {
return -(viewTier(level) === 'file' ? maxFileDepth(level) : maxCrateDepth(level));
}
window.digFloor = digFloor;
function overviewBaseDig(level) {
return viewTier(level) === 'file' ? clampDig(digFloor(level) + 1) : 0;
}
window.overviewBaseDig = overviewBaseDig;
function clearGroupingCache() { _crateRootCache.clear(); _crateDirsCache.clear(); _fileDepthCache.clear(); }
window.clearGroupingCache = clearGroupingCache;
function groupKeyAtDig(level, n, dig) {
if (isExternalNode(n, level))
return (nodeKindSpec(level, n.kind).plural || 'external').toLowerCase();
const d = dig | 0;
const gk = levelUi(level).grouping?.key;
const crate = (viewTier(level) === 'crate' && gk) ? n[gk] : null;
const dirs = nodeDirSegs(n.id);
if (crate == null || crate === '') {
const keepN = Math.max(0, Math.min(dirs.length, maxFileDepth(level) + d));
const keep = dirs.slice(0, keepN);
return keep.length ? keep.join('/') : '_root';
}
if (d >= 0) {
const root = crateRoots(level).get(String(crate)) || [];
const underCrate = dirs.slice(root.length);
return [String(crate), ...underCrate.slice(0, d)].join('/');
}
const { dirOf, maxDepth } = crateDirs(level);
const path = dirOf.get(String(crate)) || [];
const cap = maxDepth + d;
const keep = path.slice(0, Math.max(0, cap));
return keep.length ? keep.join('/') : '_root';
}
function groupCountAtDig(level, dig) {
const d = dig | 0;
if (d > DIG_MAX || d < digFloor(level)) return null;
const keys = new Set();
for (const n of (unionGraph(level).nodes || [])) keys.add(groupKeyAtDig(level, n, d));
return keys.size;
}
window.groupCountAtDig = groupCountAtDig;
function grouperForDig(level, dig) {
return n => groupKeyAtDig(level, n, dig || 0);
}
window.grouperForDig = grouperForDig;
function groupLabel(level, key, dig) {
const d = dig | 0;
if (key === '_root') return '/'; if (viewTier(level) === 'file') return '/' + key;
if (d > 0) {
const cut = key.indexOf('/');
const crate = cut >= 0 ? key.slice(0, cut) : key;
const under = cut >= 0 ? key.slice(cut + 1).split('/') : [];
const root = crateRoots(level).get(crate) || [];
const crateD = crateDirs(level).dirOf.get(crate) || [];
const srcTail = root.slice(crateD.length); const full = [...crateD, ...srcTail, ...under]; return full.length ? '/' + full.join('/') : key;
}
if (d < 0) return key; return key.includes('/') ? key.slice(key.lastIndexOf('/') + 1) : key;
}
window.groupLabel = groupLabel;
function focusDirPath(level) {
const grp = window.drillGroup;
if (grp == null) return '';
const gOf = grouperForDig(level, window.drillDig ?? 0);
let common = null;
for (const n of (unionGraph(level).nodes || [])) {
if (gOf(n) !== grp) continue;
const segs = nodeDirSegs(n.id);
if (common === null) common = segs.slice();
else { let i = 0; while (i < common.length && i < segs.length && common[i] === segs[i]) i++; common.length = i; }
}
return (common && common.length) ? '/' + common.join('/') : '';
}
window.focusDirPath = focusDirPath;
function focusStripBase(level) {
const f = focusDirPath(level);
if (!f || f === '/') return '';
const i = f.lastIndexOf('/');
return i > 0 ? f.slice(0, i) : '';
}
window.focusStripBase = focusStripBase;
function stripDirPrefix(base, full) {
if (base && (full === base || full.startsWith(base + '/'))) return full.slice(base.length) || '/';
return full;
}
window.stripDirPrefix = stripDirPrefix;
function crateRelDir(level, n) {
const gk = levelUi(level).grouping?.key;
const segs = nodeDirSegs(n.id); const crate = gk ? n[gk] : null;
if (crate == null || crate === '') return segs.length ? '/' + segs.join('/') : '/';
const cdir = crateDirs(level).dirOf.get(String(crate)) || [];
const rel = segs.slice(cdir.length);
return rel.length ? '/' + rel.join('/') : '/';
}
window.crateRelDir = crateRelDir;
function nodeFullDir(n) {
const segs = nodeDirSegs(n.id); return segs.length ? '/' + segs.join('/') : '/';
}
window.nodeFullDir = nodeFullDir;
function crateKeyToFileKey(level, key) {
if (key == null || key === '_root') return '_root';
const cut = key.indexOf('/');
const crate = cut >= 0 ? key.slice(0, cut) : key;
const tail = cut >= 0 ? key.slice(cut + 1).split('/') : [];
const root = crateRoots(level).get(String(crate));
if (!root) return null; const full = [...root, ...tail];
return full.length ? full.join('/') : '_root';
}
window.crateKeyToFileKey = crateKeyToFileKey;
function fileKeyToCrateKey(level, key) {
if (key == null || key === '_root') return null; const segs = key.split('/');
let best = null, bestLen = -1;
for (const [crate, root] of crateRoots(level)) {
if (root.length > segs.length) continue;
let ok = true;
for (let i = 0; i < root.length; i++) if (root[i] !== segs[i]) { ok = false; break; }
if (ok && root.length > bestLen) { best = String(crate); bestLen = root.length; }
}
if (best == null) return null; const tail = segs.slice(bestLen);
return [best, ...tail].join('/');
}
window.fileKeyToCrateKey = fileKeyToCrateKey;
function aggCycleStatus(statuses) {
let b = false, c = false, both = false;
for (const s of statuses) {
if (s === 'both') both = true;
else if (s === 'baseline-only') b = true;
else if (s === 'current-only') c = true;
}
if (both || (b && c)) return 'both';
if (b) return 'baseline-only';
if (c) return 'current-only';
return 'none';
}