400 lines
19 KiB
JavaScript
400 lines
19 KiB
JavaScript
"use strict";
|
|
|
|
function lineageSplitByActualEdges(component) {
|
|
const family = lineageEdgeFamily(world.family || {});
|
|
const nodes = Array.from(new Map(lineageCleanComponent(component, family).filter(Boolean).map(n => [n.id, n])).values());
|
|
const idSet = new Set(nodes.map(n => n.id));
|
|
const parts = window.TarinaiFamilyGraph.connectedComponents(nodes, {
|
|
sort: lineageNodeSort,
|
|
edgesOf: (n) => [
|
|
...lineageMutualParentIds(n, family, idSet),
|
|
...lineageMutualChildIds(n, family, idSet),
|
|
],
|
|
filter: part => part.length && part.some(x => x.alive),
|
|
});
|
|
return parts;
|
|
}
|
|
|
|
function lineageExpandComponent(component, family) {
|
|
family = lineageEdgeFamily(family);
|
|
const ids = new Set(component.filter(Boolean).map(n => n.id));
|
|
let changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const id of Array.from(ids)) {
|
|
const n = family[id];
|
|
if (!n) continue;
|
|
for (const p of lineageMutualParentIds(n, family)) {
|
|
if (!ids.has(p)) { ids.add(p); changed = true; }
|
|
}
|
|
for (const c of lineageMutualChildIds(n, family)) {
|
|
if (!ids.has(c)) { ids.add(c); changed = true; }
|
|
}
|
|
}
|
|
}
|
|
return Array.from(ids).map(id => family[id]).filter(Boolean);
|
|
}
|
|
|
|
function lineageCleanComponent(component, family) {
|
|
family = lineageEdgeFamily(family);
|
|
const expanded = lineageExpandComponent(component, family);
|
|
const idSet = new Set(expanded.map(n => n.id));
|
|
return expanded.filter(n => lineageMutualParentIds(n, family, idSet).length || lineageMutualChildIds(n, family, idSet).length);
|
|
}
|
|
|
|
function lineageFastLayoutNeeded(nodes, idSet, family) {
|
|
const edgeCount = nodes.reduce((sum, n) => sum + lineageParentIds(n, idSet, family).length, 0);
|
|
const maxGenerationWidth = Math.max(0, ...Array.from(new Set(nodes.map(n => n.generation || 1))).map(g => nodes.filter(n => (n.generation || 1) === g).length));
|
|
return {
|
|
edgeCount,
|
|
fast: nodes.length > 140 || edgeCount > 260 || maxGenerationWidth > 34,
|
|
};
|
|
}
|
|
|
|
function lineageLayoutRowsFast(rows, idSet) {
|
|
const NODE_W = 132;
|
|
const NODE_H = 84;
|
|
const SIBLING_GAP = 22;
|
|
const ROW_GAP = 116;
|
|
const LEFT_PAD = 82;
|
|
const TOP_PAD = 44;
|
|
const RIGHT_PAD = 48;
|
|
const BOTTOM_PAD = 30;
|
|
const positions = new Map();
|
|
let width = 420;
|
|
rows.forEach((row, rowIndex) => {
|
|
const y = TOP_PAD + rowIndex * (NODE_H + ROW_GAP);
|
|
let x = LEFT_PAD;
|
|
for (const n of row) {
|
|
positions.set(n.id, { x, y, rowIndex, node: n });
|
|
x += NODE_W + SIBLING_GAP;
|
|
}
|
|
width = Math.max(width, x - SIBLING_GAP + RIGHT_PAD);
|
|
});
|
|
const height = TOP_PAD + Math.max(0, rows.length - 1) * (NODE_H + ROW_GAP) + NODE_H + BOTTOM_PAD;
|
|
return { positions, width, height, nodeW: NODE_W, nodeH: NODE_H, topPad: TOP_PAD, rowGap: ROW_GAP, fastMode: true };
|
|
}
|
|
|
|
function lineageBuildLinksFast(nodes, idSet, layout) {
|
|
const { positions, nodeW, nodeH } = layout;
|
|
const partnerLinks = [];
|
|
const childLinks = [];
|
|
const seenPartners = new Set();
|
|
let childLinkGroups = 0;
|
|
let childLinkEdges = 0;
|
|
for (const child of nodes) {
|
|
const childPos = positions.get(child.id);
|
|
if (!childPos) continue;
|
|
const parents = lineageParentIds(child, idSet).map(id => positions.get(id)).filter(Boolean).sort((a, b) => a.x - b.x);
|
|
if (!parents.length) continue;
|
|
childLinkGroups += 1;
|
|
childLinkEdges += parents.length;
|
|
const childX = childPos.x + nodeW / 2;
|
|
const childY = childPos.y;
|
|
const fromY = Math.max(...parents.map(p => p.y + nodeH));
|
|
const joinX = parents.reduce((sum, p) => sum + p.x + nodeW / 2, 0) / parents.length;
|
|
if (parents.length >= 2) {
|
|
const left = parents[0];
|
|
const right = parents[parents.length - 1];
|
|
const partnerKey = parents.map(p => p.node?.id).filter(Boolean).sort().join("+");
|
|
if (partnerKey && !seenPartners.has(partnerKey)) {
|
|
seenPartners.add(partnerKey);
|
|
const y = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2;
|
|
partnerLinks.push(`M ${(left.x + nodeW).toFixed(1)} ${y.toFixed(1)} L ${right.x.toFixed(1)} ${y.toFixed(1)}`);
|
|
}
|
|
}
|
|
const laneY = Math.min(childY - 18, fromY + Math.max(28, (childY - fromY) * 0.48));
|
|
childLinks.push(`M ${joinX.toFixed(1)} ${fromY.toFixed(1)} L ${joinX.toFixed(1)} ${laneY.toFixed(1)} L ${childX.toFixed(1)} ${laneY.toFixed(1)} L ${childX.toFixed(1)} ${childY.toFixed(1)}`);
|
|
}
|
|
return { partnerLinks, childLinks, childLinkGroups, childLinkEdges };
|
|
}
|
|
|
|
function buildLineageTreeLayout(component, family) {
|
|
family = lineageEdgeFamily(family);
|
|
const cleaned = lineageCleanComponent(component, family);
|
|
const nodes = Array.from(new Map(cleaned.filter(Boolean).map(n => [n.id, n])).values()).sort(lineageNodeSort);
|
|
const idToNode = new Map(nodes.map(n => [n.id, n]));
|
|
const idSet = new Set(idToNode.keys());
|
|
const generations = Array.from(new Set(nodes.map(n => n.generation || 1))).sort((a, b) => a - b);
|
|
const rows = lineageRowsForLayout(nodes, generations, idToNode, idSet);
|
|
const perf = lineageFastLayoutNeeded(nodes, idSet, family);
|
|
const layout = perf.fast ? lineageLayoutRowsFast(rows, idSet) : lineageLayoutRows(rows, idSet);
|
|
const links = perf.fast ? lineageBuildLinksFast(nodes, idSet, layout) : lineageBuildLinks(nodes, idSet, layout);
|
|
|
|
const labels = generations.map((g, rowIndex) => ({ generation: g, y: layout.topPad + rowIndex * (layout.nodeH + layout.rowGap) + 18 }));
|
|
const renderedNodes = nodes.map(n => ({ node: n, ...(layout.positions.get(n.id) || {}) })).filter(n => Number.isFinite(n.x) && Number.isFinite(n.y));
|
|
return { ...layout, labels, nodes: renderedNodes, childLinks: links.childLinks, partnerLinks: links.partnerLinks, childLinkGroups: links.childLinkGroups, childLinkEdges: links.childLinkEdges, fastMode: perf.fast, edgeCount: perf.edgeCount };
|
|
}
|
|
|
|
function lineageFamilyHtml(component, index, family, signature = "") {
|
|
const uniqueComponent = Array.from(new Map(component.filter(Boolean).map(n => [n.id, n])).values()).sort(lineageNodeSort);
|
|
const cacheKey = lineageFamilyCacheKey(uniqueComponent, family, signature);
|
|
if (uiCache.archiveHtmlCache?.has(cacheKey)) return lineageHydrateFamilyHtml(uiCache.archiveHtmlCache.get(cacheKey), index);
|
|
const layout = buildLineageTreeLayout(uniqueComponent, family);
|
|
const aliveCount = uniqueComponent.filter(n => n.alive).length;
|
|
const childEdgeCount = lineageExpectedChildEdgeCount(uniqueComponent, family);
|
|
const maskId = "{{LINEAGE_MASK_ID}}";
|
|
const nodeMaskRects = layout.nodes
|
|
.map(p => `<rect x="${(p.x + 1).toFixed(1)}" y="${(p.y + 1).toFixed(1)}" width="${(layout.nodeW - 2).toFixed(1)}" height="${(layout.nodeH - 2).toFixed(1)}" rx="9" fill="black"></rect>`)
|
|
.join("");
|
|
const labels = layout.labels.map(label => `<div class="tree-generation-label" style="top:${Math.round(label.y)}px">\u4e16\u4ee3 ${label.generation}</div>`).join("");
|
|
const partnerPaths = layout.partnerLinks.map(d => `<path class="partner-link" d="${d}"></path>`).join("");
|
|
const childPaths = layout.childLinks.map(d => `<path class="child-link" d="${d}"></path>`).join("");
|
|
const nodeHtml = layout.nodes.map(p => lineageTreeNodeHtml(p.node, p.x, p.y)).join("");
|
|
const empty = !layout.childLinks.length ? `<div class="lineage-empty-family">\u89aa\u5b50\u95a2\u4fc2\u306e\u7dda\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002</div>` : "";
|
|
|
|
const html = `<section class="lineage-family lineage-family-tree" data-lineage-index="{{LINEAGE_INDEX}}" data-lineage-key="${escapeHtml(cacheKey)}" data-lineage-child-edges="${childEdgeCount}" data-lineage-rendered-child-edges="${layout.childLinkEdges || 0}" data-lineage-child-groups="${layout.childLinkGroups || 0}" data-lineage-child-paths="${layout.childLinks.length}">
|
|
<h3>\u5bb6\u7cfb {{LINEAGE_INDEX}} / ${uniqueComponent.length}\u5339 / \u751f\u5b58 ${aliveCount}</h3>
|
|
<div class="lineage-tree-panel" style="width:${Math.ceil(layout.width)}px;height:${Math.ceil(layout.height)}px">
|
|
<svg class="lineage-links" viewBox="0 0 ${Math.ceil(layout.width)} ${Math.ceil(layout.height)}" aria-hidden="true">
|
|
<defs><mask id="${maskId}" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="${Math.ceil(layout.width)}" height="${Math.ceil(layout.height)}" fill="white"></rect>${nodeMaskRects}</mask></defs>
|
|
<g mask="url(#${maskId})">${partnerPaths}${childPaths}</g>
|
|
</svg>
|
|
${labels}${nodeHtml}${empty}
|
|
</div>
|
|
</section>`;
|
|
uiCache.archiveHtmlCache?.set(cacheKey, html);
|
|
return lineageHydrateFamilyHtml(html, index);
|
|
}
|
|
|
|
function renderArchive() {
|
|
if (!ui.archiveContent) return;
|
|
if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) {
|
|
uiCache.archiveScheduled = false;
|
|
if (!uiCache.archiveVersion) {
|
|
ui.archiveContent.innerHTML = `<div class="selected-info empty">\u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u306fOFF\u3067\u3059\u3002\u300c\u66f4\u65b0\u300d\u3092ON\u306b\u3059\u308b\u3068\u63cf\u753b\u3057\u307e\u3059\u3002</div>`;
|
|
} else if (world.familyTreeDirty) {
|
|
lineageSetArchiveStaleStatus(true);
|
|
}
|
|
return;
|
|
}
|
|
if (uiCache.deferArchiveUntil && performance.now() < uiCache.deferArchiveUntil) return;
|
|
uiCache.deferArchiveUntil = 0;
|
|
uiCache.archiveScheduled = false;
|
|
const scrollState = lineageCaptureArchiveScroll();
|
|
const familyVersion = world.familyVersion || 0;
|
|
const familyDirty = Boolean(world.familyTreeDirty);
|
|
if (uiCache.archiveRenderRunning) {
|
|
if (familyDirty) uiCache.archivePendingDirtyAfterRun = true;
|
|
return;
|
|
}
|
|
if (uiCache.archiveVersion && uiCache.archiveFamilyVersion === familyVersion && !familyDirty) return;
|
|
if (uiCache.archiveVersion && !lineageArchiveNearViewport()) {
|
|
uiCache.archivePendingOffscreen = true;
|
|
const hiddenInterval = 30000;
|
|
const hiddenNow = performance.now();
|
|
if (hiddenNow - (uiCache.archiveLastHiddenCheckAt || -Infinity) < hiddenInterval) return;
|
|
uiCache.archiveLastHiddenCheckAt = hiddenNow;
|
|
}
|
|
const now = performance.now();
|
|
const minUpdateGap = 4800;
|
|
if (uiCache.archiveVersion && !uiCache.archiveRenderForce && now - (uiCache.archiveLastRenderedAt || 0) < minUpdateGap) {
|
|
if (!uiCache.archiveRenderTimer) {
|
|
uiCache.archiveRenderTimer = setTimeout(() => {
|
|
uiCache.archiveRenderTimer = 0;
|
|
uiCache.archiveRenderForce = true;
|
|
renderArchive();
|
|
uiCache.archiveRenderForce = false;
|
|
}, Math.max(80, minUpdateGap - (now - (uiCache.archiveLastRenderedAt || 0))));
|
|
}
|
|
return;
|
|
}
|
|
if (world.familyPrunePending && world.pruneExtinctFamilies) {
|
|
world.familyPrunePending = false;
|
|
world.pruneExtinctFamilies();
|
|
}
|
|
const family = world.family || {};
|
|
const version = String(world.familyVersion || 0);
|
|
if (uiCache.archiveVersion === version) {
|
|
world.familyTreeDirty = false;
|
|
uiCache.archivePendingDirtyAfterRun = false;
|
|
return;
|
|
}
|
|
const renderStarted = performance.now();
|
|
lineageSetArchiveStaleStatus(false);
|
|
if (uiCache.archiveRenderTimer) {
|
|
clearTimeout(uiCache.archiveRenderTimer);
|
|
uiCache.archiveRenderTimer = 0;
|
|
}
|
|
uiCache.archiveVersion = version;
|
|
uiCache.archiveFamilyVersion = world.familyVersion || 0;
|
|
uiCache.archiveLastScheduleFamilyVersion = world.familyVersion || 0;
|
|
uiCache.archiveLastRenderedAt = performance.now();
|
|
uiCache.lastArchiveWindow = "";
|
|
if (!uiCache.archiveHtmlCache) uiCache.archiveHtmlCache = new Map();
|
|
const activeCacheKeys = new Set();
|
|
const token = `${version}:${Math.random().toString(36).slice(2)}`;
|
|
uiCache.archiveRenderToken = token;
|
|
uiCache.archiveRenderRunning = true;
|
|
ui.archiveContent.innerHTML = `<div class="lineage-render-status">\u5bb6\u7cfb\u56f3\u3092\u6e96\u5099\u4e2d\u2026</div><div class="lineage-forest"></div>`;
|
|
lineageRestoreArchiveScroll(scrollState);
|
|
const status = ui.archiveContent.querySelector(".lineage-render-status");
|
|
const forest = ui.archiveContent.querySelector(".lineage-forest");
|
|
const renderComponents = (components) => {
|
|
if (uiCache.archiveRenderToken !== token || !forest) {
|
|
uiCache.archiveRenderRunning = false;
|
|
return;
|
|
}
|
|
const splitComponents = components.flatMap(lineageSplitByActualEdges).filter(c => c.length);
|
|
uiCache.archiveRows = splitComponents;
|
|
if (!splitComponents.length) {
|
|
ui.archiveContent.innerHTML = `<div class="selected-info empty">\u751f\u304d\u3066\u3044\u308b\u69cb\u6210\u54e1\u3092\u6301\u3064\u5bb6\u7cfb\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002</div>`;
|
|
lineageRestoreArchiveScroll(scrollState);
|
|
uiCache.archiveRenderRunning = false;
|
|
world.familyTreeDirty = false;
|
|
uiCache.archivePendingDirtyAfterRun = false;
|
|
return;
|
|
}
|
|
if (status) status.textContent = `\u5bb6\u7cfb\u56f3\u3092\u63cf\u753b\u4e2d\u2026 0 / ${splitComponents.length}`;
|
|
let index = 0;
|
|
const pump = () => {
|
|
if (uiCache.archiveRenderToken !== token || !forest) {
|
|
uiCache.archiveRenderRunning = false;
|
|
return;
|
|
}
|
|
const started = performance.now();
|
|
let nodeBudget = 28;
|
|
let renderedThisSlice = 0;
|
|
while (index < splitComponents.length) {
|
|
const component = splitComponents[index];
|
|
if (renderedThisSlice > 0 && component.length > nodeBudget) break;
|
|
const signature = lineageComponentSignature(component, family);
|
|
activeCacheKeys.add(lineageFamilyCacheKey(component, family, signature));
|
|
forest.insertAdjacentHTML("beforeend", lineageFamilyHtml(component, index, family, signature));
|
|
nodeBudget -= Math.max(1, component.length);
|
|
index += 1;
|
|
renderedThisSlice += 1;
|
|
if (index < splitComponents.length && (nodeBudget <= 0 || performance.now() - started > 6)) break;
|
|
}
|
|
lineageRestoreArchiveScroll(scrollState);
|
|
if (status) status.textContent = `\u5bb6\u7cfb\u56f3\u3092\u63cf\u753b\u4e2d\u2026 ${index} / ${splitComponents.length}`;
|
|
if (index < splitComponents.length) {
|
|
lineageIdleSchedule(pump, 220);
|
|
} else {
|
|
if (status) status.remove();
|
|
for (const key of Array.from(uiCache.archiveHtmlCache.keys())) {
|
|
if (!activeCacheKeys.has(key)) uiCache.archiveHtmlCache.delete(key);
|
|
}
|
|
lineageRestoreArchiveScroll(scrollState);
|
|
const renderDirtiedAgain = uiCache.archivePendingDirtyAfterRun || (world.familyVersion || 0) !== familyVersion;
|
|
uiCache.archiveRenderRunning = false;
|
|
world.familyTreeDirty = renderDirtiedAgain;
|
|
if (uiCache.archiveDiagnostics) {
|
|
uiCache.archiveDiagnostics.renders = (uiCache.archiveDiagnostics.renders || 0) + 1;
|
|
uiCache.archiveDiagnostics.lastMs = Math.round((performance.now() - renderStarted) * 10) / 10;
|
|
uiCache.archiveDiagnostics.lastFamilies = splitComponents.length;
|
|
uiCache.archiveDiagnostics.lastNodes = splitComponents.reduce((sum, component) => sum + component.length, 0);
|
|
}
|
|
if (renderDirtiedAgain) {
|
|
uiCache.archivePendingDirtyAfterRun = false;
|
|
scheduleArchiveWindowRender();
|
|
}
|
|
}
|
|
};
|
|
lineageIdleSchedule(pump, 160);
|
|
};
|
|
lineageBuildComponentsIdle(
|
|
family,
|
|
token,
|
|
(label, done, total) => {
|
|
if (status) status.textContent = `${label}\u2026 ${Math.min(done || 0, total || 0)} / ${total || 0}`;
|
|
},
|
|
renderComponents
|
|
);
|
|
}
|
|
|
|
function resetArchiveRenderState() {
|
|
if (uiCache.archiveRenderTimer) {
|
|
clearTimeout(uiCache.archiveRenderTimer);
|
|
uiCache.archiveRenderTimer = 0;
|
|
}
|
|
uiCache.archiveRenderToken = "";
|
|
uiCache.archiveRenderRunning = false;
|
|
uiCache.archiveScheduled = false;
|
|
uiCache.archivePendingOffscreen = false;
|
|
uiCache.archivePendingWhileRunning = false;
|
|
uiCache.archivePendingDirtyAfterRun = false;
|
|
uiCache.archiveLastHiddenCheckAt = -Infinity;
|
|
uiCache.archiveLastScheduleFamilyVersion = null;
|
|
uiCache.archiveVersion = "";
|
|
uiCache.archiveFamilyVersion = null;
|
|
uiCache.archiveRelationSignature = "";
|
|
uiCache.archiveRows = [];
|
|
uiCache.lastArchiveWindow = "";
|
|
uiCache.archiveHtmlCache?.clear?.();
|
|
}
|
|
|
|
function validateFamilyTree() {
|
|
const issues = world.validateFamily ? [...world.validateFamily()] : [];
|
|
const rendered = new Map();
|
|
document.querySelectorAll("[data-family-tarinai-id]").forEach(node => {
|
|
const id = node.getAttribute("data-family-tarinai-id");
|
|
rendered.set(id, (rendered.get(id) || 0) + 1);
|
|
});
|
|
for (const [id, count] of rendered.entries()) {
|
|
if (count > 1) issues.push({ type: "duplicate-render-node", id, count });
|
|
}
|
|
for (const component of uiCache.archiveRows || []) {
|
|
for (const n of component || []) {
|
|
if (n?.id && !rendered.has(n.id)) issues.push({ type: "missing-render-node", id: n.id });
|
|
}
|
|
}
|
|
document.querySelectorAll(".lineage-family-tree[data-lineage-child-edges]").forEach((section, index) => {
|
|
const expectedEdges = Number(section.getAttribute("data-lineage-child-edges") || 0);
|
|
const renderedEdges = Number(section.getAttribute("data-lineage-rendered-child-edges") || 0);
|
|
const expectedGroups = Number(section.getAttribute("data-lineage-child-groups") || 0);
|
|
const renderedPaths = Number(section.getAttribute("data-lineage-child-paths") || 0);
|
|
const pathCount = section.querySelectorAll(".lineage-links .child-link").length;
|
|
if (renderedEdges !== expectedEdges) {
|
|
issues.push({ type: "render-child-edge-count-mismatch", familyIndex: index, expectedEdges, renderedEdges });
|
|
}
|
|
if (expectedEdges > 0 && pathCount < 1) {
|
|
issues.push({ type: "missing-render-child-link", familyIndex: index, expectedEdges, pathCount });
|
|
}
|
|
if (expectedGroups > 0 && pathCount < expectedGroups) {
|
|
issues.push({ type: "missing-render-child-link-group", familyIndex: index, expectedGroups, pathCount });
|
|
}
|
|
if (renderedPaths !== pathCount) {
|
|
issues.push({ type: "render-child-path-count-mismatch", familyIndex: index, renderedPaths, pathCount });
|
|
}
|
|
});
|
|
return issues;
|
|
}
|
|
function scheduleArchiveWindowRender() {
|
|
if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) {
|
|
uiCache.archiveScheduled = false;
|
|
if (world.familyTreeDirty) lineageSetArchiveStaleStatus(true);
|
|
return;
|
|
}
|
|
const familyDirty = Boolean(world.familyTreeDirty);
|
|
if (uiCache.archiveVersion && uiCache.archiveFamilyVersion === (world.familyVersion || 0) && !familyDirty) return;
|
|
if (uiCache.archiveRenderRunning) {
|
|
if (familyDirty) uiCache.archivePendingDirtyAfterRun = true;
|
|
return;
|
|
}
|
|
if (uiCache.archiveVersion && !lineageArchiveNearViewport()) {
|
|
uiCache.archivePendingOffscreen = true;
|
|
const now = performance.now();
|
|
if (now - (uiCache.archiveLastHiddenCheckAt || -Infinity) < 30000) return;
|
|
uiCache.archiveLastHiddenCheckAt = now;
|
|
}
|
|
if (uiCache.archiveRenderTimer) return;
|
|
if (uiCache.archiveScheduled) return;
|
|
uiCache.archiveScheduled = true;
|
|
const schedule = window.requestIdleCallback || ((fn) => setTimeout(fn, 0));
|
|
schedule(() => {
|
|
renderArchive();
|
|
}, { timeout: 1200 });
|
|
}
|
|
|
|
function schedulePendingArchiveRender() {
|
|
if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) return;
|
|
if (!uiCache.archivePendingOffscreen) return;
|
|
if (!lineageArchiveNearViewport()) return;
|
|
uiCache.archivePendingOffscreen = false;
|
|
scheduleArchiveWindowRender();
|
|
}
|
|
|
|
window.validateFamilyTree = validateFamilyTree;
|
|
window.familyTreeDiagnostics = () => ({ ...(uiCache.archiveDiagnostics || {}) });
|
|
window.resetArchiveRenderState = resetArchiveRenderState;
|