123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743 |
- import {
- isArray,
- find,
- without,
- assign
- } from 'min-dash';
- import {
- getOrientation,
- getMid
- } from './LayoutUtil';
- import {
- pointInRect,
- pointDistance,
- pointsAligned,
- pointsOnLine
- } from '../util/Geometry';
- var MIN_SEGMENT_LENGTH = 20,
- POINT_ORIENTATION_PADDING = 5;
- var round = Math.round;
- var INTERSECTION_THRESHOLD = 20,
- ORIENTATION_THRESHOLD = {
- 'h:h': 20,
- 'v:v': 20,
- 'h:v': -10,
- 'v:h': -10
- };
- function needsTurn(orientation, startDirection) {
- return !{
- t: /top/,
- r: /right/,
- b: /bottom/,
- l: /left/,
- h: /./,
- v: /./
- }[startDirection].test(orientation);
- }
- function canLayoutStraight(direction, targetOrientation) {
- return {
- t: /top/,
- r: /right/,
- b: /bottom/,
- l: /left/,
- h: /left|right/,
- v: /top|bottom/
- }[direction].test(targetOrientation);
- }
- function getSegmentBendpoints(a, b, directions) {
- var orientation = getOrientation(b, a, POINT_ORIENTATION_PADDING);
- var startDirection = directions.split(':')[0];
- var xmid = round((b.x - a.x) / 2 + a.x),
- ymid = round((b.y - a.y) / 2 + a.y);
- var segmentEnd, segmentDirections;
- var layoutStraight = canLayoutStraight(startDirection, orientation),
- layoutHorizontal = /h|r|l/.test(startDirection),
- layoutTurn = false;
- var turnNextDirections = false;
- if (layoutStraight) {
- segmentEnd = layoutHorizontal ? { x: xmid, y: a.y } : { x: a.x, y: ymid };
- segmentDirections = layoutHorizontal ? 'h:h' : 'v:v';
- } else {
- layoutTurn = needsTurn(orientation, startDirection);
- segmentDirections = layoutHorizontal ? 'h:v' : 'v:h';
- if (layoutTurn) {
- if (layoutHorizontal) {
- turnNextDirections = ymid === a.y;
- segmentEnd = {
- x: a.x + MIN_SEGMENT_LENGTH * (/l/.test(startDirection) ? -1 : 1),
- y: turnNextDirections ? ymid + MIN_SEGMENT_LENGTH : ymid
- };
- } else {
- turnNextDirections = xmid === a.x;
- segmentEnd = {
- x: turnNextDirections ? xmid + MIN_SEGMENT_LENGTH : xmid,
- y: a.y + MIN_SEGMENT_LENGTH * (/t/.test(startDirection) ? -1 : 1)
- };
- }
- } else {
- segmentEnd = {
- x: xmid,
- y: ymid
- };
- }
- }
- return {
- waypoints: getBendpoints(a, segmentEnd, segmentDirections).concat(segmentEnd),
- directions: segmentDirections,
- turnNextDirections: turnNextDirections
- };
- }
- function getStartSegment(a, b, directions) {
- return getSegmentBendpoints(a, b, directions);
- }
- function getEndSegment(a, b, directions) {
- var invertedSegment = getSegmentBendpoints(b, a, invertDirections(directions));
- return {
- waypoints: invertedSegment.waypoints.slice().reverse(),
- directions: invertDirections(invertedSegment.directions),
- turnNextDirections: invertedSegment.turnNextDirections
- };
- }
- function getMidSegment(startSegment, endSegment) {
- var startDirection = startSegment.directions.split(':')[1],
- endDirection = endSegment.directions.split(':')[0];
- if (startSegment.turnNextDirections) {
- startDirection = startDirection == 'h' ? 'v' : 'h';
- }
- if (endSegment.turnNextDirections) {
- endDirection = endDirection == 'h' ? 'v' : 'h';
- }
- var directions = startDirection + ':' + endDirection;
- var bendpoints = getBendpoints(
- startSegment.waypoints[startSegment.waypoints.length - 1],
- endSegment.waypoints[0],
- directions
- );
- return {
- waypoints: bendpoints,
- directions: directions
- };
- }
- function invertDirections(directions) {
- return directions.split(':').reverse().join(':');
- }
- /**
- * Handle simple layouts with maximum two bendpoints.
- */
- function getSimpleBendpoints(a, b, directions) {
- var xmid = round((b.x - a.x) / 2 + a.x),
- ymid = round((b.y - a.y) / 2 + a.y);
- // one point, right or left from a
- if (directions === 'h:v') {
- return [ { x: b.x, y: a.y } ];
- }
- // one point, above or below a
- if (directions === 'v:h') {
- return [ { x: a.x, y: b.y } ];
- }
- // vertical segment between a and b
- if (directions === 'h:h') {
- return [
- { x: xmid, y: a.y },
- { x: xmid, y: b.y }
- ];
- }
- // horizontal segment between a and b
- if (directions === 'v:v') {
- return [
- { x: a.x, y: ymid },
- { x: b.x, y: ymid }
- ];
- }
- throw new Error('invalid directions: can only handle varians of [hv]:[hv]');
- }
- /**
- * Returns the mid points for a manhattan connection between two points.
- *
- * @example h:h (horizontal:horizontal)
- *
- * [a]----[x]
- * |
- * [x]----[b]
- *
- * @example h:v (horizontal:vertical)
- *
- * [a]----[x]
- * |
- * [b]
- *
- * @example h:r (horizontal:right)
- *
- * [a]----[x]
- * |
- * [b]-[x]
- *
- * @param {Point} a
- * @param {Point} b
- * @param {String} directions
- *
- * @return {Array<Point>}
- */
- function getBendpoints(a, b, directions) {
- directions = directions || 'h:h';
- if (!isValidDirections(directions)) {
- throw new Error(
- 'unknown directions: <' + directions + '>: ' +
- 'must be specified as <start>:<end> ' +
- 'with start/end in { h,v,t,r,b,l }'
- );
- }
- // compute explicit directions, involving trbl dockings
- // using a three segmented layouting algorithm
- if (isExplicitDirections(directions)) {
- var startSegment = getStartSegment(a, b, directions),
- endSegment = getEndSegment(a, b, directions),
- midSegment = getMidSegment(startSegment, endSegment);
- return [].concat(
- startSegment.waypoints,
- midSegment.waypoints,
- endSegment.waypoints
- );
- }
- // handle simple [hv]:[hv] cases that can be easily computed
- return getSimpleBendpoints(a, b, directions);
- }
- /**
- * Create a connection between the two points according
- * to the manhattan layout (only horizontal and vertical) edges.
- *
- * @param {Point} a
- * @param {Point} b
- *
- * @param {String} [directions='h:h'] specifies manhattan directions for each point as {adirection}:{bdirection}.
- A directionfor a point is either `h` (horizontal) or `v` (vertical)
- *
- * @return {Array<Point>}
- */
- export function connectPoints(a, b, directions) {
- var points = getBendpoints(a, b, directions);
- points.unshift(a);
- points.push(b);
- return withoutRedundantPoints(points);
- }
- /**
- * Connect two rectangles using a manhattan layouted connection.
- *
- * @param {Bounds} source source rectangle
- * @param {Bounds} target target rectangle
- * @param {Point} [start] source docking
- * @param {Point} [end] target docking
- *
- * @param {Object} [hints]
- * @param {String} [hints.preserveDocking=source] preserve docking on selected side
- * @param {Array<String>} [hints.preferredLayouts]
- * @param {Point|Boolean} [hints.connectionStart] whether the start changed
- * @param {Point|Boolean} [hints.connectionEnd] whether the end changed
- *
- * @return {Array<Point>} connection points
- */
- export function connectRectangles(source, target, start, end, hints) {
- var preferredLayouts = hints && hints.preferredLayouts || [];
- var preferredLayout = without(preferredLayouts, 'straight')[0] || 'h:h';
- var threshold = ORIENTATION_THRESHOLD[preferredLayout] || 0;
- var orientation = getOrientation(source, target, threshold);
- var directions = getDirections(orientation, preferredLayout);
- start = start || getMid(source);
- end = end || getMid(target);
- var directionSplit = directions.split(':');
- // compute actual docking points for start / end
- // this ensures we properly layout only parts of the
- // connection that lies in between the two rectangles
- var startDocking = getDockingPoint(start, source, directionSplit[0], invertOrientation(orientation)),
- endDocking = getDockingPoint(end, target, directionSplit[1], orientation);
- return connectPoints(startDocking, endDocking, directions);
- }
- /**
- * Repair the connection between two rectangles, of which one has been updated.
- *
- * @param {Bounds} source
- * @param {Bounds} target
- * @param {Point} [start]
- * @param {Point} [end]
- * @param {Array<Point>} [waypoints]
- * @param {Object} [hints]
- * @param {Array<String>} [hints.preferredLayouts] list of preferred layouts
- * @param {Boolean} [hints.connectionStart]
- * @param {Boolean} [hints.connectionEnd]
- *
- * @return {Array<Point>} repaired waypoints
- */
- export function repairConnection(source, target, start, end, waypoints, hints) {
- if (isArray(start)) {
- waypoints = start;
- hints = end;
- start = getMid(source);
- end = getMid(target);
- }
- hints = assign({ preferredLayouts: [] }, hints);
- waypoints = waypoints || [];
- var preferredLayouts = hints.preferredLayouts,
- preferStraight = preferredLayouts.indexOf('straight') !== -1,
- repairedWaypoints;
- // just layout non-existing or simple connections
- // attempt to render straight lines, if required
- // attempt to layout a straight line
- repairedWaypoints = preferStraight && tryLayoutStraight(source, target, start, end, hints);
- if (repairedWaypoints) {
- return repairedWaypoints;
- }
- // try to layout from end
- repairedWaypoints = hints.connectionEnd && tryRepairConnectionEnd(target, source, end, waypoints);
- if (repairedWaypoints) {
- return repairedWaypoints;
- }
- // try to layout from start
- repairedWaypoints = hints.connectionStart && tryRepairConnectionStart(source, target, start, waypoints);
- if (repairedWaypoints) {
- return repairedWaypoints;
- }
- // or whether nothing seems to have changed
- if (!hints.connectionStart && !hints.connectionEnd && waypoints && waypoints.length) {
- return waypoints;
- }
- // simply reconnect if nothing else worked
- return connectRectangles(source, target, start, end, hints);
- }
- function inRange(a, start, end) {
- return a >= start && a <= end;
- }
- function isInRange(axis, a, b) {
- var size = {
- x: 'width',
- y: 'height'
- };
- return inRange(a[axis], b[axis], b[axis] + b[size[axis]]);
- }
- /**
- * Layout a straight connection
- *
- * @param {Bounds} source
- * @param {Bounds} target
- * @param {Point} start
- * @param {Point} end
- * @param {Object} [hints]
- *
- * @return {Array<Point>|null} waypoints if straight layout worked
- */
- export function tryLayoutStraight(source, target, start, end, hints) {
- var axis = {},
- primaryAxis,
- orientation;
- orientation = getOrientation(source, target);
- // only layout a straight connection if shapes are
- // horizontally or vertically aligned
- if (!/^(top|bottom|left|right)$/.test(orientation)) {
- return null;
- }
- if (/top|bottom/.test(orientation)) {
- primaryAxis = 'x';
- }
- if (/left|right/.test(orientation)) {
- primaryAxis = 'y';
- }
- if (hints.preserveDocking === 'target') {
- if (!isInRange(primaryAxis, end, source)) {
- return null;
- }
- axis[primaryAxis] = end[primaryAxis];
- return [
- {
- x: axis.x !== undefined ? axis.x : start.x,
- y: axis.y !== undefined ? axis.y : start.y,
- original: {
- x: axis.x !== undefined ? axis.x : start.x,
- y: axis.y !== undefined ? axis.y : start.y
- }
- },
- {
- x: end.x,
- y: end.y
- }
- ];
- } else {
- if (!isInRange(primaryAxis, start, target)) {
- return null;
- }
- axis[primaryAxis] = start[primaryAxis];
- return [
- {
- x: start.x,
- y: start.y
- },
- {
- x: axis.x !== undefined ? axis.x : end.x,
- y: axis.y !== undefined ? axis.y : end.y,
- original: {
- x: axis.x !== undefined ? axis.x : end.x,
- y: axis.y !== undefined ? axis.y : end.y
- }
- }
- ];
- }
- }
- /**
- * Repair a connection from start.
- *
- * @param {Bounds} moved
- * @param {Bounds} other
- * @param {Point} newDocking
- * @param {Array<Point>} points originalPoints from moved to other
- *
- * @return {Array<Point>|null} the repaired points between the two rectangles
- */
- function tryRepairConnectionStart(moved, other, newDocking, points) {
- return _tryRepairConnectionSide(moved, other, newDocking, points);
- }
- /**
- * Repair a connection from end.
- *
- * @param {Bounds} moved
- * @param {Bounds} other
- * @param {Point} newDocking
- * @param {Array<Point>} points originalPoints from moved to other
- *
- * @return {Array<Point>|null} the repaired points between the two rectangles
- */
- function tryRepairConnectionEnd(moved, other, newDocking, points) {
- var waypoints = points.slice().reverse();
- waypoints = _tryRepairConnectionSide(moved, other, newDocking, waypoints);
- return waypoints ? waypoints.reverse() : null;
- }
- /**
- * Repair a connection from one side that moved.
- *
- * @param {Bounds} moved
- * @param {Bounds} other
- * @param {Point} newDocking
- * @param {Array<Point>} points originalPoints from moved to other
- *
- * @return {Array<Point>} the repaired points between the two rectangles
- */
- function _tryRepairConnectionSide(moved, other, newDocking, points) {
- function needsRelayout(moved, other, points) {
- if (points.length < 3) {
- return true;
- }
- if (points.length > 4) {
- return false;
- }
- // relayout if two points overlap
- // this is most likely due to
- return !!find(points, function(p, idx) {
- var q = points[idx - 1];
- return q && pointDistance(p, q) < 3;
- });
- }
- function repairBendpoint(candidate, oldPeer, newPeer) {
- var alignment = pointsAligned(oldPeer, candidate);
- switch (alignment) {
- case 'v':
- // repair vertical alignment
- return { x: candidate.x, y: newPeer.y };
- case 'h':
- // repair horizontal alignment
- return { x: newPeer.x, y: candidate.y };
- }
- return { x: candidate.x, y: candidate. y };
- }
- function removeOverlapping(points, a, b) {
- var i;
- for (i = points.length - 2; i !== 0; i--) {
- // intersects (?) break, remove all bendpoints up to this one and relayout
- if (pointInRect(points[i], a, INTERSECTION_THRESHOLD) ||
- pointInRect(points[i], b, INTERSECTION_THRESHOLD)) {
- // return sliced old connection
- return points.slice(i);
- }
- }
- return points;
- }
- // (0) only repair what has layoutable bendpoints
- // (1) if only one bendpoint and on shape moved onto other shapes axis
- // (horizontally / vertically), relayout
- if (needsRelayout(moved, other, points)) {
- return null;
- }
- var oldDocking = points[0],
- newPoints = points.slice(),
- slicedPoints;
- // (2) repair only last line segment and only if it was layouted before
- newPoints[0] = newDocking;
- newPoints[1] = repairBendpoint(newPoints[1], oldDocking, newDocking);
- // (3) if shape intersects with any bendpoint after repair,
- // remove all segments up to this bendpoint and repair from there
- slicedPoints = removeOverlapping(newPoints, moved, other);
- if (slicedPoints !== newPoints) {
- return _tryRepairConnectionSide(moved, other, newDocking, slicedPoints);
- }
- return newPoints;
- }
- /**
- * Returns the manhattan directions connecting two rectangles
- * with the given orientation.
- *
- * Will always return the default layout, if it is specific
- * regarding sides already (trbl).
- *
- * @example
- *
- * getDirections('top'); // -> 'v:v'
- * getDirections('intersect'); // -> 't:t'
- *
- * getDirections('top-right', 'v:h'); // -> 'v:h'
- * getDirections('top-right', 'h:h'); // -> 'h:h'
- *
- *
- * @param {String} orientation
- * @param {String} defaultLayout
- *
- * @return {String}
- */
- function getDirections(orientation, defaultLayout) {
- // don't override specific trbl directions
- if (isExplicitDirections(defaultLayout)) {
- return defaultLayout;
- }
- switch (orientation) {
- case 'intersect':
- return 't:t';
- case 'top':
- case 'bottom':
- return 'v:v';
- case 'left':
- case 'right':
- return 'h:h';
- // 'top-left'
- // 'top-right'
- // 'bottom-left'
- // 'bottom-right'
- default:
- return defaultLayout;
- }
- }
- function isValidDirections(directions) {
- return directions && /^h|v|t|r|b|l:h|v|t|r|b|l$/.test(directions);
- }
- function isExplicitDirections(directions) {
- return directions && /t|r|b|l/.test(directions);
- }
- function invertOrientation(orientation) {
- return {
- 'top': 'bottom',
- 'bottom': 'top',
- 'left': 'right',
- 'right': 'left',
- 'top-left': 'bottom-right',
- 'bottom-right': 'top-left',
- 'top-right': 'bottom-left',
- 'bottom-left': 'top-right',
- }[orientation];
- }
- function getDockingPoint(point, rectangle, dockingDirection, targetOrientation) {
- // ensure we end up with a specific docking direction
- // based on the targetOrientation, if <h|v> is being passed
- if (dockingDirection === 'h') {
- dockingDirection = /left/.test(targetOrientation) ? 'l' : 'r';
- }
- if (dockingDirection === 'v') {
- dockingDirection = /top/.test(targetOrientation) ? 't' : 'b';
- }
- if (dockingDirection === 't') {
- return { original: point, x: point.x, y: rectangle.y };
- }
- if (dockingDirection === 'r') {
- return { original: point, x: rectangle.x + rectangle.width, y: point.y };
- }
- if (dockingDirection === 'b') {
- return { original: point, x: point.x, y: rectangle.y + rectangle.height };
- }
- if (dockingDirection === 'l') {
- return { original: point, x: rectangle.x, y: point.y };
- }
- throw new Error('unexpected dockingDirection: <' + dockingDirection + '>');
- }
- /**
- * Return list of waypoints with redundant ones filtered out.
- *
- * @example
- *
- * Original points:
- *
- * [x] ----- [x] ------ [x]
- * |
- * [x] ----- [x] - [x]
- *
- * Filtered:
- *
- * [x] ---------------- [x]
- * |
- * [x] ----------- [x]
- *
- * @param {Array<Point>} waypoints
- *
- * @return {Array<Point>}
- */
- export function withoutRedundantPoints(waypoints) {
- return waypoints.reduce(function(points, p, idx) {
- var previous = points[points.length - 1],
- next = waypoints[idx + 1];
- if (!pointsOnLine(previous, next, p, 0)) {
- points.push(p);
- }
- return points;
- }, []);
- }
|