rendx-connect-plugin 0.2.0
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/LICENSE +21 -0
- package/dist/main.cjs +546 -0
- package/dist/main.d.cts +155 -0
- package/dist/main.d.ts +155 -0
- package/dist/main.js +519 -0
- package/package.json +49 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
var __typeError = (msg) => {
|
|
2
|
+
throw TypeError(msg);
|
|
3
|
+
};
|
|
4
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
9
|
+
|
|
10
|
+
// src/connect.ts
|
|
11
|
+
import { uid8 } from "rendx-core";
|
|
12
|
+
import { Node as EngineNode } from "rendx-engine";
|
|
13
|
+
var DEFAULT_CLASS_NAME = "connectable";
|
|
14
|
+
var DEFAULT_SNAP_RADIUS = 20;
|
|
15
|
+
var DEFAULT_CURSOR = "crosshair";
|
|
16
|
+
var DEFAULT_PREVIEW_STROKE = "#1890ff";
|
|
17
|
+
var DEFAULT_PREVIEW_STROKE_WIDTH = 2;
|
|
18
|
+
var DEFAULT_PREVIEW_DASH = [6, 4];
|
|
19
|
+
var DEFAULT_LINE_STROKE = "#999";
|
|
20
|
+
var DEFAULT_LINE_STROKE_WIDTH = 2;
|
|
21
|
+
var _app, _opts, _state, _source, _sourceAnchor, _snapTarget, _previewLine, _overlayLayer, _connections, _cleanups, _ConnectPlugin_instances, createPreviewLine_fn, showPreview_fn, hidePreview_fn, bindEvents_fn, _onPointerDown, _onPointerMove, _onPointerUp, _onKeyDown, completeConnect_fn, cancelConnect_fn, cleanupState_fn, reset_fn, createEdge_fn, buildEdgeData_fn, createEngineLine_fn, resolveConnectable_fn, findSnapTarget_fn, collectConnectables_fn, isSameElement_fn, getAnchor_fn, defaultAnchor_fn, resolveElementId_fn, getGraph_fn, isDragActive_fn;
|
|
22
|
+
var ConnectPlugin = class {
|
|
23
|
+
// ──────────────────────────────────────────────────────────
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
__privateAdd(this, _ConnectPlugin_instances);
|
|
26
|
+
this.name = "connect";
|
|
27
|
+
this.state = [
|
|
28
|
+
{ key: "connect:connecting", description: "\u662F\u5426\u6B63\u5728\u8FDE\u63A5", initial: false },
|
|
29
|
+
{ key: "connect:source", description: "\u8FDE\u63A5\u8D77\u70B9 Graphics", initial: null }
|
|
30
|
+
];
|
|
31
|
+
this.layers = [{ name: "selection", zIndex: 10 }];
|
|
32
|
+
// ── 私有字段 ──
|
|
33
|
+
__privateAdd(this, _app, null);
|
|
34
|
+
__privateAdd(this, _opts);
|
|
35
|
+
/** 状态机 */
|
|
36
|
+
__privateAdd(this, _state, 0 /* Idle */);
|
|
37
|
+
/** 连接起点 */
|
|
38
|
+
__privateAdd(this, _source, null);
|
|
39
|
+
/** 起点世界坐标 */
|
|
40
|
+
__privateAdd(this, _sourceAnchor, [0, 0]);
|
|
41
|
+
/** 当前吸附的目标 */
|
|
42
|
+
__privateAdd(this, _snapTarget, null);
|
|
43
|
+
/** 预览线 Node */
|
|
44
|
+
__privateAdd(this, _previewLine, null);
|
|
45
|
+
/** overlay 层(与 selection-plugin 共享的交互层) */
|
|
46
|
+
__privateAdd(this, _overlayLayer, null);
|
|
47
|
+
/** 纯引擎模式下维护的连接列表 */
|
|
48
|
+
__privateAdd(this, _connections, /* @__PURE__ */ new Map());
|
|
49
|
+
/** 清理回调 */
|
|
50
|
+
__privateAdd(this, _cleanups, []);
|
|
51
|
+
// ════════════════════════════════════════════════════════════
|
|
52
|
+
// Internal — 事件回调
|
|
53
|
+
// ════════════════════════════════════════════════════════════
|
|
54
|
+
__privateAdd(this, _onPointerDown, (e) => {
|
|
55
|
+
if (__privateGet(this, _state) !== 0 /* Idle */) return;
|
|
56
|
+
if (__privateMethod(this, _ConnectPlugin_instances, isDragActive_fn).call(this)) return;
|
|
57
|
+
const source = __privateMethod(this, _ConnectPlugin_instances, resolveConnectable_fn).call(this, e.target);
|
|
58
|
+
if (!source) return;
|
|
59
|
+
__privateSet(this, _source, source);
|
|
60
|
+
__privateSet(this, _sourceAnchor, __privateMethod(this, _ConnectPlugin_instances, getAnchor_fn).call(this, source));
|
|
61
|
+
__privateSet(this, _state, 1 /* Connecting */);
|
|
62
|
+
__privateGet(this, _app).setCursor(__privateGet(this, _opts).cursor);
|
|
63
|
+
__privateGet(this, _app).setState("connect:connecting", true);
|
|
64
|
+
__privateGet(this, _app).setState("connect:source", source);
|
|
65
|
+
__privateMethod(this, _ConnectPlugin_instances, showPreview_fn).call(this, __privateGet(this, _sourceAnchor)[0], __privateGet(this, _sourceAnchor)[1], e.worldX, e.worldY);
|
|
66
|
+
__privateGet(this, _app).requestRender();
|
|
67
|
+
__privateGet(this, _app).bus.emit("connect:start", {
|
|
68
|
+
source,
|
|
69
|
+
origin: [e.worldX, e.worldY]
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
__privateAdd(this, _onPointerMove, (e) => {
|
|
73
|
+
if (__privateGet(this, _state) !== 1 /* Connecting */) return;
|
|
74
|
+
const worldX = e.worldX;
|
|
75
|
+
const worldY = e.worldY;
|
|
76
|
+
__privateSet(this, _snapTarget, __privateMethod(this, _ConnectPlugin_instances, findSnapTarget_fn).call(this, worldX, worldY));
|
|
77
|
+
let endX = worldX;
|
|
78
|
+
let endY = worldY;
|
|
79
|
+
if (__privateGet(this, _snapTarget)) {
|
|
80
|
+
const anchor = __privateMethod(this, _ConnectPlugin_instances, getAnchor_fn).call(this, __privateGet(this, _snapTarget));
|
|
81
|
+
endX = anchor[0];
|
|
82
|
+
endY = anchor[1];
|
|
83
|
+
}
|
|
84
|
+
__privateMethod(this, _ConnectPlugin_instances, showPreview_fn).call(this, __privateGet(this, _sourceAnchor)[0], __privateGet(this, _sourceAnchor)[1], endX, endY);
|
|
85
|
+
__privateGet(this, _app).requestRender();
|
|
86
|
+
__privateGet(this, _app).bus.emit("connect:move", {
|
|
87
|
+
source: __privateGet(this, _source),
|
|
88
|
+
cursor: [worldX, worldY],
|
|
89
|
+
snapTarget: __privateGet(this, _snapTarget)
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
__privateAdd(this, _onPointerUp, () => {
|
|
93
|
+
if (__privateGet(this, _state) !== 1 /* Connecting */) return;
|
|
94
|
+
if (__privateGet(this, _snapTarget)) {
|
|
95
|
+
__privateMethod(this, _ConnectPlugin_instances, completeConnect_fn).call(this, __privateGet(this, _snapTarget));
|
|
96
|
+
} else {
|
|
97
|
+
__privateMethod(this, _ConnectPlugin_instances, cancelConnect_fn).call(this);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
__privateAdd(this, _onKeyDown, (e) => {
|
|
101
|
+
if (e.key === "Escape" && __privateGet(this, _state) === 1 /* Connecting */) {
|
|
102
|
+
__privateMethod(this, _ConnectPlugin_instances, cancelConnect_fn).call(this);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
__privateSet(this, _opts, {
|
|
106
|
+
className: options.className ?? DEFAULT_CLASS_NAME,
|
|
107
|
+
canConnect: options.canConnect ?? null,
|
|
108
|
+
allowSelfLoop: options.allowSelfLoop ?? false,
|
|
109
|
+
anchor: options.anchor ?? null,
|
|
110
|
+
edgeType: options.edgeType ?? null,
|
|
111
|
+
edgeFactory: options.edgeFactory ?? null,
|
|
112
|
+
lineStroke: options.lineStyle?.stroke ?? DEFAULT_LINE_STROKE,
|
|
113
|
+
lineStrokeWidth: options.lineStyle?.strokeWidth ?? DEFAULT_LINE_STROKE_WIDTH,
|
|
114
|
+
previewStroke: options.previewStroke ?? DEFAULT_PREVIEW_STROKE,
|
|
115
|
+
previewStrokeWidth: options.previewStrokeWidth ?? DEFAULT_PREVIEW_STROKE_WIDTH,
|
|
116
|
+
previewDash: options.previewDash ?? DEFAULT_PREVIEW_DASH,
|
|
117
|
+
previewPath: options.previewPath ?? null,
|
|
118
|
+
snapRadius: options.snapRadius ?? DEFAULT_SNAP_RADIUS,
|
|
119
|
+
cursor: options.cursor ?? DEFAULT_CURSOR
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// ════════════════════════════════════════════════════════════
|
|
123
|
+
// Plugin lifecycle
|
|
124
|
+
// ════════════════════════════════════════════════════════════
|
|
125
|
+
install(app) {
|
|
126
|
+
__privateSet(this, _app, app);
|
|
127
|
+
__privateSet(this, _overlayLayer, app.getLayer("selection"));
|
|
128
|
+
__privateGet(this, _overlayLayer).setPointerEvents(false);
|
|
129
|
+
__privateGet(this, _overlayLayer).culling = false;
|
|
130
|
+
__privateMethod(this, _ConnectPlugin_instances, createPreviewLine_fn).call(this);
|
|
131
|
+
__privateMethod(this, _ConnectPlugin_instances, bindEvents_fn).call(this);
|
|
132
|
+
}
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
134
|
+
resize(_w, _h) {
|
|
135
|
+
}
|
|
136
|
+
serialize() {
|
|
137
|
+
const connections = [];
|
|
138
|
+
for (const conn of __privateGet(this, _connections).values()) {
|
|
139
|
+
connections.push({
|
|
140
|
+
id: conn.id,
|
|
141
|
+
sourceUid: conn.source.uid,
|
|
142
|
+
targetUid: conn.target.uid
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return { connections };
|
|
146
|
+
}
|
|
147
|
+
dispose() {
|
|
148
|
+
if (__privateGet(this, _state) === 1 /* Connecting */) {
|
|
149
|
+
__privateMethod(this, _ConnectPlugin_instances, cancelConnect_fn).call(this);
|
|
150
|
+
}
|
|
151
|
+
for (const conn of __privateGet(this, _connections).values()) {
|
|
152
|
+
if (conn.line.parent) {
|
|
153
|
+
conn.line.parent.remove(conn.line);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
__privateGet(this, _connections).clear();
|
|
157
|
+
if (__privateGet(this, _previewLine)) {
|
|
158
|
+
__privateGet(this, _overlayLayer)?.remove(__privateGet(this, _previewLine));
|
|
159
|
+
}
|
|
160
|
+
__privateSet(this, _previewLine, null);
|
|
161
|
+
for (const fn of __privateGet(this, _cleanups)) fn();
|
|
162
|
+
__privateSet(this, _cleanups, []);
|
|
163
|
+
__privateGet(this, _app)?.resetCursor();
|
|
164
|
+
__privateMethod(this, _ConnectPlugin_instances, reset_fn).call(this);
|
|
165
|
+
__privateSet(this, _app, null);
|
|
166
|
+
}
|
|
167
|
+
// ════════════════════════════════════════════════════════════
|
|
168
|
+
// Public API
|
|
169
|
+
// ════════════════════════════════════════════════════════════
|
|
170
|
+
/** 当前是否正在连接 */
|
|
171
|
+
isConnecting() {
|
|
172
|
+
return __privateGet(this, _state) === 1 /* Connecting */;
|
|
173
|
+
}
|
|
174
|
+
/** 获取当前连接起点 */
|
|
175
|
+
getSource() {
|
|
176
|
+
return __privateGet(this, _source);
|
|
177
|
+
}
|
|
178
|
+
/** 编程式取消当前连接 */
|
|
179
|
+
cancel() {
|
|
180
|
+
if (__privateGet(this, _state) === 1 /* Connecting */) {
|
|
181
|
+
__privateMethod(this, _ConnectPlugin_instances, cancelConnect_fn).call(this);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** 获取纯引擎模式下的所有连接记录(只读) */
|
|
185
|
+
getConnections() {
|
|
186
|
+
return [...__privateGet(this, _connections).values()];
|
|
187
|
+
}
|
|
188
|
+
/** 移除纯引擎模式下的连接 */
|
|
189
|
+
removeConnection(id) {
|
|
190
|
+
const conn = __privateGet(this, _connections).get(id);
|
|
191
|
+
if (!conn) return false;
|
|
192
|
+
if (conn.line.parent) {
|
|
193
|
+
conn.line.parent.remove(conn.line);
|
|
194
|
+
}
|
|
195
|
+
__privateGet(this, _connections).delete(id);
|
|
196
|
+
__privateGet(this, _app)?.requestRender();
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 同步纯引擎模式下的连接线位置。
|
|
201
|
+
* 在 render 前调用,更新所有 line 端点为最新的 anchor 坐标。
|
|
202
|
+
*/
|
|
203
|
+
syncConnections() {
|
|
204
|
+
if (__privateGet(this, _connections).size === 0) return;
|
|
205
|
+
let dirty = false;
|
|
206
|
+
for (const conn of __privateGet(this, _connections).values()) {
|
|
207
|
+
const [sx, sy] = __privateMethod(this, _ConnectPlugin_instances, getAnchor_fn).call(this, conn.source);
|
|
208
|
+
const [tx, ty] = __privateMethod(this, _ConnectPlugin_instances, getAnchor_fn).call(this, conn.target);
|
|
209
|
+
conn.line.shape.from(sx, sy, tx, ty);
|
|
210
|
+
conn.line.setDirty(true);
|
|
211
|
+
dirty = true;
|
|
212
|
+
}
|
|
213
|
+
if (dirty) {
|
|
214
|
+
__privateGet(this, _app)?.requestRender();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
_app = new WeakMap();
|
|
219
|
+
_opts = new WeakMap();
|
|
220
|
+
_state = new WeakMap();
|
|
221
|
+
_source = new WeakMap();
|
|
222
|
+
_sourceAnchor = new WeakMap();
|
|
223
|
+
_snapTarget = new WeakMap();
|
|
224
|
+
_previewLine = new WeakMap();
|
|
225
|
+
_overlayLayer = new WeakMap();
|
|
226
|
+
_connections = new WeakMap();
|
|
227
|
+
_cleanups = new WeakMap();
|
|
228
|
+
_ConnectPlugin_instances = new WeakSet();
|
|
229
|
+
// ════════════════════════════════════════════════════════════
|
|
230
|
+
// Internal — 预览线
|
|
231
|
+
// ════════════════════════════════════════════════════════════
|
|
232
|
+
createPreviewLine_fn = function() {
|
|
233
|
+
__privateSet(this, _previewLine, EngineNode.create("path", {
|
|
234
|
+
stroke: __privateGet(this, _opts).previewStroke,
|
|
235
|
+
strokeWidth: __privateGet(this, _opts).previewStrokeWidth,
|
|
236
|
+
strokeDasharray: __privateGet(this, _opts).previewDash,
|
|
237
|
+
fill: "none"
|
|
238
|
+
}));
|
|
239
|
+
__privateGet(this, _previewLine).setDisplay(false);
|
|
240
|
+
__privateGet(this, _previewLine).setName("connect-preview-line");
|
|
241
|
+
__privateGet(this, _overlayLayer).add(__privateGet(this, _previewLine));
|
|
242
|
+
};
|
|
243
|
+
showPreview_fn = function(x1, y1, x2, y2) {
|
|
244
|
+
if (!__privateGet(this, _previewLine)) return;
|
|
245
|
+
const source = [x1, y1];
|
|
246
|
+
const target = [x2, y2];
|
|
247
|
+
const pathData = __privateGet(this, _opts).previewPath ? __privateGet(this, _opts).previewPath(source, target) : `M${x1} ${y1}L${x2} ${y2}`;
|
|
248
|
+
__privateGet(this, _previewLine).shape.from(pathData);
|
|
249
|
+
__privateGet(this, _previewLine).setDisplay(true);
|
|
250
|
+
};
|
|
251
|
+
hidePreview_fn = function() {
|
|
252
|
+
if (!__privateGet(this, _previewLine)) return;
|
|
253
|
+
__privateGet(this, _previewLine).setDisplay(false);
|
|
254
|
+
};
|
|
255
|
+
// ════════════════════════════════════════════════════════════
|
|
256
|
+
// Internal — 事件绑定
|
|
257
|
+
// ════════════════════════════════════════════════════════════
|
|
258
|
+
bindEvents_fn = function() {
|
|
259
|
+
const scene = __privateGet(this, _app).scene;
|
|
260
|
+
scene.on("pointerdown", __privateGet(this, _onPointerDown));
|
|
261
|
+
__privateGet(this, _cleanups).push(() => scene.off("pointerdown", __privateGet(this, _onPointerDown)));
|
|
262
|
+
scene.on("pointermove", __privateGet(this, _onPointerMove));
|
|
263
|
+
__privateGet(this, _cleanups).push(() => scene.off("pointermove", __privateGet(this, _onPointerMove)));
|
|
264
|
+
window.addEventListener("pointerup", __privateGet(this, _onPointerUp));
|
|
265
|
+
__privateGet(this, _cleanups).push(() => window.removeEventListener("pointerup", __privateGet(this, _onPointerUp)));
|
|
266
|
+
window.addEventListener("keydown", __privateGet(this, _onKeyDown));
|
|
267
|
+
__privateGet(this, _cleanups).push(() => window.removeEventListener("keydown", __privateGet(this, _onKeyDown)));
|
|
268
|
+
};
|
|
269
|
+
_onPointerDown = new WeakMap();
|
|
270
|
+
_onPointerMove = new WeakMap();
|
|
271
|
+
_onPointerUp = new WeakMap();
|
|
272
|
+
_onKeyDown = new WeakMap();
|
|
273
|
+
// ════════════════════════════════════════════════════════════
|
|
274
|
+
// Internal — 连接操作
|
|
275
|
+
// ════════════════════════════════════════════════════════════
|
|
276
|
+
/**
|
|
277
|
+
* 完成连接:委托 graph 或自行创建 line
|
|
278
|
+
*/
|
|
279
|
+
completeConnect_fn = function(target) {
|
|
280
|
+
const source = __privateGet(this, _source);
|
|
281
|
+
__privateMethod(this, _ConnectPlugin_instances, hidePreview_fn).call(this);
|
|
282
|
+
__privateMethod(this, _ConnectPlugin_instances, createEdge_fn).call(this, source, target);
|
|
283
|
+
__privateGet(this, _app).bus.emit("connect:complete", {
|
|
284
|
+
source,
|
|
285
|
+
target
|
|
286
|
+
});
|
|
287
|
+
__privateMethod(this, _ConnectPlugin_instances, cleanupState_fn).call(this);
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* 取消连接
|
|
291
|
+
*/
|
|
292
|
+
cancelConnect_fn = function() {
|
|
293
|
+
console.log("cancel");
|
|
294
|
+
__privateMethod(this, _ConnectPlugin_instances, hidePreview_fn).call(this);
|
|
295
|
+
__privateGet(this, _app).bus.emit("connect:cancel", {
|
|
296
|
+
source: __privateGet(this, _source)
|
|
297
|
+
});
|
|
298
|
+
__privateMethod(this, _ConnectPlugin_instances, cleanupState_fn).call(this);
|
|
299
|
+
};
|
|
300
|
+
/**
|
|
301
|
+
* 连接结束后的状态清理
|
|
302
|
+
*/
|
|
303
|
+
cleanupState_fn = function() {
|
|
304
|
+
__privateGet(this, _app).resetCursor();
|
|
305
|
+
__privateGet(this, _app).setState("connect:connecting", false);
|
|
306
|
+
__privateGet(this, _app).setState("connect:source", null);
|
|
307
|
+
console.log("cleanupState", __privateGet(this, _overlayLayer)?.display, __privateGet(this, _app)?.scene);
|
|
308
|
+
__privateGet(this, _app).requestRender();
|
|
309
|
+
__privateMethod(this, _ConnectPlugin_instances, reset_fn).call(this);
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* 复位内部状态
|
|
313
|
+
*/
|
|
314
|
+
reset_fn = function() {
|
|
315
|
+
__privateSet(this, _state, 0 /* Idle */);
|
|
316
|
+
__privateSet(this, _source, null);
|
|
317
|
+
__privateSet(this, _sourceAnchor, [0, 0]);
|
|
318
|
+
__privateSet(this, _snapTarget, null);
|
|
319
|
+
};
|
|
320
|
+
// ════════════════════════════════════════════════════════════
|
|
321
|
+
// Internal — 边的创建(两种路径)
|
|
322
|
+
// ════════════════════════════════════════════════════════════
|
|
323
|
+
/**
|
|
324
|
+
* 创建边。优先走 graph-plugin,否则自行创建 line。
|
|
325
|
+
*/
|
|
326
|
+
createEdge_fn = function(source, target) {
|
|
327
|
+
const graph = __privateMethod(this, _ConnectPlugin_instances, getGraph_fn).call(this);
|
|
328
|
+
if (graph && __privateGet(this, _opts).edgeType) {
|
|
329
|
+
const edgeData = __privateMethod(this, _ConnectPlugin_instances, buildEdgeData_fn).call(this, source, target);
|
|
330
|
+
graph.add(__privateGet(this, _opts).edgeType, edgeData);
|
|
331
|
+
__privateGet(this, _app).requestRender();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
__privateMethod(this, _ConnectPlugin_instances, createEngineLine_fn).call(this, source, target);
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* Graph 模式:构建 edge data
|
|
338
|
+
*/
|
|
339
|
+
buildEdgeData_fn = function(source, target) {
|
|
340
|
+
if (__privateGet(this, _opts).edgeFactory) {
|
|
341
|
+
return __privateGet(this, _opts).edgeFactory(source, target);
|
|
342
|
+
}
|
|
343
|
+
const sourceId = __privateMethod(this, _ConnectPlugin_instances, resolveElementId_fn).call(this, source);
|
|
344
|
+
const targetId = __privateMethod(this, _ConnectPlugin_instances, resolveElementId_fn).call(this, target);
|
|
345
|
+
const data = {
|
|
346
|
+
id: `edge-${uid8()}`,
|
|
347
|
+
source: sourceId ?? source.uid,
|
|
348
|
+
target: targetId ?? target.uid
|
|
349
|
+
};
|
|
350
|
+
if (source.data && Object.keys(source.data).length > 0) {
|
|
351
|
+
data.sourcePort = { ...source.data };
|
|
352
|
+
}
|
|
353
|
+
if (target.data && Object.keys(target.data).length > 0) {
|
|
354
|
+
data.targetPort = { ...target.data };
|
|
355
|
+
}
|
|
356
|
+
return data;
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* 纯引擎模式:创建 line Node 并添加到场景
|
|
360
|
+
*/
|
|
361
|
+
createEngineLine_fn = function(source, target) {
|
|
362
|
+
const [sx, sy] = __privateMethod(this, _ConnectPlugin_instances, getAnchor_fn).call(this, source);
|
|
363
|
+
const [tx, ty] = __privateMethod(this, _ConnectPlugin_instances, getAnchor_fn).call(this, target);
|
|
364
|
+
const line = EngineNode.create("line", {
|
|
365
|
+
stroke: __privateGet(this, _opts).lineStroke,
|
|
366
|
+
strokeWidth: __privateGet(this, _opts).lineStrokeWidth
|
|
367
|
+
});
|
|
368
|
+
line.shape.from(sx, sy, tx, ty);
|
|
369
|
+
__privateGet(this, _app).scene.add(line);
|
|
370
|
+
const id = `conn-${uid8()}`;
|
|
371
|
+
__privateGet(this, _connections).set(id, { id, line, source, target });
|
|
372
|
+
__privateGet(this, _app).requestRender();
|
|
373
|
+
__privateGet(this, _app).bus.emit("connect:edge-created", { id, source, target });
|
|
374
|
+
};
|
|
375
|
+
// ════════════════════════════════════════════════════════════
|
|
376
|
+
// Internal — 目标解析
|
|
377
|
+
// ════════════════════════════════════════════════════════════
|
|
378
|
+
/**
|
|
379
|
+
* 检查 Graphics 是否是可连接的。
|
|
380
|
+
* 沿 parent chain 向上搜索,找到第一个带有 connectable className 的节点。
|
|
381
|
+
*/
|
|
382
|
+
resolveConnectable_fn = function(target) {
|
|
383
|
+
if (target === __privateGet(this, _app).scene) return null;
|
|
384
|
+
let current = target;
|
|
385
|
+
while (current) {
|
|
386
|
+
if (current.hasClassName(__privateGet(this, _opts).className)) {
|
|
387
|
+
return current;
|
|
388
|
+
}
|
|
389
|
+
current = current.parent;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* 在 snapRadius 内寻找最近的可连接目标
|
|
395
|
+
*/
|
|
396
|
+
findSnapTarget_fn = function(worldX, worldY) {
|
|
397
|
+
const candidates = __privateMethod(this, _ConnectPlugin_instances, collectConnectables_fn).call(this);
|
|
398
|
+
const radius = __privateGet(this, _opts).snapRadius;
|
|
399
|
+
const radiusSq = radius * radius;
|
|
400
|
+
let best = null;
|
|
401
|
+
let bestDistSq = Infinity;
|
|
402
|
+
for (const candidate of candidates) {
|
|
403
|
+
if (!__privateGet(this, _opts).allowSelfLoop && __privateMethod(this, _ConnectPlugin_instances, isSameElement_fn).call(this, candidate, __privateGet(this, _source))) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (__privateGet(this, _opts).canConnect && !__privateGet(this, _opts).canConnect(__privateGet(this, _source), candidate)) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const [ax, ay] = __privateMethod(this, _ConnectPlugin_instances, getAnchor_fn).call(this, candidate);
|
|
410
|
+
const dx = worldX - ax;
|
|
411
|
+
const dy = worldY - ay;
|
|
412
|
+
const distSq = dx * dx + dy * dy;
|
|
413
|
+
if (distSq < radiusSq && distSq < bestDistSq) {
|
|
414
|
+
bestDistSq = distSq;
|
|
415
|
+
best = candidate;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return best;
|
|
419
|
+
};
|
|
420
|
+
/**
|
|
421
|
+
* 收集场景中所有可连接的 Graphics
|
|
422
|
+
*/
|
|
423
|
+
collectConnectables_fn = function() {
|
|
424
|
+
const result = [];
|
|
425
|
+
const className = __privateGet(this, _opts).className;
|
|
426
|
+
__privateGet(this, _app).scene.traverse((g) => {
|
|
427
|
+
if (g.hasClassName(className) && g.display) {
|
|
428
|
+
result.push(g);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
return result;
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* 判断两个端口是否属于同一个元素。
|
|
435
|
+
* 有 graph-plugin 时按 element ID 比较;否则按 Graphics 引用比较。
|
|
436
|
+
*/
|
|
437
|
+
isSameElement_fn = function(a, b) {
|
|
438
|
+
const graph = __privateMethod(this, _ConnectPlugin_instances, getGraph_fn).call(this);
|
|
439
|
+
if (graph) {
|
|
440
|
+
const idA = __privateMethod(this, _ConnectPlugin_instances, resolveElementId_fn).call(this, a);
|
|
441
|
+
const idB = __privateMethod(this, _ConnectPlugin_instances, resolveElementId_fn).call(this, b);
|
|
442
|
+
if (idA && idB) return idA === idB;
|
|
443
|
+
}
|
|
444
|
+
return a.uid === b.uid;
|
|
445
|
+
};
|
|
446
|
+
// ════════════════════════════════════════════════════════════
|
|
447
|
+
// Internal — 坐标与溯源
|
|
448
|
+
// ════════════════════════════════════════════════════════════
|
|
449
|
+
/**
|
|
450
|
+
* 获取 Graphics 的连接锚点(世界坐标)。
|
|
451
|
+
*/
|
|
452
|
+
getAnchor_fn = function(target) {
|
|
453
|
+
if (__privateGet(this, _opts).anchor) {
|
|
454
|
+
return __privateGet(this, _opts).anchor(target);
|
|
455
|
+
}
|
|
456
|
+
return __privateMethod(this, _ConnectPlugin_instances, defaultAnchor_fn).call(this, target);
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* 默认锚点计算:
|
|
460
|
+
* - Node(type=3): 取 getWorldBBox() 中心
|
|
461
|
+
* - Group / 其他: 取 worldMatrix 的 translation 部分
|
|
462
|
+
*/
|
|
463
|
+
defaultAnchor_fn = function(target) {
|
|
464
|
+
if (target.type === 3) {
|
|
465
|
+
const bbox = target.getWorldBBox();
|
|
466
|
+
if (bbox && !bbox.empty) {
|
|
467
|
+
return [bbox.cx, bbox.cy];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const m = target.worldMatrix;
|
|
471
|
+
return [m[4], m[5]];
|
|
472
|
+
};
|
|
473
|
+
/**
|
|
474
|
+
* 从端口 Graphics 沿 parent chain 向上走,
|
|
475
|
+
* 找到 graph-plugin 认识的 element group(group.name === element.id)。
|
|
476
|
+
*/
|
|
477
|
+
resolveElementId_fn = function(graphics) {
|
|
478
|
+
const graph = __privateMethod(this, _ConnectPlugin_instances, getGraph_fn).call(this);
|
|
479
|
+
if (!graph) return null;
|
|
480
|
+
let current = graphics;
|
|
481
|
+
while (current) {
|
|
482
|
+
if (current.name && graph.has(current.name)) {
|
|
483
|
+
return current.name;
|
|
484
|
+
}
|
|
485
|
+
current = current.parent;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
};
|
|
489
|
+
// ════════════════════════════════════════════════════════════
|
|
490
|
+
// Internal — 插件感知
|
|
491
|
+
// ════════════════════════════════════════════════════════════
|
|
492
|
+
/**
|
|
493
|
+
* 获取 graph-plugin 实例(软感知,无则返回 null)
|
|
494
|
+
*/
|
|
495
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
496
|
+
getGraph_fn = function() {
|
|
497
|
+
if (!__privateGet(this, _app)) return null;
|
|
498
|
+
const graph = __privateGet(this, _app).getPlugin("graph");
|
|
499
|
+
if (!graph || typeof graph.add !== "function" || typeof graph.has !== "function") return null;
|
|
500
|
+
return graph;
|
|
501
|
+
};
|
|
502
|
+
/**
|
|
503
|
+
* 检查 drag-plugin 是否正在拖拽
|
|
504
|
+
*/
|
|
505
|
+
isDragActive_fn = function() {
|
|
506
|
+
if (!__privateGet(this, _app)) return false;
|
|
507
|
+
try {
|
|
508
|
+
return __privateGet(this, _app).getState("drag:dragging") === true;
|
|
509
|
+
} catch {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
function connectPlugin(options) {
|
|
514
|
+
return new ConnectPlugin(options);
|
|
515
|
+
}
|
|
516
|
+
export {
|
|
517
|
+
ConnectPlugin,
|
|
518
|
+
connectPlugin
|
|
519
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rendx-connect-plugin",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Connect interaction plugin for Rendx engine — interactive edge creation",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "wei.liang (https://github.com/weiliang0121)",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"rendx",
|
|
10
|
+
"2d",
|
|
11
|
+
"canvas",
|
|
12
|
+
"rendering",
|
|
13
|
+
"visualization",
|
|
14
|
+
"scene-graph",
|
|
15
|
+
"connect",
|
|
16
|
+
"edge"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://weiliang0121.github.io/rendx/",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/weiliang0121/rendx.git",
|
|
22
|
+
"directory": "packages/connect-plugin"
|
|
23
|
+
},
|
|
24
|
+
"main": "dist/main.cjs",
|
|
25
|
+
"module": "dist/main.js",
|
|
26
|
+
"types": "dist/main.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/main.d.ts",
|
|
30
|
+
"import": "./dist/main.js",
|
|
31
|
+
"require": "./dist/main.cjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"rendx-core": "^0.1.1",
|
|
36
|
+
"rendx-engine": "^0.4.1"
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"files": [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"dev": "tsup --watch"
|
|
48
|
+
}
|
|
49
|
+
}
|