Skip to content

Commit

Permalink
Fix edge views that consider intersections
Browse files Browse the repository at this point in the history
Also...
  * Fixes launch config for running single tests
  * Adds line gaps to random-graph example for testing
  * Mention that intersection finder only works for straight segments (eclipse-sprotty#287)

Fixes eclipse-sprotty#277

Change-Id: I3c72de91188e32fcb296ef81f27c6b03e18c8bd0
Signed-off-by: Philip Langer <planger@eclipsesource.com>
  • Loading branch information
planger committed May 6, 2022
1 parent df45775 commit aa27f85
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 28 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions examples/random-graph/src/di.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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, {
Expand All @@ -52,6 +52,7 @@ export default (containerId: string) => {

const container = new Container();
loadDefaultModules(container);
container.load(edgeIntersectionModule);
container.load(elkLayoutModule, randomGraphModule);
return container;
};
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand Down
109 changes: 89 additions & 20 deletions packages/sprotty/src/graph/views.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -132,31 +142,79 @@ export class JumpingPolylineEdgeView extends PolylineEdgeView {
return <path d={path} />;
}

/**
* Returns a path that takes the intersections into account by drawing a line jump or a skip 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.shouldDrawLineSkipOnIntersection(currentLineSegment, otherLineSegment)) {
path += this.createSkipPath(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 `shouldDrawLineSkipOnIntersection()`.
*/
protected shouldDrawLineJumpOnIntersection(currentLineSegment: PointToPointLine, otherLineSegment: PointToPointLine) {
return Math.abs(currentLineSegment.slopeOrMax) < Math.abs(otherLineSegment.slopeOrMax);
}

/**
* Whether or not to draw a line skip on an intersection for the `currentLineSegment`.
* This should usually be inverse of `shouldDrawLineJumpOnIntersection()`.
*/
protected shouldDrawLineSkipOnIntersection(currentLineSegment: PointToPointLine, otherLineSegment: PointToPointLine) {
return !this.shouldDrawLineJumpOnIntersection(currentLineSegment, otherLineSegment);
}

protected getLineSegment(edge: SRoutableElement, intersection: Intersection, args?: IViewArgs, segments?: Point[]): PointToPointLine {
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -215,7 +284,7 @@ export class JumpingPolylineEdgeView extends PolylineEdgeView {
return ` L ${anchorBefore.x},${anchorBefore.y} M ${anchorAfter.x},${anchorAfter.y}`;
}

}
}

@injectable()
export class BezierCurveEdgeView extends RoutableView {
Expand Down Expand Up @@ -372,7 +441,7 @@ export class SBezierCreateHandleView extends SRoutingHandleView {
const text = (handle.kind === "bezier-add") ? "+" : "-";
const node =
<g transform={translation} class-sprotty-routing-handle={true}
class-selected={handle.selected} class-mouseover={handle.hoverFeedback}>
class-selected={handle.selected} class-mouseover={handle.hoverFeedback}>
<circle r={this.getRadius()} />
<text x={textOffsetX} y={textOffsetY} attrs-text-align="middle"
style-font-family="monospace" style-pointer-events="none" style-fill="white">{text}</text>
Expand Down Expand Up @@ -421,7 +490,7 @@ export class SBezierControlHandleView extends SRoutingHandleView {
</g>;
} else {
node = <circle class-sprotty-routing-handle={true} class-selected={handle.selected} class-mouseover={handle.hoverFeedback}
cx={position.x} cy={position.y} r={this.getRadius()} />;
cx={position.x} cy={position.y} r={this.getRadius()} />;
}

setAttr(node, 'data-kind', handle.kind);
Expand Down
39 changes: 38 additions & 1 deletion packages/sprotty/src/utils/geometry.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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');
});
});
});
35 changes: 33 additions & 2 deletions packages/sprotty/src/utils/geometry.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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`.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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`.
Expand Down

0 comments on commit aa27f85

Please sign in to comment.