@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.
Files changed (58) hide show
  1. package/README.md +4 -2
  2. package/dist/geometry.js +129 -124
  3. package/dist/geometry.min.js +4 -3
  4. package/dist/joint.d.ts +352 -160
  5. package/dist/joint.js +3654 -2191
  6. package/dist/joint.min.js +4 -3
  7. package/dist/joint.nowrap.js +3653 -2188
  8. package/dist/joint.nowrap.min.js +4 -3
  9. package/dist/vectorizer.js +489 -279
  10. package/dist/vectorizer.min.js +4 -3
  11. package/dist/version.mjs +1 -1
  12. package/package.json +33 -27
  13. package/src/V/create.mjs +51 -0
  14. package/src/V/index.mjs +89 -159
  15. package/src/V/namespace.mjs +9 -0
  16. package/src/V/transform.mjs +183 -0
  17. package/src/V/traverse.mjs +16 -0
  18. package/src/alg/Deque.mjs +126 -0
  19. package/src/anchors/index.mjs +140 -33
  20. package/src/cellTools/Boundary.mjs +15 -13
  21. package/src/cellTools/Button.mjs +7 -5
  22. package/src/cellTools/Control.mjs +38 -15
  23. package/src/cellTools/HoverConnect.mjs +5 -1
  24. package/src/cellTools/helpers.mjs +44 -3
  25. package/src/config/index.mjs +8 -0
  26. package/src/connectionPoints/index.mjs +24 -9
  27. package/src/connectionStrategies/index.mjs +1 -1
  28. package/src/connectors/jumpover.mjs +1 -1
  29. package/src/dia/Cell.mjs +32 -12
  30. package/src/dia/CellView.mjs +53 -38
  31. package/src/dia/Element.mjs +81 -35
  32. package/src/dia/ElementView.mjs +2 -1
  33. package/src/dia/HighlighterView.mjs +54 -11
  34. package/src/dia/LinkView.mjs +118 -98
  35. package/src/dia/Paper.mjs +831 -231
  36. package/src/dia/PaperLayer.mjs +9 -2
  37. package/src/dia/ToolView.mjs +4 -0
  38. package/src/dia/ToolsView.mjs +12 -3
  39. package/src/dia/attributes/text.mjs +16 -5
  40. package/src/dia/layers/GridLayer.mjs +5 -0
  41. package/src/dia/ports.mjs +344 -111
  42. package/src/elementTools/HoverConnect.mjs +14 -8
  43. package/src/env/index.mjs +7 -4
  44. package/src/g/rect.mjs +7 -0
  45. package/src/highlighters/stroke.mjs +1 -1
  46. package/src/layout/ports/port.mjs +30 -15
  47. package/src/layout/ports/portLabel.mjs +1 -1
  48. package/src/linkAnchors/index.mjs +2 -2
  49. package/src/linkTools/Anchor.mjs +2 -2
  50. package/src/linkTools/Vertices.mjs +4 -6
  51. package/src/mvc/View.mjs +4 -0
  52. package/src/mvc/ViewBase.mjs +1 -1
  53. package/src/util/util.mjs +1 -1
  54. package/src/util/utilHelpers.mjs +2 -0
  55. package/types/geometry.d.ts +65 -59
  56. package/types/joint.d.ts +278 -102
  57. package/types/vectorizer.d.ts +11 -1
  58. 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
+ }
@@ -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(view, magnet, ref, opt) {
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
- var rotate = !!opt.rotate;
10
- var bbox = (rotate) ? view.getNodeUnrotatedBBox(magnet) : view.getNodeBBox(magnet);
11
- var anchor = bbox[method]();
97
+ const anchor = bbox[method]();
12
98
 
13
- var dx = opt.dx;
99
+ let dx = opt.dx;
14
100
  if (dx) {
15
- var dxPercentage = util.isPercentage(dx);
16
- dx = parseFloat(dx);
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 (dxPercentage) {
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
- var dy = opt.dy;
118
+ let dy = opt.dy;
27
119
  if (dy) {
28
- var dyPercentage = util.isPercentage(dy);
29
- dy = parseFloat(dy);
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 (dyPercentage) {
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(view.model.getBBox().center(), -view.model.angle()) : anchor;
137
+ return (rotate) ? anchor.rotate(center, -angle) : anchor;
40
138
  };
41
139
  }
42
140
 
43
- function _perpendicular(view, magnet, refPoint, opt) {
141
+ function _perpendicular(elementView, magnet, refPoint, opt, endType, linkView) {
44
142
 
45
- var angle = view.model.angle();
46
- var bbox = view.getNodeBBox(magnet);
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
- var padding = opt.padding;
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 bbox, angle, center;
71
- if (rotate) {
72
- bbox = view.getNodeUnrotatedBBox(magnet);
73
- center = view.model.getBBox().center();
74
- angle = view.model.angle();
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.sideNearestToPoint(refPoint);
191
+ var side = getMiddleSide(bbox, refPoint, opt);
85
192
  var anchor;
86
193
  switch (side) {
87
- case 'left':
194
+ case Side.LEFT:
88
195
  anchor = bbox.leftMiddle();
89
196
  break;
90
- case 'right':
197
+ case Side.RIGHT:
91
198
  anchor = bbox.rightMiddle();
92
199
  break;
93
- case 'top':
200
+ case Side.TOP:
94
201
  anchor = bbox.topMiddle();
95
202
  break;
96
- case 'bottom':
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, options, vel } = this;
25
- const { useModelGeometry, rotate } = options;
26
- const padding = util.normalizeSides(options.padding);
27
- let bbox = getViewBBox(view, useModelGeometry).moveAndExpand({
28
- x: -padding.left,
29
- y: -padding.top,
30
- width: padding.left + padding.right,
31
- height: padding.top + padding.bottom
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
- var model = view.model;
34
- if (model.isElement()) {
35
- var angle = model.angle();
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
- var origin = model.getBBox().center();
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);
@@ -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, options } = this;
36
- let { x = 0, y = 0, offset = {}, useModelGeometry, rotate, scale } = options;
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
- if (rotate) matrix = matrix.rotate(angle);
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 absolutePos = model.getAbsolutePointFromRelative(relativePos);
79
- const { handleAttributes, scale } = options;
80
- let transformString = `translate(${absolutePos.x},${absolutePos.y})`;
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
- handleNode.setAttribute('transform', transformString);
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.options;
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
- const magnet = relatedView.findNode(selector);
99
- if (!magnet) throw new Error('Control: invalid selector.');
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
- const angle = model.angle();
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.getBBox().center(), -angle);
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.getTrackMatrix().inverse());
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
- export function getViewBBox(view, useModelGeometry) {
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
- if (useModelGeometry) return model.getBBox();
6
- return (model.isLink()) ? view.getConnection().bbox() : view.getNodeUnrotatedBBox(view.el);
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) {
@@ -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
- var bbox = view.getNodeBBox(magnet);
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
- var intersections = line.intersect(bbox);
84
- var cp = (intersections)
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
- var angle = view.model.angle();
94
+ const angle = view.model.angle();
93
95
  if (angle === 0) {
94
96
  return bboxIntersection(line, view, magnet, opt);
95
97
  }
96
98
 
97
- var bboxWORotation = view.getNodeUnrotatedBBox(magnet);
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
- var center = bboxWORotation.center();
100
- var lineWORotation = line.clone().rotate(center, angle);
101
- var intersections = lineWORotation.setLength(1e6).intersect(bboxWORotation);
102
- var cp = (intersections)
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;