File size: 4,487 Bytes
b704fe2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21b5186
b704fe2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21b5186
 
b704fe2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21b5186
b704fe2
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import * as d3 from 'd3';

/** linear-arc:相邻节点矩形水平方向「外侧边与边之间的空隙」(px,SVG 内部坐标) */
export const LINEAR_ARC_ADJACENT_GAP_DEFAULT = 0;
export const LINEAR_ARC_ADJACENT_GAP_MIN = 0;
export const LINEAR_ARC_ADJACENT_GAP_MAX = 400;

/** prompt→生成 首邻:在 `adjacentGapPx` 之上多出的水平空隙(节点已有 `step`,仅此一处判断) */
const LINEAR_ARC_PROMPT_GEN_EXTRA_GAP_PX = 12;

/**
 * 连边的三次贝塞尔:P1/P2 沿水平边方向向对端内收,相对半跨距的比例,[0,1]。
 * 0 表示控制点与端点同竖线(切线竖直向上);1 表示收到跨度中点(最圆)。
 */
export const LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION = 0.25;

/** 首节点中心 x(与 translate(cx - w/2) 一致) */
const LINEAR_ARC_FIRST_CENTER_X = 20;
const LINEAR_ARC_BASELINE_Y = 0;

export function clampLinearArcAdjacentGap(px: number): number {
    return Math.max(
        LINEAR_ARC_ADJACENT_GAP_MIN,
        Math.min(LINEAR_ARC_ADJACENT_GAP_MAX, Math.round(px))
    );
}

type LinearArcNodeLike = { nodeW: number; nodeH: number; ciVisualScale: number };

/** `step === -1` 表示 prompt(与 `genAttributeDagView` 中 `DagNode.step` 约定一致) */
type LinearArcSteppedNode = LinearArcNodeLike & { step: number };

function computeNodeCenterXs(nodes: LinearArcSteppedNode[], adjacentGapPx: number): number[] {
    const xs: number[] = [];
    if (nodes.length === 0) return xs;
    xs.push(LINEAR_ARC_FIRST_CENTER_X);
    for (let i = 1; i < nodes.length; i++) {
        const prev = nodes[i - 1]!;
        const curr = nodes[i]!;
        const gap =
            adjacentGapPx +
            (prev.step === -1 && curr.step !== -1 ? LINEAR_ARC_PROMPT_GEN_EXTRA_GAP_PX : 0);
        xs.push(xs[i - 1]! + prev.nodeW / 2 + gap + curr.nodeW / 2);
    }
    return xs;
}

/** linear-arc 模式:节点线性排布,边使用顶部向上弧线。
 *
 * `nodes` 为参与布局的可见节点子集(可能少于 `nodeSel` 绑定的全量节点);
 * 不在 `nodes` 中的节点(如被隐藏的 excluded 节点)transform 保持不变——调用方已将它们设为 `display:none`。
 */
export function paintLinearArcLayout<
    LinkDatum,
    NodeDatum extends LinearArcSteppedNode,
>(params: {
    linkSel: d3.Selection<SVGGElement, LinkDatum, SVGGElement, unknown>;
    nodeSel: d3.Selection<SVGGElement, NodeDatum, SVGGElement, unknown>;
    nodes: NodeDatum[];
    adjacentGapPx: number;
    getLinkNodes: (link: LinkDatum) => { src: NodeDatum; tgt: NodeDatum };
}): void {
    const { linkSel, nodeSel, nodes, adjacentGapPx, getLinkNodes } = params;

    const centerXs = computeNodeCenterXs(nodes, adjacentGapPx);

    // Map datum → centerX:支持 nodeSel 含超出 nodes 范围的节点(如被隐藏的节点)。
    const centerXByNode = new Map<NodeDatum, number>();
    for (let i = 0; i < nodes.length; i++) {
        centerXByNode.set(nodes[i]!, centerXs[i]!);
    }

    const arcPathBetweenNodes = (src: NodeDatum, tgt: NodeDatum): string => {
        const srcCx = centerXByNode.get(src);
        const tgtCx = centerXByNode.get(tgt);
        if (srcCx === undefined || tgtCx === undefined) {
            throw new Error('paintLinearArcLayout: link endpoint not in linear node list');
        }
        // 用未放大的半高(nodeH / ciVisualScale / 2)定位弧端点,使所有节点顶部对齐同一 y 基线。
        const y = LINEAR_ARC_BASELINE_Y - src.nodeH / (2 * src.ciVisualScale);
        const dx = Math.abs(tgtCx - srcCx);
        const arcH = dx * 0.4;
        const upY = y - arcH;
        const t = Math.max(0, Math.min(1, LINEAR_ARC_BEZIER_HANDLE_INSET_FRACTION));
        const inset = t * (dx / 2);
        const dir = tgtCx >= srcCx ? 1 : -1;
        const p1x = srcCx + dir * inset;
        const p2x = tgtCx - dir * inset;
        return `M ${srcCx} ${y} C ${p1x} ${upY}, ${p2x} ${upY}, ${tgtCx} ${y}`;
    };

    linkSel.each(function(d) {
        const { src, tgt } = getLinkNodes(d);
        d3.select(this)
            .selectAll('path.gen-attr-dag-link-visible')
            .attr('d', arcPathBetweenNodes(src, tgt));
    });

    nodeSel.attr('transform', (d) => {
        const cx = centerXByNode.get(d);
        if (cx === undefined) return null; // 不在布局列表中(已 display:none),不更新 transform
        return `translate(${cx - d.nodeW / 2},${LINEAR_ARC_BASELINE_Y - d.nodeH / 2})`;
    });
}