Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/Modeler.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import InteractionEventsModule from './features/interaction-events';
import KeyboardModule from './features/keyboard';
import KeyboardMoveSelectionModule from 'diagram-js/lib/features/keyboard-move-selection';
import LabelEditingModule from './features/label-editing';
import LabelLink from './features/label-link';
import ModelingModule from './features/modeling';
import ModelingFeedbackModule from './features/modeling-feedback';
import MoveModule from 'diagram-js/lib/features/move';
Expand Down Expand Up @@ -181,6 +182,7 @@ Modeler.prototype._modelingModules = [
KeyboardModule,
KeyboardMoveSelectionModule,
LabelEditingModule,
LabelLink,
ModelingModule,
ModelingFeedbackModule,
MoveModule,
Expand Down
11 changes: 9 additions & 2 deletions lib/draw/BpmnRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
} from '../util/DiUtil';

import {
getLabel
getLabel,
isLabel
} from '../util/LabelUtil';

import {
Expand Down Expand Up @@ -70,7 +71,8 @@ var markerIds = new Ids();
var ELEMENT_LABEL_DISTANCE = 10,
INNER_OUTER_DIST = 3,
PARTICIPANT_STROKE_WIDTH = 1.5,
TASK_BORDER_RADIUS = 10;
TASK_BORDER_RADIUS = 10,
EXTERNAL_LABEL_BORDER_RADIUS = 4;

var DEFAULT_OPACITY = 0.95,
FULL_OPACITY = 1,
Expand Down Expand Up @@ -2379,6 +2381,11 @@ BpmnRenderer.prototype.drawConnection = function(parentGfx, connection, attrs =
* @return {string} path
*/
BpmnRenderer.prototype.getShapePath = function(shape) {

if (isLabel(shape)) {
return getRoundRectPath(shape, EXTERNAL_LABEL_BORDER_RADIUS);
}

if (is(shape, 'bpmn:Event')) {
return getCirclePath(shape);
}
Expand Down
202 changes: 202 additions & 0 deletions lib/features/label-link/LabelLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { queryAll as domQueryAll } from 'min-dom';

import {
append as svgAppend,
attr as svgAttr,
remove as svgRemove,
} from 'tiny-svg';

import { createLine, updateLine } from 'diagram-js/lib/util/RenderUtil';
import { getMid, getElementLineIntersection } from 'diagram-js/lib/layout/LayoutUtil';
import { getDistancePointPoint } from 'diagram-js/lib/features/bendpoints/GeometricUtil';
import { isLabel } from 'diagram-js/lib/util/ModelUtil';

import { isAny } from '../modeling/util/ModelingUtil';
import { getRoundRectPath, getCirclePath } from '../../draw/BpmnRenderUtil';

/**
* @typedef {import('diagram-js/lib/core/EventBus').default} EventBus
* @typedef {import('diagram-js/lib/core/Canvas').default} Canvas
* @typedef {import('diagram-js/lib/core/GraphicsFactory').default} GraphicsFactory
* @typedef {import('../outline/OutlineProvider').default} Outline
* @typedef {import('diagram-js/lib/features/selection').default} Selection
*
* @typedef {import('diagram-js/lib/model/Types').Element} Element
*/

const ALLOWED_ELEMENTS = [ 'bpmn:Event', 'bpmn:SequenceFlow', 'bpmn:Gateway' ];

const LINE_STYLE = {
class: 'bjs-label-link',
stroke: 'var(--element-selected-outline-secondary-stroke-color)',
strokeDasharray: '5, 5',
};

const DISTANCE_THRESHOLD = 15;
const PATH_OFFSET = 2;

/**
* Render a line between an external label and its target element,
* when either is selected.
*
* @param {EventBus} eventBus
* @param {Canvas} canvas
* @param {GraphicsFactory} graphicsFactory
* @param {Outline} outline
*/
export default function LabelLink(eventBus, canvas, graphicsFactory, outline, selection) {

const layer = canvas.getLayer('overlays');

eventBus.on([ 'selection.changed', 'shape.changed' ], function() {
cleanUp();
});

eventBus.on('selection.changed', function({ newSelection }) {

const allowedElements = newSelection.filter(element => isAny(element, ALLOWED_ELEMENTS));

if (allowedElements.length === 1) {
const element = allowedElements[0];
if (isLabel(element)) {
createLink(element, element.labelTarget, newSelection);
} else if (element.labels?.length) {
createLink(element.labels[0], element, newSelection);
}
}

// Only allowed when both label and its target are selected
if (allowedElements.length === 2) {
const label = allowedElements.find(isLabel);
const target = allowedElements.find(el => el.labels?.includes(label));
if (label && target) {
createLink(label, target, newSelection);
}
}
});

eventBus.on('shape.changed', function({ element }) {

if (!isAny(element, ALLOWED_ELEMENTS)) {
return;
}

if (isLabel(element)) {
createLink(element, element.labelTarget, selection.get());
} else if (element.labels?.length) {
createLink(element.labels[0], element, selection.get());
}
});

/**
* Render a line between an external label and its target.
*
* @param {Element} label
* @param {Element} target
* @param {Element[]} selection
*/
function createLink(label, target, selection = []) {

// Create an auxiliary line between label and target mid points
const line = createLine(
[ getMid(target), getMid(label) ],
LINE_STYLE
);
const linePath = line.getAttribute('d');

// Calculate the intersection point between line and label
const labelSelected = selection.includes(label);
const labelPath = labelSelected ? getElementOutlinePath(label) : getElementPath(label);
const labelInter = getElementLineIntersection(labelPath, linePath);

// Label on top of the target
if (!labelInter) {
return;
}

// Calculate the intersection point between line and label
// If the target is a sequence flow, there is no intersection,
// so we link to the middle of it.
const targetSelected = selection.includes(target);
const targetPath = targetSelected ? getElementOutlinePath(target) : getElementPath(target);
const targetInter = getElementLineIntersection(targetPath, linePath) || getMid(target);

// Do not draw a link if the points are too close
const distance = getDistancePointPoint(targetInter, labelInter);
if (distance < DISTANCE_THRESHOLD) {
return;
}

// Connect the actual closest points
updateLine(line, [ targetInter, labelInter ]);
svgAppend(layer, line);
}

/**
* Remove all existing label links.
*/
function cleanUp() {
domQueryAll(`.${LINE_STYLE.class}`, layer).forEach(svgRemove);
}

/**
* Get element's slightly expanded outline path.
*
* @param {Element} element
* @returns {string} svg path
*/
function getElementOutlinePath(element) {
const outlineShape = outline.getOutline(element);
const outlineOffset = outline.offset;

if (!outlineShape) {
return getElementPath(element);
}

if (outlineShape.x) {
const shape = {
x: element.x + parseSvgNumAttr(outlineShape, 'x') - PATH_OFFSET,
y: element.y + parseSvgNumAttr(outlineShape, 'y') - PATH_OFFSET,
width: parseSvgNumAttr(outlineShape, 'width') + PATH_OFFSET * 2,
height: parseSvgNumAttr(outlineShape, 'height') + PATH_OFFSET * 2
};

return getRoundRectPath(shape, parseSvgNumAttr(outlineShape, 'rx'));
}

if (outlineShape.cx) {
const shape = {
x: element.x - outlineOffset,
y: element.y - outlineOffset,
width: parseSvgNumAttr(outlineShape, 'r') * 2,
height: parseSvgNumAttr(outlineShape, 'r') * 2,
};

return getCirclePath(shape);
}
}

function getElementPath(element) {
return graphicsFactory.getShapePath(element);
}
}

LabelLink.$inject = [
'eventBus',
'canvas',
'graphicsFactory',
'outline',
'selection'
];

/**
* Get numeric attribute from SVG element
* or 0 if not present.
*
* @param {SVGElement} node
* @param {string} attr
* @returns {number}
*/
function parseSvgNumAttr(node, attr) {
return parseFloat(svgAttr(node, attr) || 0);
}
15 changes: 15 additions & 0 deletions lib/features/label-link/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import SelectionModule from 'diagram-js/lib/features/selection';
import OutlineModule from 'diagram-js/lib/features/outline';

import LabelLink from './LabelLink';

export default {
__depends__: [
SelectionModule,
OutlineModule
],
__init__: [
'labelLink'
],
labelLink: [ 'type', LabelLink ]
};
16 changes: 15 additions & 1 deletion lib/features/outline/OutlineProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
isAny
} from '../../util/ModelUtil';

import { isLabel } from '../../util/LabelUtil';
import { isLabel, isExternalLabel } from '../../util/LabelUtil';

import {
DATA_OBJECT_REFERENCE_OUTLINE_PATH,
Expand Down Expand Up @@ -64,6 +64,20 @@ OutlineProvider.prototype.getOutline = function(element) {

var outline;

if (isExternalLabel(element)) {
outline = svgCreate('rect');

svgAttr(outline, assign({
x: -DEFAULT_OFFSET,
y: -DEFAULT_OFFSET,
rx: 4,
width: element.width + DEFAULT_OFFSET * 2,
height: element.height + DEFAULT_OFFSET * 2
}, OUTLINE_STYLE));

return outline;
}

if (isLabel(element)) {
return;
}
Expand Down
8 changes: 4 additions & 4 deletions lib/features/outline/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Ouline from 'diagram-js/lib/features/outline';
import OulineProvider from './OutlineProvider';
import Outline from 'diagram-js/lib/features/outline';
import OutlineProvider from './OutlineProvider';

export default {
__depends__: [
Ouline
Outline
],
__init__: [ 'outlineProvider' ],
outlineProvider: [ 'type', OulineProvider ]
outlineProvider: [ 'type', OutlineProvider ]
};
11 changes: 11 additions & 0 deletions lib/util/LabelUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,14 @@ export function setLabel(element, text) {

return element;
}

/**
* Returns true if the given element is an external label.
*
* @param {Element} element
*
* @return {boolean}
*/
export function isExternalLabel(element) {
return isLabel(element) && isLabelExternal(element.labelTarget);
}
1 change: 1 addition & 0 deletions test/spec/ModelerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ describe('Modeler', function() {
expect(modeler.get('keyboard')).to.exist;
expect(modeler.get('keyboardMoveSelection')).to.exist;
expect(modeler.get('labelEditingProvider')).to.exist;
expect(modeler.get('labelLink')).to.exist;
expect(modeler.get('modeling')).to.exist;
expect(modeler.get('move')).to.exist;
expect(modeler.get('paletteProvider')).to.exist;
Expand Down
Loading
Loading