Skip to content

Commit

Permalink
WIP feat: add auto-connect feature
Browse files Browse the repository at this point in the history
  • Loading branch information
philippfromme committed Aug 7, 2023
1 parent 4a68a2a commit a164d69
Show file tree
Hide file tree
Showing 6 changed files with 443 additions and 64 deletions.
132 changes: 132 additions & 0 deletions lib/features/auto-connect/AutoConnect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
asTRBL,
getMid
} from 'diagram-js/lib/layout/LayoutUtil';

import { getDistancePointPoint } from 'diagram-js/lib/features/bendpoints/GeometricUtil';

import {
is,
isAny
} from '../../util/ModelUtil';

const AUTO_CONNECT_PADDING = 200;

const IGNORED_SOURCES = [
'bpmn:Participant'
];

const IGNORED_TARGETS = [
'bpmn:Participant'
];

export default function AutoConnect(elementDetection, modeling, rules) {
this._elementDetection = elementDetection;
this._modeling = modeling;
this._rules = rules;
}

AutoConnect.prototype.canConnect = function(element) {
if (isAny(element, IGNORED_SOURCES)) {
return false;
}

const elementTrbl = asTRBL(element);

const rects = [
{
top: elementTrbl.top - AUTO_CONNECT_PADDING,
right: elementTrbl.right + AUTO_CONNECT_PADDING,
bottom: elementTrbl.bottom + AUTO_CONNECT_PADDING,
left: elementTrbl.right + 10
},
{
top: elementTrbl.top - AUTO_CONNECT_PADDING,
right: elementTrbl.right + AUTO_CONNECT_PADDING,
bottom: elementTrbl.bottom + AUTO_CONNECT_PADDING,
left: elementTrbl.left - 10
}
];

return rects.reduce((target, rect) => {
return target || this._findTarget(element, rect);
}, false);
};

AutoConnect.prototype._findTarget = function(element, rect) {
const possibleTargets = this._elementDetection.detectAt(rect).filter(possibleTarget => {
return possibleTarget !== element
&& !isConnection(possibleTarget)
&& !isLabel(possibleTarget)
&& !isAny(possibleTarget, IGNORED_TARGETS);
});

console.log('possible targets', possibleTargets);

// find closest element to connect to
return possibleTargets.reduce((target, possibleTarget) => {
if (isConnected(element, possibleTarget)
|| hasMaxOutgoingConnections(element)
|| hasMaxIncomingConnections(possibleTarget)) {
return target;
}

const canConnect = this.getConnectionType(element, possibleTarget);

if (!canConnect) {
return target;
}

const distance = getDistancePointPoint(getMid(element), getMid(possibleTarget));

if (!target || distance < getDistancePointPoint(getMid(element), getMid(target))) {
return possibleTarget;
}

return target;
}, null);
};

AutoConnect.prototype.getConnectionType = function(source, target) {
return this._rules.allowed('connection.create', {
source,
target
});
};

AutoConnect.prototype.connect = function(element) {
const target = this.canConnect(element);

if (!target) {
return;
}

this._modeling.connect(element, target);
};

AutoConnect.$inject = [
'elementDetection',
'modeling',
'rules'
];

function isConnection(element) {
return element.waypoints;
}

function isLabel(element) {
return element.labelTarget;
}

function isConnected(source, target) {
return source.incoming.some(connection => connection.source === target)
|| source.outgoing.some(connection => connection.target === target);
}

function hasMaxIncomingConnections(element) {
return !is(element, 'bpmn:Gateway') && element.incoming.length;
}

function hasMaxOutgoingConnections(element) {
return !is(element, 'bpmn:Gateway') && element.outgoing.length;
}
189 changes: 189 additions & 0 deletions lib/features/auto-connect/ElementDetection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import {
isNumber,
isUndefined,
some
} from 'min-dash';

import {
append as svgAppend,
attr as svgAttr,
clear as svgClear,
create as svgCreate
} from 'tiny-svg';

const THRESHOLD = 2;


export default class ElementDetection {
constructor(canvas, elementRegistry) {
this._canvas = canvas;
this._elementRegistry = elementRegistry;
}

/**
* Detect elements intersecting point or rect.
* Uses element registry.
*
* @param {Object} pointOrRect
*
* @return {Array}
*/
detectAt(pointOrRect) {
if (isPoint(pointOrRect)) {
pointOrRect = {
...pointOrRect,
width: 0,
height: 0
};
}

if (!isTRBL(pointOrRect)) {
pointOrRect = toTRBL(pointOrRect);
}

// drawRect(pointOrRect, this._canvas);

const elements = this._elementRegistry.filter(elementIntersects(pointOrRect));

return elements;
}
}

