"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 => ``) .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 renderArchive() { if (!ui.archiveContent) return; if (typeof archiveAutoUpdateEnabled === "function" && !archiveAutoUpdateEnabled()) { uiCache.archiveScheduled = false; if (!uiCache.archiveVersion) { ui.archiveContent.innerHTML = `
\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
`; } 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 = `
\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); uiCache.archiveRows = splitComponents; 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 (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;