UNPKG

sprotty

Version:

A next-gen framework for graphical views

526 lines (485 loc) 27 kB
/******************************************************************************** * Copyright (c) 2019-2020 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { almostEquals, Bounds, Point } from "sprotty-protocol/lib/utils/geometry"; import { translatePoint } from "../../base/model/smodel-utils"; import { ResolvedHandleMove } from "../move/move"; import { DefaultAnchors, AbstractEdgeRouter, LinearRouteOptions, Side } from "./abstract-edge-router"; import { SRoutableElementImpl, RoutingHandleKind, SRoutingHandleImpl } from "./model"; import { RoutedPoint } from "./routing"; export interface ManhattanRouterOptions extends LinearRouteOptions { standardDistance: number; } export class ManhattanEdgeRouter extends AbstractEdgeRouter { static readonly KIND = 'manhattan'; get kind() { return ManhattanEdgeRouter.KIND; } protected getOptions(edge: SRoutableElementImpl): ManhattanRouterOptions { return { standardDistance: 20, minimalPointDistance: 3, selfEdgeOffset: 0.25 }; } route(edge: SRoutableElementImpl): RoutedPoint[] { if (!edge.source || !edge.target) return []; const routedCorners = this.createRoutedCorners(edge); const sourceRefPoint = routedCorners[0] || translatePoint(Bounds.center(edge.target.bounds), edge.target.parent, edge.parent); const sourceAnchor = this.getTranslatedAnchor(edge.source, sourceRefPoint, edge.parent, edge, edge.sourceAnchorCorrection); const targetRefPoint = routedCorners[routedCorners.length - 1] || translatePoint(Bounds.center(edge.source.bounds), edge.source.parent, edge.parent); const targetAnchor = this.getTranslatedAnchor(edge.target, targetRefPoint, edge.parent, edge, edge.targetAnchorCorrection); if (!sourceAnchor || !targetAnchor) return []; const routedPoints: RoutedPoint[] = []; routedPoints.push({ kind: 'source', ...sourceAnchor}); routedCorners.forEach(corner => routedPoints.push(corner)); routedPoints.push({ kind: 'target', ...targetAnchor}); return routedPoints; } protected createRoutedCorners(edge: SRoutableElementImpl): RoutedPoint[] { const sourceAnchors = new DefaultAnchors(edge.source!, edge.parent, 'source'); const targetAnchors = new DefaultAnchors(edge.target!, edge.parent, 'target'); if (edge.routingPoints.length > 0) { const routingPointsCopy = edge.routingPoints.slice(); this.cleanupRoutingPoints(edge, routingPointsCopy, false, true); if (routingPointsCopy.length > 0) return routingPointsCopy.map((routingPoint, index) => { return <RoutedPoint> { kind: 'linear', pointIndex: index, ...routingPoint}; }); } const options = this.getOptions(edge); const corners = this.calculateDefaultCorners(edge, sourceAnchors, targetAnchors, options); return corners.map(corner => { return <RoutedPoint> { kind: 'linear', ...corner }; }); } createRoutingHandles(edge: SRoutableElementImpl) { const routedPoints = this.route(edge); this.commitRoute(edge, routedPoints); if (routedPoints.length > 0) { this.addHandle(edge, 'source', 'routing-point', -2); for (let i = 0; i < routedPoints.length - 1; ++i) this.addHandle(edge, 'manhattan-50%', 'volatile-routing-point', i - 1); this.addHandle(edge, 'target', 'routing-point', routedPoints.length - 2); } } protected getInnerHandlePosition(edge: SRoutableElementImpl, route: RoutedPoint[], handle: SRoutingHandleImpl) { const fraction = this.getFraction(handle.kind); if (fraction !== undefined) { const { start, end } = this.findRouteSegment(edge, route, handle.pointIndex); if (start !== undefined && end !== undefined) return Point.linear(start, end, fraction); } return undefined; } protected getFraction(kind: RoutingHandleKind): number | undefined { switch (kind) { case 'manhattan-50%': return 0.5; default: return undefined; } } protected applyInnerHandleMoves(edge: SRoutableElementImpl, moves: ResolvedHandleMove[]) { const route = this.route(edge); const routingPoints = edge.routingPoints; const minimalPointDistance = this.getOptions(edge).minimalPointDistance; moves.forEach(move => { const handle = move.handle; const index = handle.pointIndex; const correctedX = this.correctX(routingPoints, index, move.toPosition.x, minimalPointDistance); const correctedY = this.correctY(routingPoints, index, move.toPosition.y, minimalPointDistance); switch (handle.kind) { case 'manhattan-50%': if (index < 0) { if (routingPoints.length === 0) { routingPoints.push({ x: correctedX, y: correctedY }); handle.pointIndex = 0; } else if (almostEquals(route[0].x, route[1].x)) { this.alignX(routingPoints, 0, correctedX); } else { this.alignY(routingPoints, 0, correctedY); } } else if (index < routingPoints.length - 1) { if (almostEquals(routingPoints[index].x, routingPoints[index + 1].x)) { this.alignX(routingPoints, index, correctedX); this.alignX(routingPoints, index + 1, correctedX); } else { this.alignY(routingPoints, index, correctedY); this.alignY(routingPoints, index + 1, correctedY); } } else { if (almostEquals(route[route.length - 2].x, route[route.length - 1].x)) { this.alignX(routingPoints, routingPoints.length - 1, correctedX); } else { this.alignY(routingPoints, routingPoints.length - 1, correctedY); } } break; } }); } protected correctX(routingPoints: Point[], index: number, x: number, minimalPointDistance: number) { if (index > 0 && Math.abs(x - routingPoints[index - 1].x) < minimalPointDistance) return routingPoints[index - 1].x; else if (index < routingPoints.length - 2 && Math.abs(x - routingPoints[index + 2].x) < minimalPointDistance) return routingPoints[index + 2].x; else return x; } protected alignX(routingPoints: Point[], index: number, x: number) { if (index >= 0 && index < routingPoints.length) routingPoints[index] = { x, y: routingPoints[index].y }; } protected correctY(routingPoints: Point[], index: number, y: number, minimalPointDistance: number) { if (index > 0 && Math.abs(y - routingPoints[index - 1].y) < minimalPointDistance) return routingPoints[index - 1].y; else if (index < routingPoints.length - 2 && Math.abs(y - routingPoints[index + 2].y) < minimalPointDistance) return routingPoints[index + 2].y; else return y; } protected alignY(routingPoints: Point[], index: number, y: number) { if (index >= 0 && index < routingPoints.length) routingPoints[index] = { x: routingPoints[index].x, y }; } override cleanupRoutingPoints(edge: SRoutableElementImpl, routingPoints: Point[], updateHandles: boolean, addRoutingPoints: boolean) { const sourceAnchors = new DefaultAnchors(edge.source!, edge.parent, "source"); const targetAnchors = new DefaultAnchors(edge.target!, edge.parent, "target"); if (this.resetRoutingPointsOnReconnect(edge, routingPoints, updateHandles, sourceAnchors, targetAnchors)) return; // delete leading RPs inside the bounds of the source for (let i = 0; i < routingPoints.length; ++i) if (Bounds.includes(sourceAnchors.bounds, routingPoints[i])) { routingPoints.splice(0, 1); if (updateHandles) { this.removeHandle(edge, -1); } } else { break; } // delete trailing RPs inside the bounds of the target for (let i = routingPoints.length - 1; i >= 0; --i) if (Bounds.includes(targetAnchors.bounds, routingPoints[i])) { routingPoints.splice(i, 1); if (updateHandles) { this.removeHandle(edge, i); } } else { break; } if (routingPoints.length >= 2) { const options = this.getOptions(edge); for (let i = routingPoints.length - 2; i >= 0; --i) { if (Point.manhattanDistance(routingPoints[i], routingPoints[i + 1]) < options.minimalPointDistance) { routingPoints.splice(i, 2); --i; if (updateHandles) { this.removeHandle(edge, i - 1); this.removeHandle(edge, i); } } } } if (addRoutingPoints) { this.addAdditionalCorner(edge, routingPoints, sourceAnchors, targetAnchors, updateHandles); this.addAdditionalCorner(edge, routingPoints, targetAnchors, sourceAnchors, updateHandles); this.manhattanify(edge, routingPoints); } } protected removeHandle(edge: SRoutableElementImpl, pointIndex: number) { const toBeRemoved: SRoutingHandleImpl[] = []; edge.children.forEach(child => { if (child instanceof SRoutingHandleImpl) { if (child.pointIndex > pointIndex) --child.pointIndex; else if (child.pointIndex === pointIndex) toBeRemoved.push(child); } }); toBeRemoved.forEach(child => edge.remove(child)); } protected addAdditionalCorner(edge: SRoutableElementImpl, routingPoints: Point[], currentAnchors: DefaultAnchors, otherAnchors: DefaultAnchors, updateHandles: boolean) { if (routingPoints.length === 0) return; const refPoint = currentAnchors.kind === 'source' ? routingPoints[0] : routingPoints[routingPoints.length - 1]; const index = currentAnchors.kind === 'source' ? 0 : routingPoints.length; const shiftIndex = index - (currentAnchors.kind === 'source' ? 1 : 0); let isHorizontal: boolean; if (routingPoints.length > 1) { isHorizontal = index === 0 ? almostEquals(routingPoints[0].x, routingPoints[1].x) : almostEquals(routingPoints[routingPoints.length - 1].x, routingPoints[routingPoints.length - 2].x); } else { const nearestSide = otherAnchors.getNearestSide(refPoint); isHorizontal = nearestSide === Side.TOP || nearestSide === Side.BOTTOM; } if (isHorizontal) { if (refPoint.y < currentAnchors.get(Side.TOP).y || refPoint.y > currentAnchors.get(Side.BOTTOM).y) { const newPoint = { x: currentAnchors.get(Side.TOP).x, y: refPoint.y }; routingPoints.splice(index, 0, newPoint); if (updateHandles) { edge.children.forEach(child => { if (child instanceof SRoutingHandleImpl && child.pointIndex >= shiftIndex) ++child.pointIndex; }); this.addHandle(edge, 'manhattan-50%', 'volatile-routing-point', shiftIndex); } } } else { if (refPoint.x < currentAnchors.get(Side.LEFT).x || refPoint.x > currentAnchors.get(Side.RIGHT).x) { const newPoint = { x: refPoint.x, y: currentAnchors.get(Side.LEFT).y }; routingPoints.splice(index, 0, newPoint); if (updateHandles) { edge.children.forEach(child => { if (child instanceof SRoutingHandleImpl && child.pointIndex >= shiftIndex) ++child.pointIndex; }); this.addHandle(edge, 'manhattan-50%', 'volatile-routing-point', shiftIndex); } } } } /** * Add artificial routing points to keep all angles rectilinear. * * This makes edge morphing look a lot smoother, where RP positions are interpolated * linearly probably resulting in non-rectilinear angles. We don't add handles for * these additional RPs. */ protected manhattanify(edge: SRoutableElementImpl, routingPoints: Point[]) { for (let i = 1; i < routingPoints.length; ++i) { const isVertical = Math.abs(routingPoints[i - 1].x - routingPoints[i].x) < 1; const isHorizontal = Math.abs(routingPoints[i - 1].y - routingPoints[i].y) < 1; if (!isVertical && !isHorizontal) { routingPoints.splice(i, 0, { x: routingPoints[i - 1].x, y: routingPoints[i].y }); ++i; } } } protected override calculateDefaultCorners(edge: SRoutableElementImpl, sourceAnchors: DefaultAnchors, targetAnchors: DefaultAnchors, options: ManhattanRouterOptions): Point[] { const selfEdge = super.calculateDefaultCorners(edge, sourceAnchors, targetAnchors, options); if (selfEdge.length > 0) return selfEdge; const bestAnchors = this.getBestConnectionAnchors(edge, sourceAnchors, targetAnchors, options); const sourceSide = bestAnchors.source; const targetSide = bestAnchors.target; const corners: Point[] = []; const startPoint = sourceAnchors.get(sourceSide); let endPoint = targetAnchors.get(targetSide); switch (sourceSide) { case Side.RIGHT: switch (targetSide) { case Side.BOTTOM: corners.push({ x: endPoint.x, y: startPoint.y }); break; case Side.TOP: corners.push({ x: endPoint.x, y: startPoint.y }); break; case Side.RIGHT: corners.push({ x: Math.max(startPoint.x, endPoint.x) + 1.5 * options.standardDistance, y: startPoint.y }); corners.push({ x: Math.max(startPoint.x, endPoint.x) + 1.5 * options.standardDistance, y: endPoint.y }); break; case Side.LEFT: if (endPoint.y !== startPoint.y) { corners.push({ x: (startPoint.x + endPoint.x) / 2, y: startPoint.y }); corners.push({ x: (startPoint.x + endPoint.x) / 2, y: endPoint.y }); } break; } break; case Side.LEFT: switch (targetSide) { case Side.BOTTOM: corners.push({ x: endPoint.x, y: startPoint.y }); break; case Side.TOP: corners.push({ x: endPoint.x, y: startPoint.y }); break; default: endPoint = targetAnchors.get(Side.RIGHT); if (endPoint.y !== startPoint.y) { corners.push({ x: (startPoint.x + endPoint.x) / 2, y: startPoint.y }); corners.push({ x: (startPoint.x + endPoint.x) / 2, y: endPoint.y }); } break; } break; case Side.TOP: switch (targetSide) { case Side.RIGHT: if ((endPoint.x - startPoint.x) > 0) { corners.push({ x: startPoint.x, y: startPoint.y - options.standardDistance }); corners.push({ x: endPoint.x + 1.5 * options.standardDistance, y: startPoint.y - options.standardDistance }); corners.push({ x: endPoint.x + 1.5 * options.standardDistance, y: endPoint.y }); } else { corners.push({ x: startPoint.x, y: endPoint.y }); } break; case Side.LEFT: if ((endPoint.x - startPoint.x) < 0) { corners.push({ x: startPoint.x, y: startPoint.y - options.standardDistance }); corners.push({ x: endPoint.x - 1.5 * options.standardDistance, y: startPoint.y - options.standardDistance }); corners.push({ x: endPoint.x - 1.5 * options.standardDistance, y: endPoint.y }); } else { corners.push({ x: startPoint.x, y: endPoint.y }); } break; case Side.TOP: corners.push({ x: startPoint.x, y: Math.min(startPoint.y, endPoint.y) - 1.5 * options.standardDistance }); corners.push({ x: endPoint.x, y: Math.min(startPoint.y, endPoint.y) - 1.5 * options.standardDistance }); break; case Side.BOTTOM: if (endPoint.x !== startPoint.x) { corners.push({ x: startPoint.x, y: (startPoint.y + endPoint.y) / 2 }); corners.push({ x: endPoint.x, y: (startPoint.y + endPoint.y) / 2 }); } break; } break; case Side.BOTTOM: switch (targetSide) { case Side.RIGHT: if ((endPoint.x - startPoint.x) > 0) { corners.push({ x: startPoint.x, y: startPoint.y + options.standardDistance }); corners.push({ x: endPoint.x + 1.5 * options.standardDistance, y: startPoint.y + options.standardDistance }); corners.push({ x: endPoint.x + 1.5 * options.standardDistance, y: endPoint.y }); } else { corners.push({ x: startPoint.x, y: endPoint.y }); } break; case Side.LEFT: if ((endPoint.x - startPoint.x) < 0) { corners.push({ x: startPoint.x, y: startPoint.y + options.standardDistance }); corners.push({ x: endPoint.x - 1.5 * options.standardDistance, y: startPoint.y + options.standardDistance }); corners.push({ x: endPoint.x - 1.5 * options.standardDistance, y: endPoint.y }); } else { corners.push({ x: startPoint.x, y: endPoint.y }); } break; default: endPoint = targetAnchors.get(Side.TOP); if (endPoint.x !== startPoint.x) { corners.push({ x: startPoint.x, y: (startPoint.y + endPoint.y) / 2 }); corners.push({ x: endPoint.x, y: (startPoint.y + endPoint.y) / 2 }); } break; } break; } return corners; } protected getBestConnectionAnchors(edge: SRoutableElementImpl, sourceAnchors: DefaultAnchors, targetAnchors: DefaultAnchors, options: ManhattanRouterOptions): { source: Side, target: Side } { // distance is enough let sourcePoint = sourceAnchors.get(Side.RIGHT); let targetPoint = targetAnchors.get(Side.LEFT); if ((targetPoint.x - sourcePoint.x) > options.standardDistance) return { source: Side.RIGHT, target: Side.LEFT }; sourcePoint = sourceAnchors.get(Side.LEFT); targetPoint = targetAnchors.get(Side.RIGHT); if ((sourcePoint.x - targetPoint.x) > options.standardDistance) return { source: Side.LEFT, target: Side.RIGHT }; sourcePoint = sourceAnchors.get(Side.TOP); targetPoint = targetAnchors.get(Side.BOTTOM); if ((sourcePoint.y - targetPoint.y) > options.standardDistance) return { source: Side.TOP, target: Side.BOTTOM }; sourcePoint = sourceAnchors.get(Side.BOTTOM); targetPoint = targetAnchors.get(Side.TOP); if ((targetPoint.y - sourcePoint.y) > options.standardDistance) return { source: Side.BOTTOM, target: Side.TOP }; // One additional point sourcePoint = sourceAnchors.get(Side.RIGHT); targetPoint = targetAnchors.get(Side.TOP); if (((targetPoint.x - sourcePoint.x) > 0.5 * options.standardDistance) && ((targetPoint.y - sourcePoint.y) > options.standardDistance)) return { source: Side.RIGHT, target: Side.TOP }; targetPoint = targetAnchors.get(Side.BOTTOM); if (((targetPoint.x - sourcePoint.x) > 0.5 * options.standardDistance) && ((sourcePoint.y - targetPoint.y) > options.standardDistance)) return { source: Side.RIGHT, target: Side.BOTTOM }; sourcePoint = sourceAnchors.get(Side.LEFT); targetPoint = targetAnchors.get(Side.BOTTOM); if (((sourcePoint.x - targetPoint.x) > 0.5 * options.standardDistance) && ((sourcePoint.y - targetPoint.y) > options.standardDistance)) return { source: Side.LEFT, target: Side.BOTTOM }; targetPoint = targetAnchors.get(Side.TOP); if (((sourcePoint.x - targetPoint.x) > 0.5 * options.standardDistance) && ((targetPoint.y - sourcePoint.y) > options.standardDistance)) return { source: Side.LEFT, target: Side.TOP }; sourcePoint = sourceAnchors.get(Side.TOP); targetPoint = targetAnchors.get(Side.RIGHT); if (((sourcePoint.y - targetPoint.y) > 0.5 * options.standardDistance) && ((sourcePoint.x - targetPoint.x) > options.standardDistance)) return { source: Side.TOP, target: Side.RIGHT }; targetPoint = targetAnchors.get(Side.LEFT); if (((sourcePoint.y - targetPoint.y) > 0.5 * options.standardDistance) && ((targetPoint.x - sourcePoint.x) > options.standardDistance)) return { source: Side.TOP, target: Side.LEFT }; sourcePoint = sourceAnchors.get(Side.BOTTOM); targetPoint = targetAnchors.get(Side.RIGHT); if (((targetPoint.y - sourcePoint.y) > 0.5 * options.standardDistance) && ((sourcePoint.x - targetPoint.x) > options.standardDistance)) return { source: Side.BOTTOM, target: Side.RIGHT }; targetPoint = targetAnchors.get(Side.LEFT); if (((targetPoint.y - sourcePoint.y) > 0.5 * options.standardDistance) && ((targetPoint.x - sourcePoint.x) > options.standardDistance)) return { source: Side.BOTTOM, target: Side.LEFT }; // Two points // priority NN >> EE >> NE >> NW >> SE >> SW sourcePoint = sourceAnchors.get(Side.TOP); targetPoint = targetAnchors.get(Side.TOP); if (!Bounds.includes(targetAnchors.bounds, sourcePoint) && !Bounds.includes(sourceAnchors.bounds, targetPoint)) { if ((sourcePoint.y - targetPoint.y) < 0) { if (Math.abs(sourcePoint.x - targetPoint.x) > ((sourceAnchors.bounds.width + options.standardDistance) / 2)) return { source: Side.TOP, target: Side.TOP }; } else { if (Math.abs(sourcePoint.x - targetPoint.x) > (targetAnchors.bounds.width / 2)) return { source: Side.TOP, target: Side.TOP }; } } sourcePoint = sourceAnchors.get(Side.RIGHT); targetPoint = targetAnchors.get(Side.RIGHT); if (!Bounds.includes(targetAnchors.bounds, sourcePoint) && !Bounds.includes(sourceAnchors.bounds, targetPoint)) { if ((sourcePoint.x - targetPoint.x) > 0) { if (Math.abs(sourcePoint.y - targetPoint.y) > ((sourceAnchors.bounds.height + options.standardDistance) / 2)) return { source: Side.RIGHT, target: Side.RIGHT }; } else if (Math.abs(sourcePoint.y - targetPoint.y) > (targetAnchors.bounds.height / 2)) return { source: Side.RIGHT, target: Side.RIGHT }; } // Secondly, judge NE NW is available sourcePoint = sourceAnchors.get(Side.TOP); targetPoint = targetAnchors.get(Side.RIGHT); if (!Bounds.includes(targetAnchors.bounds, sourcePoint) && !Bounds.includes(sourceAnchors.bounds, targetPoint)) return { source: Side.TOP, target: Side.RIGHT }; targetPoint = targetAnchors.get(Side.LEFT); if (!Bounds.includes(targetAnchors.bounds, sourcePoint) && !Bounds.includes(sourceAnchors.bounds, targetPoint)) return { source: Side.TOP, target: Side.LEFT }; // Finally, judge SE SW is available sourcePoint = sourceAnchors.get(Side.BOTTOM); targetPoint = targetAnchors.get(Side.RIGHT); if (!Bounds.includes(targetAnchors.bounds, sourcePoint) && !Bounds.includes(sourceAnchors.bounds, targetPoint)) return { source: Side.BOTTOM, target: Side.RIGHT }; targetPoint = targetAnchors.get(Side.LEFT); if (!Bounds.includes(targetAnchors.bounds, sourcePoint) && !Bounds.includes(sourceAnchors.bounds, targetPoint)) return { source: Side.BOTTOM, target: Side.LEFT }; // Only to return to the return { source: Side.RIGHT, target: Side.BOTTOM }; } }