This commit is contained in:
33333-33333 2026-07-05 18:01:36 +09:00
commit 8f9f5b5100
108 changed files with 2139 additions and 1500 deletions

View file

@ -75,45 +75,6 @@ function lineageLayoutRowsFast(rows, idSet) {
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;
let fromY = Math.max(...parents.map(p => p.y + nodeH));
let 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("+");
const y = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2;
const x1 = left.x + nodeW;
const x2 = right.x;
if (x2 > x1) {
joinX = clamp(joinX, x1 + 8, x2 - 8);
fromY = y;
if (partnerKey && !seenPartners.has(partnerKey)) {
seenPartners.add(partnerKey);
partnerLinks.push(`M ${x1.toFixed(1)} ${y.toFixed(1)} L ${x2.toFixed(1)} ${y.toFixed(1)}`);
}
}
}
const laneY = Math.min(childY - 22, fromY + Math.max(36, (childY - fromY) * 0.54));
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);
@ -125,7 +86,11 @@ function buildLineageTreeLayout(component, family) {
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);
// 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));
@ -149,7 +114,7 @@ function lineageFamilyHtml(component, index, family, signature = "") {
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}">
const html = `<section class="lineage-family lineage-family-tree" data-lineage-key="${escapeHtml(cacheKey)}">
<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">
@ -163,6 +128,32 @@ function lineageFamilyHtml(component, index, family, signature = "") {
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".
@ -193,14 +184,8 @@ function renderArchive() {
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;
}
if (!lineageArchiveNeedsRender()) return;
if (lineageDeferArchiveIfOffscreen()) return;
const now = performance.now();
const minUpdateGap = 4800;
if (uiCache.archiveVersion && !uiCache.archiveRenderForce && now - (uiCache.archiveLastRenderedAt || 0) < minUpdateGap) {
@ -315,10 +300,13 @@ function resetArchiveRenderState() {
}
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?.();
@ -331,24 +319,13 @@ function scheduleArchiveWindowRender() {
return;
}
const familyDirty = Boolean(world.familyTreeDirty);
if (uiCache.archiveVersion && uiCache.archiveFamilyVersion === (world.familyVersion || 0) && !familyDirty) return;
if (!lineageArchiveNeedsRender()) 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 });
if (lineageDeferArchiveIfOffscreen()) return;
lineageScheduleArchiveIdle(1200);
}
function schedulePendingArchiveRender() {
@ -356,7 +333,7 @@ function schedulePendingArchiveRender() {
if (!uiCache.archivePendingOffscreen) return;
if (!lineageArchiveNearViewport()) return;
uiCache.archivePendingOffscreen = false;
scheduleArchiveWindowRender();
if (lineageArchiveNeedsRender()) scheduleArchiveWindowRender();
}
window.resetArchiveRenderState = resetArchiveRenderState;