This commit is contained in:
33333-33333 2026-07-01 16:55:49 +09:00
commit c9dd7182aa
35 changed files with 478 additions and 288 deletions

View file

@ -102,6 +102,24 @@ function lineagePartnerPathFromPositions(a, b, nodeW, nodeH, unionY, attachOffse
if (a.rowIndex !== b.rowIndex) return lineageCrossGenerationPartnerPath(a, b, nodeW, nodeH, unionY, context);
return lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA, attachOffsetB);
}
function lineageChildAnchorOnMarriageLine(partner, fallbackX, fallbackY) {
const horizontals = (partner?.segments || [])
.filter(seg => seg.kind === "h" && Math.abs(seg.x2 - seg.x1) > 10)
.sort((a, b) => {
const ty = Number.isFinite(partner?.unionY) ? partner.unionY : fallbackY;
const dy = Math.abs(a.y1 - ty) - Math.abs(b.y1 - ty);
if (Math.abs(dy) > 0.01) return dy;
return Math.abs(b.x2 - b.x1) - Math.abs(a.x2 - a.x1);
});
const seg = horizontals[0] || null;
if (!seg) return { x: fallbackX, y: fallbackY };
const minX = Math.min(seg.x1, seg.x2) + 8;
const maxX = Math.max(seg.x1, seg.x2) - 8;
const midX = (seg.x1 + seg.x2) / 2;
const x = minX <= maxX ? clamp(Number.isFinite(fallbackX) ? fallbackX : midX, minX, maxX) : midX;
return { x, y: seg.y1 };
}
function lineageVerticalBridgePath(x, y1, y2, bridges = [], radius = 8.5) {
if (!Number.isFinite(x) || !Number.isFinite(y1) || !Number.isFinite(y2)) return '';
@ -170,6 +188,44 @@ function lineageBuildChildPathGroup(group, nodeW, nodeH, bridgeHorizontals) {
return { d };
}
function lineageChildBusSpan(group, nodeW) {
const childCenters = (group.children || []).map(c => c.x + nodeW / 2);
if (!childCenters.length) return null;
return {
left: Math.min(group.joinX, ...childCenters),
right: Math.max(group.joinX, ...childCenters),
};
}
function lineageSpansOverlap(a, b, pad = 12) {
if (!a || !b) return false;
return Math.max(a.left, b.left) <= Math.min(a.right, b.right) + pad;
}
function lineageSeparateChildBusLanes(groupsInGap, nodeW) {
const lanes = [];
const ordered = (groupsInGap || [])
.filter(group => group && Number.isFinite(group.busY))
.map(group => ({ group, span: lineageChildBusSpan(group, nodeW), baseY: group.busY }))
.filter(entry => entry.span)
.sort((a, b) => a.baseY - b.baseY || a.span.left - b.span.left || a.span.right - b.span.right);
for (const entry of ordered) {
let laneIndex = 0;
while (lanes[laneIndex]?.some(prev => Math.abs(prev.group.busY - entry.baseY) < 12 && lineageSpansOverlap(prev.span, entry.span))) {
laneIndex += 1;
}
if (!lanes[laneIndex]) lanes[laneIndex] = [];
const fromY = Number(entry.group.fromY);
const childTop = Number(entry.group.childTop);
const minY = Number.isFinite(fromY) ? fromY + 26 : -Infinity;
const maxY = Number.isFinite(childTop) ? childTop - 18 : Infinity;
const offset = laneIndex * 9;
entry.group.busY = clamp(entry.baseY - offset, minY, maxY);
lanes[laneIndex].push(entry);
}
}
function lineageComponentSignature(component, family) {
family = lineageEdgeFamily(family);
const idSet = new Set(component.map(x => x?.id).filter(Boolean));
@ -281,7 +337,7 @@ function lineageAssignPartnerAttachOffsets(linkGroups) {
for (const [key, groups] of buckets.entries()) {
groups.sort((a, b) => a.joinX - b.joinX || a.childTop - b.childTop);
const n = groups.length;
const step = 9;
const step = 14;
groups.forEach((group, i) => {
const offset = n <= 1 ? 0 : (i - (n - 1) / 2) * step;
const [nodeId] = key.split(':');
@ -328,8 +384,8 @@ function lineageBuildLinks(nodes, idSet, layout) {
for (const groupsInGap of byGap.values()) {
groupsInGap.sort((a, b) => a.joinX - b.joinX || a.children.length - b.children.length);
const top = Math.min(...groupsInGap.map(g => g.parentBottom)) + 44;
const bottom = Math.max(...groupsInGap.map(g => g.childTop)) - 42;
const top = Math.min(...groupsInGap.map(g => g.parentBottom)) + 58;
const bottom = Math.max(...groupsInGap.map(g => g.childTop)) - 54;
const span = Math.max(16, bottom - top);
groupsInGap.forEach((group, i) => {
const laneY = top + span * ((i + 1) / (groupsInGap.length + 1));
@ -343,22 +399,21 @@ function lineageBuildLinks(nodes, idSet, layout) {
const attachB = 0;
const partnerY = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2;
const partner = lineagePartnerPathFromPositions(left, right, nodeW, nodeH, partnerY, attachA, attachB, layout);
if (Number.isFinite(partner.unionX)) group.joinX = partner.unionX;
const unionY = Number.isFinite(partner.unionY) ? partner.unionY : partnerY;
const clearY = clamp(group.parentBottom + 22 + i * 2, unionY + 10, group.childTop - 36);
group.fromY = clearY;
group.busY = group.children.length === 1 ? Math.min(group.childTop - 18, clearY + 28) : Math.min(group.childTop - 24, Math.max(clearY + 24, laneY + 18 + i * 3));
const anchor = lineageChildAnchorOnMarriageLine(partner, Number.isFinite(partner.unionX) ? partner.unionX : group.joinX, unionY);
group.joinX = anchor.x;
group.fromY = anchor.y;
group.busY = group.children.length === 1 ? Math.min(group.childTop - 22, anchor.y + 38) : Math.min(group.childTop - 28, Math.max(anchor.y + 34, laneY + 24 + i * 5));
if (partner.d) {
partnerLinks.push(partner.d);
horizontalSegments.push(...partner.segments.filter(seg => seg.kind === 'h'));
}
const trunk = lineageVerticalBridgePath(group.joinX, unionY, group.fromY, []);
if (trunk) childLinks.push(trunk);
} else {
group.fromY = group.parentBottom;
group.busY = group.children.length === 1 ? Math.min(group.childTop - 18, group.fromY + 30) : Math.min(group.childTop - 24, Math.max(group.fromY + 28, laneY + 18 + i * 3));
group.busY = group.children.length === 1 ? Math.min(group.childTop - 22, group.fromY + 38) : Math.min(group.childTop - 28, Math.max(group.fromY + 34, laneY + 24 + i * 5));
}
});
lineageSeparateChildBusLanes(groupsInGap, nodeW);
}
for (const groupsInGap of byGap.values()) {