@bpmnkit/editor 0.0.33 → 0.0.34
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/README.md +1 -1
- package/dist/editor.d.ts +13 -4
- package/dist/editor.js +180 -109
- package/dist/overlay.js +2 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://github.com/bpmnkit/monorepo)
|
|
10
10
|
[](https://github.com/bpmnkit/monorepo)
|
|
11
11
|
|
|
12
|
-
[Website](https://bpmnkit.com) · [Documentation](https://
|
|
12
|
+
[Website](https://bpmnkit.com) · [Documentation](https://bpmnkit.com/docs) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/editor/CHANGELOG.md)
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
15
|
---
|
package/dist/editor.d.ts
CHANGED
|
@@ -3,10 +3,6 @@ import type { Theme } from "@bpmnkit/canvas";
|
|
|
3
3
|
import type { BpmnDefinitions, DiColor } from "@bpmnkit/core";
|
|
4
4
|
import type { Translate } from "./i18n.js";
|
|
5
5
|
import type { CreateShapeType, EditorEvents, EditorOptions, LabelPosition, Tool } from "./types.js";
|
|
6
|
-
/**
|
|
7
|
-
* BpmnEditor — a full BPMN 2.0 diagram editor with create, move, resize,
|
|
8
|
-
* connect, delete, label-edit, undo/redo, and copy/paste.
|
|
9
|
-
*/
|
|
10
6
|
export declare class BpmnEditor {
|
|
11
7
|
private readonly _id;
|
|
12
8
|
private readonly _host;
|
|
@@ -28,6 +24,12 @@ export declare class BpmnEditor {
|
|
|
28
24
|
private readonly _labelEditor;
|
|
29
25
|
private readonly _plugins;
|
|
30
26
|
private _shapes;
|
|
27
|
+
/** id → rendered shape/edge/flow, rebuilt with every render so lookups stay O(1). */
|
|
28
|
+
private readonly _shapeById;
|
|
29
|
+
private readonly _edgeById;
|
|
30
|
+
private readonly _flowById;
|
|
31
|
+
/** Equal-spacing snap candidates for the current drag; see _spacingCandidates. */
|
|
32
|
+
private _spacingIndex;
|
|
31
33
|
private _edges;
|
|
32
34
|
private _defs;
|
|
33
35
|
private _selectedIds;
|
|
@@ -195,6 +197,13 @@ export declare class BpmnEditor {
|
|
|
195
197
|
private _computeSnap;
|
|
196
198
|
private _computeAlignGuides;
|
|
197
199
|
private _computeSpacingSnap;
|
|
200
|
+
/**
|
|
201
|
+
* Candidate snap positions from every ordered pair (A, B) of static shapes
|
|
202
|
+
* where B lies right of / below A: the moving shape may sit right of B or
|
|
203
|
+
* left of A (below B / above A) at the same gap. Cached for the drag: it is
|
|
204
|
+
* rebuilt only when the rendered shapes or the selection change.
|
|
205
|
+
*/
|
|
206
|
+
private _spacingCandidates;
|
|
198
207
|
private _computeCreateSnap;
|
|
199
208
|
private _computeCreateGuides;
|
|
200
209
|
private _findCreateEdgeDrop;
|
package/dist/editor.js
CHANGED
|
@@ -195,10 +195,49 @@ function intermediateEventDefType(type) {
|
|
|
195
195
|
return null;
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
+
/** Sort by value and drop duplicate values, keeping the earliest pair for each. */
|
|
199
|
+
function sortCandidates(entries) {
|
|
200
|
+
entries.sort((x, y) => x.value - y.value || x.ord - y.ord);
|
|
201
|
+
const out = [];
|
|
202
|
+
for (const e of entries) {
|
|
203
|
+
const last = out[out.length - 1];
|
|
204
|
+
if (last && last.value === e.value)
|
|
205
|
+
continue;
|
|
206
|
+
out.push(e);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
198
210
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
211
|
+
* The candidate strictly closer than `threshold` to its target, choosing the
|
|
212
|
+
* earliest pair on exact ties — the same winner the full pair scan produced.
|
|
201
213
|
*/
|
|
214
|
+
function nearestCandidate(queries, threshold) {
|
|
215
|
+
let best = null;
|
|
216
|
+
for (const { list, target } of queries) {
|
|
217
|
+
// Binary search for the insertion point; only the two neighbours can be nearest.
|
|
218
|
+
let lo = 0;
|
|
219
|
+
let hi = list.length;
|
|
220
|
+
while (lo < hi) {
|
|
221
|
+
const mid = (lo + hi) >> 1;
|
|
222
|
+
if (list[mid].value < target)
|
|
223
|
+
lo = mid + 1;
|
|
224
|
+
else
|
|
225
|
+
hi = mid;
|
|
226
|
+
}
|
|
227
|
+
for (const i of [lo - 1, lo]) {
|
|
228
|
+
const entry = list[i];
|
|
229
|
+
if (!entry)
|
|
230
|
+
continue;
|
|
231
|
+
const dist = Math.abs(target - entry.value);
|
|
232
|
+
if (dist >= threshold)
|
|
233
|
+
continue;
|
|
234
|
+
if (!best || dist < best.dist || (dist === best.dist && entry.ord < best.entry.ord)) {
|
|
235
|
+
best = { entry, target, dist };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return best;
|
|
240
|
+
}
|
|
202
241
|
export class BpmnEditor {
|
|
203
242
|
// ── DOM ────────────────────────────────────────────────────────────
|
|
204
243
|
_id;
|
|
@@ -223,6 +262,12 @@ export class BpmnEditor {
|
|
|
223
262
|
_plugins = [];
|
|
224
263
|
// ── State ──────────────────────────────────────────────────────────
|
|
225
264
|
_shapes = [];
|
|
265
|
+
/** id → rendered shape/edge/flow, rebuilt with every render so lookups stay O(1). */
|
|
266
|
+
_shapeById = new Map();
|
|
267
|
+
_edgeById = new Map();
|
|
268
|
+
_flowById = new Map();
|
|
269
|
+
/** Equal-spacing snap candidates for the current drag; see _spacingCandidates. */
|
|
270
|
+
_spacingIndex = null;
|
|
226
271
|
_edges = [];
|
|
227
272
|
_defs = null;
|
|
228
273
|
_selectedIds = [];
|
|
@@ -367,7 +412,7 @@ export class BpmnEditor {
|
|
|
367
412
|
cancelEndpointMove: () => {
|
|
368
413
|
this._overlay.setEndpointDragGhost(null);
|
|
369
414
|
if (this._selectedEdgeId) {
|
|
370
|
-
const edge = this.
|
|
415
|
+
const edge = this._edgeById.get(this._selectedEdgeId);
|
|
371
416
|
this._overlay.setEdgeEndpoints(edge?.edge.waypoints ?? null, this._selectedEdgeId);
|
|
372
417
|
}
|
|
373
418
|
},
|
|
@@ -430,7 +475,7 @@ export class BpmnEditor {
|
|
|
430
475
|
this._overlay.setEdgeHoverDot(null);
|
|
431
476
|
},
|
|
432
477
|
showEdgeWaypointBalls: (edgeId) => {
|
|
433
|
-
const edge = this.
|
|
478
|
+
const edge = this._edgeById.get(edgeId);
|
|
434
479
|
if (edge)
|
|
435
480
|
this._overlay.setEdgeWaypointBalls(edge.edge.waypoints, edgeId);
|
|
436
481
|
},
|
|
@@ -451,7 +496,7 @@ export class BpmnEditor {
|
|
|
451
496
|
});
|
|
452
497
|
// ── Keyboard ─────────────────────────────────────────────────
|
|
453
498
|
this._keyboard = new KeyboardHandler(this._host, this._viewport, () => this.fitView(), (id) => {
|
|
454
|
-
const shape = this.
|
|
499
|
+
const shape = this._shapeById.get(id);
|
|
455
500
|
if (shape) {
|
|
456
501
|
this._emit("element:click", id, new PointerEvent("click"));
|
|
457
502
|
}
|
|
@@ -585,7 +630,7 @@ export class BpmnEditor {
|
|
|
585
630
|
_selectedShapeBounds() {
|
|
586
631
|
const out = [];
|
|
587
632
|
for (const id of this._selectedIds) {
|
|
588
|
-
const shape = this.
|
|
633
|
+
const shape = this._shapeById.get(id);
|
|
589
634
|
if (shape)
|
|
590
635
|
out.push({ id, ...shape.shape.bounds });
|
|
591
636
|
}
|
|
@@ -807,7 +852,7 @@ export class BpmnEditor {
|
|
|
807
852
|
}
|
|
808
853
|
/** Pans the viewport to center on the element with the given id (preserves zoom). */
|
|
809
854
|
scrollToElement(id) {
|
|
810
|
-
const shape = this.
|
|
855
|
+
const shape = this._shapeById.get(id);
|
|
811
856
|
if (!shape)
|
|
812
857
|
return;
|
|
813
858
|
const { x, y, width, height } = shape.shape.bounds;
|
|
@@ -904,6 +949,16 @@ export class BpmnEditor {
|
|
|
904
949
|
this._shapes = result.shapes;
|
|
905
950
|
this._edges = result.edges;
|
|
906
951
|
this._defs = defs;
|
|
952
|
+
this._shapeById.clear();
|
|
953
|
+
for (const shape of this._shapes)
|
|
954
|
+
this._shapeById.set(shape.id, shape);
|
|
955
|
+
this._edgeById.clear();
|
|
956
|
+
for (const edge of this._edges)
|
|
957
|
+
this._edgeById.set(edge.id, edge);
|
|
958
|
+
this._flowById.clear();
|
|
959
|
+
for (const flow of defs.processes[0]?.sequenceFlows ?? [])
|
|
960
|
+
this._flowById.set(flow.id, flow);
|
|
961
|
+
this._spacingIndex = null;
|
|
907
962
|
// Add transparent hit-area polylines for edge clicking
|
|
908
963
|
for (const edge of this._edges) {
|
|
909
964
|
const waypoints = edge.edge.waypoints;
|
|
@@ -920,7 +975,7 @@ export class BpmnEditor {
|
|
|
920
975
|
this._overlay.setSelection(this._selectedIds, this._shapes, this._getResizableIds());
|
|
921
976
|
// Restore edge selection if the edge still exists after re-render
|
|
922
977
|
if (this._selectedEdgeId) {
|
|
923
|
-
const edge = this.
|
|
978
|
+
const edge = this._edgeById.get(this._selectedEdgeId);
|
|
924
979
|
if (edge) {
|
|
925
980
|
this._overlay.setEdgeEndpoints(edge.edge.waypoints, this._selectedEdgeId);
|
|
926
981
|
}
|
|
@@ -962,7 +1017,7 @@ export class BpmnEditor {
|
|
|
962
1017
|
}
|
|
963
1018
|
/** Human-readable name for an element (its label, else its type). */
|
|
964
1019
|
_displayName(id) {
|
|
965
|
-
const el = this.
|
|
1020
|
+
const el = this._shapeById.get(id)?.flowElement;
|
|
966
1021
|
return el?.name ?? el?.type ?? id;
|
|
967
1022
|
}
|
|
968
1023
|
/** Pushes `message` to the aria-live region for assistive technology. */
|
|
@@ -1004,7 +1059,7 @@ export class BpmnEditor {
|
|
|
1004
1059
|
const finalDy = useSpacingY ? spacingResult.dy : alignSnap.dy;
|
|
1005
1060
|
this._snapDelta = { dx: finalDx, dy: finalDy };
|
|
1006
1061
|
for (const id of this._selectedIds) {
|
|
1007
|
-
const shape = this.
|
|
1062
|
+
const shape = this._shapeById.get(id);
|
|
1008
1063
|
if (!shape)
|
|
1009
1064
|
continue;
|
|
1010
1065
|
const { x, y } = shape.shape.bounds;
|
|
@@ -1023,7 +1078,7 @@ export class BpmnEditor {
|
|
|
1023
1078
|
this._overlay.setDistanceGuides([]);
|
|
1024
1079
|
this._setEdgeDropHighlight(null);
|
|
1025
1080
|
for (const id of this._selectedIds) {
|
|
1026
|
-
const shape = this.
|
|
1081
|
+
const shape = this._shapeById.get(id);
|
|
1027
1082
|
if (!shape)
|
|
1028
1083
|
continue;
|
|
1029
1084
|
const { x, y } = shape.shape.bounds;
|
|
@@ -1159,7 +1214,7 @@ export class BpmnEditor {
|
|
|
1159
1214
|
const boundaryHostId = this._boundaryHostId;
|
|
1160
1215
|
this._setBoundaryHost(null);
|
|
1161
1216
|
if (boundaryHostId && isIntermediateEventType(type) && this._defs) {
|
|
1162
|
-
const hostShape = this.
|
|
1217
|
+
const hostShape = this._shapeById.get(boundaryHostId);
|
|
1163
1218
|
if (hostShape) {
|
|
1164
1219
|
const hostBounds = hostShape.shape.bounds;
|
|
1165
1220
|
// Snap the event center to the nearest point on the host boundary
|
|
@@ -1186,8 +1241,8 @@ export class BpmnEditor {
|
|
|
1186
1241
|
this._emit("editor:select", [result.id]);
|
|
1187
1242
|
}
|
|
1188
1243
|
_doConnect(srcId, tgtId) {
|
|
1189
|
-
const srcShape = this.
|
|
1190
|
-
const tgtShape = this.
|
|
1244
|
+
const srcShape = this._shapeById.get(srcId);
|
|
1245
|
+
const tgtShape = this._shapeById.get(tgtId);
|
|
1191
1246
|
if (!srcShape || !tgtShape)
|
|
1192
1247
|
return;
|
|
1193
1248
|
const srcType = srcShape.flowElement?.type;
|
|
@@ -1227,7 +1282,7 @@ export class BpmnEditor {
|
|
|
1227
1282
|
_startLabelEdit(id) {
|
|
1228
1283
|
if (this._readOnly || !id)
|
|
1229
1284
|
return;
|
|
1230
|
-
const shape = this.
|
|
1285
|
+
const shape = this._shapeById.get(id);
|
|
1231
1286
|
if (!shape)
|
|
1232
1287
|
return;
|
|
1233
1288
|
const defs = this._defs;
|
|
@@ -1244,7 +1299,7 @@ export class BpmnEditor {
|
|
|
1244
1299
|
const sourceId = this._connectSourceId();
|
|
1245
1300
|
if (!sourceId)
|
|
1246
1301
|
return null;
|
|
1247
|
-
const shape = this.
|
|
1302
|
+
const shape = this._shapeById.get(sourceId);
|
|
1248
1303
|
return shape ? shape.shape.bounds : null;
|
|
1249
1304
|
}
|
|
1250
1305
|
_connectSourceId() {
|
|
@@ -1263,14 +1318,14 @@ export class BpmnEditor {
|
|
|
1263
1318
|
return false;
|
|
1264
1319
|
if (targetId === sourceId)
|
|
1265
1320
|
return true;
|
|
1266
|
-
const srcType = this.
|
|
1267
|
-
const tgtType = this.
|
|
1321
|
+
const srcType = this._shapeById.get(sourceId)?.flowElement?.type;
|
|
1322
|
+
const tgtType = this._shapeById.get(targetId)?.flowElement?.type;
|
|
1268
1323
|
return srcType !== undefined && tgtType !== undefined && !canConnect(srcType, tgtType);
|
|
1269
1324
|
}
|
|
1270
1325
|
// ── New public helpers ─────────────────────────────────────────────
|
|
1271
1326
|
/** Returns screen-space bounds of a shape (for positioning overlays). */
|
|
1272
1327
|
getShapeBounds(id) {
|
|
1273
|
-
const shape = this.
|
|
1328
|
+
const shape = this._shapeById.get(id);
|
|
1274
1329
|
if (!shape)
|
|
1275
1330
|
return null;
|
|
1276
1331
|
const b = shape.shape.bounds;
|
|
@@ -1281,7 +1336,7 @@ export class BpmnEditor {
|
|
|
1281
1336
|
}
|
|
1282
1337
|
/** Returns the BPMN element type for a given id, or null if not found. */
|
|
1283
1338
|
getElementType(id) {
|
|
1284
|
-
const shape = this.
|
|
1339
|
+
const shape = this._shapeById.get(id);
|
|
1285
1340
|
if (!shape) {
|
|
1286
1341
|
if (this._edges.some((e) => e.id === id))
|
|
1287
1342
|
return "sequenceFlow";
|
|
@@ -1314,7 +1369,7 @@ export class BpmnEditor {
|
|
|
1314
1369
|
addConnectedElement(sourceId, type, name) {
|
|
1315
1370
|
if (!this._defs)
|
|
1316
1371
|
return null;
|
|
1317
|
-
const srcShape = this.
|
|
1372
|
+
const srcShape = this._shapeById.get(sourceId);
|
|
1318
1373
|
if (!srcShape)
|
|
1319
1374
|
return null;
|
|
1320
1375
|
const srcBounds = srcShape.shape.bounds;
|
|
@@ -1354,7 +1409,7 @@ export class BpmnEditor {
|
|
|
1354
1409
|
for (const flow of process.sequenceFlows) {
|
|
1355
1410
|
if (flow.sourceRef !== sourceId)
|
|
1356
1411
|
continue;
|
|
1357
|
-
const tgt = this.
|
|
1412
|
+
const tgt = this._shapeById.get(flow.targetRef);
|
|
1358
1413
|
if (!tgt)
|
|
1359
1414
|
continue;
|
|
1360
1415
|
const tCx = tgt.shape.bounds.x + tgt.shape.bounds.width / 2;
|
|
@@ -1434,7 +1489,7 @@ export class BpmnEditor {
|
|
|
1434
1489
|
* Sets the external label position for an event or gateway shape.
|
|
1435
1490
|
*/
|
|
1436
1491
|
setLabelPosition(shapeId, position) {
|
|
1437
|
-
const shape = this.
|
|
1492
|
+
const shape = this._shapeById.get(shapeId);
|
|
1438
1493
|
if (!shape)
|
|
1439
1494
|
return;
|
|
1440
1495
|
const labelBounds = labelBoundsForPosition(shape.shape.bounds, position);
|
|
@@ -1454,7 +1509,7 @@ export class BpmnEditor {
|
|
|
1454
1509
|
* The user then moves the mouse and clicks a target shape to complete the connection.
|
|
1455
1510
|
*/
|
|
1456
1511
|
startConnectionFrom(sourceId) {
|
|
1457
|
-
const shape = this.
|
|
1512
|
+
const shape = this._shapeById.get(sourceId);
|
|
1458
1513
|
if (!shape)
|
|
1459
1514
|
return;
|
|
1460
1515
|
this._viewport.lock(true);
|
|
@@ -1467,7 +1522,7 @@ export class BpmnEditor {
|
|
|
1467
1522
|
createAnnotationFor(sourceId) {
|
|
1468
1523
|
if (!this._defs)
|
|
1469
1524
|
return;
|
|
1470
|
-
const srcShape = this.
|
|
1525
|
+
const srcShape = this._shapeById.get(sourceId);
|
|
1471
1526
|
if (!srcShape)
|
|
1472
1527
|
return;
|
|
1473
1528
|
const srcBounds = srcShape.shape.bounds;
|
|
@@ -1501,7 +1556,7 @@ export class BpmnEditor {
|
|
|
1501
1556
|
}
|
|
1502
1557
|
this._selectedEdgeId = edgeId;
|
|
1503
1558
|
if (edgeId) {
|
|
1504
|
-
const edge = this.
|
|
1559
|
+
const edge = this._edgeById.get(edgeId);
|
|
1505
1560
|
this._overlay.setEdgeEndpoints(edge?.edge.waypoints ?? null, edgeId);
|
|
1506
1561
|
this._emit("editor:select", [edgeId]);
|
|
1507
1562
|
}
|
|
@@ -1512,7 +1567,7 @@ export class BpmnEditor {
|
|
|
1512
1567
|
}
|
|
1513
1568
|
/** Changes a flow element's type (e.g. exclusiveGateway → parallelGateway). */
|
|
1514
1569
|
changeElementType(id, newType) {
|
|
1515
|
-
const current = this.
|
|
1570
|
+
const current = this._shapeById.get(id)?.flowElement?.type;
|
|
1516
1571
|
if (current && !canMorph(current, newType))
|
|
1517
1572
|
return;
|
|
1518
1573
|
this._executeCommand((d) => changeElementTypeFn(d, id, newType), "Change type");
|
|
@@ -1523,7 +1578,7 @@ export class BpmnEditor {
|
|
|
1523
1578
|
const id = this._selectedIds[0];
|
|
1524
1579
|
if (!id || !this._defs)
|
|
1525
1580
|
return null;
|
|
1526
|
-
const shape = this.
|
|
1581
|
+
const shape = this._shapeById.get(id);
|
|
1527
1582
|
if (!shape)
|
|
1528
1583
|
return null;
|
|
1529
1584
|
const b = shape.shape.bounds;
|
|
@@ -1534,7 +1589,7 @@ export class BpmnEditor {
|
|
|
1534
1589
|
return null;
|
|
1535
1590
|
const TOLERANCE = 20;
|
|
1536
1591
|
for (const edge of this._edges) {
|
|
1537
|
-
const flow =
|
|
1592
|
+
const flow = this._flowById.get(edge.id);
|
|
1538
1593
|
if (!flow)
|
|
1539
1594
|
continue;
|
|
1540
1595
|
// Skip edges that are already connected to the shape being moved
|
|
@@ -1559,19 +1614,19 @@ export class BpmnEditor {
|
|
|
1559
1614
|
}
|
|
1560
1615
|
_setEdgeDropHighlight(edgeId) {
|
|
1561
1616
|
if (this._edgeDropTarget) {
|
|
1562
|
-
const prev = this.
|
|
1617
|
+
const prev = this._edgeById.get(this._edgeDropTarget);
|
|
1563
1618
|
prev?.element.classList.remove("bpmnkit-edge-split-highlight");
|
|
1564
1619
|
}
|
|
1565
1620
|
this._edgeDropTarget = edgeId;
|
|
1566
1621
|
if (edgeId) {
|
|
1567
|
-
const edge = this.
|
|
1622
|
+
const edge = this._edgeById.get(edgeId);
|
|
1568
1623
|
edge?.element.classList.add("bpmnkit-edge-split-highlight");
|
|
1569
1624
|
}
|
|
1570
1625
|
}
|
|
1571
1626
|
_previewEndpointMove(edgeId, isStart, diagPoint) {
|
|
1572
1627
|
if (!this._defs)
|
|
1573
1628
|
return;
|
|
1574
|
-
const edge = this.
|
|
1629
|
+
const edge = this._edgeById.get(edgeId);
|
|
1575
1630
|
if (!edge)
|
|
1576
1631
|
return;
|
|
1577
1632
|
const flow = this._defs.processes[0]?.sequenceFlows.find((sf) => sf.id === edgeId);
|
|
@@ -1604,7 +1659,7 @@ export class BpmnEditor {
|
|
|
1604
1659
|
if (!this._defs)
|
|
1605
1660
|
return;
|
|
1606
1661
|
this._overlay.setEndpointDragGhost(null);
|
|
1607
|
-
const edge = this.
|
|
1662
|
+
const edge = this._edgeById.get(edgeId);
|
|
1608
1663
|
if (!edge)
|
|
1609
1664
|
return;
|
|
1610
1665
|
const flow = this._defs.processes[0]?.sequenceFlows.find((sf) => sf.id === edgeId);
|
|
@@ -1623,7 +1678,7 @@ export class BpmnEditor {
|
|
|
1623
1678
|
this._executeCommand((d) => updateEdgeEndpoint(d, edgeId, isStart, newPort), "Reconnect");
|
|
1624
1679
|
}
|
|
1625
1680
|
_isResizable(id) {
|
|
1626
|
-
const shape = this.
|
|
1681
|
+
const shape = this._shapeById.get(id);
|
|
1627
1682
|
if (!shape)
|
|
1628
1683
|
return false;
|
|
1629
1684
|
if (shape.annotation !== undefined)
|
|
@@ -1743,81 +1798,98 @@ export class BpmnEditor {
|
|
|
1743
1798
|
const moving = movingShape.shape.bounds;
|
|
1744
1799
|
const scale = this._viewport.state.scale;
|
|
1745
1800
|
const threshold = 8 / scale;
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
const
|
|
1751
|
-
const
|
|
1801
|
+
// Every candidate position derives from a pair of static shapes and is
|
|
1802
|
+
// independent of the pointer, so the O(S²) pair scan runs once per drag
|
|
1803
|
+
// and each move is a binary search over the sorted candidates.
|
|
1804
|
+
const index = this._spacingCandidates(staticShapes);
|
|
1805
|
+
const mx = moving.x + dx;
|
|
1806
|
+
const my = moving.y + dy;
|
|
1752
1807
|
const mCy = moving.y + dy + moving.height / 2;
|
|
1753
1808
|
const mCx = moving.x + dx + moving.width / 2;
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
minDistX = distX;
|
|
1770
|
-
bestDx = dx + (candLeft - (moving.x + dx));
|
|
1771
|
-
hGuides.length = 0;
|
|
1772
|
-
hGuides.push({ x1: aRight, y1: mCy, x2: bLeft, y2: mCy }, { x1: bRight, y1: mCy, x2: candLeft, y2: mCy });
|
|
1773
|
-
}
|
|
1774
|
-
// Candidate: moving is to the left of A by the same gap
|
|
1775
|
-
const aLeft = A.shape.bounds.x;
|
|
1776
|
-
const candRight = aLeft - gap;
|
|
1777
|
-
const movRight = moving.x + dx + moving.width;
|
|
1778
|
-
const distX2 = Math.abs(movRight - candRight);
|
|
1779
|
-
if (distX2 < minDistX) {
|
|
1780
|
-
minDistX = distX2;
|
|
1781
|
-
bestDx = dx + (candRight - movRight);
|
|
1782
|
-
hGuides.length = 0;
|
|
1783
|
-
hGuides.push({ x1: candRight, y1: mCy, x2: aLeft, y2: mCy }, { x1: aRight, y1: mCy, x2: bLeft, y2: mCy });
|
|
1784
|
-
}
|
|
1809
|
+
const guides = [];
|
|
1810
|
+
let bestDx = dx;
|
|
1811
|
+
let bestDy = dy;
|
|
1812
|
+
const bestX = nearestCandidate([
|
|
1813
|
+
{ list: index.h0, target: mx },
|
|
1814
|
+
{ list: index.h1, target: mx + moving.width },
|
|
1815
|
+
], threshold);
|
|
1816
|
+
if (bestX) {
|
|
1817
|
+
const { entry, target } = bestX;
|
|
1818
|
+
bestDx = dx + (entry.value - target);
|
|
1819
|
+
if (entry.kind === 0) {
|
|
1820
|
+
guides.push({ x1: entry.a, y1: mCy, x2: entry.b, y2: mCy }, { x1: entry.c, y1: mCy, x2: entry.value, y2: mCy });
|
|
1821
|
+
}
|
|
1822
|
+
else {
|
|
1823
|
+
guides.push({ x1: entry.value, y1: mCy, x2: entry.c, y2: mCy }, { x1: entry.a, y1: mCy, x2: entry.b, y2: mCy });
|
|
1785
1824
|
}
|
|
1786
1825
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1826
|
+
const bestY = nearestCandidate([
|
|
1827
|
+
{ list: index.v0, target: my },
|
|
1828
|
+
{ list: index.v1, target: my + moving.height },
|
|
1829
|
+
], threshold);
|
|
1830
|
+
if (bestY) {
|
|
1831
|
+
const { entry, target } = bestY;
|
|
1832
|
+
bestDy = dy + (entry.value - target);
|
|
1833
|
+
if (entry.kind === 0) {
|
|
1834
|
+
guides.push({ x1: mCx, y1: entry.a, x2: mCx, y2: entry.b }, { x1: mCx, y1: entry.c, x2: mCx, y2: entry.value });
|
|
1835
|
+
}
|
|
1836
|
+
else {
|
|
1837
|
+
guides.push({ x1: mCx, y1: entry.value, x2: mCx, y2: entry.c }, { x1: mCx, y1: entry.a, x2: mCx, y2: entry.b });
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return { dx: bestDx, dy: bestDy, guides };
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Candidate snap positions from every ordered pair (A, B) of static shapes
|
|
1844
|
+
* where B lies right of / below A: the moving shape may sit right of B or
|
|
1845
|
+
* left of A (below B / above A) at the same gap. Cached for the drag: it is
|
|
1846
|
+
* rebuilt only when the rendered shapes or the selection change.
|
|
1847
|
+
*/
|
|
1848
|
+
_spacingCandidates(staticShapes) {
|
|
1849
|
+
const key = this._selectedIds.join("\0");
|
|
1850
|
+
const cached = this._spacingIndex;
|
|
1851
|
+
if (cached && cached.shapes === this._shapes && cached.key === key)
|
|
1852
|
+
return cached;
|
|
1853
|
+
const h = [];
|
|
1854
|
+
const v = [];
|
|
1855
|
+
const count = staticShapes.length;
|
|
1856
|
+
for (let ia = 0; ia < count; ia++) {
|
|
1857
|
+
const A = staticShapes[ia].shape.bounds;
|
|
1858
|
+
const aRight = A.x + A.width;
|
|
1859
|
+
const aBottom = A.y + A.height;
|
|
1860
|
+
for (let ib = 0; ib < count; ib++) {
|
|
1861
|
+
if (ia === ib)
|
|
1795
1862
|
continue;
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
const
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1863
|
+
const B = staticShapes[ib].shape.bounds;
|
|
1864
|
+
const ord = (ia * count + ib) * 2;
|
|
1865
|
+
const bLeft = B.x;
|
|
1866
|
+
if (bLeft > aRight) {
|
|
1867
|
+
const gap = bLeft - aRight;
|
|
1868
|
+
const bRight = B.x + B.width;
|
|
1869
|
+
// kind 0: moving sits right of B; a→b is the reference gap, c→value the new one.
|
|
1870
|
+
h.push({ kind: 0, value: bRight + gap, ord, a: aRight, b: bLeft, c: bRight });
|
|
1871
|
+
// kind 1: moving sits left of A; value→c is the new gap, a→b the reference.
|
|
1872
|
+
h.push({ kind: 1, value: A.x - gap, ord: ord + 1, a: aRight, b: bLeft, c: A.x });
|
|
1806
1873
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
minDistY = distY2;
|
|
1814
|
-
bestDy = dy + (candBottom - movBottom);
|
|
1815
|
-
vGuides.length = 0;
|
|
1816
|
-
vGuides.push({ x1: mCx, y1: candBottom, x2: mCx, y2: aTop }, { x1: mCx, y1: aBottom, x2: mCx, y2: bTop });
|
|
1874
|
+
const bTop = B.y;
|
|
1875
|
+
if (bTop > aBottom) {
|
|
1876
|
+
const gap = bTop - aBottom;
|
|
1877
|
+
const bBottom = B.y + B.height;
|
|
1878
|
+
v.push({ kind: 0, value: bBottom + gap, ord, a: aBottom, b: bTop, c: bBottom });
|
|
1879
|
+
v.push({ kind: 1, value: A.y - gap, ord: ord + 1, a: aBottom, b: bTop, c: A.y });
|
|
1817
1880
|
}
|
|
1818
1881
|
}
|
|
1819
1882
|
}
|
|
1820
|
-
|
|
1883
|
+
const index = {
|
|
1884
|
+
shapes: this._shapes,
|
|
1885
|
+
key,
|
|
1886
|
+
h0: sortCandidates(h.filter((e) => e.kind === 0)),
|
|
1887
|
+
h1: sortCandidates(h.filter((e) => e.kind === 1)),
|
|
1888
|
+
v0: sortCandidates(v.filter((e) => e.kind === 0)),
|
|
1889
|
+
v1: sortCandidates(v.filter((e) => e.kind === 1)),
|
|
1890
|
+
};
|
|
1891
|
+
this._spacingIndex = index;
|
|
1892
|
+
return index;
|
|
1821
1893
|
}
|
|
1822
1894
|
// ── Create-mode helpers ────────────────────────────────────────────
|
|
1823
1895
|
_computeCreateSnap(bounds) {
|
|
@@ -1892,7 +1964,7 @@ export class BpmnEditor {
|
|
|
1892
1964
|
return null;
|
|
1893
1965
|
const TOLERANCE = 20;
|
|
1894
1966
|
for (const edge of this._edges) {
|
|
1895
|
-
const flow =
|
|
1967
|
+
const flow = this._flowById.get(edge.id);
|
|
1896
1968
|
if (!flow)
|
|
1897
1969
|
continue;
|
|
1898
1970
|
const wps = edge.edge.waypoints;
|
|
@@ -1916,7 +1988,7 @@ export class BpmnEditor {
|
|
|
1916
1988
|
return;
|
|
1917
1989
|
this._boundaryHostId = shapeId;
|
|
1918
1990
|
if (shapeId) {
|
|
1919
|
-
const shape = this.
|
|
1991
|
+
const shape = this._shapeById.get(shapeId);
|
|
1920
1992
|
this._overlay.setBoundaryHostHighlight(shape ? shape.shape.bounds : null);
|
|
1921
1993
|
}
|
|
1922
1994
|
else {
|
|
@@ -1927,12 +1999,12 @@ export class BpmnEditor {
|
|
|
1927
1999
|
if (this._createEdgeDropTarget === edgeId)
|
|
1928
2000
|
return;
|
|
1929
2001
|
if (this._createEdgeDropTarget) {
|
|
1930
|
-
const prev = this.
|
|
2002
|
+
const prev = this._edgeById.get(this._createEdgeDropTarget);
|
|
1931
2003
|
prev?.element.classList.remove("bpmnkit-edge-split-highlight");
|
|
1932
2004
|
}
|
|
1933
2005
|
this._createEdgeDropTarget = edgeId;
|
|
1934
2006
|
if (edgeId) {
|
|
1935
|
-
const edge = this.
|
|
2007
|
+
const edge = this._edgeById.get(edgeId);
|
|
1936
2008
|
edge?.element.classList.add("bpmnkit-edge-split-highlight");
|
|
1937
2009
|
}
|
|
1938
2010
|
}
|
|
@@ -1964,7 +2036,7 @@ export class BpmnEditor {
|
|
|
1964
2036
|
this._setCreateEdgeDropHighlight(this._findCreateEdgeDrop(snapped));
|
|
1965
2037
|
// Detect boundary event attachment target for intermediate event types
|
|
1966
2038
|
if (isIntermediateEventType(mode.elementType) && hit.type === "shape") {
|
|
1967
|
-
const shape = this.
|
|
2039
|
+
const shape = this._shapeById.get(hit.id);
|
|
1968
2040
|
if (shape?.flowElement && canAttach(shape.flowElement.type)) {
|
|
1969
2041
|
this._setBoundaryHost(hit.id);
|
|
1970
2042
|
}
|
|
@@ -2234,8 +2306,7 @@ export class BpmnEditor {
|
|
|
2234
2306
|
}
|
|
2235
2307
|
/** Finds the SVG group for a shape or edge by BPMN id. */
|
|
2236
2308
|
_elementById(id) {
|
|
2237
|
-
return
|
|
2238
|
-
this._edges.find((e) => e.id === id)?.element);
|
|
2309
|
+
return this._shapeById.get(id)?.element ?? this._edgeById.get(id)?.element;
|
|
2239
2310
|
}
|
|
2240
2311
|
/** Element bounding box in screen pixels relative to the host, or `null`. */
|
|
2241
2312
|
_absoluteBBox(id) {
|
|
@@ -2252,12 +2323,12 @@ export class BpmnEditor {
|
|
|
2252
2323
|
}
|
|
2253
2324
|
/** Diagram-coordinate bounds of a shape (from DI) or edge (waypoint bbox). */
|
|
2254
2325
|
_boundsById(id) {
|
|
2255
|
-
const shape = this.
|
|
2326
|
+
const shape = this._shapeById.get(id);
|
|
2256
2327
|
if (shape) {
|
|
2257
2328
|
const { x, y, width, height } = shape.shape.bounds;
|
|
2258
2329
|
return { x, y, width, height };
|
|
2259
2330
|
}
|
|
2260
|
-
const edge = this.
|
|
2331
|
+
const edge = this._edgeById.get(id);
|
|
2261
2332
|
if (edge && edge.edge.waypoints.length > 0) {
|
|
2262
2333
|
const xs = edge.edge.waypoints.map((w) => w.x);
|
|
2263
2334
|
const ys = edge.edge.waypoints.map((w) => w.y);
|
package/dist/overlay.js
CHANGED
|
@@ -67,7 +67,8 @@ export class OverlayRenderer {
|
|
|
67
67
|
// ── Selection + handles ───────────────────────────────────────────
|
|
68
68
|
setSelection(ids, shapes, resizableIds) {
|
|
69
69
|
this._selectionG.innerHTML = "";
|
|
70
|
-
const
|
|
70
|
+
const idSet = new Set(ids);
|
|
71
|
+
const selected = shapes.filter((s) => idSet.has(s.id));
|
|
71
72
|
if (selected.length === 0)
|
|
72
73
|
return;
|
|
73
74
|
const isSingle = selected.length === 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bpmnkit/editor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"dist/**/*.d.ts"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@bpmnkit/canvas": "0.0.
|
|
21
|
-
"@bpmnkit/core": "0.
|
|
20
|
+
"@bpmnkit/canvas": "0.0.31",
|
|
21
|
+
"@bpmnkit/core": "0.2.0"
|
|
22
22
|
},
|
|
23
23
|
"description": "Full-featured interactive BPMN editor with undo/redo, HUD, and side-dock UI",
|
|
24
24
|
"keywords": [
|