function specSnap() {
return (window.viewSide === 'baseline')
? (window.BASELINE ?? window.CURRENT)
: (window.CURRENT ?? window.BASELINE);
}
function levelSpec(level) {
return specSnap()?.graphs?.[level] || {};
}
function attrSpec(level, key) { return levelSpec(level).node_attributes?.[key] || {}; }
function edgeAttrSpec(level, k) { return levelSpec(level).edge_attributes?.[k] || {}; }
function attrLabel(level, key) { const s = attrSpec(level, key); return s.label || key; }
function attrName(level, key) { const s = attrSpec(level, key); return s.name || s.label || key; }
function attrShort(level, key) { const s = attrSpec(level, key); return s.short || s.label || key; }
function attrDesc(level, key) { return attrSpec(level, key).description || ''; }
function attrFormula(level, key){ return attrSpec(level, key).formula || ''; }
function attrType(level, key) { return attrSpec(level, key).value_type || 'str'; }
function attrGroup(level, key) { return attrSpec(level, key).group || null; }
function attrThresholds(level, key) { return attrSpec(level, key).thresholds || null; }
function attrAbbrev(level, key) { return attrSpec(level, key).abbreviate === true; }
function attrDirection(level, key) { return attrSpec(level, key).direction || null; }
function numericAttrKeys(level) {
const na = levelSpec(level).node_attributes || {};
return Object.keys(na).filter(k => na[k].value_type === 'int' || na[k].value_type === 'float');
}
function nodeKindSpec(level, kind) { return levelSpec(level).node_kinds?.[kind] || {}; }
function isExternalNode(node, level) {
if (!node) return false;
if (node.external === true) return true; return nodeKindSpec(level, node.kind).external === true; }
function externalIdSet(graph, level) {
return new Set((graph?.nodes || []).filter(n => isExternalNode(n, level)).map(n => n.id));
}
function edgeIsFlow(level, kind) {
const s = levelSpec(level).edge_kinds?.[kind];
return s ? s.flow !== false : true;
}
function edgeKindLabel(level, kind) { return levelSpec(level).edge_kinds?.[kind]?.label || kind; }
function edgeKindDesc(level, kind) { return levelSpec(level).edge_kinds?.[kind]?.description || ''; }
function cycleKindLabel(level, kind) { return levelSpec(level).cycle_kinds?.[kind]?.label || kind; }
function cycleKindDesc(level, kind) { return levelSpec(level).cycle_kinds?.[kind]?.description || ''; }
function levelUi(level) { return levelSpec(level).ui || {}; }
function attributeGroups(level){ return levelSpec(level).attribute_groups || {}; }
function snapshotPresets() { return specSnap()?.presets || []; }
function evalCalc(calc, node, keys) {
if (!calc) return null;
try {
const vals = keys.map(k => Number(node[k] ?? 0));
const fn = Function('Math', ...keys, `return (${calc});`);
const r = fn(Math, ...vals);
return typeof r === 'number' && isFinite(r) ? r : null;
} catch {
return null;
}
}
function calcNum(v) {
if (typeof v !== 'number' || !isFinite(v)) return String(v);
if (v === Math.round(v)) return String(v);
return String(Math.round(v * 1000) / 1000);
}
function calcDisplay(level, key, node) {
const spec = attrSpec(level, key);
if (!spec.calc) return '';
const keys = numericAttrKeys(level);
const result = evalCalc(spec.calc, node, keys);
if (result === null) return '';
let shown = spec.formula || spec.calc;
for (const k of keys) {
if (shown.includes(k)) {
shown = shown.replace(new RegExp(`\\b${k}\\b`, 'g'), calcNum(Number(node[k] ?? 0)));
}
}
return `${shown} = ${calcNum(result)}`;
}
function nodeAttr(node, key) {
const v = node?.[key];
return v === undefined ? null : v;
}