duu
This commit is contained in:
parent
88f618cadf
commit
8f9f5b5100
108 changed files with 2139 additions and 1500 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue