"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;