Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Junction Finder #434

Merged
merged 8 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/sprotty/src/features/edge-junction/di.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/********************************************************************************
* Copyright (c) 2024 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 { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import { JunctionFinder } from "./junction-finder";

const edgeJunctionModule = new ContainerModule(bind => {
bind(JunctionFinder).toSelf().inSingletonScope();
bind(TYPES.IEdgeRoutePostprocessor).toService(JunctionFinder);
});

export default edgeJunctionModule;
229 changes: 229 additions & 0 deletions packages/sprotty/src/features/edge-junction/junction-finder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/********************************************************************************
* Copyright (c) 2021-2022 EclipseSource and others.
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
*
* 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 { injectable } from "inversify";
import { SParentElementImpl } from "../../base/model/smodel";
import { SEdgeImpl } from "../../graph/sgraph";
import { EdgeRouting, IEdgeRoutePostprocessor, RoutedPoint } from "../routing/routing";

/**
* Finds junction points in the edge routes. A junction point is a point where two or more edges split.
* This excludes the source and target points of the edges.
*
* Only works with straight line segments.
*/
@injectable()
export class JunctionFinder implements IEdgeRoutePostprocessor {
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
apply(routing: EdgeRouting, parent: SParentElementImpl): void {
this.findJunctions(routing, parent);
}

findJunctions(routing: EdgeRouting, parent: SParentElementImpl) {
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
// gather all edges from the parent
const edges = parent.children.filter(child => child instanceof SEdgeImpl) as SEdgeImpl[];
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved

routing.routes.forEach((route, routeId) => {
// for each route we find the corresponding edge from the model by matching the route id and the edge id
const edge = edges.find(e => e.id === routeId);

if (!edge) {
return;
}

// we find all edges with the same source as the current edge, excluding the current edge
const edgesWithSameSource = edges.filter(e => e.sourceId === edge.sourceId && e.id !== edge.id);
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
// for each edge with the same source we find the corresponding route from the routing
const routesWithSameSource: RoutedPoint[][] = [];
edgesWithSameSource.forEach(e => {
const foundRoute = routing.get(e.id);
if (!foundRoute) {
return;
}
routesWithSameSource.push(foundRoute);
});
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved

// if there are any routes with the same source, we find the junction points
if (routesWithSameSource.length > 0) {
this.findJunctionPointsWithSameSource(route, routesWithSameSource);
}

// we find all edges with the same target as the current edge, excluding the current edge
const edgesWithSameTarget = edges.filter(e => e.targetId === edge.targetId && e.id !== edge.id);
// for each edge with the same target we find the corresponding route from the routing
const routesWithSameTarget: RoutedPoint[][] = [];
edgesWithSameTarget.forEach(e => {
const routeOfGivenEdge = routing.get(e.id);
if (!routeOfGivenEdge) {
return;
}
routesWithSameTarget.push(routeOfGivenEdge);
});

// if there are any routes with the same target, we find the junction points
if (routesWithSameTarget.length > 0) {
this.findJunctionPointsWithSameTarget(route, routesWithSameTarget);
}
});


}

/**
* Finds the junction points of routes with the same source
*/
findJunctionPointsWithSameSource(route: RoutedPoint[], otherRoutes: RoutedPoint[][]) {
for (const otherRoute of otherRoutes) {
// finds the index where the two routes diverge
const junctionIndex: number = this.getJunctionIndex(route, otherRoute);

// if no junction point has been found (i.e. the routes are identical)
// or if the junction point is the first point of the routes (i.e the routes diverge at the source)
// we can skip this route
if (junctionIndex === -1 || junctionIndex === 0) {
continue;
}

this.setJunctionPoints(route, otherRoute, junctionIndex);
}
}

/**
* Finds the junction points of routes with the same target
*/
findJunctionPointsWithSameTarget(route: RoutedPoint[], otherRoutes: RoutedPoint[][]) {
// we reverse the route so that the target is considered the source for the algorithm
route.reverse();
for (const otherRoute of otherRoutes) {
// we reverse the other route so that the target is considered the source for the algorithm
otherRoute.reverse();
// finds the index where the two routes diverge
const junctionIndex: number = this.getJunctionIndex(route, otherRoute);

// if no junction point has been found (i.e. the routes are identical)
// or if the junction point is the first point of the routes (i.e the routes diverge at the source)
// we can skip this route
if (junctionIndex === -1 || junctionIndex === 0) {
continue;
}
this.setJunctionPoints(route, otherRoute, junctionIndex);
// we reverse the other route back to its original order
otherRoute.reverse();
}
// we reverse the route back to their original order
route.reverse();
}

/**
* Set the junction points of two routes according to the segments direction.
* If the segments have different directions, the junction point is the previous common point.
* If the segments have the same direction, the junction point is the point with the greatest or lowest value axis value depending on the direction.
*/
setJunctionPoints(route: RoutedPoint[], otherRoute: RoutedPoint[], junctionIndex: number) {
const firstSegmentDirection = this.getSegmentDirection(route[junctionIndex - 1], route[junctionIndex]);
const secondSegmentDirection = this.getSegmentDirection(otherRoute[junctionIndex - 1], otherRoute[junctionIndex]);

// if the two segments have different directions, then the previous common point is the junction point
if (firstSegmentDirection !== secondSegmentDirection) {
this.setPreviousPointAsJunction(route, otherRoute, junctionIndex);
} else { // the two segments have the same direction

if (firstSegmentDirection === 'left' || firstSegmentDirection === 'right') {
// if the segments are going horizontally, but their y values are different, then the previous common point is the junction point
if (route[junctionIndex].y !== otherRoute[junctionIndex].y) {
this.setPreviousPointAsJunction(route, otherRoute, junctionIndex);
return;
}
// depending on the direction, the junction point is the point with the greatest or lowest x value
route[junctionIndex].isJunction = firstSegmentDirection === 'left' ?
route[junctionIndex].x > otherRoute[junctionIndex].x
: route[junctionIndex].x < otherRoute[junctionIndex].x;

otherRoute[junctionIndex].isJunction = firstSegmentDirection === 'left' ?
otherRoute[junctionIndex].x > route[junctionIndex].x
: otherRoute[junctionIndex].x < route[junctionIndex].x;

} else {
// if the segments are going vertically, but their x values are different, then the previous common point is the junction point
if (route[junctionIndex].x !== otherRoute[junctionIndex].x) {
this.setPreviousPointAsJunction(route, otherRoute, junctionIndex);
return;
}
// depending on the direction, the junction point is the point with the greatest or lowest y value
route[junctionIndex].isJunction = firstSegmentDirection === 'up' ?
route[junctionIndex].y > otherRoute[junctionIndex].y
: route[junctionIndex].y < otherRoute[junctionIndex].y;

otherRoute[junctionIndex].isJunction = firstSegmentDirection === 'up' ?
otherRoute[junctionIndex].y > route[junctionIndex].y
: otherRoute[junctionIndex].y < route[junctionIndex].y;
}
}
}

/**
* Set the previous point as a junction point.
* This is used when two segments have the same direction but the other axis is different.
* For example if the routes are going in opposite directions, or if the route don't split orthogonally.
*/
setPreviousPointAsJunction(route: RoutedPoint[], sameSourceRoute: RoutedPoint[], junctionIndex: number) {
route[junctionIndex - 1].isJunction = true;
sameSourceRoute[junctionIndex - 1].isJunction = true;
}

/**
* Get the main direction of a segment.
* The main direction is the axis with the greatest difference between the two points.
*/
getSegmentDirection(firstPoint: RoutedPoint, secondPoint: RoutedPoint) {
const dX = secondPoint.x - firstPoint.x;
const dY = secondPoint.y - firstPoint.y;

let mainDirection = 'horizontal';
if (Math.abs(dX) < Math.abs(dY)) {
mainDirection = 'vertical';
}

if (mainDirection === 'horizontal') {
if (dX > 0) {
return 'right';
} else {
return 'left';
}
} else {
if (dY > 0) {
return 'down';
} else {
return 'up';
}
}
}

/**
* Finds the index where two routes diverge.
* Returns -1 if no divergence can be found.
*/
getJunctionIndex(firstRoute: RoutedPoint[], secondRoute: RoutedPoint[]): number {
let idx = 0;
while (idx < firstRoute.length && idx < secondRoute.length) {
if (firstRoute[idx].x !== secondRoute[idx].x
|| firstRoute[idx].y !== secondRoute[idx].y) {
return idx;
}
idx++;
}
return -1;
}
}
5 changes: 3 additions & 2 deletions packages/sprotty/src/features/routing/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { PolylineEdgeRouter } from "./polyline-edge-router";
export interface RoutedPoint extends Point {
kind: 'source' | 'target' | 'linear' | 'bezier-control-before' | 'bezier-junction' | 'bezier-control-after'
pointIndex?: number
isJunction?: boolean
}