ElementDetection.$inject = [
'canvas',
'elementRegistry'
];

// helpers //////////

function elementIntersects(rect) {
return function(element) {
if (isConnection(element)) {
return connectionIntersects(element, rect);
}

return element.x <= rect.right &&
element.x + element.width >= rect.left &&
element.y <= rect.bottom &&
element.y + element.height >= rect.top;
}

Check failure on line 69 in lib/features/auto-connect/ElementDetection.js

View workflow job for this annotation

GitHub Actions / Build (ubuntu-20.04)

Missing semicolon
}

function isConnection(element) {
return !!element.waypoints;
}

function connectionIntersects(connection, rect) {
const segments = getSegments(connection);

return some(segments, segmentIntersects(rect));
}

function getSegments({ waypoints }) {
return waypoints.reduce((segments, waypoint, index) => {
if (isLastIndex(index, waypoints)) {
return segments;
}

return [
...segments,
{
start: waypoint,
end: waypoints[ index + 1]
}
];
}, []);
}

function segmentIntersects(rect) {
return function(segment) {
let { start, end } = segment;

if (isHorizontal(segment)) {
if (start.x > end.x) {
start = segment.end;
end = segment.start;
}

return start.y >= rect.top &&
start.y <= rect.bottom &&
start.x <= rect.right &&
end.x >= rect.left;
} else {
if (start.y > end.y) {
start = segment.end;
end = segment.start;
}

return start.x >= rect.left &&
start.x <= rect.right &&
start.y <= rect.bottom &&
end.y >= rect.top;
}
};
}

function isHorizontal({ start, end }) {
return Math.abs(start.y - end.y) <= THRESHOLD;
}

function isLastIndex(index, array) {
return index + 1 === array.length;
}

function isPoint(pointOrRect) {
return isNumber(pointOrRect.x) &&
isNumber(pointOrRect.y) &&
isUndefined(pointOrRect.width) &&
isUndefined(pointOrRect.height);
}

function isTRBL(pointOrRect) {
return isNumber(pointOrRect.top) &&
isNumber(pointOrRect.right) &&
isNumber(pointOrRect.bottom) &&
isNumber(pointOrRect.left);
}

function toTRBL(rect) {
return {
top: rect.y,
right: rect.x + rect.width,
bottom: rect.y + rect.height,
left: rect.x
};
}

let counter = 0;

const colors = [ 'red', /*'green', 'blue'*/ ];

Check failure on line 159 in lib/features/auto-connect/ElementDetection.js

View workflow job for this annotation

GitHub Actions / Build (ubuntu-20.04)

Expected space or tab after '/*' in comment

function drawRect(rect, canvas) {

Check failure on line 161 in lib/features/auto-connect/ElementDetection.js

View workflow job for this annotation

GitHub Actions / Build (ubuntu-20.04)

'drawRect' is defined but never used. Allowed unused vars must match /^_/u
const layer = canvas.getLayer('element-detection');

svgClear(layer);

const gfx = svgCreate('rect');

gfx.style.pointerEvents = 'none';

svgAttr(gfx, {
fill: colors[ counter % colors.length ],
fillOpacity: 0.25,
stroke: colors[ counter % colors.length ],
strokeOpacity: 0.25,
strokeWidth: 2,
x: rect.left,
y: rect.top,
width: Math.max(rect.right - rect.left, 2),
height: Math.max(rect.bottom - rect.top, 2)
});

svgAppend(layer, gfx);

setTimeout(() => {
// svgClear(layer);

Check failure on line 185 in lib/features/auto-connect/ElementDetection.js

View workflow job for this annotation

GitHub Actions / Build (ubuntu-20.04)

Expected line before comment
}, 1000);

counter++;
}
8 changes: 8 additions & 0 deletions lib/features/auto-connect/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import AutoConnect from './AutoConnect';
import ElementDetection from './ElementDetection';

export default {
_init_: [ 'autoConnect' ],
autoConnect: [ 'type', AutoConnect ],
elementDetection: [ 'type', ElementDetection ]
};
Loading

0 comments on commit a164d69

Please sign in to comment.