JavaScript is required

交互式编辑

本文档描述了交互式编辑系统,它使用户能够通过直接操作来修改图中的节点和连线。该编辑系统提供可视化控制器,用于调整节点大小、重新定位元素、调整连线路径以及编辑文本标签。

有关用户交互事件(点击、拖拽、展开/折叠),请参见用户交互与事件。有关布局与定位算法,请参见布局系统


编辑系统架构

编辑系统在 RelationGraphWith91Editing 中实现,由四个专用控制器组成,用于管理交互式编辑的不同方面:

  1. editingController:节点选择、多节点包围框以及缩放操作
  2. editingLineController:连线顶点操控、控制点以及文本编辑
  3. nodeConnectController:创建连线期间的连接目标高亮
  4. editingReferenceLine:节点拖拽时带可选吸附的对齐辅助线

图:编辑系统组件与数据流

graph TB
    subgraph "核心实现"
        With91Editing["RelationGraphWith91Editing
packages/.../RelationGraphWith91Editing.ts"] With7Event["RelationGraphWith7Event
packages/.../RelationGraphWith7Event.ts"] end subgraph "四个编辑控制器" editingController["options.editingController
nodes: RGNode[]
show, x, y, width, height"] editingLineController["options.editingLineController
line: RGLine | null
startPoint, endPoint
ctrlPoint1, ctrlPoint2
line44Splits: RGCtrlPointForLine44[]
line49Points: RGPosition[]
text: {x, y, width, height}"] nodeConnectController["options.nodeConnectController
node: RGLineTarget
show, x, y, width, height"] editingReferenceLine["options.editingReferenceLine
show, directionV, directionH
v_x, v_y, v_height
h_x, h_y, h_width"] end subgraph "公共 API 方法" setEditingNodes["setEditingNodes(nodes)"] addEditingNode["addEditingNode(node)"] setEditingLine["setEditingLine(line)"] onResizeStart["onResizeStart(type, e)"] startMoveLineVertex["startMoveLineVertex(type, e, cb)"] startMoveLine6CtrlPoint["startMoveLine6CtrlPoint(idx, e, cb)"] startMoveLine44CtrlPoint["startMoveLine44CtrlPoint(split, e, cb)"] end subgraph "内部更新方法" updateEditingControllerView["_updateEditingControllerView()"] updateEditingLineView["_updateEditingLineView()"] updateEditingConnectControllerView["_updateEditingConnectControllerView()"] updateReferenceLineView["updateReferenceLineView(node, x, y, buff_x, buff_y)"] end With91Editing --> editingController With91Editing --> editingLineController With91Editing --> nodeConnectController With91Editing --> editingReferenceLine setEditingNodes --> updateEditingControllerView addEditingNode --> updateEditingControllerView setEditingLine --> updateEditingLineView onResizeStart --> updateEditingControllerView With7Event --> With91Editing With7Event -.提供.-> startMoveLineVertex With7Event -.提供.-> startMoveLine6CtrlPoint editingController -.被读取于.-> updateEditingControllerView editingLineController -.被读取于.-> updateEditingLineView nodeConnectController -.被读取于.-> updateEditingConnectControllerView editingReferenceLine -.被读取于.-> updateReferenceLineView

节点选择与编辑

选择状态

编辑系统在 options.editingController.nodes 中维护当前被选中的节点集合。节点可以通过编程方式或通过用户交互被添加到编辑集合中或从中移除。

graph LR
    subgraph "选择 API"
        setEditingNodes["setEditingNodes(nodes)
RelationGraphWith91Editing:36"] addEditingNode["addEditingNode(node)
RelationGraphWith91Editing:52"] removeEditingNode["removeEditingNode(node)
RelationGraphWith91Editing:66"] toggleEditingNode["toggleEditingNode(node)
RelationGraphWith91Editing:76"] end subgraph "内部状态" editingController["editingController
nodes: RGNode[]
show: boolean
x, y, width, height"] end subgraph "视图更新" updateView["_updateEditingControllerView()
RelationGraphWith91Editing:98
计算包围框
定位覆盖层"] end setEditingNodes --> editingController addEditingNode --> editingController removeEditingNode --> editingController toggleEditingNode --> editingController editingController --> updateView

控制器的位置与尺寸会根据所有已选节点的包围框自动计算;当选择多个节点时,可选地应用内边距(padding)。


节点缩放

当节点被选中时,会在 RGResizeHandlePosition 类型定义的编辑控制器周围八个位置显示缩放手柄。用户拖拽这些手柄以按比例缩放已选节点。

手柄位置 调整项 实现逻辑
't' Y 位置、高度 newY = startEventXy + buff_y
newHeight = startHeight * scale - buff_y
'r' 仅宽度 newWidth = startWidth * scale + buff_x
'b' 仅高度 newHeight = startHeight * scale + buff_y
'l' X 位置、宽度 newX = startEventXy + buff_x
newWidth = startWidth * scale - buff_x
'tl' X、Y、宽度、高度 组合顶部与左侧逻辑
'tr' Y、宽度、高度 组合顶部与右侧逻辑
'bl' X、宽度、高度 组合底部与左侧逻辑
'br' 宽度、高度 组合底部与右侧逻辑

图:缩放操作流程

graph TB
    subgraph "缩放方法调用"
        onResizeStart["onResizeStart(type: RGResizeHandlePosition, e)
Line 150"] setupListeners["设置 mousemove/mouseup 监听器
_onResizing, _onResizeEnd"] startRAF["启动 requestAnimationFrame 循环
_resizeDraggingTimer"] onResizingRequest["onResizingRequest()
Line 204
每帧调用"] applyResizeScale["_applyResizeScale(e)
Line 272
计算 scale_x, scale_y
更新所有已选节点"] emitBeforeResize["触发 RGEventNames.beforeNodeResize
逐节点,可取消"] updateNodes["dataProvider.updateNode(id, {x, y, width, height})"] onResizeEnd["onResizeEnd(e)
Line 327
取消 RAF,清理监听器"] end subgraph "状态变量" startPoint["_startPoint: {x, y}"] startSize["_startSize: {x, y, width, height,
widthOnCanvas, heightOnCanvas}"] nodeStartSizeMap["_nodeStartSizeMap
WeakMap<RGNode, size>"] resizeType["_resizeType: RGResizeHandlePosition"] resizeDraggingEvent["_resizeDraggingEvent: RGUserEvent"] end onResizeStart --> startPoint onResizeStart --> startSize onResizeStart --> nodeStartSizeMap onResizeStart --> resizeType onResizeStart --> setupListeners onResizeStart --> startRAF setupListeners --> resizeDraggingEvent startRAF --> onResizingRequest onResizingRequest --> applyResizeScale applyResizeScale --> emitBeforeResize emitBeforeResize --> updateNodes onResizingRequest --> onResizingRequest setupListeners --> onResizeEnd

缩放系统使用 requestAnimationFrame 以获得平滑渲染,并将初始节点尺寸存储在 _nodeStartSizeMap(WeakMap)中。会根据缩放手柄类型分别为 X 轴与 Y 轴计算缩放系数。RGEventNames.beforeNodeResize 事件允许自定义逻辑来修改或取消单个节点的缩放。


带参考线的节点拖拽

拖拽系统通过 editingReferenceLine 状态提供可视化对齐辅助线。当启用 options.showReferenceLine 时,系统会在被拖拽节点与附近节点对齐时显示垂直与水平参考线。

图:参考线计算流程

graph TB
    subgraph "拖拽事件流程"
        onNodeDragStart["onNodeDragStart(willMoveNode, e)
RelationGraphWith7Event:80"] draggingCallback["draggingCallback()
每个 RAF tick 调用"] draggingSelectedNodes["draggingSelectedNodes(node, newX, newY, buff_x, buff_y)
RelationGraphWith91Editing:347"] updateReferenceLineView["updateReferenceLineView(node, x, y, buff_x, buff_y)
RelationGraphWith91Editing:429
返回: {showV, fixedX, showH, fixedY} | undefined"] end subgraph "对齐检测" filterNearNodes["筛选 600px 内节点
排除正在拖拽的节点
按与 draggedNode 的距离排序"] checkAlignments["对每个 nearNode:
检查 6 种对齐:
- XStart, XCenter, XEnd
- YStart, YCenter, YEnd
matchDistance: 5px"] setLineState["更新 options.editingReferenceLine:
directionV, directionH
v_x, v_y, v_height
h_x, h_y, h_width"] applySnap["如果 options.referenceLineAdsorption:
返回 fixedX, fixedY
以吸附位置"] end onNodeDragStart --> draggingCallback draggingCallback --> draggingSelectedNodes draggingSelectedNodes --> updateReferenceLineView updateReferenceLineView --> filterNearNodes filterNearNodes --> checkAlignments filterNearNodes --> checkAlignments checkAlignments --> setLineState checkAlignments --> applySnap applySnap --> draggingSelectedNodes

对齐检测逻辑:

对齐类型 比较方式 参考线位置
XStart abs(draggedNode.x - node.x) < 5 node.x 处的垂直线
XCenter abs(draggedNode.centerX - node.centerX) < 5 node.centerX 处的垂直线
XEnd abs(draggedNode.endX - node.endX) < 5 node.endX 处的垂直线
YStart abs(draggedNode.y - node.y) < 5 node.y 处的水平线
YCenter abs(draggedNode.centerY - node.centerY) < 5 node.centerY 处的水平线
YEnd abs(draggedNode.endY - node.endY) < 5 node.endY 处的水平线

系统使用 RGGraphMath 中的 getNodeDistance() 按邻近程度对候选节点排序。当 options.referenceLineAdsorption 为 true 时,返回的 fixedXfixedY 值会覆盖拖拽位置,从而将节点吸附到对齐位置。


连线编辑组件

连线编辑控制器概览

RGEditingLineController 组件提供用于修改连线属性的交互控件。它会在连线顶点显示可拖拽手柄、为曲线提供控制点、为正交线提供分段点,并提供可编辑的文本标签。

graph TB
    subgraph "连线编辑状态"
        EditingLine["editingLineController.line
正在编辑的 RGLine 对象"] StartEnd["startPoint: x, y
endPoint: x, y"] CtrlPoints["ctrlPoint1: x, y
ctrlPoint2: x, y
用于 lineShape 6"] Splits["line44Splits[]
RGCtrlPointForLine44[]
用于 lineShape 44, 49"] TextPos["text: x, y
文本标签位置"] end subgraph "可视元素" StartDot["起点顶点手柄
class: rg-line-ctrl-dot start-dot"] EndDot["终点顶点手柄
class: rg-line-ctrl-dot end-dot"] CtrlDot["控制点手柄
class: rg-line-ctrl-dot ctrl-dot"] SplitDot["分段点手柄
class: rg-line-ctrl-dot ctrl-split"] TextEditor["文本编辑器
class: rg-line-ctrl-text
双击编辑"] CtrlLines["SVG 引导线
连接顶点与控制点"] end EditingLine --> StartEnd EditingLine --> CtrlPoints EditingLine --> Splits EditingLine --> TextPos StartEnd --> StartDot StartEnd --> EndDot CtrlPoints --> CtrlDot Splits --> SplitDot TextPos --> TextEditor CtrlPoints --> CtrlLines

控制器可见性由 editingLineController.show 控制。当使用 graphInstance.setEditingLine(line) 将某条连线设置为可编辑时,控制器会自动定位所有手柄并更新视图。


顶点操控

连线的起点与终点顶点可以被拖拽以重新连接连线。startMoveLineVertex() 方法实现了带目标检测的顶点拖拽。

图:顶点拖放流程

graph TB
    subgraph "拖拽发起"
        mousedown["用户在顶点手柄上按下鼠标
.rg-line-ctrl-dot.start-dot
.rg-line-ctrl-dot.end-dot"] startMoveLineVertex["startMoveLineVertex(type, e, callback)
type: 'start' | 'end'
Line 718"] setupDrag["RGDragUtils.startDrag()
设置 mousemove/mouseup"] end subgraph "拖拽循环" updatePreviewLine["更新 newLinkTemplate
fromNode 或 toNode 位置
跟随鼠标光标"] detectTarget["检查 event.target:
- isNode(element)?
- .rg-connect-target?
- 画布点"] showConnectCtrl["如果悬停在节点上
显示 nodeConnectController"] end subgraph "释放处理" onLineVertexBeDropped["onLineVertexBeDropped(e)
Line 755"] determineTarget["确定释放目标:
- RGNode
- RGConnectTarget (targetType, targetData)
- {x, y} 画布位置"] createLineJson["创建 JsonLine:
from: fromNode.id | position
to: toNode.id | position
复制连线属性"] invokeCallback["callback(fromNode, toNode, lineJson)
或 defaultLineVertexBeChangedHandler"] end mousedown --> startMoveLineVertex startMoveLineVertex --> setupDrag setupDrag --> updatePreviewLine updatePreviewLine --> detectTarget detectTarget --> showConnectCtrl setupDrag --> onLineVertexBeDropped onLineVertexBeDropped --> determineTarget determineTarget --> createLineJson createLineJson --> invokeCallback

目标类型判定:

// From RelationGraphWith91Editing.ts:755-820
if (isNode(targetElement)) {
    // 节点连接
    toNode = targetElement as RGNode;
    toNodeTargetType = RGInnerConnectTargetType.Node;
} else if (targetElement.closest('.rg-connect-target')) {
    // 自定义连接目标
    const targetData = connectTargetElement.dataset.targetData;
    const targetType = connectTargetElement.dataset.targetType;
    toNodeTargetType = targetType;
} else {
    // 画布点
    const canvasXy = getCanvasXyByClientXy(clientXy);
    toNode = canvasXy;
    toNodeTargetType = RGInnerConnectTargetType.CanvasPoint;
}

defaultLineVertexBeChangedHandler(line 1029)会自动移除旧连线并将新连线添加到图中。自定义回调可以覆盖该行为。


控制点操控

对于曲线连线形状(lineShape = 6),两个贝塞尔控制点允许调整路径。startMoveLine6CtrlPoint() 方法处理控制点拖拽。

图:贝塞尔控制点编辑

graph TB
    subgraph "控制点状态"
        ctrlPoint1["editingLineController.ctrlPoint1
{x, y} 视图坐标"] ctrlPoint2["editingLineController.ctrlPoint2
{x, y} 视图坐标"] end subgraph "拖拽方法" startMoveLine6CtrlPoint["startMoveLine6CtrlPoint(ctrlPointIndex, e, callback)
ctrlPointIndex: 1 | 2
Line 902"] dragLoop["RGDragUtils.startDrag()
更新控制点位置"] calcOffset["计算相对默认位置的偏移
相对于连线连接点"] storeInData["存入 line.data:
cp1_offsetX, cp1_offsetY
cp2_offsetX, cp2_offsetY"] invokeCallback["callback(line, ctrlPointIndex)"] end subgraph "路径重新生成" updateLine["dataProvider.updateLine(line.id, {data})"] createLineDrawInfo["createLineDrawInfo(link, line)
读取控制点偏移
生成新的 SVG path"] end ctrlPoint1 -.用户拖拽.-> startMoveLine6CtrlPoint ctrlPoint2 -.用户拖拽.-> startMoveLine6CtrlPoint startMoveLine6CtrlPoint --> dragLoop dragLoop --> calcOffset calcOffset --> storeInData storeInData --> invokeCallback invokeCallback --> updateLine updateLine --> createLineDrawInfo

控制点存储结构:

// 存储在 line.data 对象中
interface Line6CtrlData {
    cp1_offsetX?: number;  // 控制点 1 相对默认位置的 X 偏移
    cp1_offsetY?: number;  // 控制点 1 相对默认位置的 Y 偏移
    cp2_offsetX?: number;  // 控制点 2 相对默认位置的 X 偏移
    cp2_offsetY?: number;  // 控制点 2 相对默认位置的 Y 偏移
}

默认控制点位置在 generateLineForCurve() 中基于连接点方向计算。用户偏移会叠加在这些默认值之上。


正交连线分段点

对于正交连线(lineShape 44 和 49),系统会在分段连接处显示可拖拽的分段点。startMoveLine44CtrlPoint() 方法实现分段点操控。

图:正交连线分段点编辑

graph TB
    subgraph "分段点数据结构"
        RGCtrlPointForLine44["RGCtrlPointForLine44
pIndex: number (分段索引)
optionName: string (fd/td/cx/cy/cp-N)
direction: 'v' | 'h'
x, y: number (视图坐标)
startDirection, endDirection
hide?: boolean"] end subgraph "Line 44 分段类型" fd["'fd' - 起点距离
距起始节点的距离"] td["'td' - 终点距离
距结束节点的距离"] cx["'cx' - 中心 X
垂直中心线"] cy["'cy' - 中心 Y
水平中心线"] end subgraph "操控流程" startMoveLine44CtrlPoint["startMoveLine44CtrlPoint(split, e, callback)
Line 956"] dragLogic["RGDragUtils.startDrag()
限制移动:
- 垂直:仅 X
- 水平:仅 Y"] updateLinePoints["updateLinePoints(line, points)
From RGLinePath.ts
用新的分段位置重新计算路径"] storeData["存入 line.data.pathPoints
RGPosition 数组"] invokeCallback["callback(line)
更新完成"] end RGCtrlPointForLine44 --> fd RGCtrlPointForLine44 --> td RGCtrlPointForLine44 --> cx RGCtrlPointForLine44 --> cy startMoveLine44CtrlPoint --> dragLogic dragLogic --> updateLinePoints updateLinePoints --> storeData storeData --> invokeCallback

三段线的分段点命名:

分段数量 分段点位置 optionName 取值
3 段 2 个分段点 由拓扑确定:
- 中心线:'cx''cy'
- 靠近起点:'fd'
- 靠近终点:'td'
4 段 3 个分段点 split[1] = 'fd'
split[2] = 'td'
5 段 4 个分段点 split[1] = 'fd'
split[2] = 'cx''cy'
split[3] = 'td'

direction 字段会约束拖拽移动方向:垂直分段点水平移动(改变 X),水平分段点垂直移动(改变 Y)。更新后的路径存储在 line.data.pathPoints 中,并由 generateLineFor49() 用于渲染。


文本标签编辑

连线文本标签支持拖拽重新定位,以及通过双击进行行内编辑。startMoveLineText() 方法处理文本位置拖拽。

图:文本标签操控

graph TB
    subgraph "文本拖拽流程"
        startMoveLineText["startMoveLineText(e, callback)
Line 1015"] dragLoop["RGDragUtils.startDrag()
更新文本位置"] calcTextOffset["计算相对默认位置的偏移:
textOffset_x = dragX - defaultX
textOffset_y = dragY - defaultY"] updateLineData["存入 line:
line.textOffsetX
line.textOffsetY"] invokeCallback["callback(line)"] end subgraph "文本编辑流程" detectDoubleClick["点击检测
若两次点击间隔 < 500ms:
进入编辑模式"] showInput["在文本标签上方
显示 input 元素"] onTextChange["onLineTextChange(newText)
dataProvider.updateLine(id, {text})"] hideInput["退出编辑模式
隐藏 input 元素"] end subgraph "文本位置计算" defaultPos["默认位置来自
lineDrawInfo.textPosition
(路径中心)"] applyOffset["应用偏移:
finalX = defaultX + textOffsetX
finalY = defaultY + textOffsetY"] viewCoords["转换为视图坐标
editingLineController.text.x, .y"] end startMoveLineText --> dragLoop dragLoop --> calcTextOffset calcTextOffset --> updateLineData updateLineData --> invokeCallback detectDoubleClick --> showInput showInput --> onTextChange onTextChange --> hideInput defaultPos --> applyOffset applyOffset --> viewCoords

文本位置存储:

文本偏移直接存储在连线对象上,而不是存储在 line.data 中:

interface RGLine {
    textOffsetX?: number;  // 相对默认 X 位置的偏移
    textOffsetY?: number;  // 相对默认 Y 位置的偏移
    text?: string;         // 文本内容
}

默认文本位置在 createLineDrawInfo() 中按路径中点计算。渲染时,用户应用的偏移会加到该默认位置上。


交互式创建连线

连线创建工作流允许用户通过选择源节点与目标节点来交互式创建连接。startCreatingLinePlot() 方法用于进入该模式。

图:连线创建状态机

graph TB
    subgraph "发起"
        startCreatingLinePlot["startCreatingLinePlot(e, setting)
Line 487
Set options.creatingLinePlot = true"] setupTemplate["创建 newLineTemplate
来自 setting.template
创建 newLinkTemplate"] setFromNode["如果 setting.fromNode:
设置 newLinkTemplate.fromNode
跳过步骤 1"] addListeners["添加 mousemove 监听器:
onMovingWhenCreatingLinePlot"] end subgraph "步骤 1:选择起始节点" clickNode1["用户点击节点
onNodeClickWhenCreatingLinePlot
Line 828"] setFromNode1["设置 newLinkTemplate.fromNode
记录 _step1EventTime"] end subgraph "步骤 2:鼠标移动" onMovingWhenCreatingLinePlot["onMovingWhenCreatingLinePlot($event)
Line 580
更新 toNode.x, toNode.y"] detectHover["检测悬停目标:
- isNode() → 显示 nodeConnectController
- .rg-connect-target → 解析 targetType/Data
- 否则 → 画布点"] updateJunctionPoint["根据悬停元素
更新 toJunctionPoint"] updateTempLink["更新 newLinkTemplate.toNode
跟随鼠标位置"] end subgraph "步骤 3:选择目标" clickTarget["用户点击目标
onNodeClickWhenCreatingLinePlot (node)
或 onCanvasClickWhenCreatingLinePlot (canvas)"] onReadyToCreateLine["onReadyToCreateLine(fromNode, toNode)
Line 867"] createLineJson["从模板创建 JsonLine
根据 isReverse 设置 from/to"] emitEvent["触发 RGEventNames.onLineBeCreated
{fromNode, toNode, lineJson}"] callbackHandler["调用 _onCreateLineCallback
或 listener.onLineBeCreated"] stopCreatingLinePlot["stopCreatingLinePlot()
Line 553
清理状态与监听器"] end startCreatingLinePlot --> setupTemplate setupTemplate --> setFromNode setFromNode --> addListeners addListeners --> clickNode1 clickNode1 --> setFromNode1 setFromNode1 --> onMovingWhenCreatingLinePlot onMovingWhenCreatingLinePlot --> detectHover detectHover --> updateJunctionPoint updateJunctionPoint --> updateTempLink updateTempLink --> clickTarget clickTarget --> onReadyToCreateLine onReadyToCreateLine --> createLineJson createLineJson --> emitEvent emitEvent --> callbackHandler callbackHandler --> stopCreatingLinePlot

创建模式状态:

// 创建期间的关键状态属性
interface CreatingLineState {
    creatingLinePlot: boolean;           // 模式激活标志
    newLineTemplate: RGLine;             // 连线外观模板
    newLinkTemplate: {                   // 连接状态
        fromNode: RGLineTarget | null;   // 源节点/位置
        toNode: RGLineTarget;            // 目标(跟随鼠标)
        toNodeObject: RGNode | null;     // 最终目标节点
    };
    nodeConnectController: {             // 连接 UI
        show: boolean;
        node: RGLineTarget;
        x, y, width, height: number;
    };
}

nodeConnectController 会在鼠标悬停于节点时显示连接点手柄。对于自定义连接目标,请使用 RGConnectTarget 组件,并设置 data-target-typedata-target-data 属性。


编辑状态持久化

编辑状态存储在 RGOptionsFull 对象中,并与框架特定的数据提供器同步。

状态属性 类型 用途
editingController.nodes RGNode[] 当前用于编辑的已选节点
editingController.show boolean 节点编辑控制器是否可见
editingController.x, y, width, height number 编辑覆盖层的位置与尺寸
editingLineController.line RGLine 当前正在编辑的连线
editingLineController.show boolean 连线编辑控制器是否可见
editingLineController.startPoint {x, y} 起点顶点手柄在视图上的位置
editingLineController.endPoint {x, y} 终点顶点手柄在视图上的位置
editingLineController.ctrlPoint1 {x, y} 第一贝塞尔控制点位置
editingLineController.ctrlPoint2 {x, y} 第二贝塞尔控制点位置
editingLineController.line44Splits RGCtrlPointForLine44[] 正交连线分段点
editingLineController.text {x, y} 文本标签在视图上的位置
editingReferenceLine Object 对齐辅助线配置
showReferenceLine boolean 拖拽期间是否显示参考线
referenceLineAdsorption boolean 是否吸附到参考线

对这些属性的更新会通过响应式数据提供器系统触发视图更新。_updateEditingControllerView()_updateEditingLineView() 方法会重新计算位置并将状态同步到 UI 组件。