/**
Expand Down Expand Up @@ -147,7 +148,7 @@ function isMultipleEdgesRouter(

/** A postprocessor that is applied to all routes, once they are computed. */
export interface IEdgeRoutePostprocessor {
apply(routing: EdgeRouting): void;
apply(routing: EdgeRouting, parent?: SParentElementImpl): void;
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
}

@injectable()
Expand Down Expand Up @@ -178,7 +179,7 @@ export class EdgeRouterRegistry extends InstanceRegistry<IEdgeRouter> {
routeAllChildren(parent: Readonly<SParentElementImpl>): EdgeRouting {
const routing = this.doRouteAllChildren(parent);
for (const postProcessor of this.postProcessors) {
postProcessor.apply(routing);
postProcessor.apply(routing, parent);
}
return routing;
}
Expand Down
15 changes: 15 additions & 0 deletions packages/sprotty/src/graph/views.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,25 @@ export class PolylineEdgeView extends RoutableView {
return <g class-sprotty-edge={true} class-mouseover={edge.hoverFeedback}>
{this.renderLine(edge, route, context, args)}
{this.renderAdditionals(edge, route, context)}
{this.renderJunctionPoints(edge, route, context, args)}
{context.renderChildren(edge, { route })}
</g>;
}

protected renderJunctionPoints(edge: Readonly<SEdgeImpl>, route: RoutedPoint[], context: RenderingContext, args: IViewArgs | undefined) {
const junctionPoints = [];
for (let i = 1; i < route.length; i++) {
if (route[i].isJunction) {
junctionPoints.push(<circle cx={route[i].x} cy={route[i].y} r={5} class-sprotty-junction={true}/>);
gfontorbe marked this conversation as resolved.
Show resolved Hide resolved
}
}
if (junctionPoints.length > 0) {
return <g>{junctionPoints}</g>;
}

return undefined;
}

protected renderLine(edge: SEdgeImpl, segments: Point[], context: RenderingContext, args?: IViewArgs): VNode {
const firstPoint = segments[0];
let path = `M ${firstPoint.x},${firstPoint.y}`;
Expand Down
5 changes: 4 additions & 1 deletion packages/sprotty/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export * from './features/decoration/decoration-placer';
export * from './features/edge-intersection/intersection-finder';
export * from './features/edge-intersection/sweepline';

export * from './features/edge-junction/junction-finder';

export * from './features/move/model';
export * from './features/move/move';
export * from './features/move/snap';
Expand Down Expand Up @@ -159,6 +161,7 @@ import commandPaletteModule from './features/command-palette/di.config';
import contextMenuModule from './features/context-menu/di.config';
import decorationModule from './features/decoration/di.config';
import edgeIntersectionModule from './features/edge-intersection/di.config';
import edgeJunctionModule from './features/edge-junction/di.config';
import edgeLayoutModule from './features/edge-layout/di.config';
import expandModule from './features/expand/di.config';
import exportModule from './features/export/di.config';
Expand All @@ -175,7 +178,7 @@ import zorderModule from './features/zorder/di.config';

export {
boundsModule, buttonModule, commandPaletteModule, contextMenuModule, decorationModule,
edgeIntersectionModule, edgeLayoutModule, expandModule, exportModule, fadeModule, hoverModule, moveModule,
edgeIntersectionModule, edgeJunctionModule, edgeLayoutModule, expandModule, exportModule, fadeModule, hoverModule, moveModule,
openModule, routingModule, selectModule, undoRedoModule, updateModule, viewportModule, zorderModule
};

Expand Down
Loading