"use strict";
function escapeHtml(value) { return window.TarinaiUIHelpers.htmlEscape(value); }
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 = 150;
const NODE_H = 104;
const SIBLING_GAP = 18;
const ROW_GAP = 68;
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 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);
// Keep the compact fast layout for very large families, but still use the
// robust routed edge builder. The old fast edge builder emitted straight
// child and partner lines, which could be masked behind unrelated nodes and
// look like disconnected line fragments in complex trees.
const links = 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 => ``)
.join("");
const labels = layout.labels.map(label => `
\u4e16\u4ee3 ${label.generation}
`).join("");
const partnerPaths = layout.partnerLinks.map(d => ``).join("");
const childPaths = layout.childLinks.map(d => ``).join("");
const nodeHtml = layout.nodes.map(p => lineageTreeNodeHtml(p.node, p.x, p.y)).join("");
const empty = !layout.childLinks.length ? `\u89aa\u5b50\u95a2\u4fc2\u306e\u7dda\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002
` : "";
const html = `
\u5bb6\u7cfb {{LINEAGE_INDEX}} / ${uniqueComponent.length}\u5339 / \u751f\u5b58 ${aliveCount}
${labels}${nodeHtml}${empty}
`;
uiCache.archiveHtmlCache?.set(cacheKey, html);
return lineageHydrateFamilyHtml(html, index);
}
function lineageArchiveNeedsRender() {
const familyVersion = world.familyVersion || 0;
const familyDirty = Boolean(world.familyTreeDirty);
return !uiCache.archiveVersion || uiCache.archiveFamilyVersion !== familyVersion || familyDirty;
}
function lineageDeferArchiveIfOffscreen(hiddenInterval = 30000) {
if (!uiCache.archiveVersion || lineageArchiveNearViewport()) return false;
uiCache.archivePendingOffscreen = true;
const now = performance.now();
if (now - (uiCache.archiveLastHiddenCheckAt || -Infinity) < hiddenInterval) return true;
uiCache.archiveLastHiddenCheckAt = now;
return false;
}
function lineageScheduleArchiveIdle(timeout = 1200) {
if (uiCache.archiveRenderTimer || uiCache.archiveScheduled) return false;
uiCache.archiveScheduled = true;
const schedule = window.requestIdleCallback || ((fn) => setTimeout(fn, 0));
schedule(() => {
renderArchive();
}, { timeout });
return true;
}
function renderArchive() {
if (!ui.archiveContent) return;
// Auto update OFF now means "do not keep refreshing", not "hide the tree".
// Draw the current snapshot once so the family tree is always available under
// the event UI, then mark it stale when the family data changes.
if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) {
uiCache.archiveScheduled = false;
if (uiCache.archiveVersion && world.familyTreeDirty) {
const hasRenderedGraph = Boolean(ui.archiveContent.querySelector(".lineage-family-tree, [data-family-tarinai-id]"));
const familyHasEdges = Object.values(world.family || {}).some(n => (n?.parents || []).length || (n?.children || []).length);
// If the first snapshot was empty, keep OFF but allow the first real
// family graph to appear. Subsequent changes are marked stale.
if (!hasRenderedGraph && familyHasEdges) {
resetArchiveRenderState();
} else {
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 (!lineageArchiveNeedsRender()) return;
if (lineageDeferArchiveIfOffscreen()) return;
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;
}
lineageSetArchiveStaleStatus(false);
if (uiCache.archiveRenderTimer) {
clearTimeout(uiCache.archiveRenderTimer);
uiCache.archiveRenderTimer = 0;
}
uiCache.archiveVersion = version;
uiCache.archiveFamilyVersion = world.familyVersion || 0;
uiCache.archiveLastRenderedAt = performance.now();
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 = `\u5bb6\u7cfb\u56f3\u3092\u6e96\u5099\u4e2d\u2026
`;
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);
if (!splitComponents.length) {
ui.archiveContent.innerHTML = `\u751f\u304d\u3066\u3044\u308b\u69cb\u6210\u54e1\u3092\u6301\u3064\u5bb6\u7cfb\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002
`;
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 (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.archiveRenderForce = false;
uiCache.archiveScheduled = false;
uiCache.archivePendingOffscreen = false;
uiCache.archivePendingDirtyAfterRun = false;
uiCache.archiveLastHiddenCheckAt = -Infinity;
uiCache.archiveLastRenderedAt = 0;
uiCache.deferArchiveUntil = 0;
uiCache.archiveVersion = "";
uiCache.archiveFamilyVersion = null;
uiCache.archiveHtmlCache?.clear?.();
}
function scheduleArchiveWindowRender() {
if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) {
uiCache.archiveScheduled = false;
if (world.familyTreeDirty) lineageSetArchiveStaleStatus(true);
return;
}
const familyDirty = Boolean(world.familyTreeDirty);
if (!lineageArchiveNeedsRender()) return;
if (uiCache.archiveRenderRunning) {
if (familyDirty) uiCache.archivePendingDirtyAfterRun = true;
return;
}
if (lineageDeferArchiveIfOffscreen()) return;
lineageScheduleArchiveIdle(1200);
}
function schedulePendingArchiveRender() {
if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) return;
if (!uiCache.archivePendingOffscreen) return;
if (!lineageArchiveNearViewport()) return;
uiCache.archivePendingOffscreen = false;
if (lineageArchiveNeedsRender()) scheduleArchiveWindowRender();
}
window.resetArchiveRenderState = resetArchiveRenderState;