"use strict"; function lineageSegmentKind(x1, y1, x2, y2) { if (Math.abs(y2 - y1) < 0.01) return 'h'; if (Math.abs(x2 - x1) < 0.01) return 'v'; return 'd'; } function lineageSegmentsFromPoints(points) { const segments = []; for (let i = 1; i < points.length; i++) { const [x1, y1] = points[i - 1]; const [x2, y2] = points[i]; segments.push({ kind: lineageSegmentKind(x1, y1, x2, y2), x1, y1, x2, y2 }); } return segments; } 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(' '); } function lineageCrossGenerationPartnerPath(a, b, nodeW, nodeH, unionY) { const ax = a.x + nodeW / 2; const bx = b.x + nodeW / 2; const ay = a.y + nodeH; const by = b.y + nodeH; const y = Math.max(unionY, ay + 12, by + 12); const unionX = Math.abs(ax - bx) < 18 ? Math.max(ax, bx) + 30 : (ax + bx) / 2; const points = [[ax, ay], [ax, y], [unionX, y], [bx, y], [bx, by]]; return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(points), unionX, unionY: y }; } function lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0) { 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; 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 && (!Number.isFinite(unionY) || unionY <= Math.max(midYLeft, midYRight) + 16); if (direct) { const y = (midYLeft + midYRight) / 2; 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 points = [ [x1, midYLeft], [x1 + bend, midYLeft], [x1 + bend, y], [x2 - bend, y], [x2 - bend, midYRight], [x2, midYRight] ]; return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(points), unionX: (x1 + x2) / 2, unionY: y }; } function lineagePartnerPathFromPositions(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0) { if (a.rowIndex !== b.rowIndex) return lineageCrossGenerationPartnerPath(a, b, nodeW, nodeH, unionY); return lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA, attachOffsetB); } function lineageVerticalBridgePath(x, y1, y2, bridges = [], radius = 8.5) { if (!Number.isFinite(x) || !Number.isFinite(y1) || !Number.isFinite(y2)) return ''; const down = y2 >= y1; const sign = down ? 1 : -1; const sorted = bridges.slice().sort((a, b) => down ? a - b : b - a).filter(y => Math.abs(y - y1) > radius + 1 && Math.abs(y - y2) > radius + 1); let d = `M ${x.toFixed(1)} ${y1.toFixed(1)}`; let cursor = y1; for (const by of sorted) { const before = by - sign * radius; const after = by + sign * radius; d += ` L ${x.toFixed(1)} ${before.toFixed(1)}`; d += ` Q ${(x + radius * 1.55).toFixed(1)} ${by.toFixed(1)} ${x.toFixed(1)} ${after.toFixed(1)}`; cursor = after; } if (Math.abs(cursor - y2) > 0.01) d += ` L ${x.toFixed(1)} ${y2.toFixed(1)}`; return d; } function lineageIntersectionYsForVertical(x, y1, y2, horizontalSegments) { const top = Math.min(y1, y2); const bottom = Math.max(y1, y2); const ys = []; for (const seg of horizontalSegments || []) { if (seg.kind !== 'h') continue; const minX = Math.min(seg.x1, seg.x2); const maxX = Math.max(seg.x1, seg.x2); const y = seg.y1; if (x > minX + 3 && x < maxX - 3 && y > top + 3 && y < bottom - 3) ys.push(y); } return ys; } function lineageBuildChildPathGroup(group, nodeW, nodeH, bridgeHorizontals) { const children = group.children; if (!children.length) return { d: '' }; const joinX = group.joinX; const fromY = group.fromY; const laneY = group.busY; 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) }; } 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.]+/, ""); 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)}`; for (const p of childPoints) { const bridges = lineageIntersectionYsForVertical(p.x, laneY, p.y, bridgeHorizontals); const branch = lineageVerticalBridgePath(p.x, laneY, p.y, bridges); d += ` ${branch}`; } return { d }; } function lineageComponentSignature(component, family) { family = lineageEdgeFamily(family); const idSet = new Set(component.map(x => x?.id).filter(Boolean)); return component .filter(Boolean) .map(n => { const parents = lineageMutualParentIds(n, family, idSet).join(","); const children = lineageMutualChildIds(n, family, idSet).join(","); return [ n.id, n.name || "", n.type || "", n.generation || 1, n.birthTime || 0, Number(n.scale || 0).toFixed(3), Number(n.adultScale || 0).toFixed(3), Number(n.growth || 0).toFixed(2), n.hasPaired ? 1 : 0, n.alive === false ? 0 : 1, n.deathReason || "", parents, children, ].join(":"); }) .sort() .join("|"); } function lineageComponentIdentity(component) { return Array.from(new Set((component || []).map(n => n?.id).filter(Boolean))).sort().join(","); } function lineageFamilyCacheKey(component, family, signature = "") { const uniqueComponent = Array.from(new Map((component || []).filter(Boolean).map(n => [n.id, n])).values()).sort(lineageNodeSort); return `${lineageComponentIdentity(uniqueComponent)}:${signature || lineageComponentSignature(uniqueComponent, family)}`; } function lineageHydrateFamilyHtml(html, index) { return html .split("{{LINEAGE_INDEX}}").join(String(index + 1)) .split("{{LINEAGE_MASK_ID}}").join(`lineage-node-mask-${index}`); } function lineageCaptureArchiveScroll() { const root = ui.archiveContent; if (!root) return null; const familyScroll = {}; for (const el of root.querySelectorAll(".lineage-family-tree[data-lineage-key]")) { const key = el.getAttribute("data-lineage-key"); if (key) familyScroll[key] = el.scrollLeft || 0; } return { top: root.scrollTop || 0, left: root.scrollLeft || 0, familyScroll }; } function lineageRestoreArchiveScroll(state) { const root = ui.archiveContent; if (!root || !state) return; const maxTop = Math.max(0, root.scrollHeight - root.clientHeight); const maxLeft = Math.max(0, root.scrollWidth - root.clientWidth); root.scrollTop = Math.min(state.top || 0, maxTop); root.scrollLeft = Math.min(state.left || 0, maxLeft); for (const el of root.querySelectorAll(".lineage-family-tree[data-lineage-key]")) { const key = el.getAttribute("data-lineage-key"); if (!key || !Object.prototype.hasOwnProperty.call(state.familyScroll || {}, key)) continue; el.scrollLeft = Math.min(state.familyScroll[key] || 0, Math.max(0, el.scrollWidth - el.clientWidth)); } } function lineageExpectedChildEdgeCount(component, family) { family = lineageEdgeFamily(family); const idSet = new Set(component.map(x => x?.id).filter(Boolean)); let count = 0; for (const n of component || []) count += lineageParentIds(n, idSet, family).length; return count; } function lineageVisibleRelationSignature(family) { family = lineageEdgeFamily(family); const relationIndex = lineageRelationIndex(family); const relatedIds = new Set(); for (const [childId, parents] of relationIndex.parentsByChild.entries()) { if (!parents.size) continue; relatedIds.add(childId); for (const parentId of parents) relatedIds.add(parentId); } return Array.from(relatedIds) .sort() .map(id => { const n = family[id] || {}; const parents = Array.from(relationIndex.parentsByChild.get(id) || []).sort().join(","); const children = Array.from(relationIndex.childrenByParent.get(id) || []).sort().join(","); return [ id, n.name || "", n.type || "", n.generation || 1, n.birthTime || 0, n.alive === false ? 0 : 1, n.deathReason || "", parents, children, ].join(":"); }) .join("|"); } function lineageArchiveNearViewport() { if (!ui.archiveContent?.getBoundingClientRect) return true; const rect = ui.archiveContent.getBoundingClientRect(); const h = window.innerHeight || document.documentElement?.clientHeight || 0; return rect.top < h + 260 && rect.bottom > -260; } function lineageSetArchiveStaleStatus(show) { if (!ui.archiveContent) return; const existing = ui.archiveContent.querySelector(".lineage-stale-status"); if (!show) { existing?.remove(); return; } if (existing) return; ui.archiveContent.insertAdjacentHTML("afterbegin", `