bpmn-js 18.7.0 → 18.9.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/dist/bpmn-modeler.development.js +522 -288
- package/dist/bpmn-modeler.production.min.js +36 -36
- package/dist/bpmn-navigated-viewer.development.js +23 -6
- package/dist/bpmn-navigated-viewer.production.min.js +5 -5
- package/dist/bpmn-viewer.development.js +23 -6
- package/dist/bpmn-viewer.production.min.js +5 -5
- package/lib/Modeler.js +2 -0
- package/lib/draw/BpmnRenderer.js +9 -2
- package/lib/features/copy-paste/ModdleCopy.js +2 -1
- package/lib/features/label-link/LabelLink.d.ts +27 -0
- package/lib/features/label-link/LabelLink.js +202 -0
- package/lib/features/label-link/index.d.ts +14 -0
- package/lib/features/label-link/index.js +15 -0
- package/lib/features/modeling/behavior/CreateDataObjectBehavior.js +3 -0
- package/lib/features/outline/OutlineProvider.js +15 -1
- package/lib/features/outline/index.d.ts +2 -2
- package/lib/features/outline/index.js +4 -4
- package/lib/util/LabelUtil.d.ts +9 -0
- package/lib/util/LabelUtil.js +11 -0
- package/package.json +2 -2
- package/test/util/svgHelpers.js +52 -0
package/lib/Modeler.js
CHANGED
|
@@ -26,6 +26,7 @@ import InteractionEventsModule from './features/interaction-events';
|
|
|
26
26
|
import KeyboardModule from './features/keyboard';
|
|
27
27
|
import KeyboardMoveSelectionModule from 'diagram-js/lib/features/keyboard-move-selection';
|
|
28
28
|
import LabelEditingModule from './features/label-editing';
|
|
29
|
+
import LabelLink from './features/label-link';
|
|
29
30
|
import ModelingModule from './features/modeling';
|
|
30
31
|
import ModelingFeedbackModule from './features/modeling-feedback';
|
|
31
32
|
import MoveModule from 'diagram-js/lib/features/move';
|
|
@@ -181,6 +182,7 @@ Modeler.prototype._modelingModules = [
|
|
|
181
182
|
KeyboardModule,
|
|
182
183
|
KeyboardMoveSelectionModule,
|
|
183
184
|
LabelEditingModule,
|
|
185
|
+
LabelLink,
|
|
184
186
|
ModelingModule,
|
|
185
187
|
ModelingFeedbackModule,
|
|
186
188
|
MoveModule,
|
package/lib/draw/BpmnRenderer.js
CHANGED
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
} from '../util/DiUtil';
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
|
-
getLabel
|
|
18
|
+
getLabel,
|
|
19
|
+
isLabel
|
|
19
20
|
} from '../util/LabelUtil';
|
|
20
21
|
|
|
21
22
|
import {
|
|
@@ -70,7 +71,8 @@ var markerIds = new Ids();
|
|
|
70
71
|
var ELEMENT_LABEL_DISTANCE = 10,
|
|
71
72
|
INNER_OUTER_DIST = 3,
|
|
72
73
|
PARTICIPANT_STROKE_WIDTH = 1.5,
|
|
73
|
-
TASK_BORDER_RADIUS = 10
|
|
74
|
+
TASK_BORDER_RADIUS = 10,
|
|
75
|
+
EXTERNAL_LABEL_BORDER_RADIUS = 4;
|
|
74
76
|
|
|
75
77
|
var DEFAULT_OPACITY = 0.95,
|
|
76
78
|
FULL_OPACITY = 1,
|
|
@@ -2379,6 +2381,11 @@ BpmnRenderer.prototype.drawConnection = function(parentGfx, connection, attrs =
|
|
|
2379
2381
|
* @return {string} path
|
|
2380
2382
|
*/
|
|
2381
2383
|
BpmnRenderer.prototype.getShapePath = function(shape) {
|
|
2384
|
+
|
|
2385
|
+
if (isLabel(shape)) {
|
|
2386
|
+
return getRoundRectPath(shape, EXTERNAL_LABEL_BORDER_RADIUS);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2382
2389
|
if (is(shape, 'bpmn:Event')) {
|
|
2383
2390
|
return getCirclePath(shape);
|
|
2384
2391
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a line between an external label and its target element,
|
|
3
|
+
* when either is selected.
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
export default class LabelLink {
|
|
7
|
+
static $inject: string[];
|
|
8
|
+
/**
|
|
9
|
+
* @param eventBus
|
|
10
|
+
* @param canvas
|
|
11
|
+
* @param graphicsFactory
|
|
12
|
+
* @param outline
|
|
13
|
+
*/
|
|
14
|
+
constructor(
|
|
15
|
+
eventBus: EventBus,
|
|
16
|
+
canvas: Canvas,
|
|
17
|
+
graphicsFactory: GraphicsFactory,
|
|
18
|
+
outline: Outline
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type EventBus = import("diagram-js/lib/core/EventBus").default;
|
|
23
|
+
type Canvas = import("diagram-js/lib/core/Canvas").default;
|
|
24
|
+
type GraphicsFactory = import("diagram-js/lib/core/GraphicsFactory").default;
|
|
25
|
+
type Outline = import("../outline/OutlineProvider").default;
|
|
26
|
+
export type Selection = any;
|
|
27
|
+
type Element = import("diagram-js/lib/model/Types").Element;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { queryAll as domQueryAll } from 'min-dom';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
append as svgAppend,
|
|
5
|
+
attr as svgAttr,
|
|
6
|
+
remove as svgRemove,
|
|
7
|
+
} from 'tiny-svg';
|
|
8
|
+
|
|
9
|
+
import { createLine, updateLine } from 'diagram-js/lib/util/RenderUtil';
|
|
10
|
+
import { getMid, getElementLineIntersection } from 'diagram-js/lib/layout/LayoutUtil';
|
|
11
|
+
import { getDistancePointPoint } from 'diagram-js/lib/features/bendpoints/GeometricUtil';
|
|
12
|
+
import { isLabel } from 'diagram-js/lib/util/ModelUtil';
|
|
13
|
+
|
|
14
|
+
import { isAny } from '../modeling/util/ModelingUtil';
|
|
15
|
+
import { getRoundRectPath, getCirclePath } from '../../draw/BpmnRenderUtil';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
|
|
19
|
+
* @typedef {import('diagram-js/lib/core/Canvas').default} Canvas
|
|
20
|
+
* @typedef {import('diagram-js/lib/core/GraphicsFactory').default} GraphicsFactory
|
|
21
|
+
* @typedef {import('../outline/OutlineProvider').default} Outline
|
|
22
|
+
* @typedef {import('diagram-js/lib/features/selection').default} Selection
|
|
23
|
+
*
|
|
24
|
+
* @typedef {import('diagram-js/lib/model/Types').Element} Element
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const ALLOWED_ELEMENTS = [ 'bpmn:Event', 'bpmn:SequenceFlow', 'bpmn:Gateway' ];
|
|
28
|
+
|
|
29
|
+
const LINE_STYLE = {
|
|
30
|
+
class: 'bjs-label-link',
|
|
31
|
+
stroke: 'var(--element-selected-outline-secondary-stroke-color)',
|
|
32
|
+
strokeDasharray: '5, 5',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DISTANCE_THRESHOLD = 15;
|
|
36
|
+
const PATH_OFFSET = 2;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Render a line between an external label and its target element,
|
|
40
|
+
* when either is selected.
|
|
41
|
+
*
|
|
42
|
+
* @param {EventBus} eventBus
|
|
43
|
+
* @param {Canvas} canvas
|
|
44
|
+
* @param {GraphicsFactory} graphicsFactory
|
|
45
|
+
* @param {Outline} outline
|
|
46
|
+
*/
|
|
47
|
+
export default function LabelLink(eventBus, canvas, graphicsFactory, outline, selection) {
|
|
48
|
+
|
|
49
|
+
const layer = canvas.getLayer('overlays');
|
|
50
|
+
|
|
51
|
+
eventBus.on([ 'selection.changed', 'shape.changed' ], function() {
|
|
52
|
+
cleanUp();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
eventBus.on('selection.changed', function({ newSelection }) {
|
|
56
|
+
|
|
57
|
+
const allowedElements = newSelection.filter(element => isAny(element, ALLOWED_ELEMENTS));
|
|
58
|
+
|
|
59
|
+
if (allowedElements.length === 1) {
|
|
60
|
+
const element = allowedElements[0];
|
|
61
|
+
if (isLabel(element)) {
|
|
62
|
+
createLink(element, element.labelTarget, newSelection);
|
|
63
|
+
} else if (element.labels?.length) {
|
|
64
|
+
createLink(element.labels[0], element, newSelection);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Only allowed when both label and its target are selected
|
|
69
|
+
if (allowedElements.length === 2) {
|
|
70
|
+
const label = allowedElements.find(isLabel);
|
|
71
|
+
const target = allowedElements.find(el => el.labels?.includes(label));
|
|
72
|
+
if (label && target) {
|
|
73
|
+
createLink(label, target, newSelection);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
eventBus.on('shape.changed', function({ element }) {
|
|
79
|
+
|
|
80
|
+
if (!isAny(element, ALLOWED_ELEMENTS)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isLabel(element)) {
|
|
85
|
+
createLink(element, element.labelTarget, selection.get());
|
|
86
|
+
} else if (element.labels?.length) {
|
|
87
|
+
createLink(element.labels[0], element, selection.get());
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Render a line between an external label and its target.
|
|
93
|
+
*
|
|
94
|
+
* @param {Element} label
|
|
95
|
+
* @param {Element} target
|
|
96
|
+
* @param {Element[]} selection
|
|
97
|
+
*/
|
|
98
|
+
function createLink(label, target, selection = []) {
|
|
99
|
+
|
|
100
|
+
// Create an auxiliary line between label and target mid points
|
|
101
|
+
const line = createLine(
|
|
102
|
+
[ getMid(target), getMid(label) ],
|
|
103
|
+
LINE_STYLE
|
|
104
|
+
);
|
|
105
|
+
const linePath = line.getAttribute('d');
|
|
106
|
+
|
|
107
|
+
// Calculate the intersection point between line and label
|
|
108
|
+
const labelSelected = selection.includes(label);
|
|
109
|
+
const labelPath = labelSelected ? getElementOutlinePath(label) : getElementPath(label);
|
|
110
|
+
const labelInter = getElementLineIntersection(labelPath, linePath);
|
|
111
|
+
|
|
112
|
+
// Label on top of the target
|
|
113
|
+
if (!labelInter) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Calculate the intersection point between line and label
|
|
118
|
+
// If the target is a sequence flow, there is no intersection,
|
|
119
|
+
// so we link to the middle of it.
|
|
120
|
+
const targetSelected = selection.includes(target);
|
|
121
|
+
const targetPath = targetSelected ? getElementOutlinePath(target) : getElementPath(target);
|
|
122
|
+
const targetInter = getElementLineIntersection(targetPath, linePath) || getMid(target);
|
|
123
|
+
|
|
124
|
+
// Do not draw a link if the points are too close
|
|
125
|
+
const distance = getDistancePointPoint(targetInter, labelInter);
|
|
126
|
+
if (distance < DISTANCE_THRESHOLD) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Connect the actual closest points
|
|
131
|
+
updateLine(line, [ targetInter, labelInter ]);
|
|
132
|
+
svgAppend(layer, line);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Remove all existing label links.
|
|
137
|
+
*/
|
|
138
|
+
function cleanUp() {
|
|
139
|
+
domQueryAll(`.${LINE_STYLE.class}`, layer).forEach(svgRemove);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get element's slightly expanded outline path.
|
|
144
|
+
*
|
|
145
|
+
* @param {Element} element
|
|
146
|
+
* @returns {string} svg path
|
|
147
|
+
*/
|
|
148
|
+
function getElementOutlinePath(element) {
|
|
149
|
+
const outlineShape = outline.getOutline(element);
|
|
150
|
+
const outlineOffset = outline.offset;
|
|
151
|
+
|
|
152
|
+
if (!outlineShape) {
|
|
153
|
+
return getElementPath(element);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (outlineShape.x) {
|
|
157
|
+
const shape = {
|
|
158
|
+
x: element.x + parseSvgNumAttr(outlineShape, 'x') - PATH_OFFSET,
|
|
159
|
+
y: element.y + parseSvgNumAttr(outlineShape, 'y') - PATH_OFFSET,
|
|
160
|
+
width: parseSvgNumAttr(outlineShape, 'width') + PATH_OFFSET * 2,
|
|
161
|
+
height: parseSvgNumAttr(outlineShape, 'height') + PATH_OFFSET * 2
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return getRoundRectPath(shape, parseSvgNumAttr(outlineShape, 'rx'));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (outlineShape.cx) {
|
|
168
|
+
const shape = {
|
|
169
|
+
x: element.x - outlineOffset,
|
|
170
|
+
y: element.y - outlineOffset,
|
|
171
|
+
width: parseSvgNumAttr(outlineShape, 'r') * 2,
|
|
172
|
+
height: parseSvgNumAttr(outlineShape, 'r') * 2,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return getCirclePath(shape);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getElementPath(element) {
|
|
180
|
+
return graphicsFactory.getShapePath(element);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
LabelLink.$inject = [
|
|
185
|
+
'eventBus',
|
|
186
|
+
'canvas',
|
|
187
|
+
'graphicsFactory',
|
|
188
|
+
'outline',
|
|
189
|
+
'selection'
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get numeric attribute from SVG element
|
|
194
|
+
* or 0 if not present.
|
|
195
|
+
*
|
|
196
|
+
* @param {SVGElement} node
|
|
197
|
+
* @param {string} attr
|
|
198
|
+
* @returns {number}
|
|
199
|
+
*/
|
|
200
|
+
function parseSvgNumAttr(node, attr) {
|
|
201
|
+
return parseFloat(svgAttr(node, attr) || 0);
|
|
202
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare namespace _default {
|
|
2
|
+
let __depends__: ({
|
|
3
|
+
selection: (string | typeof import("diagram-js/lib/features/selection/Selection").default)[];
|
|
4
|
+
selectionVisuals: (string | typeof import("diagram-js/lib/features/selection/SelectionVisuals").default)[];
|
|
5
|
+
selectionBehavior: (string | typeof import("diagram-js/lib/features/selection/SelectionBehavior").default)[];
|
|
6
|
+
} | {
|
|
7
|
+
outline: (string | typeof import("diagram-js/lib/features/outline/Outline").default)[];
|
|
8
|
+
multiSelectionOutline: (string | typeof import("diagram-js/lib/features/outline/MultiSelectionOutline").default)[];
|
|
9
|
+
})[];
|
|
10
|
+
let __init__: string[];
|
|
11
|
+
let labelLink: (string | typeof LabelLink)[];
|
|
12
|
+
}
|
|
13
|
+
export default _default;
|
|
14
|
+
import LabelLink from './LabelLink';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import SelectionModule from 'diagram-js/lib/features/selection';
|
|
2
|
+
import OutlineModule from 'diagram-js/lib/features/outline';
|
|
3
|
+
|
|
4
|
+
import LabelLink from './LabelLink';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
__depends__: [
|
|
8
|
+
SelectionModule,
|
|
9
|
+
OutlineModule
|
|
10
|
+
],
|
|
11
|
+
__init__: [
|
|
12
|
+
'labelLink'
|
|
13
|
+
],
|
|
14
|
+
labelLink: [ 'type', LabelLink ]
|
|
15
|
+
};
|
|
@@ -29,6 +29,9 @@ export default function CreateDataObjectBehavior(eventBus, bpmnFactory) {
|
|
|
29
29
|
// create a DataObject every time a DataObjectReference is created
|
|
30
30
|
var dataObject = bpmnFactory.create('bpmn:DataObject');
|
|
31
31
|
|
|
32
|
+
// Copy the isCollection property if needed.
|
|
33
|
+
dataObject.isCollection = shape.businessObject.dataObjectRef?.isCollection || false;
|
|
34
|
+
|
|
32
35
|
// set the reference to the DataObject
|
|
33
36
|
shape.businessObject.dataObjectRef = dataObject;
|
|
34
37
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
isAny
|
|
11
11
|
} from '../../util/ModelUtil';
|
|
12
12
|
|
|
13
|
-
import { isLabel } from '../../util/LabelUtil';
|
|
13
|
+
import { isLabel, isExternalLabel } from '../../util/LabelUtil';
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
DATA_OBJECT_REFERENCE_OUTLINE_PATH,
|
|
@@ -64,6 +64,20 @@ OutlineProvider.prototype.getOutline = function(element) {
|
|
|
64
64
|
|
|
65
65
|
var outline;
|
|
66
66
|
|
|
67
|
+
if (isExternalLabel(element)) {
|
|
68
|
+
outline = svgCreate('rect');
|
|
69
|
+
|
|
70
|
+
svgAttr(outline, assign({
|
|
71
|
+
x: -DEFAULT_OFFSET,
|
|
72
|
+
y: -DEFAULT_OFFSET,
|
|
73
|
+
rx: 4,
|
|
74
|
+
width: element.width + DEFAULT_OFFSET * 2,
|
|
75
|
+
height: element.height + DEFAULT_OFFSET * 2
|
|
76
|
+
}, OUTLINE_STYLE));
|
|
77
|
+
|
|
78
|
+
return outline;
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
if (isLabel(element)) {
|
|
68
82
|
return;
|
|
69
83
|
}
|
|
@@ -4,7 +4,7 @@ declare namespace _default {
|
|
|
4
4
|
multiSelectionOutline: (string | typeof import("diagram-js/lib/features/outline/MultiSelectionOutline").default)[];
|
|
5
5
|
}[];
|
|
6
6
|
let __init__: string[];
|
|
7
|
-
let outlineProvider: (string | typeof
|
|
7
|
+
let outlineProvider: (string | typeof OutlineProvider)[];
|
|
8
8
|
}
|
|
9
9
|
export default _default;
|
|
10
|
-
import
|
|
10
|
+
import OutlineProvider from './OutlineProvider';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import Outline from 'diagram-js/lib/features/outline';
|
|
2
|
+
import OutlineProvider from './OutlineProvider';
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
__depends__: [
|
|
6
|
-
|
|
6
|
+
Outline
|
|
7
7
|
],
|
|
8
8
|
__init__: [ 'outlineProvider' ],
|
|
9
|
-
outlineProvider: [ 'type',
|
|
9
|
+
outlineProvider: [ 'type', OutlineProvider ]
|
|
10
10
|
};
|
package/lib/util/LabelUtil.d.ts
CHANGED
|
@@ -69,6 +69,15 @@ export function getLabel(element: Element): string;
|
|
|
69
69
|
*/
|
|
70
70
|
export function setLabel(element: Element, text: string): Element;
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Returns true if the given element is an external label.
|
|
74
|
+
*
|
|
75
|
+
* @param element
|
|
76
|
+
*
|
|
77
|
+
* @return
|
|
78
|
+
*/
|
|
79
|
+
export function isExternalLabel(element: Element): boolean;
|
|
80
|
+
|
|
72
81
|
export { isLabel } from "diagram-js/lib/util/ModelUtil";
|
|
73
82
|
|
|
74
83
|
export namespace DEFAULT_LABEL_SIZE {
|
package/lib/util/LabelUtil.js
CHANGED
|
@@ -264,3 +264,14 @@ export function setLabel(element, text) {
|
|
|
264
264
|
|
|
265
265
|
return element;
|
|
266
266
|
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Returns true if the given element is an external label.
|
|
270
|
+
*
|
|
271
|
+
* @param {Element} element
|
|
272
|
+
*
|
|
273
|
+
* @return {boolean}
|
|
274
|
+
*/
|
|
275
|
+
export function isExternalLabel(element) {
|
|
276
|
+
return isLabel(element) && isLabelExternal(element.labelTarget);
|
|
277
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bpmn-js",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.9.0",
|
|
4
4
|
"description": "A bpmn 2.0 toolkit and web modeler",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@babel/core": "^7.25.8",
|
|
67
67
|
"@bpmn-io/a11y": "^0.1.0",
|
|
68
|
-
"@rollup/plugin-commonjs": "^
|
|
68
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
69
69
|
"@rollup/plugin-json": "^6.1.0",
|
|
70
70
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
71
71
|
"@rollup/plugin-replace": "^6.0.2",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assert if two SVG paths are approximately equal within a given tolerance.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} actual
|
|
5
|
+
* @param {string} expected
|
|
6
|
+
* @param {number} [tolerance=2]
|
|
7
|
+
*/
|
|
8
|
+
export function expectSvgPath(actual, expected, tolerance = 2) {
|
|
9
|
+
const result = compareSvgPaths(actual, expected, tolerance);
|
|
10
|
+
expect(result).to.be.true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns true if two SVG paths are approximately equal within a given tolerance.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} pathA
|
|
17
|
+
* @param {string} pathB
|
|
18
|
+
* @param {number} [tolerance=2]
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
export function compareSvgPaths(pathA, pathB, tolerance = 2) {
|
|
22
|
+
const actualNumbers = pathToNumbers(pathA);
|
|
23
|
+
const expectedNumbers = pathToNumbers(pathB);
|
|
24
|
+
|
|
25
|
+
if (actualNumbers.length !== expectedNumbers.length) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < actualNumbers.length; i++) {
|
|
30
|
+
if (Math.abs(actualNumbers[i] - expectedNumbers[i]) > tolerance) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get an array of numeric values from an SVG path string.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* `pathToNumber('M10,20L30,40')` => `[10, 20, 30, 40]`
|
|
43
|
+
*
|
|
44
|
+
* @param {string} path
|
|
45
|
+
* @returns {number[]}
|
|
46
|
+
*/
|
|
47
|
+
export function pathToNumbers(path) {
|
|
48
|
+
const normalized = path.toLowerCase();
|
|
49
|
+
const parts = normalized.split(/[a-z,]/g).filter(s => s !== '');
|
|
50
|
+
|
|
51
|
+
return parts.map(parseFloat);
|
|
52
|
+
}
|