"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", `
\u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u306fOFF\u3067\u3059\u3002\u300c\u66f4\u65b0\u300d\u3092ON\u306b\u3059\u308b\u3068\u53cd\u6620\u3055\u308c\u307e\u3059\u3002
`); } function lineageAssignPartnerAttachOffsets(linkGroups) { const buckets = new Map(); const pushBucket = (nodeId, side, group) => { const key = `${nodeId}:${side}`; if (!buckets.has(key)) buckets.set(key, []); buckets.get(key).push(group); }; for (const group of linkGroups) { if ((group.parents || []).length < 2) continue; const left = group.parents[0]; const right = group.parents[group.parents.length - 1]; if (!group.attachOffsets) group.attachOffsets = new Map(); pushBucket(left.node.id, 'right', group); pushBucket(right.node.id, 'left', group); } 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; groups.forEach((group, i) => { const offset = n <= 1 ? 0 : (i - (n - 1) / 2) * step; const [nodeId] = key.split(':'); group.attachOffsets.set(nodeId, offset); }); } } function lineageBuildLinks(nodes, idSet, layout) { const { positions, nodeW, nodeH } = layout; const groups = new Map(); for (const child of nodes) { const parents = lineageParentIds(child, idSet).filter(id => positions.has(id)); if (parents.length < 1 || !positions.has(child.id)) continue; const key = parents.slice().sort().join('+'); if (!groups.has(key)) groups.set(key, { parentIds: parents.slice().sort(), childIds: [] }); groups.get(key).childIds.push(child.id); } const linkGroups = []; for (const group of groups.values()) { const parents = group.parentIds.map(id => positions.get(id)).filter(Boolean).sort((a, b) => a.x - b.x); const children = group.childIds.map(id => positions.get(id)).filter(Boolean).sort((a, b) => a.x - b.x); if (!parents.length || !children.length) continue; const parentBottom = Math.max(...parents.map(p => p.y + nodeH)); const childTop = Math.min(...children.map(c => c.y)); const joinX = parents.reduce((sum, p) => sum + p.x + nodeW / 2, 0) / parents.length; const directCouple = parents.length >= 2 && parents[0].rowIndex === parents[1].rowIndex && (parents[1].x - (parents[0].x + nodeW) <= 168); linkGroups.push({ parents, children, parentBottom, childTop, joinX, directCouple, attachOffsets: new Map() }); } lineageAssignPartnerAttachOffsets(linkGroups); const childLinkEdges = linkGroups.reduce((sum, group) => sum + group.children.length * group.parents.length, 0); const byGap = new Map(); for (const group of linkGroups) { const key = `${group.parentBottom.toFixed(1)}:${group.childTop.toFixed(1)}`; if (!byGap.has(key)) byGap.set(key, []); byGap.get(key).push(group); } const partnerLinks = []; const childLinks = []; const horizontalSegments = []; 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 span = Math.max(16, bottom - top); groupsInGap.forEach((group, i) => { const laneY = top + span * ((i + 1) / (groupsInGap.length + 1)); if (group.parents.length >= 2) { const left = group.parents[0]; const right = group.parents[group.parents.length - 1]; // 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 partnerY = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2; const partner = lineagePartnerPathFromPositions(left, right, nodeW, nodeH, partnerY, attachA, attachB); 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)); 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)); } }); } for (const groupsInGap of byGap.values()) { groupsInGap.sort((a, b) => a.joinX - b.joinX || a.children.length - b.children.length); for (const group of groupsInGap) { const childCenters = group.children.map(c => c.x + nodeW / 2).sort((a, b) => a - b); 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); if (child.d) childLinks.push(child.d); } } return { partnerLinks, childLinks, childLinkGroups: linkGroups.length, childLinkEdges }; }