duu
This commit is contained in:
parent
88f618cadf
commit
8f9f5b5100
108 changed files with 2139 additions and 1500 deletions
|
|
@ -16,9 +16,227 @@ function lineageSegmentsFromPoints(points) {
|
|||
return segments;
|
||||
}
|
||||
|
||||
function lineageRoutePointKey(x, y) {
|
||||
return `${Math.round(Number(x || 0) * 10) / 10},${Math.round(Number(y || 0) * 10) / 10}`;
|
||||
}
|
||||
|
||||
function lineageRouteSamePoint(a, b) {
|
||||
return Math.abs(Number(a?.[0]) - Number(b?.[0])) < 0.05 && Math.abs(Number(a?.[1]) - Number(b?.[1])) < 0.05;
|
||||
}
|
||||
|
||||
function lineageCleanRoutePoints(points) {
|
||||
const raw = (points || [])
|
||||
.map(p => [Number(p?.[0]), Number(p?.[1])])
|
||||
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
||||
const deduped = [];
|
||||
for (const p of raw) {
|
||||
if (!deduped.length || !lineageRouteSamePoint(deduped[deduped.length - 1], p)) deduped.push(p);
|
||||
}
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (let i = 1; i < deduped.length - 1; i++) {
|
||||
const a = deduped[i - 1];
|
||||
const b = deduped[i];
|
||||
const c = deduped[i + 1];
|
||||
const sameX = Math.abs(a[0] - b[0]) < 0.05 && Math.abs(b[0] - c[0]) < 0.05;
|
||||
const sameY = Math.abs(a[1] - b[1]) < 0.05 && Math.abs(b[1] - c[1]) < 0.05;
|
||||
const reverse = lineageRouteSamePoint(a, c);
|
||||
if (sameX || sameY || reverse) {
|
||||
deduped.splice(i, 1);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function lineagePathFromPoints(points) {
|
||||
if (!points.length) return "";
|
||||
return `M ${points[0][0].toFixed(1)} ${points[0][1].toFixed(1)} ` + points.slice(1).map(p => `L ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' ');
|
||||
const clean = lineageForceOrthogonalPoints(points);
|
||||
if (!clean.length) return "";
|
||||
return `M ${clean[0][0].toFixed(1)} ${clean[0][1].toFixed(1)}` + clean.slice(1).map(p => ` L ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join('');
|
||||
}
|
||||
|
||||
function lineagePointInsideRouteRect(x, y, rects = []) {
|
||||
return (rects || []).some(r => x > r.left && x < r.right && y > r.top && y < r.bottom);
|
||||
}
|
||||
|
||||
function lineageRouteSegmentClear(x1, y1, x2, y2, rects = []) {
|
||||
if (!Number.isFinite(x1) || !Number.isFinite(y1) || !Number.isFinite(x2) || !Number.isFinite(y2)) return false;
|
||||
if (Math.abs(x1 - x2) < 0.05 && Math.abs(y1 - y2) < 0.05) return true;
|
||||
const vertical = Math.abs(x1 - x2) < 0.05;
|
||||
const horizontal = Math.abs(y1 - y2) < 0.05;
|
||||
if (!vertical && !horizontal) return false;
|
||||
for (const r of rects || []) {
|
||||
if (vertical) {
|
||||
const top = Math.min(y1, y2);
|
||||
const bottom = Math.max(y1, y2);
|
||||
if (x1 > r.left && x1 < r.right && Math.max(top, r.top) < Math.min(bottom, r.bottom)) return false;
|
||||
} else {
|
||||
const left = Math.min(x1, x2);
|
||||
const right = Math.max(x1, x2);
|
||||
if (y1 > r.top && y1 < r.bottom && Math.max(left, r.left) < Math.min(right, r.right)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function lineageOrthogonalFallbackPoints(x1, y1, x2, y2, rects = []) {
|
||||
const candidates = [
|
||||
[[x1, y1], [x2, y1], [x2, y2]],
|
||||
[[x1, y1], [x1, y2], [x2, y2]],
|
||||
];
|
||||
const score = points => {
|
||||
let bad = 0;
|
||||
let length = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const a = points[i - 1];
|
||||
const b = points[i];
|
||||
length += Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
|
||||
if (!lineageRouteSegmentClear(a[0], a[1], b[0], b[1], rects)) bad += 100000;
|
||||
}
|
||||
return bad + length;
|
||||
};
|
||||
candidates.sort((a, b) => score(a) - score(b));
|
||||
return lineageCleanRoutePoints(candidates[0]);
|
||||
}
|
||||
|
||||
function lineageForceOrthogonalPoints(points) {
|
||||
const clean = lineageCleanRoutePoints(points);
|
||||
if (clean.length <= 1) return clean;
|
||||
const out = [clean[0]];
|
||||
for (let i = 1; i < clean.length; i++) {
|
||||
const prev = out[out.length - 1];
|
||||
const cur = clean[i];
|
||||
const diagonal = Math.abs(prev[0] - cur[0]) >= 0.05 && Math.abs(prev[1] - cur[1]) >= 0.05;
|
||||
if (diagonal) {
|
||||
const via = [cur[0], prev[1]];
|
||||
if (!lineageRouteSamePoint(prev, via)) out.push(via);
|
||||
}
|
||||
if (!lineageRouteSamePoint(out[out.length - 1], cur)) out.push(cur);
|
||||
}
|
||||
return lineageCleanRoutePoints(out);
|
||||
}
|
||||
|
||||
|
||||
function lineageRelevantRouteRects(x1, y1, x2, y2, rects = [], margin = 92) {
|
||||
const left = Math.min(x1, x2) - margin;
|
||||
const right = Math.max(x1, x2) + margin;
|
||||
const top = Math.min(y1, y2) - margin;
|
||||
const bottom = Math.max(y1, y2) + margin;
|
||||
return (rects || []).filter(r => r.right >= left && r.left <= right && r.bottom >= top && r.top <= bottom);
|
||||
}
|
||||
|
||||
function lineageOrthogonalRoutePoints(x1, y1, x2, y2, rects = [], context = {}) {
|
||||
if (!Number.isFinite(x1) || !Number.isFinite(y1) || !Number.isFinite(x2) || !Number.isFinite(y2)) return [];
|
||||
if (Math.abs(x1 - x2) < 0.05 && Math.abs(y1 - y2) < 0.05) return [[x1, y1]];
|
||||
const relevant = lineageRelevantRouteRects(x1, y1, x2, y2, rects, Number(context.routeMargin || 110) || 110);
|
||||
if (lineageRouteSegmentClear(x1, y1, x2, y2, relevant)) return [[x1, y1], [x2, y2]];
|
||||
|
||||
const width = Number(context.width || 0) || 0;
|
||||
const height = Number(context.height || 0) || 0;
|
||||
const minX = 8;
|
||||
const minY = Math.max(8, Number(context.minRouteY ?? 8) || 8);
|
||||
const maxX = width > 24 ? width - 8 : Math.max(x1, x2, ...relevant.map(r => r.right + 30), 240);
|
||||
const maxY = height > 24 ? height - 8 : Math.max(y1, y2, ...relevant.map(r => r.bottom + 30), 240);
|
||||
const xs = [x1, x2, minX, maxX];
|
||||
const ys = [y1, y2, minY, maxY];
|
||||
if (Number.isFinite(Number(context.preferredX))) xs.push(Number(context.preferredX));
|
||||
if (Number.isFinite(Number(context.preferredY))) ys.push(Number(context.preferredY));
|
||||
for (const r of relevant) {
|
||||
xs.push(r.left - 8, r.left, r.right, r.right + 8);
|
||||
ys.push(r.top - 8, r.top, r.bottom, r.bottom + 8);
|
||||
}
|
||||
const norm = values => Array.from(new Set(values
|
||||
.map(v => Math.round(Number(v) * 10) / 10)
|
||||
.filter(Number.isFinite)))
|
||||
.sort((a, b) => a - b);
|
||||
let ux = norm(xs).filter(v => v >= minX && v <= maxX);
|
||||
let uy = norm(ys).filter(v => v >= minY && v <= maxY);
|
||||
if (!ux.some(v => Math.abs(v - x1) < 0.05)) ux.push(x1);
|
||||
if (!ux.some(v => Math.abs(v - x2) < 0.05)) ux.push(x2);
|
||||
if (!uy.some(v => Math.abs(v - y1) < 0.05)) uy.push(y1);
|
||||
if (!uy.some(v => Math.abs(v - y2) < 0.05)) uy.push(y2);
|
||||
ux = Array.from(new Set(ux.map(v => Math.round(v * 10) / 10))).sort((a, b) => a - b);
|
||||
uy = Array.from(new Set(uy.map(v => Math.round(v * 10) / 10))).sort((a, b) => a - b);
|
||||
|
||||
const passable = new Set();
|
||||
const keyFor = (i, j) => `${i}:${j}`;
|
||||
for (let i = 0; i < ux.length; i++) {
|
||||
for (let j = 0; j < uy.length; j++) {
|
||||
if (!lineagePointInsideRouteRect(ux[i], uy[j], relevant)) passable.add(keyFor(i, j));
|
||||
}
|
||||
}
|
||||
const ix1 = ux.findIndex(v => Math.abs(v - x1) < 0.05);
|
||||
const iy1 = uy.findIndex(v => Math.abs(v - y1) < 0.05);
|
||||
const ix2 = ux.findIndex(v => Math.abs(v - x2) < 0.05);
|
||||
const iy2 = uy.findIndex(v => Math.abs(v - y2) < 0.05);
|
||||
const start = keyFor(ix1, iy1);
|
||||
const goal = keyFor(ix2, iy2);
|
||||
passable.add(start);
|
||||
passable.add(goal);
|
||||
|
||||
const dist = new Map([[start, 0]]);
|
||||
const prev = new Map();
|
||||
const open = new Set([start]);
|
||||
const score = key => {
|
||||
const [i, j] = key.split(':').map(Number);
|
||||
return (dist.get(key) || 0) + Math.abs(ux[i] - x2) + Math.abs(uy[j] - y2) * 1.08;
|
||||
};
|
||||
let guard = 0;
|
||||
while (open.size && guard++ < 20000) {
|
||||
let cur = null;
|
||||
for (const k of open) if (cur === null || score(k) < score(cur)) cur = k;
|
||||
if (cur === goal) break;
|
||||
open.delete(cur);
|
||||
const [i, j] = cur.split(':').map(Number);
|
||||
const neighbors = [[i - 1, j], [i + 1, j], [i, j - 1], [i, j + 1]];
|
||||
for (const [ni, nj] of neighbors) {
|
||||
if (ni < 0 || nj < 0 || ni >= ux.length || nj >= uy.length) continue;
|
||||
const nk = keyFor(ni, nj);
|
||||
if (!passable.has(nk)) continue;
|
||||
if (!lineageRouteSegmentClear(ux[i], uy[j], ux[ni], uy[nj], relevant)) continue;
|
||||
const turnPenalty = (() => {
|
||||
const pk = prev.get(cur);
|
||||
if (!pk) return 0;
|
||||
const [pi, pj] = pk.split(':').map(Number);
|
||||
const wasH = pi !== i;
|
||||
const nowH = ni !== i;
|
||||
return wasH === nowH ? 0 : 5;
|
||||
})();
|
||||
const step = Math.abs(ux[i] - ux[ni]) + Math.abs(uy[j] - uy[nj]) + turnPenalty;
|
||||
const nd = (dist.get(cur) || 0) + step;
|
||||
if (nd + 0.001 < (dist.get(nk) ?? Infinity)) {
|
||||
dist.set(nk, nd);
|
||||
prev.set(nk, cur);
|
||||
open.add(nk);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dist.has(goal)) {
|
||||
const keys = [];
|
||||
let cur = goal;
|
||||
while (cur) {
|
||||
keys.push(cur);
|
||||
if (cur === start) break;
|
||||
cur = prev.get(cur);
|
||||
}
|
||||
keys.reverse();
|
||||
return lineageCleanRoutePoints(keys.map(k => {
|
||||
const [i, j] = k.split(':').map(Number);
|
||||
return [ux[i], uy[j]];
|
||||
}));
|
||||
}
|
||||
|
||||
// Last-resort route: prefer an L shape that is at least connected and then
|
||||
// simplify it. This should rarely be used, but avoids emitting orphaned SVG
|
||||
// fragments when the obstacle grid is over-constrained.
|
||||
const viaA = [[x1, y1], [x1, y2], [x2, y2]];
|
||||
if (lineageRouteSegmentClear(x1, y1, x1, y2, relevant) && lineageRouteSegmentClear(x1, y2, x2, y2, relevant)) return lineageCleanRoutePoints(viaA);
|
||||
const viaB = [[x1, y1], [x2, y1], [x2, y2]];
|
||||
if (lineageRouteSegmentClear(x1, y1, x2, y1, relevant) && lineageRouteSegmentClear(x2, y1, x2, y2, relevant)) return lineageCleanRoutePoints(viaB);
|
||||
return lineageOrthogonalFallbackPoints(x1, y1, x2, y2, relevant);
|
||||
}
|
||||
|
||||
function lineageNodeRectsBetween(positions, nodeW, nodeH, y1, y2, excludeIds = new Set(), pad = 10) {
|
||||
|
|
@ -63,6 +281,71 @@ function lineageHorizontalLaneClear(y, x1, x2, rects) {
|
|||
return !(rects || []).some(r => y > r.top && y < r.bottom && Math.max(left, r.left) < Math.min(right, r.right));
|
||||
}
|
||||
|
||||
|
||||
function lineageRouteVerticalClear(x, y1, y2, rects) {
|
||||
const top = Math.min(y1, y2);
|
||||
const bottom = Math.max(y1, y2);
|
||||
return !(rects || []).some(r => x > r.left && x < r.right && Math.max(top, r.top) < Math.min(bottom, r.bottom));
|
||||
}
|
||||
|
||||
function lineageRouteHorizontalClear(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 lineageChooseVerticalDetourX(x, y1, y2, rects, context = {}) {
|
||||
const top = Math.min(y1, y2);
|
||||
const bottom = Math.max(y1, y2);
|
||||
const blockers = (rects || []).filter(r => Math.max(top, r.top) < Math.min(bottom, r.bottom));
|
||||
const width = Number(context.width || 0) || 0;
|
||||
const minX = 8;
|
||||
const maxX = width > 16 ? width - 8 : Math.max(x + 120, ...blockers.map(r => r.right + 18), 240);
|
||||
const candidates = [x - 34, x + 34, x - 58, x + 58];
|
||||
for (const r of blockers) {
|
||||
candidates.push(r.left - 12, r.right + 12, r.left - 22, r.right + 22);
|
||||
}
|
||||
const unique = Array.from(new Set(candidates
|
||||
.map(v => clamp(Math.round(v * 10) / 10, minX, maxX))))
|
||||
.filter(v => Number.isFinite(v) && lineageRouteVerticalClear(v, y1, y2, rects));
|
||||
const pool = unique.length ? unique : Array.from(new Set(candidates.map(v => clamp(Math.round(v * 10) / 10, minX, maxX)))).filter(Number.isFinite);
|
||||
if (!pool.length) return clamp(x, minX, maxX);
|
||||
return pool.sort((a, b) => Math.abs(a - x) - Math.abs(b - x))[0];
|
||||
}
|
||||
|
||||
function lineageChooseChildDetourY(y, x1, x2, rects, context = {}) {
|
||||
const height = Number(context.height || 0) || 0;
|
||||
const minY = 8;
|
||||
const maxY = height > 16 ? height - 8 : Math.max(y + 120, ...((rects || []).map(r => r.bottom + 18)), 240);
|
||||
const blockers = (rects || []).filter(r => Math.max(Math.min(x1, x2), r.left) < Math.min(Math.max(x1, x2), r.right));
|
||||
const candidates = [y - 18, y + 18, y - 32, y + 32, y - 52, y + 52];
|
||||
for (const r of blockers) {
|
||||
candidates.push(r.top - 12, r.bottom + 12, r.top - 22, r.bottom + 22);
|
||||
}
|
||||
const unique = Array.from(new Set(candidates
|
||||
.map(v => clamp(Math.round(v * 10) / 10, minY, maxY))))
|
||||
.filter(v => Number.isFinite(v) && lineageRouteHorizontalClear(v, x1, x2, rects));
|
||||
const pool = unique.length ? unique : Array.from(new Set(candidates.map(v => clamp(Math.round(v * 10) / 10, minY, maxY)))).filter(Number.isFinite);
|
||||
if (!pool.length) return clamp(y, minY, maxY);
|
||||
return pool.sort((a, b) => Math.abs(a - y) - Math.abs(b - y))[0];
|
||||
}
|
||||
|
||||
function lineageVerticalPathAvoidingNodes(x, y1, y2, bridges = [], rects = [], context = {}) {
|
||||
if (lineageRouteVerticalClear(x, y1, y2, rects)) return lineageVerticalBridgePath(x, y1, y2, bridges);
|
||||
return lineagePathFromPoints(lineageOrthogonalRoutePoints(x, y1, x, y2, rects, context));
|
||||
}
|
||||
|
||||
function lineageHorizontalConnectorAvoidingNodes(x1, y, x2, rects = [], context = {}) {
|
||||
if (Math.abs(x2 - x1) <= 0.5) return '';
|
||||
if (lineageRouteHorizontalClear(y, x1, x2, rects)) return ` L ${x2.toFixed(1)} ${y.toFixed(1)}`;
|
||||
const points = lineageOrthogonalRoutePoints(x1, y, x2, y, rects, context).slice(1);
|
||||
return points.map(p => ` L ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join('');
|
||||
}
|
||||
|
||||
function lineageHorizontalPathAvoidingNodes(x1, y, x2, rects = [], context = {}) {
|
||||
return lineagePathFromPoints(lineageOrthogonalRoutePoints(x1, y, x2, y, rects, context));
|
||||
}
|
||||
|
||||
function lineageLanePenalty(y, x1, x2, rects) {
|
||||
let penalty = 0;
|
||||
for (const r of rects || []) {
|
||||
|
|
@ -132,8 +415,14 @@ function lineageCrossGenerationPartnerPath(a, b, nodeW, nodeH, unionY, context =
|
|||
const bMidY = b.y + nodeH / 2;
|
||||
const aSideX = railX >= a.x + nodeW / 2 ? a.x + nodeW : a.x;
|
||||
const bSideX = railX >= b.x + nodeW / 2 ? b.x + nodeW : b.x;
|
||||
const points = [[aSideX, aMidY], [railX, aMidY], [railX, bMidY], [bSideX, bMidY]];
|
||||
return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(points), unionX: railX, unionY: Math.max(aMidY, bMidY) };
|
||||
const exclude = new Set([a.node?.id, b.node?.id].filter(Boolean));
|
||||
const rects = lineageRouteNodeRects(context.positions, nodeW, nodeH, exclude, 10);
|
||||
const points = lineageOrthogonalRoutePoints(aSideX, aMidY, bSideX, bMidY, rects, { ...context, preferredX: railX, routeMargin: 180 });
|
||||
const clean = lineageCleanRoutePoints(points);
|
||||
const segments = lineageSegmentsFromPoints(clean);
|
||||
const verticals = segments.filter(seg => seg.kind === "v").sort((sa, sb) => Math.abs(sb.y2 - sb.y1) - Math.abs(sa.y2 - sa.y1));
|
||||
const rail = verticals[0];
|
||||
return { d: lineagePathFromPoints(clean), segments, unionX: rail ? rail.x1 : railX, unionY: Math.max(aMidY, bMidY) };
|
||||
}
|
||||
|
||||
function lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0, context = {}) {
|
||||
|
|
@ -153,8 +442,10 @@ function lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffs
|
|||
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 = 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 points = Math.abs(midYLeft - midYRight) < 0.5
|
||||
? [[x1, midYLeft], [x2, midYRight]]
|
||||
: [[x1, midYLeft], [(x1 + x2) / 2, midYLeft], [(x1 + x2) / 2, midYRight], [x2, midYRight]];
|
||||
return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(lineageCleanRoutePoints(points)), unionX: (x1 + x2) / 2, unionY: (midYLeft + midYRight) / 2 };
|
||||
}
|
||||
const gap = x2 - x1;
|
||||
const bend = Math.max(10, Math.min(24, gap * 0.20));
|
||||
|
|
@ -172,12 +463,17 @@ function lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffs
|
|||
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], [xLeftBend, midYLeft], [xLeftBend, y], [xRightBend, y], [xRightBend, midYRight], [x2, midYRight]
|
||||
];
|
||||
return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(points), unionX: (xLeftBend + xRightBend) / 2, unionY: y };
|
||||
const routeContext = { ...context, preferredY: y, routeMargin: Math.max(140, Math.abs(x2 - x1) + nodeW) };
|
||||
const points = lineageOrthogonalRoutePoints(x1, midYLeft, x2, midYRight, rects, routeContext);
|
||||
const clean = lineageCleanRoutePoints(points);
|
||||
const horizontals = lineageSegmentsFromPoints(clean).filter(seg => seg.kind === "h");
|
||||
const mainH = horizontals.sort((sa, sb) => Math.abs(sb.x2 - sb.x1) - Math.abs(sa.x2 - sa.x1))[0];
|
||||
return {
|
||||
d: lineagePathFromPoints(clean),
|
||||
segments: lineageSegmentsFromPoints(clean),
|
||||
unionX: mainH ? (mainH.x1 + mainH.x2) / 2 : (x1 + x2) / 2,
|
||||
unionY: mainH ? mainH.y1 : y
|
||||
};
|
||||
}
|
||||
|
||||
function lineagePartnerPathFromPositions(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0, context = {}) {
|
||||
|
|
@ -235,36 +531,37 @@ function lineageIntersectionYsForVertical(x, y1, y2, horizontalSegments) {
|
|||
return ys;
|
||||
}
|
||||
|
||||
function lineageBuildChildPathGroup(group, nodeW, nodeH, bridgeHorizontals) {
|
||||
function lineageBuildChildPathGroup(group, nodeW, nodeH, bridgeHorizontals, context = {}) {
|
||||
const children = group.children;
|
||||
if (!children.length) return { d: '' };
|
||||
const joinX = group.joinX;
|
||||
const fromY = group.fromY;
|
||||
const laneY = group.busY;
|
||||
const routeRects = group.routeRects || [];
|
||||
const childPoints = children
|
||||
.map(c => ({ x: c.x + nodeW / 2, y: c.y }))
|
||||
.sort((a, b) => a.x - b.x || a.y - b.y);
|
||||
if (childPoints.length === 1 && Math.abs(childPoints[0].x - joinX) <= 0.5) {
|
||||
const p = childPoints[0];
|
||||
const bridges = lineageIntersectionYsForVertical(p.x, fromY, p.y, bridgeHorizontals);
|
||||
return { d: lineageVerticalBridgePath(p.x, fromY, p.y, bridges) };
|
||||
return { d: lineageVerticalPathAvoidingNodes(p.x, fromY, p.y, bridges, routeRects, context) };
|
||||
}
|
||||
if (childPoints.length === 1) {
|
||||
const p = childPoints[0];
|
||||
const parentBridges = lineageIntersectionYsForVertical(joinX, fromY, laneY, bridgeHorizontals);
|
||||
const childBridges = lineageIntersectionYsForVertical(p.x, laneY, p.y, bridgeHorizontals);
|
||||
const parentLane = lineageVerticalBridgePath(joinX, fromY, laneY, parentBridges);
|
||||
const horizontal = ` L ${p.x.toFixed(1)} ${laneY.toFixed(1)}`;
|
||||
const childLane = lineageVerticalBridgePath(p.x, laneY, p.y, childBridges).replace(/^M\s+[-\d.]+\s+[-\d.]+/, "");
|
||||
const parentLane = lineageVerticalPathAvoidingNodes(joinX, fromY, laneY, parentBridges, routeRects, context);
|
||||
const horizontal = lineageHorizontalConnectorAvoidingNodes(joinX, laneY, p.x, routeRects, context);
|
||||
const childLane = lineageVerticalPathAvoidingNodes(p.x, laneY, p.y, childBridges, routeRects, context).replace(/^M\s+[-\d.]+\s+[-\d.]+/, "");
|
||||
return { d: `${parentLane}${horizontal}${childLane}` };
|
||||
}
|
||||
const minX = Math.min(joinX, ...childPoints.map(p => p.x));
|
||||
const maxX = Math.max(joinX, ...childPoints.map(p => p.x));
|
||||
let d = lineageVerticalBridgePath(joinX, fromY, laneY, lineageIntersectionYsForVertical(joinX, fromY, laneY, bridgeHorizontals));
|
||||
d += ` M ${minX.toFixed(1)} ${laneY.toFixed(1)} L ${maxX.toFixed(1)} ${laneY.toFixed(1)}`;
|
||||
let d = lineageVerticalPathAvoidingNodes(joinX, fromY, laneY, lineageIntersectionYsForVertical(joinX, fromY, laneY, bridgeHorizontals), routeRects, context);
|
||||
d += ` ${lineageHorizontalPathAvoidingNodes(minX, laneY, maxX, routeRects, context)}`;
|
||||
for (const p of childPoints) {
|
||||
const bridges = lineageIntersectionYsForVertical(p.x, laneY, p.y, bridgeHorizontals);
|
||||
const branch = lineageVerticalBridgePath(p.x, laneY, p.y, bridges);
|
||||
const branch = lineageVerticalPathAvoidingNodes(p.x, laneY, p.y, bridges, routeRects, context);
|
||||
d += ` ${branch}`;
|
||||
}
|
||||
return { d };
|
||||
|
|
@ -419,7 +716,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 = 14;
|
||||
const step = 18;
|
||||
groups.forEach((group, i) => {
|
||||
const offset = n <= 1 ? 0 : (i - (n - 1) / 2) * step;
|
||||
const [nodeId] = key.split(':');
|
||||
|
|
@ -505,7 +802,12 @@ function lineageBuildLinks(nodes, idSet, layout) {
|
|||
if (group.children.length > 1 || (childCenters.length === 1 && Math.abs(childCenters[0] - group.joinX) > 0.5)) {
|
||||
horizontalSegments.push({ kind: 'h', x1: Math.min(group.joinX, ...childCenters), y1: group.busY, x2: Math.max(group.joinX, ...childCenters), y2: group.busY });
|
||||
}
|
||||
const child = lineageBuildChildPathGroup(group, nodeW, nodeH, horizontalSegments);
|
||||
const excludeIds = new Set([
|
||||
...group.parents.map(p => p?.node?.id).filter(Boolean),
|
||||
...group.children.map(c => c?.node?.id).filter(Boolean),
|
||||
]);
|
||||
group.routeRects = lineageRouteNodeRects(positions, nodeW, nodeH, excludeIds, 12);
|
||||
const child = lineageBuildChildPathGroup(group, nodeW, nodeH, horizontalSegments, layout);
|
||||
if (child.d) childLinks.push(child.d);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue