@joint/core 4.1.3 → 4.2.0-alpha.1
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 +4 -2
- package/dist/geometry.js +129 -124
- package/dist/geometry.min.js +4 -3
- package/dist/joint.d.ts +352 -160
- package/dist/joint.js +3654 -2191
- package/dist/joint.min.js +4 -3
- package/dist/joint.nowrap.js +3653 -2188
- package/dist/joint.nowrap.min.js +4 -3
- package/dist/vectorizer.js +489 -279
- package/dist/vectorizer.min.js +4 -3
- package/dist/version.mjs +1 -1
- package/package.json +33 -27
- package/src/V/create.mjs +51 -0
- package/src/V/index.mjs +89 -159
- package/src/V/namespace.mjs +9 -0
- package/src/V/transform.mjs +183 -0
- package/src/V/traverse.mjs +16 -0
- package/src/alg/Deque.mjs +126 -0
- package/src/anchors/index.mjs +140 -33
- package/src/cellTools/Boundary.mjs +15 -13
- package/src/cellTools/Button.mjs +7 -5
- package/src/cellTools/Control.mjs +38 -15
- package/src/cellTools/HoverConnect.mjs +5 -1
- package/src/cellTools/helpers.mjs +44 -3
- package/src/config/index.mjs +8 -0
- package/src/connectionPoints/index.mjs +24 -9
- package/src/connectionStrategies/index.mjs +1 -1
- package/src/connectors/jumpover.mjs +1 -1
- package/src/dia/Cell.mjs +32 -12
- package/src/dia/CellView.mjs +53 -38
- package/src/dia/Element.mjs +81 -35
- package/src/dia/ElementView.mjs +2 -1
- package/src/dia/HighlighterView.mjs +54 -11
- package/src/dia/LinkView.mjs +118 -98
- package/src/dia/Paper.mjs +831 -231
- package/src/dia/PaperLayer.mjs +9 -2
- package/src/dia/ToolView.mjs +4 -0
- package/src/dia/ToolsView.mjs +12 -3
- package/src/dia/attributes/text.mjs +16 -5
- package/src/dia/layers/GridLayer.mjs +5 -0
- package/src/dia/ports.mjs +344 -111
- package/src/elementTools/HoverConnect.mjs +14 -8
- package/src/env/index.mjs +7 -4
- package/src/g/rect.mjs +7 -0
- package/src/highlighters/stroke.mjs +1 -1
- package/src/layout/ports/port.mjs +30 -15
- package/src/layout/ports/portLabel.mjs +1 -1
- package/src/linkAnchors/index.mjs +2 -2
- package/src/linkTools/Anchor.mjs +2 -2
- package/src/linkTools/Vertices.mjs +4 -6
- package/src/mvc/View.mjs +4 -0
- package/src/mvc/ViewBase.mjs +1 -1
- package/src/util/util.mjs +1 -1
- package/src/util/utilHelpers.mjs +2 -0
- package/types/geometry.d.ts +65 -59
- package/types/joint.d.ts +278 -102
- package/types/vectorizer.d.ts +11 -1
- package/src/V/annotation.mjs +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {SVGElement} node1
|
|
3
|
+
* @param {SVGElement} node2
|
|
4
|
+
* @returns {SVGElement|null}
|
|
5
|
+
* @description Finds the common ancestor node of two nodes.
|
|
6
|
+
*/
|
|
7
|
+
export function getCommonAncestor(node1, node2) {
|
|
8
|
+
// Find the common ancestor node of two nodes.
|
|
9
|
+
let parent = node1;
|
|
10
|
+
do {
|
|
11
|
+
if (parent.contains(node2)) return parent;
|
|
12
|
+
parent = parent.parentNode;
|
|
13
|
+
} while (parent);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deque implementation for managing a double-ended queue.
|
|
3
|
+
* This implementation uses a doubly linked list for efficient operations.
|
|
4
|
+
* It supports operations like push, pop, move to head, and delete.
|
|
5
|
+
* The deque maintains a map for O(1) access to nodes by key.
|
|
6
|
+
*/
|
|
7
|
+
export class Deque {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.head = null;
|
|
10
|
+
this.tail = null;
|
|
11
|
+
this.map = new Map(); // key -> node
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Return an array of keys in the deque
|
|
15
|
+
keys() {
|
|
16
|
+
let current = this.head;
|
|
17
|
+
const keys = [];
|
|
18
|
+
while (current) {
|
|
19
|
+
keys.push(current.key);
|
|
20
|
+
current = current.next;
|
|
21
|
+
}
|
|
22
|
+
return keys;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Return the first node and remove it from the deque
|
|
26
|
+
popHead() {
|
|
27
|
+
if (!this.head) return null;
|
|
28
|
+
const node = this.head;
|
|
29
|
+
this.map.delete(node.key);
|
|
30
|
+
this.head = node.next;
|
|
31
|
+
if (this.head) {
|
|
32
|
+
this.head.prev = null;
|
|
33
|
+
} else {
|
|
34
|
+
this.tail = null;
|
|
35
|
+
}
|
|
36
|
+
return node;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Add a new node to the back of the deque
|
|
40
|
+
pushTail(key, value) {
|
|
41
|
+
if (this.map.has(key)) {
|
|
42
|
+
throw new Error(`Key "${key}" already exists in the deque.`);
|
|
43
|
+
}
|
|
44
|
+
const node = {
|
|
45
|
+
key,
|
|
46
|
+
value,
|
|
47
|
+
prev: null,
|
|
48
|
+
next: null
|
|
49
|
+
};
|
|
50
|
+
this.map.set(key, node);
|
|
51
|
+
if (!this.tail) {
|
|
52
|
+
this.head = this.tail = node;
|
|
53
|
+
} else {
|
|
54
|
+
this.tail.next = node;
|
|
55
|
+
node.prev = this.tail;
|
|
56
|
+
this.tail = node;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Move a node from the deque to the head
|
|
61
|
+
moveToHead(key) {
|
|
62
|
+
const node = this.map.get(key);
|
|
63
|
+
if (!node) return;
|
|
64
|
+
if (node === this.head) return; // already at head
|
|
65
|
+
// Remove node from its current position
|
|
66
|
+
if (node.prev) node.prev.next = node.next;
|
|
67
|
+
if (node.next) node.next.prev = node.prev;
|
|
68
|
+
if (node === this.tail) this.tail = node.prev; // if it's the tail
|
|
69
|
+
if (node === this.head) this.head = node.next; // if it's the head
|
|
70
|
+
// Move node to head
|
|
71
|
+
node.prev = null;
|
|
72
|
+
node.next = this.head;
|
|
73
|
+
if (this.head) {
|
|
74
|
+
this.head.prev = node; // link old head back to new head
|
|
75
|
+
}
|
|
76
|
+
this.head = node; // update head to be the moved node
|
|
77
|
+
if (!this.tail) {
|
|
78
|
+
this.tail = node; // if it was the only node, set tail as well
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Return the first node without removing it
|
|
83
|
+
peekHead() {
|
|
84
|
+
return this.head || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Move the head node to the back of the deque
|
|
88
|
+
rotate() {
|
|
89
|
+
if (!this.head || !this.head.next) return;
|
|
90
|
+
this.tail.next = this.head; // link tail to head
|
|
91
|
+
this.head.prev = this.tail; // link head back to tail
|
|
92
|
+
this.tail = this.head; // update tail to be the old head
|
|
93
|
+
this.head = this.head.next; // move head to the next node
|
|
94
|
+
this.tail.next = null; // set new tail's next to null
|
|
95
|
+
this.head.prev = null; // set new head's prev to null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Remove a node from the deque
|
|
99
|
+
delete(key) {
|
|
100
|
+
const node = this.map.get(key);
|
|
101
|
+
if (!node) return;
|
|
102
|
+
|
|
103
|
+
if (node.prev) node.prev.next = node.next;
|
|
104
|
+
else this.head = node.next;
|
|
105
|
+
|
|
106
|
+
if (node.next) node.next.prev = node.prev;
|
|
107
|
+
else this.tail = node.prev;
|
|
108
|
+
|
|
109
|
+
this.map.delete(key);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Does the deque contain a node with the given key?
|
|
113
|
+
has(key) {
|
|
114
|
+
return this.map.has(key);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get the node with the given key
|
|
118
|
+
get(key) {
|
|
119
|
+
return this.map.get(key) || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Number of nodes in the deque
|
|
123
|
+
get length() {
|
|
124
|
+
return this.map.size;
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/anchors/index.mjs
CHANGED
|
@@ -2,20 +2,112 @@ import * as util from '../util/index.mjs';
|
|
|
2
2
|
import { toRad } from '../g/index.mjs';
|
|
3
3
|
import { resolveRef } from '../linkAnchors/index.mjs';
|
|
4
4
|
|
|
5
|
+
const Side = {
|
|
6
|
+
LEFT: 'left',
|
|
7
|
+
RIGHT: 'right',
|
|
8
|
+
TOP: 'top',
|
|
9
|
+
BOTTOM: 'bottom',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const SideMode = {
|
|
13
|
+
PREFER_HORIZONTAL: 'prefer-horizontal',
|
|
14
|
+
PREFER_VERTICAL: 'prefer-vertical',
|
|
15
|
+
HORIZONTAL: 'horizontal',
|
|
16
|
+
VERTICAL: 'vertical',
|
|
17
|
+
AUTO: 'auto',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function getModelBBoxFromConnectedLink(element, link, endType, rotate) {
|
|
21
|
+
|
|
22
|
+
const portId = link.get(endType).port;
|
|
23
|
+
if (element.hasPort(portId)) {
|
|
24
|
+
return element.getPortBBox(portId, { rotate });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return element.getBBox({ rotate });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getMiddleSide(rect, point, opt) {
|
|
31
|
+
|
|
32
|
+
const { preferenceThreshold = 0, mode } = opt;
|
|
33
|
+
const { x, y } = point;
|
|
34
|
+
const { x: left , y: top, width, height } = rect;
|
|
35
|
+
|
|
36
|
+
switch (mode) {
|
|
37
|
+
|
|
38
|
+
case SideMode.PREFER_VERTICAL: {
|
|
39
|
+
const {
|
|
40
|
+
top: topThreshold,
|
|
41
|
+
bottom: bottomThreshold
|
|
42
|
+
} = util.normalizeSides(preferenceThreshold);
|
|
43
|
+
const bottom = top + height;
|
|
44
|
+
if (y > top - topThreshold && y < bottom + bottomThreshold) {
|
|
45
|
+
const cx = left + width / 2;
|
|
46
|
+
return (x < cx) ? Side.LEFT : Side.RIGHT;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// eslint-disable-next-line no-fallthrough
|
|
50
|
+
case SideMode.VERTICAL: {
|
|
51
|
+
const cy = top + height / 2;
|
|
52
|
+
return (y < cy) ? Side.TOP : Side.BOTTOM;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case SideMode.PREFER_HORIZONTAL: {
|
|
56
|
+
const {
|
|
57
|
+
left: leftThreshold,
|
|
58
|
+
right: rightThreshold
|
|
59
|
+
} = util.normalizeSides(preferenceThreshold);
|
|
60
|
+
const right = left + width;
|
|
61
|
+
if (x > left - leftThreshold && x < right + rightThreshold) {
|
|
62
|
+
const cy = top + height / 2;
|
|
63
|
+
return (y < cy) ? Side.TOP : Side.BOTTOM;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// eslint-disable-next-line no-fallthrough
|
|
67
|
+
case SideMode.HORIZONTAL: {
|
|
68
|
+
const cx = left + width / 2;
|
|
69
|
+
return (x < cx) ? Side.LEFT : Side.RIGHT;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case SideMode.AUTO:
|
|
73
|
+
default: {
|
|
74
|
+
return rect.sideNearestToPoint(point);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
5
79
|
function bboxWrapper(method) {
|
|
6
80
|
|
|
7
|
-
return function(
|
|
81
|
+
return function(elementView, magnet, ref, opt, endType, linkView) {
|
|
82
|
+
|
|
83
|
+
const rotate = !!opt.rotate;
|
|
84
|
+
const element = elementView.model;
|
|
85
|
+
const link = linkView.model;
|
|
86
|
+
const angle = element.angle();
|
|
87
|
+
|
|
88
|
+
let bbox, center;
|
|
89
|
+
if (opt.useModelGeometry) {
|
|
90
|
+
bbox = getModelBBoxFromConnectedLink(element, link, endType, !rotate);
|
|
91
|
+
center = bbox.center();
|
|
92
|
+
} else {
|
|
93
|
+
center = element.getCenter();
|
|
94
|
+
bbox = (rotate) ? elementView.getNodeUnrotatedBBox(magnet) : elementView.getNodeBBox(magnet);
|
|
95
|
+
}
|
|
8
96
|
|
|
9
|
-
|
|
10
|
-
var bbox = (rotate) ? view.getNodeUnrotatedBBox(magnet) : view.getNodeBBox(magnet);
|
|
11
|
-
var anchor = bbox[method]();
|
|
97
|
+
const anchor = bbox[method]();
|
|
12
98
|
|
|
13
|
-
|
|
99
|
+
let dx = opt.dx;
|
|
14
100
|
if (dx) {
|
|
15
|
-
|
|
16
|
-
|
|
101
|
+
const isDxPercentage = util.isPercentage(dx);
|
|
102
|
+
if (!isDxPercentage && util.isCalcExpression(dx)) {
|
|
103
|
+
// calc expression
|
|
104
|
+
dx = Number(util.evalCalcExpression(dx, bbox));
|
|
105
|
+
} else {
|
|
106
|
+
// percentage or a number
|
|
107
|
+
dx = parseFloat(dx);
|
|
108
|
+
}
|
|
17
109
|
if (isFinite(dx)) {
|
|
18
|
-
if (
|
|
110
|
+
if (isDxPercentage) {
|
|
19
111
|
dx /= 100;
|
|
20
112
|
dx *= bbox.width;
|
|
21
113
|
}
|
|
@@ -23,12 +115,18 @@ function bboxWrapper(method) {
|
|
|
23
115
|
}
|
|
24
116
|
}
|
|
25
117
|
|
|
26
|
-
|
|
118
|
+
let dy = opt.dy;
|
|
27
119
|
if (dy) {
|
|
28
|
-
|
|
29
|
-
|
|
120
|
+
const isDyPercentage = util.isPercentage(dy);
|
|
121
|
+
if (!isDyPercentage && util.isCalcExpression(dy)) {
|
|
122
|
+
// calc expression
|
|
123
|
+
dy = Number(util.evalCalcExpression(dy, bbox));
|
|
124
|
+
} else {
|
|
125
|
+
// percentage or a number
|
|
126
|
+
dy = parseFloat(dy);
|
|
127
|
+
}
|
|
30
128
|
if (isFinite(dy)) {
|
|
31
|
-
if (
|
|
129
|
+
if (isDyPercentage) {
|
|
32
130
|
dy /= 100;
|
|
33
131
|
dy *= bbox.height;
|
|
34
132
|
}
|
|
@@ -36,19 +134,27 @@ function bboxWrapper(method) {
|
|
|
36
134
|
}
|
|
37
135
|
}
|
|
38
136
|
|
|
39
|
-
return (rotate) ? anchor.rotate(
|
|
137
|
+
return (rotate) ? anchor.rotate(center, -angle) : anchor;
|
|
40
138
|
};
|
|
41
139
|
}
|
|
42
140
|
|
|
43
|
-
function _perpendicular(
|
|
141
|
+
function _perpendicular(elementView, magnet, refPoint, opt, endType, linkView) {
|
|
44
142
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
var anchor = bbox.center();
|
|
48
|
-
var topLeft = bbox.origin();
|
|
49
|
-
var bottomRight = bbox.corner();
|
|
143
|
+
const element = elementView.model;
|
|
144
|
+
const angle = element.angle();
|
|
50
145
|
|
|
51
|
-
|
|
146
|
+
let bbox;
|
|
147
|
+
if (opt.useModelGeometry) {
|
|
148
|
+
bbox = getModelBBoxFromConnectedLink(element, linkView.model, endType, true);
|
|
149
|
+
} else {
|
|
150
|
+
bbox = elementView.getNodeBBox(magnet);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const anchor = bbox.center();
|
|
154
|
+
const topLeft = bbox.origin();
|
|
155
|
+
const bottomRight = bbox.corner();
|
|
156
|
+
|
|
157
|
+
let padding = opt.padding;
|
|
52
158
|
if (!isFinite(padding)) padding = 0;
|
|
53
159
|
|
|
54
160
|
if ((topLeft.y + padding) <= refPoint.y && refPoint.y <= (bottomRight.y - padding)) {
|
|
@@ -64,16 +170,17 @@ function _perpendicular(view, magnet, refPoint, opt) {
|
|
|
64
170
|
return anchor;
|
|
65
171
|
}
|
|
66
172
|
|
|
67
|
-
function _midSide(view, magnet, refPoint, opt) {
|
|
68
|
-
|
|
173
|
+
function _midSide(view, magnet, refPoint, opt, endType, linkView) {
|
|
69
174
|
var rotate = !!opt.rotate;
|
|
70
|
-
var
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
175
|
+
var angle = view.model.angle();
|
|
176
|
+
var center = view.model.getCenter();
|
|
177
|
+
|
|
178
|
+
var bbox;
|
|
179
|
+
if (opt.useModelGeometry) {
|
|
180
|
+
bbox = getModelBBoxFromConnectedLink(view.model, linkView.model, endType, !rotate);
|
|
181
|
+
center = bbox.center();
|
|
75
182
|
} else {
|
|
76
|
-
bbox = view.getNodeBBox(magnet);
|
|
183
|
+
bbox = rotate ? view.getNodeUnrotatedBBox(magnet) : view.getNodeBBox(magnet);
|
|
77
184
|
}
|
|
78
185
|
|
|
79
186
|
var padding = opt.padding;
|
|
@@ -81,19 +188,19 @@ function _midSide(view, magnet, refPoint, opt) {
|
|
|
81
188
|
|
|
82
189
|
if (rotate) refPoint.rotate(center, angle);
|
|
83
190
|
|
|
84
|
-
var side = bbox
|
|
191
|
+
var side = getMiddleSide(bbox, refPoint, opt);
|
|
85
192
|
var anchor;
|
|
86
193
|
switch (side) {
|
|
87
|
-
case
|
|
194
|
+
case Side.LEFT:
|
|
88
195
|
anchor = bbox.leftMiddle();
|
|
89
196
|
break;
|
|
90
|
-
case
|
|
197
|
+
case Side.RIGHT:
|
|
91
198
|
anchor = bbox.rightMiddle();
|
|
92
199
|
break;
|
|
93
|
-
case
|
|
200
|
+
case Side.TOP:
|
|
94
201
|
anchor = bbox.topMiddle();
|
|
95
202
|
break;
|
|
96
|
-
case
|
|
203
|
+
case Side.BOTTOM:
|
|
97
204
|
anchor = bbox.bottomMiddle();
|
|
98
205
|
break;
|
|
99
206
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import * as util from '../util/index.mjs';
|
|
3
3
|
import { ToolView } from '../dia/ToolView.mjs';
|
|
4
|
-
import { getViewBBox } from './helpers.mjs';
|
|
4
|
+
import { getToolOptions, getViewBBox } from './helpers.mjs';
|
|
5
5
|
|
|
6
6
|
export const Boundary = ToolView.extend({
|
|
7
7
|
name: 'boundary',
|
|
@@ -21,21 +21,23 @@ export const Boundary = ToolView.extend({
|
|
|
21
21
|
this.update();
|
|
22
22
|
},
|
|
23
23
|
update: function() {
|
|
24
|
-
const { relatedView: view,
|
|
25
|
-
const { useModelGeometry, rotate } =
|
|
26
|
-
const
|
|
27
|
-
let bbox = getViewBBox(view, useModelGeometry).moveAndExpand({
|
|
28
|
-
x: -
|
|
29
|
-
y: -
|
|
30
|
-
width:
|
|
31
|
-
height:
|
|
24
|
+
const { relatedView: view, vel } = this;
|
|
25
|
+
const { useModelGeometry, rotate, relative, padding } = getToolOptions(this);
|
|
26
|
+
const normalizedPadding = util.normalizeSides(padding);
|
|
27
|
+
let bbox = getViewBBox(view, { useModelGeometry, relative }).moveAndExpand({
|
|
28
|
+
x: -normalizedPadding.left,
|
|
29
|
+
y: -normalizedPadding.top,
|
|
30
|
+
width: normalizedPadding.left + normalizedPadding.right,
|
|
31
|
+
height: normalizedPadding.top + normalizedPadding.bottom
|
|
32
32
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const model = view.model;
|
|
34
|
+
// With relative positioning, rotation is implicit
|
|
35
|
+
// (the tool rotates along with the element).
|
|
36
|
+
if (model.isElement() && !relative) {
|
|
37
|
+
const angle = model.angle();
|
|
36
38
|
if (angle) {
|
|
37
39
|
if (rotate) {
|
|
38
|
-
|
|
40
|
+
const origin = model.getCenter();
|
|
39
41
|
vel.rotate(angle, origin.x, origin.y, { absolute: true });
|
|
40
42
|
} else {
|
|
41
43
|
bbox = bbox.bbox(angle);
|
package/src/cellTools/Button.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ToolView } from '../dia/ToolView.mjs';
|
|
2
|
-
import { getViewBBox } from './helpers.mjs';
|
|
2
|
+
import { getToolOptions, getViewBBox } from './helpers.mjs';
|
|
3
3
|
import * as util from '../util/index.mjs';
|
|
4
4
|
import * as g from '../g/index.mjs';
|
|
5
5
|
import V from '../V/index.mjs';
|
|
@@ -32,9 +32,9 @@ export const Button = ToolView.extend({
|
|
|
32
32
|
return this.relatedView.model.isLink() ? this.getLinkMatrix() : this.getElementMatrix();
|
|
33
33
|
},
|
|
34
34
|
getElementMatrix() {
|
|
35
|
-
const { relatedView: view
|
|
36
|
-
let { x = 0, y = 0, offset = {}, useModelGeometry, rotate, scale } =
|
|
37
|
-
let bbox = getViewBBox(view, useModelGeometry);
|
|
35
|
+
const { relatedView: view } = this;
|
|
36
|
+
let { x = 0, y = 0, offset = {}, useModelGeometry, rotate, scale, relative } = getToolOptions(this);
|
|
37
|
+
let bbox = getViewBBox(view, { useModelGeometry, relative });
|
|
38
38
|
const angle = view.model.angle();
|
|
39
39
|
if (!rotate) bbox = bbox.bbox(angle);
|
|
40
40
|
const { x: offsetX = 0, y: offsetY = 0 } = offset;
|
|
@@ -49,7 +49,9 @@ export const Button = ToolView.extend({
|
|
|
49
49
|
y = Number(util.evalCalcExpression(y, bbox));
|
|
50
50
|
}
|
|
51
51
|
let matrix = V.createSVGMatrix().translate(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2);
|
|
52
|
-
|
|
52
|
+
// With relative positioning, rotation is implicit
|
|
53
|
+
// (the tool rotates along with the element).
|
|
54
|
+
if (rotate && !relative) matrix = matrix.rotate(angle);
|
|
53
55
|
matrix = matrix.translate(x + offsetX - bbox.width / 2, y + offsetY - bbox.height / 2);
|
|
54
56
|
if (scale) matrix = matrix.scale(scale);
|
|
55
57
|
return matrix;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ToolView } from '../dia/ToolView.mjs';
|
|
2
2
|
import * as util from '../util/index.mjs';
|
|
3
|
+
import { getToolOptions, getViewBBox } from './helpers.mjs';
|
|
3
4
|
|
|
4
5
|
export const Control = ToolView.extend({
|
|
5
6
|
tagName: 'g',
|
|
@@ -72,38 +73,60 @@ export const Control = ToolView.extend({
|
|
|
72
73
|
return this;
|
|
73
74
|
},
|
|
74
75
|
updateHandle: function(handleNode) {
|
|
76
|
+
const { options: { handleAttributes }} = this;
|
|
77
|
+
handleNode.setAttribute('transform', this.getHandleTransformString());
|
|
78
|
+
if (handleAttributes) {
|
|
79
|
+
for (let attrName in handleAttributes) {
|
|
80
|
+
handleNode.setAttribute(attrName, handleAttributes[attrName]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
getHandleTransformString() {
|
|
75
85
|
const { relatedView, options } = this;
|
|
86
|
+
const { scale } = options;
|
|
76
87
|
const { model } = relatedView;
|
|
77
88
|
const relativePos = this.getPosition(relatedView, this);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
const translate = this.isOverlay()
|
|
90
|
+
// The tool is rendered in the coordinate system of the paper
|
|
91
|
+
? model.getAbsolutePointFromRelative(relativePos)
|
|
92
|
+
// The tool is rendered in the coordinate system of the relatedView
|
|
93
|
+
: relativePos;
|
|
94
|
+
let transformString = `translate(${translate.x},${translate.y})`;
|
|
81
95
|
if (scale) {
|
|
82
96
|
transformString += ` scale(${scale})`;
|
|
83
97
|
}
|
|
84
|
-
|
|
85
|
-
if (handleAttributes) {
|
|
86
|
-
for (let attrName in handleAttributes) {
|
|
87
|
-
handleNode.setAttribute(attrName, handleAttributes[attrName]);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
98
|
+
return transformString;
|
|
90
99
|
},
|
|
91
100
|
updateExtras: function(extrasNode) {
|
|
92
101
|
const { relatedView, options } = this;
|
|
93
|
-
const { selector } = this
|
|
102
|
+
const { selector, relative, useModelGeometry } = getToolOptions(this);
|
|
94
103
|
if (!selector) {
|
|
104
|
+
// Hide the extras if no selector is given.
|
|
95
105
|
this.toggleExtras(false);
|
|
96
106
|
return;
|
|
97
107
|
}
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
// Get the size for the extras rectangle and update it.
|
|
109
|
+
let bbox;
|
|
110
|
+
if (useModelGeometry) {
|
|
111
|
+
if (selector !== 'root') {
|
|
112
|
+
// A selector other than null or `root` was provided.
|
|
113
|
+
console.warn('Control: selector will be ignored when `useModelGeometry` is used.');
|
|
114
|
+
}
|
|
115
|
+
bbox = getViewBBox(relatedView, { useModelGeometry, relative });
|
|
116
|
+
} else {
|
|
117
|
+
// The reference node for calculating the bounding box of the extras.
|
|
118
|
+
const el = relatedView.findNode(selector);
|
|
119
|
+
if (!el) throw new Error('Control: invalid selector.');
|
|
120
|
+
bbox = getViewBBox(relatedView, { el });
|
|
121
|
+
}
|
|
100
122
|
let padding = options.padding;
|
|
101
123
|
if (!isFinite(padding)) padding = 0;
|
|
102
|
-
const bbox = relatedView.getNodeUnrotatedBBox(magnet);
|
|
103
124
|
const model = relatedView.model;
|
|
104
|
-
|
|
125
|
+
// With relative positioning, rotation is implicit
|
|
126
|
+
// (the tool rotates along with the element).
|
|
127
|
+
const angle = relative ? 0 : model.angle();
|
|
105
128
|
const center = bbox.center();
|
|
106
|
-
if (angle) center.rotate(model.
|
|
129
|
+
if (angle) center.rotate(model.getCenter(), -angle);
|
|
107
130
|
bbox.inflate(padding);
|
|
108
131
|
extrasNode.setAttribute('x', -bbox.width / 2);
|
|
109
132
|
extrasNode.setAttribute('y', -bbox.height / 2);
|
|
@@ -121,10 +121,14 @@ export const HoverConnect = Connect.extend({
|
|
|
121
121
|
return V.createSVGMatrix();
|
|
122
122
|
},
|
|
123
123
|
|
|
124
|
+
getTrackMatrixAbsolute() {
|
|
125
|
+
return this.getTrackMatrix();
|
|
126
|
+
},
|
|
127
|
+
|
|
124
128
|
getTrackRatioFromEvent(evt) {
|
|
125
129
|
const { relatedView, trackPath } = this;
|
|
126
130
|
const localPoint = relatedView.paper.clientToLocalPoint(evt.clientX, evt.clientY);
|
|
127
|
-
const trackPoint = V.transformPoint(localPoint, this.
|
|
131
|
+
const trackPoint = V.transformPoint(localPoint, this.getTrackMatrixAbsolute().inverse());
|
|
128
132
|
return trackPath.closestPointLength(trackPoint);
|
|
129
133
|
},
|
|
130
134
|
|
|
@@ -1,9 +1,50 @@
|
|
|
1
1
|
import * as connectionStrategies from '../connectionStrategies/index.mjs';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Common helper for getting a cell view’s bounding box,
|
|
5
|
+
* configurable with `useModelGeometry`, `relative`, and `el`.
|
|
6
|
+
*/
|
|
7
|
+
export function getViewBBox(view, {
|
|
8
|
+
useModelGeometry = false,
|
|
9
|
+
relative = false,
|
|
10
|
+
el = view.el
|
|
11
|
+
} = {}) {
|
|
4
12
|
const { model } = view;
|
|
5
|
-
|
|
6
|
-
|
|
13
|
+
let bbox;
|
|
14
|
+
if (useModelGeometry) {
|
|
15
|
+
// cell model bbox
|
|
16
|
+
bbox = model.getBBox();
|
|
17
|
+
} else if (model.isLink()) {
|
|
18
|
+
// link view bbox
|
|
19
|
+
bbox = view.getConnection().bbox();
|
|
20
|
+
} else {
|
|
21
|
+
// element view bbox
|
|
22
|
+
bbox = view.getNodeUnrotatedBBox(el);
|
|
23
|
+
}
|
|
24
|
+
if (relative) {
|
|
25
|
+
// Relative to the element position.
|
|
26
|
+
const position = model.position();
|
|
27
|
+
bbox.x -= position.x;
|
|
28
|
+
bbox.y -= position.y;
|
|
29
|
+
}
|
|
30
|
+
return bbox;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Retrieves the tool options.
|
|
35
|
+
* Automatically overrides `useModelGeometry` and `rotate`
|
|
36
|
+
* if the tool is positioned relative to the element.
|
|
37
|
+
*/
|
|
38
|
+
export function getToolOptions(toolView) {
|
|
39
|
+
// Positioning is relative if the tool is drawn within the element view.
|
|
40
|
+
const relative = !toolView.isOverlay();
|
|
41
|
+
const { useModelGeometry, rotate, ...otherOptions } = toolView.options;
|
|
42
|
+
return {
|
|
43
|
+
...otherOptions,
|
|
44
|
+
useModelGeometry: useModelGeometry || relative,
|
|
45
|
+
rotate: rotate || relative,
|
|
46
|
+
relative,
|
|
47
|
+
};
|
|
7
48
|
}
|
|
8
49
|
|
|
9
50
|
export function getAnchor(coords, view, magnet) {
|
package/src/config/index.mjs
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export const config = {
|
|
2
|
+
// How the cell attributes are merged when `cell.prop()` is called.
|
|
3
|
+
// DEFAULT: the arrays are merged into the source array.
|
|
4
|
+
cellMergeStrategy: null,
|
|
5
|
+
// How the cell default attributes are merged with the attributes provided
|
|
6
|
+
// in the cell constructor.
|
|
7
|
+
// DEFAULT: the arrays are merged by replacing the source array
|
|
8
|
+
// with the destination array.
|
|
9
|
+
cellDefaultsMergeStrategy: null,
|
|
2
10
|
// When set to `true` the cell selectors could be defined as CSS selectors.
|
|
3
11
|
// If not, only JSON Markup selectors are taken into account.
|
|
4
12
|
useCSSSelectors: false,
|
|
@@ -78,10 +78,12 @@ function anchorConnectionPoint(line, _view, _magnet, opt) {
|
|
|
78
78
|
|
|
79
79
|
function bboxIntersection(line, view, magnet, opt) {
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
const bbox = (opt.useModelGeometry)
|
|
82
|
+
? getNodeModelBBox(view, magnet, true)
|
|
83
|
+
: view.getNodeBBox(magnet);
|
|
82
84
|
if (opt.stroke) bbox.inflate(stroke(magnet) / 2);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
const intersections = line.intersect(bbox);
|
|
86
|
+
const cp = (intersections)
|
|
85
87
|
? line.start.chooseClosest(intersections)
|
|
86
88
|
: line.end;
|
|
87
89
|
return offsetPoint(cp, line.start, opt.offset);
|
|
@@ -89,22 +91,35 @@ function bboxIntersection(line, view, magnet, opt) {
|
|
|
89
91
|
|
|
90
92
|
function rectangleIntersection(line, view, magnet, opt) {
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
const angle = view.model.angle();
|
|
93
95
|
if (angle === 0) {
|
|
94
96
|
return bboxIntersection(line, view, magnet, opt);
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
const bboxWORotation = (opt.useModelGeometry)
|
|
100
|
+
? getNodeModelBBox(view, magnet, false)
|
|
101
|
+
: view.getNodeUnrotatedBBox(magnet);
|
|
98
102
|
if (opt.stroke) bboxWORotation.inflate(stroke(magnet) / 2);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
const center = bboxWORotation.center();
|
|
104
|
+
const lineWORotation = line.clone().rotate(center, angle);
|
|
105
|
+
const intersections = lineWORotation.setLength(1e6).intersect(bboxWORotation);
|
|
106
|
+
const cp = (intersections)
|
|
103
107
|
? lineWORotation.start.chooseClosest(intersections).rotate(center, -angle)
|
|
104
108
|
: line.end;
|
|
105
109
|
return offsetPoint(cp, line.start, opt.offset);
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
function getNodeModelBBox(elementView, magnet, rotate) {
|
|
113
|
+
const element = elementView.model;
|
|
114
|
+
|
|
115
|
+
const portId = elementView.findAttribute('port', magnet);
|
|
116
|
+
if (element.hasPort(portId)) {
|
|
117
|
+
return element.getPortBBox(portId, { rotate });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return element.getBBox({ rotate });
|
|
121
|
+
}
|
|
122
|
+
|
|
108
123
|
function findShapeNode(magnet) {
|
|
109
124
|
if (!magnet) return null;
|
|
110
125
|
var node = magnet;
|