diagram-js
Version:
A toolbox for displaying and modifying diagrams on the web
448 lines (352 loc) • 12.2 kB
JavaScript
import {
pointsAligned,
pointsOnLine
} from '../../util/Geometry';
import {
addSegmentDragger,
getConnectionIntersection
} from './BendpointUtil';
import {
getMid,
getOrientation
} from '../../layout/LayoutUtil';
var MARKER_CONNECT_HOVER = 'connect-hover',
MARKER_CONNECT_UPDATING = 'djs-updating';
import {
classes as svgClasses,
remove as svgRemove
} from 'tiny-svg';
import {
translate
} from '../../util/SvgTransformUtil';
/**
* @typedef {import('../../model/Types').Shape} Shape
*
* @typedef {import('../../util/Types').Axis} Axis
* @typedef {import('../../util/Types').Point} Point
*
* @typedef {import('didi').Injector} Injector
*
* @typedef {import('../../core/Canvas').default} Canvas
* @typedef {import('../dragging/Dragging').default} Dragging
* @typedef {import('../../core/EventBus').default} EventBus
* @typedef {import('../../core/GraphicsFactory').default} GraphicsFactory
* @typedef {import('../modeling/Modeling').default} Modeling
*/
function axisAdd(point, axis, delta) {
return axisSet(point, axis, point[axis] + delta);
}
function axisSet(point, axis, value) {
return {
x: (axis === 'x' ? value : point.x),
y: (axis === 'y' ? value : point.y)
};
}
function axisFenced(position, segmentStart, segmentEnd, axis) {
var maxValue = Math.max(segmentStart[axis], segmentEnd[axis]),
minValue = Math.min(segmentStart[axis], segmentEnd[axis]);
var padding = 20;
var fencedValue = Math.min(Math.max(minValue + padding, position[axis]), maxValue - padding);
return axisSet(segmentStart, axis, fencedValue);
}
function flipAxis(axis) {
return axis === 'x' ? 'y' : 'x';
}
/**
* Get the docking point on the given element.
*
* Compute a reasonable docking, if non exists.
*
* @param {Point} point
* @param {Shape} referenceElement
* @param {Axis} moveAxis
*
* @return {Point}
*/
function getDocking(point, referenceElement, moveAxis) {
var referenceMid,
inverseAxis;
if (point.original) {
return point.original;
} else {
referenceMid = getMid(referenceElement);
inverseAxis = flipAxis(moveAxis);
return axisSet(point, inverseAxis, referenceMid[inverseAxis]);
}
}
/**
* A component that implements moving of bendpoints.
*
* @param {Injector} injector
* @param {EventBus} eventBus
* @param {Canvas} canvas
* @param {Canvas} dragging
* @param {GraphicsFactory} graphicsFactory
* @param {Modeling} modeling
*/
export default function ConnectionSegmentMove(
injector, eventBus, canvas,
dragging, graphicsFactory, modeling) {
// optional connection docking integration
var connectionDocking = injector.get('connectionDocking', false);
// API
this.start = function(event, connection, idx) {
var context,
gfx = canvas.getGraphics(connection),
segmentStartIndex = idx - 1,
segmentEndIndex = idx,
waypoints = connection.waypoints,
segmentStart = waypoints[segmentStartIndex],
segmentEnd = waypoints[segmentEndIndex],
intersection = getConnectionIntersection(canvas, waypoints, event),
direction, axis, dragPosition;
direction = pointsAligned(segmentStart, segmentEnd);
// do not move diagonal connection
if (!direction) {
return;
}
// the axis where we are going to move things
axis = direction === 'v' ? 'x' : 'y';
if (segmentStartIndex === 0) {
segmentStart = getDocking(segmentStart, connection.source, axis);
}
if (segmentEndIndex === waypoints.length - 1) {
segmentEnd = getDocking(segmentEnd, connection.target, axis);
}
if (intersection) {
dragPosition = intersection.point;
} else {
// set to segment center as default
dragPosition = {
x: (segmentStart.x + segmentEnd.x) / 2,
y: (segmentStart.y + segmentEnd.y) / 2
};
}
context = {
connection: connection,
segmentStartIndex: segmentStartIndex,
segmentEndIndex: segmentEndIndex,
segmentStart: segmentStart,
segmentEnd: segmentEnd,
axis: axis,
dragPosition: dragPosition
};
dragging.init(event, dragPosition, 'connectionSegment.move', {
cursor: axis === 'x' ? 'resize-ew' : 'resize-ns',
data: {
connection: connection,
connectionGfx: gfx,
context: context
}
});
};
/**
* Crop connection if connection cropping is provided.
*
* @param {Connection} connection
* @param {Point[]} newWaypoints
*
* @return {Point[]} cropped connection waypoints
*/
function cropConnection(connection, newWaypoints) {
// crop connection, if docking service is provided only
if (!connectionDocking) {
return newWaypoints;
}
var oldWaypoints = connection.waypoints,
croppedWaypoints;
// temporary set new waypoints
connection.waypoints = newWaypoints;
croppedWaypoints = connectionDocking.getCroppedWaypoints(connection);
// restore old waypoints
connection.waypoints = oldWaypoints;
return croppedWaypoints;
}
// DRAGGING IMPLEMENTATION
function redrawConnection(data) {
graphicsFactory.update('connection', data.connection, data.connectionGfx);
}
function updateDragger(context, segmentOffset, event) {
var newWaypoints = context.newWaypoints,
segmentStartIndex = context.segmentStartIndex + segmentOffset,
segmentStart = newWaypoints[segmentStartIndex],
segmentEndIndex = context.segmentEndIndex + segmentOffset,
segmentEnd = newWaypoints[segmentEndIndex],
axis = flipAxis(context.axis);
// make sure the dragger does not move
// outside the connection
var draggerPosition = axisFenced(event, segmentStart, segmentEnd, axis);
// update dragger
translate(context.draggerGfx, draggerPosition.x, draggerPosition.y);
}
/**
* Filter waypoints for redundant ones (i.e. on the same axis).
* Returns the filtered waypoints and the offset related to the segment move.
*
* @param {Point[]} waypoints
* @param {Integer} segmentStartIndex of moved segment start
*
* @return {Object} { filteredWaypoints, segmentOffset }
*/
function filterRedundantWaypoints(waypoints, segmentStartIndex) {
var segmentOffset = 0;
var filteredWaypoints = waypoints.filter(function(r, idx) {
if (pointsOnLine(waypoints[idx - 1], waypoints[idx + 1], r)) {
// remove point and increment offset
segmentOffset = idx <= segmentStartIndex ? segmentOffset - 1 : segmentOffset;
return false;
}
// dont remove point
return true;
});
return {
waypoints: filteredWaypoints,
segmentOffset: segmentOffset
};
}
eventBus.on('connectionSegment.move.start', function(event) {
var context = event.context,
connection = event.connection,
layer = canvas.getLayer('overlays');
context.originalWaypoints = connection.waypoints.slice();
// add dragger gfx
context.draggerGfx = addSegmentDragger(layer, context.segmentStart, context.segmentEnd);
svgClasses(context.draggerGfx).add('djs-dragging');
canvas.addMarker(connection, MARKER_CONNECT_UPDATING);
});
eventBus.on('connectionSegment.move.move', function(event) {
var context = event.context,
connection = context.connection,
segmentStartIndex = context.segmentStartIndex,
segmentEndIndex = context.segmentEndIndex,
segmentStart = context.segmentStart,
segmentEnd = context.segmentEnd,
axis = context.axis;
var newWaypoints = context.originalWaypoints.slice(),
newSegmentStart = axisAdd(segmentStart, axis, event['d' + axis]),
newSegmentEnd = axisAdd(segmentEnd, axis, event['d' + axis]);
// original waypoint count and added / removed
// from start waypoint delta. We use the later
// to retrieve the updated segmentStartIndex / segmentEndIndex
var waypointCount = newWaypoints.length,
segmentOffset = 0;
// move segment start / end by axis delta
newWaypoints[segmentStartIndex] = newSegmentStart;
newWaypoints[segmentEndIndex] = newSegmentEnd;
var sourceToSegmentOrientation,
targetToSegmentOrientation;
// handle first segment
if (segmentStartIndex < 2) {
sourceToSegmentOrientation = getOrientation(connection.source, newSegmentStart);
// first bendpoint, remove first segment if intersecting
if (segmentStartIndex === 1) {
if (sourceToSegmentOrientation === 'intersect') {
newWaypoints.shift();
newWaypoints[0] = newSegmentStart;
segmentOffset--;
}
}
// docking point, add segment if not intersecting anymore
else {
if (sourceToSegmentOrientation !== 'intersect') {
newWaypoints.unshift(segmentStart);
segmentOffset++;
}
}
}
// handle last segment
if (segmentEndIndex > waypointCount - 3) {
targetToSegmentOrientation = getOrientation(connection.target, newSegmentEnd);
// last bendpoint, remove last segment if intersecting
if (segmentEndIndex === waypointCount - 2) {
if (targetToSegmentOrientation === 'intersect') {
newWaypoints.pop();
newWaypoints[newWaypoints.length - 1] = newSegmentEnd;
}
}
// last bendpoint, remove last segment if intersecting
else {
if (targetToSegmentOrientation !== 'intersect') {
newWaypoints.push(segmentEnd);
}
}
}
// update connection waypoints
context.newWaypoints = connection.waypoints = cropConnection(connection, newWaypoints);
// update dragger position
updateDragger(context, segmentOffset, event);
// save segmentOffset in context
context.newSegmentStartIndex = segmentStartIndex + segmentOffset;
// redraw connection
redrawConnection(event);
});
eventBus.on('connectionSegment.move.hover', function(event) {
event.context.hover = event.hover;
canvas.addMarker(event.hover, MARKER_CONNECT_HOVER);
});
eventBus.on([
'connectionSegment.move.out',
'connectionSegment.move.cleanup'
], function(event) {
// remove connect marker
// if it was added
var hover = event.context.hover;
if (hover) {
canvas.removeMarker(hover, MARKER_CONNECT_HOVER);
}
});
eventBus.on('connectionSegment.move.cleanup', function(event) {
var context = event.context,
connection = context.connection;
// remove dragger gfx
if (context.draggerGfx) {
svgRemove(context.draggerGfx);
}
canvas.removeMarker(connection, MARKER_CONNECT_UPDATING);
});
eventBus.on([
'connectionSegment.move.cancel',
'connectionSegment.move.end'
], function(event) {
var context = event.context,
connection = context.connection;
connection.waypoints = context.originalWaypoints;
redrawConnection(event);
});
eventBus.on('connectionSegment.move.end', function(event) {
var context = event.context,
connection = context.connection,
newWaypoints = context.newWaypoints,
newSegmentStartIndex = context.newSegmentStartIndex;
// ensure we have actual pixel values bendpoint
// coordinates (important when zoom level was > 1 during move)
newWaypoints = newWaypoints.map(function(p) {
return {
original: p.original,
x: Math.round(p.x),
y: Math.round(p.y)
};
});
// apply filter redunant waypoints
var filtered = filterRedundantWaypoints(newWaypoints, newSegmentStartIndex);
// get filtered waypoints
var filteredWaypoints = filtered.waypoints,
croppedWaypoints = cropConnection(connection, filteredWaypoints),
segmentOffset = filtered.segmentOffset;
var hints = {
segmentMove: {
segmentStartIndex: context.segmentStartIndex,
newSegmentStartIndex: newSegmentStartIndex + segmentOffset
}
};
modeling.updateWaypoints(connection, croppedWaypoints, hints);
});
}
ConnectionSegmentMove.$inject = [
'injector',
'eventBus',
'canvas',
'dragging',
'graphicsFactory',
'modeling'
];