1.6
This commit is contained in:
parent
ee22e1a929
commit
88f618cadf
55 changed files with 1299 additions and 447 deletions
|
|
@ -42,6 +42,66 @@ function lineageVerticalLaneClear(x, y1, y2, rects) {
|
|||
return !(rects || []).some(r => x > r.left && x < r.right && Math.max(top, r.top) < Math.min(bottom, r.bottom));
|
||||
}
|
||||
|
||||
function lineageRouteNodeRects(positions, nodeW, nodeH, excludeIds = new Set(), pad = 8) {
|
||||
const rects = [];
|
||||
for (const pos of positions?.values?.() || []) {
|
||||
const id = pos?.node?.id;
|
||||
if (!pos || (id && excludeIds.has(id))) continue;
|
||||
rects.push({
|
||||
left: pos.x - pad,
|
||||
right: pos.x + nodeW + pad,
|
||||
top: pos.y - pad,
|
||||
bottom: pos.y + nodeH + pad,
|
||||
});
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
function lineageHorizontalLaneClear(y, x1, x2, rects) {
|
||||
const left = Math.min(x1, x2);
|
||||
const right = Math.max(x1, x2);
|
||||
return !(rects || []).some(r => y > r.top && y < r.bottom && Math.max(left, r.left) < Math.min(right, r.right));
|
||||
}
|
||||
|
||||
function lineageLanePenalty(y, x1, x2, rects) {
|
||||
let penalty = 0;
|
||||
for (const r of rects || []) {
|
||||
const overlap = Math.max(0, Math.min(Math.max(x1, x2), r.right) - Math.max(Math.min(x1, x2), r.left));
|
||||
if (overlap <= 0) continue;
|
||||
if (y > r.top && y < r.bottom) penalty += 100000 + overlap;
|
||||
else penalty += Math.max(0, 26 - Math.min(Math.abs(y - r.top), Math.abs(y - r.bottom))) * 3;
|
||||
}
|
||||
return penalty;
|
||||
}
|
||||
|
||||
function lineageChooseHorizontalDetourY(y, x1, x2, a, b, nodeW, nodeH, rects, preferredY = NaN, context = {}) {
|
||||
const rowTop = Math.min(a.y, b.y);
|
||||
const rowBottom = Math.max(a.y, b.y) + nodeH;
|
||||
const minY = Math.max(8, Number(context.minRouteY ?? 10) || 10);
|
||||
const maxY = Number.isFinite(Number(context.height)) ? Math.max(minY, Number(context.height) - 8) : Number.POSITIVE_INFINITY;
|
||||
const candidates = [];
|
||||
if (Number.isFinite(preferredY)) candidates.push(preferredY);
|
||||
// Escaping above the first generation is allowed, but keep the route inside
|
||||
// the visible SVG instead of letting it disappear above the canvas.
|
||||
candidates.push(rowTop - 48, rowTop - 32, rowTop - 18, rowBottom + 18, rowBottom + 32, rowBottom + 48);
|
||||
const unique = Array.from(new Set(candidates
|
||||
.map(v => clamp(Math.round(v * 10) / 10, minY, maxY))))
|
||||
.filter(Number.isFinite);
|
||||
unique.sort((ya, yb) => {
|
||||
const ca = lineageLanePenalty(ya, x1, x2, rects) + Math.abs(ya - y) * 0.08;
|
||||
const cb = lineageLanePenalty(yb, x1, x2, rects) + Math.abs(yb - y) * 0.08;
|
||||
return ca - cb;
|
||||
});
|
||||
return unique[0] ?? clamp(y, minY, maxY);
|
||||
}
|
||||
|
||||
function lineageAutoSideOffsetForLane(laneY, rowTop, rowBottom, nodeH) {
|
||||
if (!Number.isFinite(laneY)) return 0;
|
||||
if (laneY < rowTop - 2) return -nodeH * 0.30;
|
||||
if (laneY > rowBottom + 2) return nodeH * 0.30;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function lineageCrossGenerationPartnerLane(a, b, nodeW, nodeH, context = {}) {
|
||||
const ay = a.y + nodeH / 2;
|
||||
const by = b.y + nodeH / 2;
|
||||
|
|
@ -76,31 +136,53 @@ function lineageCrossGenerationPartnerPath(a, b, nodeW, nodeH, unionY, context =
|
|||
return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(points), unionX: railX, unionY: Math.max(aMidY, bMidY) };
|
||||
}
|
||||
|
||||
function lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0) {
|
||||
function lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0, context = {}) {
|
||||
const left = a.x <= b.x ? a : b;
|
||||
const right = left === a ? b : a;
|
||||
const midYLeft = left.y + nodeH / 2 + attachOffsetA;
|
||||
const midYRight = right.y + nodeH / 2 + attachOffsetB;
|
||||
let leftOffset = left === a ? attachOffsetA : attachOffsetB;
|
||||
let rightOffset = right === a ? attachOffsetA : attachOffsetB;
|
||||
const x1 = left.x + nodeW;
|
||||
const x2 = right.x;
|
||||
if (x2 <= x1) return { d: "", segments: [] };
|
||||
const direct = left.rowIndex === right.rowIndex && x2 - x1 <= 156 && Math.abs(midYLeft - midYRight) < 10
|
||||
const exclude = new Set([left.node?.id, right.node?.id].filter(Boolean));
|
||||
const rects = lineageRouteNodeRects(context.positions, nodeW, nodeH, exclude, 10);
|
||||
let midYLeft = left.y + nodeH / 2 + leftOffset;
|
||||
let midYRight = right.y + nodeH / 2 + rightOffset;
|
||||
const directY = (midYLeft + midYRight) / 2;
|
||||
const directClear = lineageHorizontalLaneClear(directY, x1, x2, rects);
|
||||
const direct = directClear && left.rowIndex === right.rowIndex && x2 - x1 <= 156 && Math.abs(midYLeft - midYRight) < 10
|
||||
&& (!Number.isFinite(unionY) || unionY <= Math.max(midYLeft, midYRight) + 16);
|
||||
if (direct) {
|
||||
const y = (midYLeft + midYRight) / 2;
|
||||
const y = directY;
|
||||
return { d: `M ${x1.toFixed(1)} ${y.toFixed(1)} L ${x2.toFixed(1)} ${y.toFixed(1)}`, segments: [{ kind: "h", x1, y1: y, x2, y2: y }] };
|
||||
}
|
||||
const bend = 10;
|
||||
const y = Number.isFinite(unionY) ? unionY : Math.max(midYLeft, midYRight);
|
||||
const gap = x2 - x1;
|
||||
const bend = Math.max(10, Math.min(24, gap * 0.20));
|
||||
const y = directClear && Number.isFinite(unionY) && lineageHorizontalLaneClear(unionY, x1, x2, rects)
|
||||
? clamp(unionY, 10, Number.isFinite(Number(context.height)) ? Number(context.height) - 8 : Number.POSITIVE_INFINITY)
|
||||
: lineageChooseHorizontalDetourY(directY, x1, x2, left, right, nodeW, nodeH, rects, unionY, context);
|
||||
// If the route escapes above/below the row, attach from the corresponding
|
||||
// side portion of the node instead of forcing every edge out of side-center.
|
||||
const autoOffset = lineageAutoSideOffsetForLane(y, Math.min(left.y, right.y), Math.max(left.y, right.y) + nodeH, nodeH);
|
||||
if (Math.abs(autoOffset) > 0.1) {
|
||||
const minOffset = -nodeH * 0.38;
|
||||
const maxOffset = nodeH * 0.38;
|
||||
leftOffset = clamp(leftOffset + autoOffset, minOffset, maxOffset);
|
||||
rightOffset = clamp(rightOffset + autoOffset, minOffset, maxOffset);
|
||||
midYLeft = left.y + nodeH / 2 + leftOffset;
|
||||
midYRight = right.y + nodeH / 2 + rightOffset;
|
||||
}
|
||||
const xLeftBend = Math.min(x1 + bend, x2 - 4);
|
||||
const xRightBend = Math.max(x2 - bend, x1 + 4);
|
||||
const points = [
|
||||
[x1, midYLeft], [x1 + bend, midYLeft], [x1 + bend, y], [x2 - bend, y], [x2 - bend, midYRight], [x2, midYRight]
|
||||
[x1, midYLeft], [xLeftBend, midYLeft], [xLeftBend, y], [xRightBend, y], [xRightBend, midYRight], [x2, midYRight]
|
||||
];
|
||||
return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(points), unionX: (x1 + x2) / 2, unionY: y };
|
||||
return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(points), unionX: (xLeftBend + xRightBend) / 2, unionY: y };
|
||||
}
|
||||
|
||||
function lineagePartnerPathFromPositions(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0, context = {}) {
|
||||
if (a.rowIndex !== b.rowIndex) return lineageCrossGenerationPartnerPath(a, b, nodeW, nodeH, unionY, context);
|
||||
return lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA, attachOffsetB);
|
||||
return lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA, attachOffsetB, context);
|
||||
}
|
||||
function lineageChildAnchorOnMarriageLine(partner, fallbackX, fallbackY) {
|
||||
const horizontals = (partner?.segments || [])
|
||||
|
|
@ -395,8 +477,8 @@ function lineageBuildLinks(nodes, idSet, layout) {
|
|||
// Marriage line stays at the parent-node center. The child trunk drops
|
||||
// to a visible point below the parents before branching, so the fork is
|
||||
// never hidden behind a node.
|
||||
const attachA = 0;
|
||||
const attachB = 0;
|
||||
const attachA = group.attachOffsets?.get?.(left.node?.id) || 0;
|
||||
const attachB = group.attachOffsets?.get?.(right.node?.id) || 0;
|
||||
const partnerY = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2;
|
||||
const partner = lineagePartnerPathFromPositions(left, right, nodeW, nodeH, partnerY, attachA, attachB, layout);
|
||||
const unionY = Number.isFinite(partner.unionY) ? partner.unionY : partnerY;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue