drawui-core 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -176,6 +176,8 @@ interface LineShape extends BaseShape {
176
176
  type: 'line';
177
177
  points: Vec2[];
178
178
  connectionType?: ConnectionType;
179
+ /** 用户手工调节过的中间折点(世界坐标),见 ArrowShape.userPoints */
180
+ userPoints?: Vec2[];
179
181
  }
180
182
  type ConnectionType = 'straight' | 'polyline' | 'orthogonal' | 'curve';
181
183
  /** 生成点位创建上下文:from 为源点(生成点位),to 为目标落点 */
@@ -185,12 +187,24 @@ interface GenerationContext {
185
187
  sourceShapeId?: string;
186
188
  edge?: 'n' | 'e' | 's' | 'w';
187
189
  }
188
- /** 生成点位预览:从源点拖出的虚线线条 + 待放置图形轮廓 */
190
+ /** 生成点位预览:与最终创建结果完全一致的连线预览(+ 待放置图形轮廓) */
189
191
  interface GenerationPreview {
190
192
  from: Vec2;
191
193
  to: Vec2;
192
194
  /** 待放置图形的类型;不提供(指向已有图形)时只画线条 */
193
195
  type?: ShapeType;
196
+ /** 源图形 id 与出线边:预览走与最终创建相同的「出线边同轴接入」路由 */
197
+ sourceShapeId?: string;
198
+ sourceEdge?: 'n' | 'e' | 's' | 'w';
199
+ /** 指向已有图形时的目标与绑定(预览即最终落点) */
200
+ targetShapeId?: string;
201
+ targetBinding?: ArrowBinding;
202
+ /** true = 拖拽中(按真实样式预览);false/缺省 = 悬停(浅色虚线提示) */
203
+ dragging?: boolean;
204
+ /** 新建连线的默认样式,与最终创建保持一致 */
205
+ arrowStyle?: ArrowStyle;
206
+ /** 连线本身的颜色/粗细,与最终创建保持一致 */
207
+ lineStyle?: ShapeStyle;
194
208
  }
195
209
  /** 生成点位 → UI 菜单载荷:拖动到空白处后触发的"选择新图形"菜单 */
196
210
  interface GenerationMenuPayload {
@@ -230,6 +244,14 @@ interface ArrowShape extends BaseShape {
230
244
  connectionType?: ConnectionType;
231
245
  arrowHead?: ArrowHeadType;
232
246
  dashed?: boolean;
247
+ /**
248
+ * 用户手工调节过的中间折点(**世界坐标**,不含两端)。
249
+ *
250
+ * 飞书式「选中连线 → 拖动线段上的手柄调整走线位置」写的就是它:
251
+ * 存在时走线 = 起点 → userPoints… → 终点(每一段仍是正交的),
252
+ * 两端依旧跟随绑定(拖动图形后两端自动跟上,中间保持用户摆好的位置)。
253
+ */
254
+ userPoints?: Vec2[];
233
255
  }
234
256
  interface TextShape extends BaseShape {
235
257
  type: 'text';
@@ -803,6 +825,8 @@ declare class Editor {
803
825
  private snapGuides;
804
826
  private connectingTargetId;
805
827
  private genPreview;
828
+ private hoverAnchorShapeId;
829
+ private hoverConnectorId;
806
830
  private eraserPoint;
807
831
  private mindMapEditingShapeId;
808
832
  private mindMapSelectedNode;
@@ -819,6 +843,23 @@ declare class Editor {
819
843
  private arrowStyle;
820
844
  constructor(canvas: HTMLCanvasElement, options?: EditorOptions);
821
845
  private setupEventListeners;
846
+ /** 窗口失焦(切页签/alt-tab):按下状态与一切瞬态预览都作废,避免"卡住" */
847
+ private handleWindowBlur;
848
+ /**
849
+ * 在画布之外按下指针(工具栏、面板、页面空白)→ 立刻收尾残留交互。
850
+ * 用户预期是「点哪儿都能把残留的虚线清掉」,这里就是那根兜底绳。
851
+ * 若此时 pointerIsDown 仍是 true,说明上一轮的 pointerup 丢了(在窗口外松手),
852
+ * 一并把按下状态复位。
853
+ */
854
+ private handleGlobalPointerDown;
855
+ /**
856
+ * 当前是否存在「本该结束、却还留着」的瞬态交互。
857
+ * 注意:只认拖拽态(工具非空闲 / 目标高亮 / 对齐线 / 框选)——
858
+ * 悬停产生的锚点与悬停预览是常驻的合法状态,不能算残留,否则每次移动都会被打断。
859
+ */
860
+ private shouldRecoverTransient;
861
+ /** 收尾:让工具回到空闲态,并清空画布上的瞬态可视化 */
862
+ private cancelTransientInteraction;
822
863
  private handlePointerDown;
823
864
  private handlePointerMove;
824
865
  private handlePointerUp;
@@ -844,8 +885,23 @@ declare class Editor {
844
885
  /** 设置对齐参考线(由工具在拖拽过程中调用) */
845
886
  private setSnapGuides;
846
887
  private setConnectingTarget;
847
- /** 设置生成点位预览(拖动/悬浮中显示的线条 + 待放置图形) */
888
+ /** 设置生成点位预览(拖动/悬浮中显示的连线 + 待放置图形虚框) */
848
889
  private setGenerationPreview;
890
+ /** 设置悬停图形(画布为其绘制四边连接锚点) */
891
+ private setHoverAnchorShape;
892
+ /** 设置悬停连线(画布为整条线描一层高亮) */
893
+ private setHoverConnector;
894
+ /**
895
+ * 某条连线当前的真实走线(世界坐标折点)。
896
+ * 交给工具层做线段手柄的命中与拖动 —— 用的是渲染器同一套计算,保证「看到哪就能拖哪」。
897
+ */
898
+ private getConnectorRoute;
899
+ /**
900
+ * 清掉所有「瞬态」的交互可视化状态:连线预览、目标高亮、对齐参考线、框选、悬停锚点。
901
+ * 这些状态只在一次交互过程中有效 —— 只要交互结束(或事件丢失)就必须清空,
902
+ * 否则会残留虚线/虚框且越点越多。返回是否确有清理(供调用方决定是否重绘)。
903
+ */
904
+ private clearTransientPreview;
849
905
  /** 拖动到空白处松开 → 弹出"选择新图形"菜单(转发给 React UI) */
850
906
  private openGenerationMenu;
851
907
  private lineStyle;
@@ -853,10 +909,7 @@ declare class Editor {
853
909
  private computeSourceBinding;
854
910
  /**
855
911
  * 连线到达目标图形时,应吸附到目标面向来线的那个边。
856
- * 关键规则:接入边与出线边「同轴」—— 从源图形左右边出线,就优先接目标的
857
- * 左/右边(面向来线的一侧);从上下边出线,就优先接目标的上/下边。
858
- * 这样多条分支连线才能共用同一条正交主干总线(飞书式分叉),而不是各自
859
- * 横着拐进目标的顶边、沿边缘爬进去。
912
+ * 规则唯一来源:`docking.ts` 的 pickDockSide(与出线边同轴,详见该文件注释)。
860
913
  */
861
914
  private connectHandle;
862
915
  /** 仅创建一条从源点引出、末端悬空的线条并选中它 */
@@ -1095,6 +1148,69 @@ declare class Editor {
1095
1148
  dispose(): void;
1096
1149
  }
1097
1150
 
1151
+ /**
1152
+ * 连线「手工调线段」的纯几何(飞书式交互的数学部分,不含任何画布/编辑器依赖)。
1153
+ *
1154
+ * 交互模型:选中一条连线 → 在每一段(够长的)线段的**中点**画一个小手柄 →
1155
+ * 拖动它就把这一段沿垂直方向平移,相邻的垂直段自然伸缩,整条线始终横平竖直;
1156
+ * 两端的停靠点/端点**永远不动**,必要时在端点旁补一个折点(这就是飞书里
1157
+ * 「拖中间那段会让线在两端各拐一下」的效果)。
1158
+ *
1159
+ * 结果写回 `ArrowShape.userPoints` / `LineShape.userPoints`(世界坐标,不含两端)。
1160
+ */
1161
+ /**
1162
+ * 短于这个长度的线段不生成手柄(太短的段拖不动、手柄比线还长也没意义,飞书同样会略过)。
1163
+ * 取 40:既排除「端点旁自动补出来的 24px 桩」,也放过那些只有几十像素、一拖就糊成一团的短段。
1164
+ */
1165
+ declare const MIN_HANDLE_SEGMENT = 40;
1166
+ /**
1167
+ * 台阶的最小高度(磁吸半径)。
1168
+ *
1169
+ * 拖动一段时,只要平移后的位置落进相邻水平线的这个范围内,就**直接吸回那条线**
1170
+ * (=把台阶抹平)。于是:
1171
+ * - 拖不出 1~19px 的浅台阶 —— 那种「两条平行线贴在一起」的观感就是用户说的「变成两条线段」;
1172
+ * - 已经存在的浅台阶,往基线方向一拖就自动抹平(不会出现「拖不动、回不去」);
1173
+ * - 想真正折出台阶,明确拖过这个距离即可(超过就按实际位置走,不再吸附)。
1174
+ */
1175
+ declare const MIN_JOG = 20;
1176
+ interface SegmentHandle {
1177
+ /** 线段序号:route[index] → route[index + 1] */
1178
+ index: number;
1179
+ /** 手柄中心(世界坐标)= 线段中点 */
1180
+ center: Vec2;
1181
+ /** 线段轴向:'H' 水平(可上下拖)/ 'V' 垂直(可左右拖) */
1182
+ axis: 'H' | 'V';
1183
+ /** 线段长度 */
1184
+ length: number;
1185
+ }
1186
+ /** 点到线段的最短距离(连线命中、手柄命中共用;SelectTool 也委托到这里) */
1187
+ declare function pointToSegmentDistance(point: Vec2, a: Vec2, b: Vec2): number;
1188
+ /** 一条走线(世界坐标折线)上所有可拖动的线段手柄 */
1189
+ declare function connectorSegmentHandles(route: Vec2[], minLength?: number): SegmentHandle[];
1190
+ /**
1191
+ * 命中要拖动的线段:优先「手柄中心 handleRadius 内」;没直接命中就给一次宽容 ——
1192
+ * 取离得最近的、够长可拖的那一段(点到该段距离 ≤ maxSegmentDist)。
1193
+ *
1194
+ * 这样「抓在选中连线的任意位置上」都能拖到最近的那段,也让「抓线段」优先于
1195
+ * 拖整条线/从锚点拉新线 —— 避免在连线上按一下却拉出一条新线。
1196
+ */
1197
+ declare function pickSegmentHandle(route: Vec2[], point: Vec2, handleRadius?: number, maxSegmentDist?: number): SegmentHandle | null;
1198
+ /**
1199
+ * 把第 index 段沿其垂直方向平移 delta(水平段:>0 = 向下;垂直段:>0 = 向右),
1200
+ * 返回平移后的**新走线**(首尾点保持不变)。
1201
+ *
1202
+ * 传入的 route 应当始终是「按下时的那条走线」,每次移动都从它重新算 —— 因为拖首/末段
1203
+ * 会插入折点、段序号会变,从原始走线重算才能得到稳定(不漂移)的结果。
1204
+ */
1205
+ declare function offsetConnectorSegment(route: Vec2[], index: number, delta: number): Vec2[];
1206
+ /**
1207
+ * 去掉连续重复点与共线冗余点,精简冗余拐点。
1208
+ * (唯一实现:Renderer.collapseLinePoints 与交互层写回前都调它 —— 别再各写一份)
1209
+ */
1210
+ declare function collapseRoute(pts: Vec2[]): Vec2[];
1211
+ /** 走线 → 可写回 shape 的中间折点(去掉两端) */
1212
+ declare function routeToUserPoints(route: Vec2[]): Vec2[];
1213
+
1098
1214
  interface SnapGuide {
1099
1215
  type: 'horizontal' | 'vertical';
1100
1216
  position: number;
@@ -1126,7 +1242,11 @@ declare class Renderer {
1126
1242
  nodeId: string;
1127
1243
  } | null,
1128
1244
  /** 导出用:非 null 时先以该颜色铺满整块画布(白底等),默认不铺(透明) */
1129
- background?: string | null): void;
1245
+ background?: string | null,
1246
+ /** 悬停的图形 id:为其绘制四边连接锚点(飞书式悬浮提示),已选中图形由选择手柄负责 */
1247
+ hoverAnchorShapeId?: string | null,
1248
+ /** 悬停的连线 id:整条线描一层浅蓝高亮(提示「这条线可点、点开能调线段」) */
1249
+ hoverConnectorId?: string | null): void;
1130
1250
  /** 橡皮擦:半透明主色圆 + 主色描边,半径与笔刷命中范围一致(屏幕像素恒定) */
1131
1251
  private drawEraserBrush;
1132
1252
  /** 连接进行中,高亮目标图形四边中点的可连锚点 */
@@ -1169,12 +1289,8 @@ declare class Renderer {
1169
1289
  private drawUmlInterface;
1170
1290
  /** 默认拐点圆角半径(页面坐标) */
1171
1291
  private static readonly DEFAULT_CORNER_RADIUS;
1172
- /** 连线与图形之间保留的最小避障间距,避免线条贴边/粘在图形上 */
1173
- private static readonly ROUTE_CLEARANCE;
1174
1292
  /** 连接点处的垂直出线桩长度:第一段垂直脱离图形边缘,避免贴角/贴边 */
1175
1293
  private static readonly CONNECTION_LEAD;
1176
- /** 连接的源/目标图形作为障碍时使用的避让间距(足够阻挡中段折回穿过,又允许紧邻节点直连) */
1177
- private static readonly CONNECTED_PAD;
1178
1294
  /**
1179
1295
  * 思维导图:先画连线(父右缘 → 子左缘的平滑曲线),再画节点
1180
1296
  * (白底圆角矩形 + 层级色描边 + 图标/文本/左上角属性说明/折叠标记)。
@@ -1184,21 +1300,68 @@ declare class Renderer {
1184
1300
  private drawMindMapNodes;
1185
1301
  /** 按像素宽度截断文本并加省略号 */
1186
1302
  private ellipsizeText;
1303
+ /**
1304
+ * 把连线两端解析成世界坐标折点 + 停靠法向(绑定落点、auto 重选、未绑定端点的轮廓吸附)。
1305
+ * 绘制与 getConnectorRoute 共用这一份,避免两处规则漂移。
1306
+ */
1307
+ private resolveConnectorEnds;
1308
+ /**
1309
+ * 用户手工调过折点的连线:走线 = 起点 → 用户折点… → 终点(两端仍跟随绑定/端点)。
1310
+ *
1311
+ * ⚠️ 两端要先做和「两端点」时同样的近似对齐:用户是照着**对齐后**的那条线拖的手柄,
1312
+ * 折点坐标是相对对齐后的轴线算出来的;若这里用未对齐的原始端点接上,就会在两端各
1313
+ * 留下几像素的斜边(画布与导出还会各画各的)。对齐后手工折点与端点严格同轴。
1314
+ */
1315
+ private withUserPoints;
1316
+ /**
1317
+ * 连线当前的真实走线(**世界坐标**折点)。
1318
+ * 手柄命中、手柄拖动都用它 —— 与绘制同一套计算,所以「看到手柄在哪,点它就一定命中」。
1319
+ */
1320
+ getConnectorRoute(shape: Shape, shapeMap: Map<string, Shape>): Vec2[];
1321
+ /**
1322
+ * 选中连线时:在每段(够长的)线段中点画一个可拖动的小手柄(飞书式「拖线段调位置」)。
1323
+ * 拖动交互在 SelectTool 的 'dragging-segment' 状态里。
1324
+ */
1325
+ private drawSegmentHandles;
1187
1326
  private drawLine;
1188
1327
  private drawArrow;
1189
1328
  /** 选中连线 → 在线条上叠加一条高亮辅助线 */
1190
1329
  private drawLinearHighlight;
1191
- /** 生成点位预览:源点圆点 + 虚线线条 + 待放置图形轮廓 */
1330
+ /**
1331
+ * 生成点位预览:源点圆点 + 连线预览 + 待放置图形虚框。
1332
+ *
1333
+ * 关键:预览不再是「直线虚线」——它把待创建的连线合成为一条临时箭头,
1334
+ * 直接走 drawArrow 的真实渲染路径(正交路由 / 垂直出入桩 / 出线边同轴接入 /
1335
+ * 箭头头部 / 虚线样式全一致),因此拖拽时看到的就是松手后得到的样子。
1336
+ * 拖拽中按真实样式绘制;悬停时降为半透明,保持「尚未确定」的观感。
1337
+ */
1192
1338
  private drawGenerationPreview;
1193
- /** 待放置图形的虚线轮廓(按默认尺寸绘制对应类型) */
1339
+ /** 待放置图形的虚线轮廓(按该类型的真实默认尺寸绘制) */
1194
1340
  private drawPreviewShapeOutline;
1195
1341
  private static readonly ALIGN_TOLERANCE;
1342
+ /** 生成预览用的临时 id / 样式(不写入文档,仅本次绘制使用) */
1343
+ private static readonly GEN_PREVIEW_ID;
1344
+ private static readonly GEN_GHOST_ID;
1345
+ /** 与 Editor.lineStyle() 一致:预览线用最终连线的真实颜色/粗细 */
1346
+ private static readonly PREVIEW_LINE_STYLE;
1347
+ /**
1348
+ * 'straight' 连线的近似对齐:**只在「就是两端点」时**把近对齐拉成一条直线。
1349
+ * 一旦用户手工折过点(>2 点),绝不能拉平 —— 否则用户拖出来的折弯会被
1350
+ * 「两端 y 取平均」整条压回原来的直线,看起来就像「拖了没反应/原来的线还在」。
1351
+ */
1352
+ private straightAligned;
1353
+ /**
1354
+ * 实际用于绘制/命中的连线类型:**'straight' 但用户手工折过点 → 按 'polyline' 处理**。
1355
+ * 直线型不参与正交路由,两端停靠点又常常不在同一轴上,原样画会出现几像素的斜边;
1356
+ * 折线化后严格横平竖直(飞书拖过线段后同样变成折线型连接线)。
1357
+ */
1358
+ private effectiveConnType;
1196
1359
  /**
1197
1360
  * 判断连线两端是否水平或垂直对齐(在容差内)。
1198
1361
  * 若在容差内,强制将坐标对齐(避免 grid snapping 导致无法形成直线)。
1199
1362
  */
1200
1363
  private getAlignedPoints;
1201
- /** 生成路径点(对齐容差已在上游处理)。 */
1364
+ /** 生成路径点。近似对齐(容差内)时先拉直,再交给对应连接类型路由。 */
1202
1365
  private buildPathPoints;
1203
1366
  /**
1204
1367
  * 使用 arcTo 绘制带圆角拐点的折线。
@@ -1212,33 +1375,25 @@ declare class Renderer {
1212
1375
  private drawSnapGuides;
1213
1376
  /** 折线:与orthogonal使用相同的H-V-H路由,差异仅在渲染(sharp vs rounded)。dock 法向在首/末段加一段垂直出线桩,垂直脱离图形边缘。 */
1214
1377
  private generatePolylinePoints;
1215
- /** 直角折线:与 polyline 使用相同的 H-V-H 正交路由,仅渲染方式(sharp)不同。 */
1216
- private generateOrthogonalPoints;
1217
- /** 去除连续重合点与共线点,精简冗余拐点 */
1218
- private collapseLinePoints;
1219
- /** 计算单段直角路由:优先无碰撞、且符合总线轴方向的 H-V-H / V-H-V 折线,必要时绕开障碍 */
1220
- private computeOrthogonalRoute;
1221
1378
  /**
1222
- * 在停靠偏好对应的总线轴上,从源侧向外逐点平移总线位置,返回第一条「垂直方向和
1223
- * 末段水平分支都避开所有中间障碍」的总线折线;找不到则返回 null。
1224
- * 平移的起点越靠源侧越好,这样多条同一来源的连线会用同一条紧贴源点的主干总线。
1379
+ * 把折线末段掰到期望轴向('H' 水平收尾 / 'V' 垂直收尾)。
1380
+ * 保证箭头始终沿接入边法向插入(或沿出线方向收尾)。
1225
1381
  */
1226
- private findClearBus;
1227
- /** 收集连线绕行所需的障碍包围盒(转为 shape 本地坐标)。
1228
- * 将本连线之外的所有图形(含其它连线/箭头/手绘)都作为障碍;
1229
- * 连接端点所在图形使用小间距(CONNECTED_PAD),其余图形使用大间距(ROUTE_CLEARANCE)。 */
1230
- private collectRoutingObstacles;
1231
- private pointInRect;
1232
- /** 整个折线路径是否与任一障碍相交 */
1233
- private routeHitsObstacles;
1234
- /** 轴对齐线段是否与任一障碍(向外扩其 pad)相交 */
1235
- private segmentHitsObstacle;
1382
+ private ensureFinalAxis;
1383
+ /** 去除连续重合点与共线点,精简冗余拐点(唯一实现见 connectorEdit.collapseRoute) */
1384
+ private collapseLinePoints;
1236
1385
  /**
1237
- * 正交避障路由器(类飞书)。
1238
- * 用「起终点坐标 + 每个障碍物扩展边界」构造一条布线网格,再用 A* 在网格上求
1239
- * 最短避障折线(半径外扩 pad 保证不贴边)。返回不含 from、含 to 的拐点序列。
1386
+ * 计算单段直角路由:最简单的正交折线(H-V-H / V-H-V / L 拐角)
1387
+ * —— **刻意不做避障**,允许从其它图形上穿过去。
1388
+ *
1389
+ * 为什么不做避障(2026-09-22 决定,对齐飞书):绕行会让路径在拖动时反复改形
1390
+ * (3 段 ↔ 6 段拐点来回跳),观感就是「绕来绕去」;而「挡路/重叠时直接穿过去、
1391
+ * 端点不挪窝、永远横平竖直」更可预期也更好读。实测绕行只在约 6% 的布局下触发,
1392
+ * 但它带来的「画布 ≠ 导出」与拖拽跳变是所有场景都在承受的。
1393
+ *
1394
+ * 规则与 export.dockRoutePoints 完全一致 → 画布所见即导出所得。
1240
1395
  */
1241
- private routeOrthogonalAvoidance;
1396
+ private computeOrthogonalRoute;
1242
1397
  /**
1243
1398
  * 曲线:用三次贝塞尔拼接出平滑曲线(离散化采样)。
1244
1399
  * 端点若停靠在图形上(dock.fromN/toN),控制点沿法向伸出,使曲线垂直脱离 / 垂直接入
@@ -1254,20 +1409,27 @@ declare class Renderer {
1254
1409
  private getTextBounds;
1255
1410
  private wrapLine;
1256
1411
  private truncateLine;
1257
- /** 连接点句柄 → 外法向(页面坐标单位向量) */
1412
+ /** 连接点句柄 → 外法向(页面坐标单位向量;规则见 docking.ts) */
1258
1413
  private static readonly HANDLE_NORMALS;
1259
1414
  /**
1260
- * 自动接入边选择:与出线边「同轴」优先 —— 从源图形左右边出线就接目标的
1261
- * 左/右边(面向来线一侧),从上下边出线就接上/下边;这样分支连线才能共用
1262
- * 同一条正交主干总线,而不是横着拐进目标顶边。无出线方向信息时退回主方向规则。
1415
+ * 自动接入边选择:与出线边「同轴」优先(规则唯一来源见 docking.ts)——
1416
+ * 从源图形左右边出线就接目标的左/右边(面向来线一侧),从上下边出线就接上/下边;
1417
+ * 这样分支连线才能共用同一条正交主干总线,而不是横着拐进目标顶边。
1263
1418
  */
1264
1419
  private pickAutoHandle;
1265
1420
  /** auto 绑定 → 按当前几何重选接入边;显式绑定原样返回 */
1266
1421
  private resolveAutoEndBinding;
1267
1422
  private getBindingPoint;
1423
+ /**
1424
+ * 求某一端的停靠法向。**优先用绑定信息**:绑定已经明确告诉我们是哪个图形的哪条边,
1425
+ * 比几何猜测可靠得多 —— 当有图形与端点所在图形重叠时(典型:新建图形虚框压在源图形上),
1426
+ * 纯几何的 getDockNormal 会挑到「离端点更近的另一张图形」,把出线/接入方向判反。
1427
+ */
1428
+ private resolveDockNormal;
1268
1429
  /**
1269
1430
  * 计算端点所在图形的外法向(垂直于该侧边缘,页面坐标单位向量)。
1270
1431
  * 用于连线垂直脱离/接入图形边缘,端点不在任何图形上时返回 undefined。
1432
+ * preferShapeId:优先认这个图形(端点正好被多张图形覆盖时用它消歧)。
1271
1433
  */
1272
1434
  private getDockNormal;
1273
1435
  /**
@@ -1303,6 +1465,12 @@ declare class Renderer {
1303
1465
  private drawFreehand;
1304
1466
  private drawSelectionOutline;
1305
1467
  private drawSelectionHandles;
1468
+ /**
1469
+ * 四边中点的连接锚点(外圈 + 内点)。
1470
+ * bounds 使用调用方的坐标系:选中手柄路径传入「本地包围盒」(画布已 translate 到图形中心),
1471
+ * 悬停提示路径传入「世界包围盒」(画布未平移)。alpha 用于弱化悬停态。
1472
+ */
1473
+ private drawConnectionDots;
1306
1474
  /** 画旋转图标:顺时针短弧 + 箭头 */
1307
1475
  private drawRotateIcon;
1308
1476
  private getShapeBounds;
@@ -1354,6 +1522,14 @@ interface ToolContext {
1354
1522
  setSnapGuides(guides: SnapGuide[]): void;
1355
1523
  setConnectingTarget(id: string | null): void;
1356
1524
  setGenerationPreview(preview: GenerationPreview | null): void;
1525
+ /** 悬停图形 → 画布为其绘制四边连接锚点(null 清除) */
1526
+ setHoverAnchorShape(id: string | null): void;
1527
+ /** 悬停连线 → 画布为整条线描一层高亮(null 清除) */
1528
+ setHoverConnector(id: string | null): void;
1529
+ /** 某条连线当前的真实走线(世界坐标折点):线段手柄的命中/拖动用它,与画布绘制同一套计算 */
1530
+ getConnectorRoute(shapeId: string): Vec2[];
1531
+ /** 新建连线的线条样式(颜色/粗细),与最终创建的连线保持一致 */
1532
+ getLineStyle(): ShapeStyle;
1357
1533
  /** 拖动到空白处:创建一条浮动线条并弹出"选择新图形"菜单 */
1358
1534
  openGenerationMenu(payload: GenerationMenuPayload): void;
1359
1535
  /** 创建"新图形 + 连线";existingArrowId 存在时把线条末端接到新图形 */
@@ -1392,6 +1568,17 @@ declare abstract class Tool {
1392
1568
  abstract onKeyUp(event: KeyboardEvent): void;
1393
1569
  /** 返回当前指针位置应显示的鼠标样式,null 表示使用默认 */
1394
1570
  getCursor(_point: Vec2): string | null;
1571
+ /**
1572
+ * 是否正处于一次未结束的交互中(拖拽/生成/框选…)。
1573
+ * 编辑器据此判断「指针没按下却仍有工具状态」= 丢事件,需要自愈。
1574
+ */
1575
+ hasTransientState(): boolean;
1576
+ /**
1577
+ * 取消一切「瞬态」交互状态(正在拖拽/生成/框选/旋转…),回到干净的空闲态。
1578
+ * 用于指针事件丢失(在窗口外松手、失焦)、点击画布外、切换工具时的自愈。
1579
+ * @returns 是否确实清理了状态(false = 本来就是空闲态,调用方可跳过重绘)
1580
+ */
1581
+ cancelTransient(): boolean;
1395
1582
  dispose(): void;
1396
1583
  }
1397
1584
 
@@ -1402,6 +1589,16 @@ declare class SelectTool extends Tool {
1402
1589
  private resizingState;
1403
1590
  private generatingState;
1404
1591
  private hoverGenPoint;
1592
+ /** 悬停(未按下)的图形 id:为其显示四边连接锚点,可直接从这里拖出连线 */
1593
+ private hoverShapeId;
1594
+ /** 悬停(未按下)的连线 id:整条线描高亮 */
1595
+ private hoverConnectorId;
1596
+ /**
1597
+ * 正在拖动某条连线的第 index 段(飞书式「拖线段调位置」)。
1598
+ * route 是**按下那一刻**的走线:每次移动都从它重新算(拖动会插入折点、段序号会变,
1599
+ * 从原始走线重算才不会累积漂移)。
1600
+ */
1601
+ private draggingSegment;
1405
1602
  private rotatingState;
1406
1603
  private draggingArrowId;
1407
1604
  private draggingTerminal;
@@ -1410,12 +1607,51 @@ declare class SelectTool extends Tool {
1410
1607
  private brushShiftKey;
1411
1608
  /** 生成点位默认创建的图形类型:源图形若为可创建类型则同型,否则用矩形 */
1412
1609
  private defaultGenType;
1610
+ /**
1611
+ * 生成点位预览:带上源图形/出线边/目标绑定/线条样式,画布据此走「与最终创建完全一致」
1612
+ * 的真实渲染路径(正交路由 + 出入桩 + 同轴接入边 + 箭头样式),做到拖拽即所见即所得。
1613
+ */
1413
1614
  private genPreviewOf;
1414
1615
  /** 把生成点位打包成创建上下文(from 为源点,to 为目标落点) */
1415
1616
  private genContext;
1416
1617
  onPointerDown(point: Vec2, event: PointerEvent): void;
1417
1618
  onPointerMove(point: Vec2, event: PointerEvent): void;
1418
1619
  onPointerUp(point: Vec2, _event: PointerEvent): void;
1620
+ /**
1621
+ * 点是否落在某条连线的**当前真实走线**上(容差 10px)。
1622
+ * 画布上看到的那条线、悬停高亮、点击命中、手柄命中全部用这一份几何 ——
1623
+ * 「看到哪就能点到哪」,不会出现悬停能亮、点击没反应。
1624
+ */
1625
+ private isPointOnConnector;
1626
+ /**
1627
+ * 悬停命中的连线 id(未选中才有高亮提示;选中的连线由线段手柄负责显示)。
1628
+ * 与点击命中同一套判定(isPointOnConnector)。
1629
+ */
1630
+ private findHoverConnector;
1631
+ /**
1632
+ * 命中已选中连线上要拖动的线段(先手柄中心 10px,再退一步「离得最近的段」14px)。
1633
+ * 这样在选中连线上任意位置按下都能拖到最近那一段,也就不会误触发「拖整条线 / 从锚点拉新线」。
1634
+ */
1635
+ private hitConnectorSegmentHandle;
1636
+ /** 拖线段时的光标:水平段能上下拖 → 上下箭头;垂直段能左右拖 → 左右箭头 */
1637
+ private segmentCursor;
1638
+ /** 拖动线段:把这一段沿垂直方向平移(相邻段自然伸缩),两端端点永不移动 */
1639
+ private handleDraggingSegment;
1640
+ /**
1641
+ * 把调整后的走线写回 shape:
1642
+ * - 绑定连线(有 start/endBinding)只写中间折点 `userPoints`,两端继续跟随绑定;
1643
+ * - 自由连线(无绑定)整条走线就是它自己的 `points`。
1644
+ * 先把走线里的重复点/共线冗余点去掉:**如果去掉后就是一条直线(≤2 点),把 userPoints 清空**,
1645
+ * 交还给自动路由 —— 这样「往基线拖、把浅台阶抹平」之后,连线是真的恢复原样,而不是留着一堆零高度折点。
1646
+ */
1647
+ private writeConnectorRoute;
1648
+ hasTransientState(): boolean;
1649
+ /**
1650
+ * 取消一切瞬态交互(拖拽/生成/框选/旋转/连线端点拖拽)并清掉画布上的瞬态预览。
1651
+ * 触发场景:指针在窗口外松手丢失 pointerup、窗口失焦、点击画布外、无按压的移动、
1652
+ * 切换工具、Esc。返回值用于让调用方决定是否需要重绘。
1653
+ */
1654
+ cancelTransient(): boolean;
1419
1655
  onKeyDown(event: KeyboardEvent): void;
1420
1656
  onKeyUp(_event: KeyboardEvent): void;
1421
1657
  /** 鼠标样式:伸缩手柄 → 对应方向的双向箭头;拖拽中 → 抓取 */
@@ -1448,20 +1684,43 @@ declare class SelectTool extends Tool {
1448
1684
  /** 生成点位命中阈值 / 单击 vs 拖动判定阈值 */
1449
1685
  private static readonly GEN_HIT_THRESHOLD;
1450
1686
  private static readonly CLICK_MOVE_THRESHOLD;
1451
- /** 收集选中图形上的全部生成点位(图形四边中点/快捷连线按钮) */
1687
+ /** 支持四边连接锚点的图形类型(矩形类、流程图形、UML 等) */
1688
+ private static readonly GEN_SUPPORTED_TYPES;
1689
+ /**
1690
+ * 收集图形上的全部生成点位(四边中点/快捷连线按钮)。
1691
+ * extraShapeId:额外把某个图形(当前悬停的图形)纳入 —— 未选中图形也能直接连出连线。
1692
+ */
1452
1693
  private computeGenPoints;
1453
1694
  /** 命中检测:指针是否落在某个生成点位附近(取最近的) */
1454
1695
  private hitGenPoint;
1696
+ /** 悬停(未按下)时指针所在的、支持连线锚点的图形 id(已选中的交给选择手柄,返回 null) */
1697
+ private findHoverAnchorShape;
1455
1698
  /** 生成点位预览的落点:拖动时跟随光标;静止/悬浮时向外延伸一个默认位置的框 */
1456
1699
  private genPreviewTarget;
1457
- /** 生成拖动:更新预览(线条 + 框)与目标高亮 */
1700
+ /** 生成拖动:更新预览(真实样式的连线 + 待放置图形虚框)与目标高亮 */
1458
1701
  private handleGenerating;
1459
1702
  private handleRotating;
1460
1703
  /** 根据绑定信息返回目标图形的实际吸附点 */
1461
1704
  private bindingPosition;
1462
1705
  private isPointInShape;
1463
1706
  private isPointInHexagon;
1707
+ /** 点到线段最短距离(唯一实现见 connectorEdit.pointToSegmentDistance) */
1464
1708
  private pointToLineDistance;
1709
+ /**
1710
+ * 待新建图形与源图形重叠时,把它沿「源→目标」方向推到不重叠(留一个引线距离)。
1711
+ *
1712
+ * 为什么必须做:图形重叠时,连线要接入的那条边会落在源图形内部/后方,
1713
+ * 路由只能往回折(实测出现「往右拖一点点 → 一条往回折的钩子甚至横穿的怪线」),
1714
+ * 而且新图形压着源图形本身就不好看。推开后接入边始终在出线方向的前方,
1715
+ * 引线距离取 CONNECTION_LEAD(24),正好让「出线桩 → 接入点」是一条直线。
1716
+ */
1717
+ private avoidSourceOverlap;
1718
+ /**
1719
+ * 预判「将要新建的图形」面向来线的那个接入点:
1720
+ * 用该类型的真实默认尺寸 + 与 Editor.connectHandle 完全相同的 docking 规则算落点,
1721
+ * 让「拖拽预览 → 松手后的浮动线条 → 插入后的真实连线」位置一致、不跳变。
1722
+ */
1723
+ private futureDockPoint;
1465
1724
  private getShapeBounds;
1466
1725
  private hitTestArrowTerminal;
1467
1726
  private findShapeNearPoint;
@@ -1658,4 +1917,4 @@ declare function countMindMapNodes(root: MindMapNode): number;
1658
1917
  */
1659
1918
  declare function layoutMindMap(root: MindMapNode): MindMapLayoutResult;
1660
1919
 
1661
- export { type ArrowBinding, type ArrowHeadType, type ArrowShape, type ArrowStyle, ArrowTool, Camera, type CameraState, type CircleShape, type ConnectionType, DEFAULT_HIGHLIGHTER_COLOR, type DiamondShape, type DrawShape, type DrawuiTheme, type DrawuiThemeSelection, Editor, type EditorData, type EditorOptions, type EditorState, type EllipseShape, type ExportFormat, type ExportOptions, type FreehandMode, FreehandTool, type GenerationContext, HandTool, type HexagonShape, type Language, type LineShape, LineTool, MINDMAP_COLORS, MINDMAP_FONT_SIZE, MINDMAP_MAX_WIDTH, MINDMAP_MIN_WIDTH, MINDMAP_NODE_HEIGHT, Mat2d, type MessageKey, type MindMapLayoutResult, type MindMapNode, type MindMapNodePos, type MindMapShape, type OctagonShape, type ParallelogramShape, type PentagonShape, type Rect, type RectangleShape, Renderer, type RoundedRectangleShape, SelectTool, type Shape, type ShapeStyle, ShapeTool, type ShapeType, type SnapGuide, type StarShape, THEME, type TextShape, type ToolType, type TrapezoidShape, type TriangleShape, UML_FONT_SIZE, UML_HEADER_HEIGHT, UML_LINE_HEIGHT, UML_MIN_WIDTH, UML_PADDING_X, UML_PADDING_Y, type UiEvent, type UmlAttribute, type UmlClassShape, type UmlInterfaceShape, type UmlMethod, type UmlVisibility, Vec, type Vec2 as Vec2Type, calcUmlClassSize, cloneMindMapNode, countMindMapNodes, downloadBlob, exportToBlob, findMindMapNode, getContentBounds, getLanguage, getMindMapNodeDepth, getMindMapParent, getTheme, hexToRgba, layoutMindMap, mindMapNodeColor, mindMapNodeSize, renderSVGString, renderToCanvas, setLanguage, setTheme, shadeColor, subscribeLanguage, subscribeTheme, t };
1920
+ export { type ArrowBinding, type ArrowHeadType, type ArrowShape, type ArrowStyle, ArrowTool, Camera, type CameraState, type CircleShape, type ConnectionType, DEFAULT_HIGHLIGHTER_COLOR, type DiamondShape, type DrawShape, type DrawuiTheme, type DrawuiThemeSelection, Editor, type EditorData, type EditorOptions, type EditorState, type EllipseShape, type ExportFormat, type ExportOptions, type FreehandMode, FreehandTool, type GenerationContext, HandTool, type HexagonShape, type Language, type LineShape, LineTool, MINDMAP_COLORS, MINDMAP_FONT_SIZE, MINDMAP_MAX_WIDTH, MINDMAP_MIN_WIDTH, MINDMAP_NODE_HEIGHT, MIN_HANDLE_SEGMENT, MIN_JOG, Mat2d, type MessageKey, type MindMapLayoutResult, type MindMapNode, type MindMapNodePos, type MindMapShape, type OctagonShape, type ParallelogramShape, type PentagonShape, type Rect, type RectangleShape, Renderer, type RoundedRectangleShape, type SegmentHandle, SelectTool, type Shape, type ShapeStyle, ShapeTool, type ShapeType, type SnapGuide, type StarShape, THEME, type TextShape, type ToolType, type TrapezoidShape, type TriangleShape, UML_FONT_SIZE, UML_HEADER_HEIGHT, UML_LINE_HEIGHT, UML_MIN_WIDTH, UML_PADDING_X, UML_PADDING_Y, type UiEvent, type UmlAttribute, type UmlClassShape, type UmlInterfaceShape, type UmlMethod, type UmlVisibility, Vec, type Vec2 as Vec2Type, calcUmlClassSize, cloneMindMapNode, collapseRoute, connectorSegmentHandles, countMindMapNodes, downloadBlob, exportToBlob, findMindMapNode, getContentBounds, getLanguage, getMindMapNodeDepth, getMindMapParent, getTheme, hexToRgba, layoutMindMap, mindMapNodeColor, mindMapNodeSize, offsetConnectorSegment, pickSegmentHandle, pointToSegmentDistance, renderSVGString, renderToCanvas, routeToUserPoints, setLanguage, setTheme, shadeColor, subscribeLanguage, subscribeTheme, t };