diff --git a/.vscode/launch.json b/.vscode/launch.json
index 8e71e34e..80cba067 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -11,8 +11,8 @@
"args": [
"${file}",
"--no-timeouts",
- "--opts",
- "${workspaceRoot}/configs/mocha.opts"
+ "--config",
+ "${workspaceRoot}/configs/.mocharc.json"
],
"env": {
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
diff --git a/examples/random-graph/src/di.config.ts b/examples/random-graph/src/di.config.ts
index 56bc7ea3..399f3f98 100644
--- a/examples/random-graph/src/di.config.ts
+++ b/examples/random-graph/src/di.config.ts
@@ -19,7 +19,7 @@ import ElkConstructor from 'elkjs/lib/elk.bundled';
import {
TYPES, configureViewerOptions, SGraphView, SLabelView, ConsoleLogger, LogLevel,
loadDefaultModules, LocalModelSource, SNode, SEdge, SLabel, configureModelElement,
- SGraph, RectangularNodeView, PolylineEdgeView
+ SGraph, RectangularNodeView, edgeIntersectionModule, PolylineEdgeViewWithGapsOnIntersections
} from 'sprotty';
import { ElkFactory, ElkLayoutEngine, elkLayoutModule } from 'sprotty-elk/lib/inversify';
@@ -41,7 +41,7 @@ export default (containerId: string) => {
const context = { bind, unbind, isBound, rebind };
configureModelElement(container, 'graph', SGraph, SGraphView);
configureModelElement(container, 'node', SNode, RectangularNodeView);
- configureModelElement(container, 'edge', SEdge, PolylineEdgeView);
+ configureModelElement(container, 'edge', SEdge, PolylineEdgeViewWithGapsOnIntersections);
configureModelElement(container, 'label', SLabel, SLabelView);
configureViewerOptions(context, {
@@ -52,6 +52,7 @@ export default (containerId: string) => {
const container = new Container();
loadDefaultModules(container);
+ container.load(edgeIntersectionModule);
container.load(elkLayoutModule, randomGraphModule);
return container;
};
diff --git a/packages/sprotty/src/features/edge-intersection/intersection-finder.ts b/packages/sprotty/src/features/edge-intersection/intersection-finder.ts
index 44fc21e3..c1e0a307 100644
--- a/packages/sprotty/src/features/edge-intersection/intersection-finder.ts
+++ b/packages/sprotty/src/features/edge-intersection/intersection-finder.ts
@@ -1,5 +1,5 @@
/********************************************************************************
- * Copyright (c) 2021 EclipseSource and others.
+ * Copyright (c) 2021-2022 EclipseSource 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
@@ -43,8 +43,31 @@ export const BY_X_THEN_Y = (a: Intersection, b: Intersection): number => {
return a.intersectionPoint.x - b.intersectionPoint.x;
};
+export const BY_DESCENDING_X_THEN_Y = (a: Intersection, b: Intersection): number => {
+ if (a.intersectionPoint.x === b.intersectionPoint.x) {
+ return a.intersectionPoint.y - b.intersectionPoint.y;
+ }
+ return b.intersectionPoint.x - a.intersectionPoint.x;
+};
+
+export const BY_X_THEN_DESCENDING_Y = (a: Intersection, b: Intersection): number => {
+ if (a.intersectionPoint.x === b.intersectionPoint.x) {
+ return b.intersectionPoint.y - a.intersectionPoint.y;
+ }
+ return a.intersectionPoint.x - b.intersectionPoint.x;
+};
+
+export const BY_DESCENDING_X_THEN_DESCENDING_Y = (a: Intersection, b: Intersection): number => {
+ if (a.intersectionPoint.x === b.intersectionPoint.x) {
+ return b.intersectionPoint.y - a.intersectionPoint.y;
+ }
+ return b.intersectionPoint.x - a.intersectionPoint.x;
+};
+
/**
* Finds intersections among edges and updates routed points to reflect those intersections.
+ *
+ * This only yields correct intersections among straight line segments and doesn't work with bezier curves.
*/
@injectable()
export class IntersectionFinder implements IEdgeRoutePostprocessor {
diff --git a/packages/sprotty/src/graph/views.tsx b/packages/sprotty/src/graph/views.tsx
index 9c13ff79..28157a7e 100644
--- a/packages/sprotty/src/graph/views.tsx
+++ b/packages/sprotty/src/graph/views.tsx
@@ -1,5 +1,5 @@
/********************************************************************************
- * Copyright (c) 2017-2018 TypeFox and others.
+ * Copyright (c) 2017-2022 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
@@ -19,10 +19,18 @@ import { inject, injectable } from 'inversify';
import { VNode } from "snabbdom";
import { Point } from 'sprotty-protocol/lib/utils/geometry';
import { getSubType } from 'sprotty-protocol/lib/utils/model-utils';
-import { IViewArgs, IView, RenderingContext } from "../base/views/view";
+import { IView, IViewArgs, RenderingContext } from "../base/views/view";
import { setAttr } from '../base/views/vnode-utils';
import { ShapeView } from '../features/bounds/views';
-import { BY_X_THEN_Y, IntersectingRoutedPoint, Intersection, isIntersectingRoutedPoint } from '../features/edge-intersection/intersection-finder';
+import {
+ BY_DESCENDING_X_THEN_DESCENDING_Y,
+ BY_DESCENDING_X_THEN_Y,
+ BY_X_THEN_DESCENDING_Y,
+ BY_X_THEN_Y,
+ IntersectingRoutedPoint,
+ Intersection,
+ isIntersectingRoutedPoint
+} from '../features/edge-intersection/intersection-finder';
import { isEdgeLayoutable } from '../features/edge-layout/model';
import { SRoutableElement, SRoutingHandle } from '../features/routing/model';
import { EdgeRouterRegistry, RoutedPoint } from '../features/routing/routing';
@@ -103,6 +111,8 @@ export class PolylineEdgeView extends RoutableView {
* In order to find intersections, `IntersectionFinder` needs to be configured as a `TYPES.IEdgeRoutePostprocessor`
* so that that intersections are declared as `IntersectingRoutedPoint` in the computed routes.
*
+ * This view only draws correct line jumps for intersections among straight line segments and doesn't work with bezier curves.
+ *
* @see IntersectionFinder
* @see IntersectingRoutedPoint
* @see EdgeRouterRegistry
@@ -132,31 +142,79 @@ export class JumpingPolylineEdgeView extends PolylineEdgeView {
return ;
}
+ /**
+ * Returns a path that takes the intersections into account by drawing a line jump or a gap for intersections on that path.
+ */
protected intersectionPath(edge: SEdge, segments: Point[], intersectingPoint: IntersectingRoutedPoint, args?: IViewArgs): string {
+ if (intersectingPoint.intersections.length < 1) {
+ return '';
+ }
+
+ const segment = this.getLineSegment(edge, intersectingPoint.intersections[0], args, segments);
+ const intersections = this.getIntersectionsSortedBySegmentDirection(segment, intersectingPoint);
+
let path = '';
- for (const intersection of intersectingPoint.intersections.sort(BY_X_THEN_Y)) {
+ for (const intersection of intersections) {
const otherLineSegment = this.getOtherLineSegment(edge, intersection, args);
if (otherLineSegment === undefined) {
continue;
}
- const lineSegment = this.getLineSegment(edge, intersection, args, segments);
+ const currentLineSegment = this.getLineSegment(edge, intersection, args, segments);
const intersectionPoint = intersection.intersectionPoint;
- if (Math.abs(lineSegment.slopeOrMax) < Math.abs(otherLineSegment.slopeOrMax)) {
- path += this.createJumpPath(intersectionPoint, lineSegment);
- } else {
- path += this.createSkipPath(intersectionPoint, lineSegment);
+ if (this.shouldDrawLineJumpOnIntersection(currentLineSegment, otherLineSegment)) {
+ path += this.createJumpPath(intersectionPoint, currentLineSegment);
+ } else if (this.shouldDrawLineGapOnIntersection(currentLineSegment, otherLineSegment)) {
+ path += this.createGapPath(intersectionPoint, currentLineSegment);
}
}
+
return path;
}
- protected getOtherLineSegment(currentEdge: SEdge, intersection: Intersection, args?: IViewArgs): PointToPointLine | undefined {
- const otherEdgeId = intersection.routable1 === currentEdge.id ? intersection.routable2 : intersection.routable1;
- const otherEdge = currentEdge.index.getById(otherEdgeId);
- if (!(otherEdge instanceof SRoutableElement)) {
- return undefined;
+ /**
+ * Returns the intersections sorted by the direction of the `lineSegment`.
+ *
+ * The coordinate system goes from left to right and top to bottom.
+ * Thus, x increases to the right and y increases downwards.
+ *
+ * We need to draw the intersections in the order of the direction of the line segment.
+ * To draw a line pointing north, we need to order intersections by Y in a descending order.
+ * To draw a line pointing south, we need to order intersections by Y in an ascending order.
+ */
+ protected getIntersectionsSortedBySegmentDirection(lineSegment: PointToPointLine, intersectingPoint: IntersectingRoutedPoint) {
+ switch (lineSegment.direction) {
+ case 'north':
+ case 'north-east':
+ return intersectingPoint.intersections.sort(BY_X_THEN_DESCENDING_Y);
+
+ case 'south':
+ case 'south-east':
+ case 'east':
+ return intersectingPoint.intersections.sort(BY_X_THEN_Y);
+
+ case 'south-west':
+ case 'west':
+ return intersectingPoint.intersections.sort(BY_DESCENDING_X_THEN_Y);
+
+ case 'north-west':
+ return intersectingPoint.intersections.sort(BY_DESCENDING_X_THEN_DESCENDING_Y);
}
- return this.getLineSegment(otherEdge, intersection, args);
+ }
+
+ /**
+ * Whether or not to draw a line jump on an intersection for the `currentLineSegment`.
+ * This should usually be inverse of `shouldDrawLineGapOnIntersection()`.
+ */
+ protected shouldDrawLineJumpOnIntersection(currentLineSegment: PointToPointLine, otherLineSegment: PointToPointLine) {
+ return Math.abs(currentLineSegment.slopeOrMax) < Math.abs(otherLineSegment.slopeOrMax);
+ }
+
+ /**
+ * Whether or not to draw a line gap on an intersection for the `currentLineSegment`.
+ * This should usually be inverse of `shouldDrawLineJumpOnIntersection()`.
+ */
+ protected shouldDrawLineGapOnIntersection(currentLineSegment: PointToPointLine, otherLineSegment: PointToPointLine) {
+ return !this.shouldDrawLineJumpOnIntersection(currentLineSegment, otherLineSegment);
}
protected getLineSegment(edge: SRoutableElement, intersection: Intersection, args?: IViewArgs, segments?: Point[]): PointToPointLine {
@@ -165,6 +223,15 @@ export class JumpingPolylineEdgeView extends PolylineEdgeView {
return new PointToPointLine(route[index], route[index + 1]);
}
+ protected getOtherLineSegment(currentEdge: SEdge, intersection: Intersection, args?: IViewArgs): PointToPointLine | undefined {
+ const otherEdgeId = intersection.routable1 === currentEdge.id ? intersection.routable2 : intersection.routable1;
+ const otherEdge = currentEdge.index.getById(otherEdgeId);
+ if (!(otherEdge instanceof SRoutableElement)) {
+ return undefined;
+ }
+ return this.getLineSegment(otherEdge, intersection, args);
+ }
+
protected createJumpPath(intersectionPoint: Point, lineSegment: PointToPointLine): string {
const anchorBefore = Point.shiftTowards(intersectionPoint, lineSegment.p1, this.jumpOffsetBefore);
const anchorAfter = Point.shiftTowards(intersectionPoint, lineSegment.p2, this.jumpOffsetAfter);
@@ -172,7 +239,7 @@ export class JumpingPolylineEdgeView extends PolylineEdgeView {
return ` L ${anchorBefore.x},${anchorBefore.y} A 1,1 0,0 ${rotation} ${anchorAfter.x},${anchorAfter.y}`;
}
- protected createSkipPath(intersectionPoint: Point, lineSegment: PointToPointLine): string {
+ protected createGapPath(intersectionPoint: Point, lineSegment: PointToPointLine): string {
let offsetBefore;
let offsetAfter;
if (intersectionPoint.y < lineSegment.p1.y) {
@@ -195,12 +262,14 @@ export class JumpingPolylineEdgeView extends PolylineEdgeView {
* In order to find intersections, `IntersectionFinder` needs to be configured as a `TYPES.IEdgeRoutePostprocessor`
* so that that intersections are declared as `IntersectingRoutedPoint` in the computed routes.
*
+ * This view only draws correct gaps for intersections among straight line segments and doesn't work with bezier curves.
+ *
* @see IntersectionFinder
* @see IntersectingRoutedPoint
* @see EdgeRouterRegistry
*/
- @injectable()
- export class PolylineEdgeViewWithGapsOnIntersections extends JumpingPolylineEdgeView {
+@injectable()
+export class PolylineEdgeViewWithGapsOnIntersections extends JumpingPolylineEdgeView {
protected skipOffsetBefore = 3;
protected skipOffsetAfter = 3;
@@ -209,13 +278,13 @@ export class JumpingPolylineEdgeView extends PolylineEdgeView {
return "";
}
- protected createSkipPath(intersectionPoint: Point, lineSegment: PointToPointLine): string {
+ protected createGapPath(intersectionPoint: Point, lineSegment: PointToPointLine): string {
const anchorBefore = Point.shiftTowards(intersectionPoint, lineSegment.p1, this.skipOffsetBefore);
const anchorAfter = Point.shiftTowards(intersectionPoint, lineSegment.p2, this.skipOffsetAfter);
return ` L ${anchorBefore.x},${anchorBefore.y} M ${anchorAfter.x},${anchorAfter.y}`;
}
- }
+}
@injectable()
export class BezierCurveEdgeView extends RoutableView {
@@ -372,7 +441,7 @@ export class SBezierCreateHandleView extends SRoutingHandleView {
const text = (handle.kind === "bezier-add") ? "+" : "-";
const node =
+ class-selected={handle.selected} class-mouseover={handle.hoverFeedback}>
{text}
@@ -421,7 +490,7 @@ export class SBezierControlHandleView extends SRoutingHandleView {
;
} else {
node = ;
+ cx={position.x} cy={position.y} r={this.getRadius()} />;
}
setAttr(node, 'data-kind', handle.kind);
diff --git a/packages/sprotty/src/utils/geometry.spec.ts b/packages/sprotty/src/utils/geometry.spec.ts
index 39a99a3f..34817860 100644
--- a/packages/sprotty/src/utils/geometry.spec.ts
+++ b/packages/sprotty/src/utils/geometry.spec.ts
@@ -1,5 +1,5 @@
/********************************************************************************
- * Copyright (c) 2017-2018 TypeFox and others.
+ * Copyright (c) 2017-2022 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
@@ -48,4 +48,41 @@ describe('PointToPointLine', () => {
expect(intersection).to.be.undefined;
});
});
+ describe('direction', () => {
+ // the coordinate system goes from left to right and top to bottom
+ // thus, x increases to the right and y increases downwards
+ // so a line going north is (x:0,y:1) -> (x:0:y0)
+ it('correctly defines line to north', () => {
+ const line = new PointToPointLine({ x: 0, y: 1 }, { x: 0, y: 0 });
+ expect(line.direction).to.equal('north');
+ });
+ it('correctly defines line to north-east', () => {
+ const line = new PointToPointLine({ x: 0, y: 1 }, { x: 1, y: 0 });
+ expect(line.direction).to.equal('north-east');
+ });
+ it('correctly defines line to east', () => {
+ const line = new PointToPointLine({ x: 0, y: 0 }, { x: 1, y: 0 });
+ expect(line.direction).to.equal('east');
+ });
+ it('correctly defines line to south-east', () => {
+ const line = new PointToPointLine({ x: 0, y: 0 }, { x: 1, y: 1 });
+ expect(line.direction).to.equal('south-east');
+ });
+ it('correctly defines line to south', () => {
+ const line = new PointToPointLine({ x: 0, y: 0 }, { x: 0, y: 1 });
+ expect(line.direction).to.equal('south');
+ });
+ it('correctly defines line to south-west', () => {
+ const line = new PointToPointLine({ x: 1, y: 0 }, { x: 0, y: 1 });
+ expect(line.direction).to.equal('south-west');
+ });
+ it('correctly defines line to west', () => {
+ const line = new PointToPointLine({ x: 1, y: 0 }, { x: 0, y: 0 });
+ expect(line.direction).to.equal('west');
+ });
+ it('correctly defines line to north-west', () => {
+ const line = new PointToPointLine({ x: 1, y: 1 }, { x: 0, y: 0 });
+ expect(line.direction).to.equal('north-west');
+ });
+ });
});
diff --git a/packages/sprotty/src/utils/geometry.ts b/packages/sprotty/src/utils/geometry.ts
index a09dd4e8..b74d1c41 100644
--- a/packages/sprotty/src/utils/geometry.ts
+++ b/packages/sprotty/src/utils/geometry.ts
@@ -1,5 +1,5 @@
/********************************************************************************
- * Copyright (c) 2017-2018 TypeFox and others.
+ * Copyright (c) 2017-2022 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
@@ -388,7 +388,7 @@ export function linear(p0: Point, p1: Point, lambda: number): Point {
/**
* A diamond or rhombus is a quadrilateral whose four sides all have the same length.
- * It consinsts of four points, a `topPoint`, `rightPoint`, `bottomPoint`, and a `leftPoint`,
+ * It consists of four points, a `topPoint`, `rightPoint`, `bottomPoint`, and a `leftPoint`,
* which are connected by four lines -- the `topRightSideLight`, `topLeftSideLine`, `bottomRightSideLine`,
* and the `bottomLeftSideLine`.
*/
@@ -472,6 +472,10 @@ export interface Line {
readonly c: number
}
+export type CardinalDirection =
+ 'north' | 'north-east' | 'east' | 'south-east' |
+ 'south' | 'south-west' | 'west' | 'north-west';
+
/**
* A line made up from two points.
*/
@@ -517,6 +521,33 @@ export class PointToPointLine implements Line {
return this.slope;
}
+ /**
+ * The direction of this line, such as 'north', 'south', or 'south-west'.
+ */
+ get direction(): CardinalDirection {
+ const hDegrees = toDegrees(this.angle);
+ const degrees = hDegrees < 0 ? 360 + hDegrees : hDegrees;
+ // degrees are relative to the x-axis
+ if (degrees === 90) {
+ return 'south';
+ } else if (degrees === 0 || degrees === 360) {
+ return 'east';
+ } else if (degrees === 270) {
+ return 'north';
+ } else if (degrees === 180) {
+ return 'west';
+ } else if (degrees > 0 && degrees < 90) {
+ return 'south-east';
+ } else if (degrees > 90 && degrees < 180) {
+ return 'south-west';
+ } else if (degrees > 180 && degrees < 270) {
+ return 'north-west';
+ } else if (degrees > 270 && degrees < 360) {
+ return 'north-east';
+ }
+ throw new Error(`Cannot determine direction of line (${this.p1.x},${this.p1.y}) to (${this.p2.x},${this.p2.y})`);
+ }
+
/**
* @param otherLine the other line
* @returns the intersection point between `this` line and the `otherLine` if exists, or `undefined`.