From ed4d63ed62e68a99688ac946c55e66393da5f260 Mon Sep 17 00:00:00 2001
From: Guillaume Fontorbe
Date: Wed, 21 Feb 2024 10:29:27 +0100
Subject: [PATCH 1/8] Implement Junction Finder
Signed-off-by: Guillaume Fontorbe
---
.../src/features/edge-junction/di.config.ts | 26 ++
.../features/edge-junction/junction-finder.ts | 229 ++++++++++++++++++
.../sprotty/src/features/routing/routing.ts | 5 +-
packages/sprotty/src/graph/views.tsx | 11 +
packages/sprotty/src/index.ts | 5 +-
5 files changed, 273 insertions(+), 3 deletions(-)
create mode 100644 packages/sprotty/src/features/edge-junction/di.config.ts
create mode 100644 packages/sprotty/src/features/edge-junction/junction-finder.ts
diff --git a/packages/sprotty/src/features/edge-junction/di.config.ts b/packages/sprotty/src/features/edge-junction/di.config.ts
new file mode 100644
index 00000000..e4bc367e
--- /dev/null
+++ b/packages/sprotty/src/features/edge-junction/di.config.ts
@@ -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;
diff --git a/packages/sprotty/src/features/edge-junction/junction-finder.ts b/packages/sprotty/src/features/edge-junction/junction-finder.ts
new file mode 100644
index 00000000..abdb7b12
--- /dev/null
+++ b/packages/sprotty/src/features/edge-junction/junction-finder.ts
@@ -0,0 +1,229 @@
+/********************************************************************************
+ * 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
+ * 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 {
+ apply(routing: EdgeRouting, parent: SParentElementImpl): void {
+ this.findJunctions(routing, parent);
+ }
+
+ findJunctions(routing: EdgeRouting, parent: SParentElementImpl) {
+ // gather all edges from the parent
+ const edges = parent.children.filter(child => child instanceof SEdgeImpl) as SEdgeImpl[];
+
+ 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);
+ // 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);
+ });
+
+ // 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;
+ }
+}
diff --git a/packages/sprotty/src/features/routing/routing.ts b/packages/sprotty/src/features/routing/routing.ts
index 8803dd36..49ed0ac6 100644
--- a/packages/sprotty/src/features/routing/routing.ts
+++ b/packages/sprotty/src/features/routing/routing.ts
@@ -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
}
/**
@@ -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;
}
@injectable()
@@ -178,7 +179,7 @@ export class EdgeRouterRegistry extends InstanceRegistry {
routeAllChildren(parent: Readonly): EdgeRouting {
const routing = this.doRouteAllChildren(parent);
for (const postProcessor of this.postProcessors) {
- postProcessor.apply(routing);
+ postProcessor.apply(routing, parent);
}
return routing;
}
diff --git a/packages/sprotty/src/graph/views.tsx b/packages/sprotty/src/graph/views.tsx
index 3400c221..a63d40c2 100644
--- a/packages/sprotty/src/graph/views.tsx
+++ b/packages/sprotty/src/graph/views.tsx
@@ -81,10 +81,21 @@ export class PolylineEdgeView extends RoutableView {
return
{this.renderLine(edge, route, context, args)}
{this.renderAdditionals(edge, route, context)}
+ {this.renderJunctionPoints(edge, route, context, args)}
{context.renderChildren(edge, { route })}
;
}
+ protected renderJunctionPoints(edge: Readonly, route: RoutedPoint[], context: RenderingContext, args: IViewArgs | undefined) {
+ const junctionPoints = [];
+ for (let i = 1; i < route.length; i++) {
+ if (route[i].isJunction) {
+ junctionPoints.push();
+ }
+ }
+ return {junctionPoints};
+ }
+
protected renderLine(edge: SEdgeImpl, segments: Point[], context: RenderingContext, args?: IViewArgs): VNode {
const firstPoint = segments[0];
let path = `M ${firstPoint.x},${firstPoint.y}`;
diff --git a/packages/sprotty/src/index.ts b/packages/sprotty/src/index.ts
index e86e164d..675a89c3 100644
--- a/packages/sprotty/src/index.ts
+++ b/packages/sprotty/src/index.ts
@@ -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';
@@ -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';
@@ -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
};
From e5458b6e208409f832e060908e666da696423177 Mon Sep 17 00:00:00 2001
From: Guillaume Fontorbe
Date: Wed, 21 Feb 2024 10:47:11 +0100
Subject: [PATCH 2/8] Render junction points group only if junction points are
present
Signed-off-by: Guillaume Fontorbe
---
packages/sprotty/src/graph/views.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/packages/sprotty/src/graph/views.tsx b/packages/sprotty/src/graph/views.tsx
index a63d40c2..05498fa4 100644
--- a/packages/sprotty/src/graph/views.tsx
+++ b/packages/sprotty/src/graph/views.tsx
@@ -93,7 +93,11 @@ export class PolylineEdgeView extends RoutableView {
junctionPoints.push();
}
}
- return {junctionPoints};
+ if (junctionPoints.length > 0) {
+ return {junctionPoints};
+ }
+
+ return undefined;
}
protected renderLine(edge: SEdgeImpl, segments: Point[], context: RenderingContext, args?: IViewArgs): VNode {
From 4a975c8189fde48a8831d84bc1b3c5432e623c6c Mon Sep 17 00:00:00 2001
From: Guillaume Fontorbe
Date: Wed, 21 Feb 2024 16:15:10 +0100
Subject: [PATCH 3/8] Update
packages/sprotty/src/features/edge-junction/junction-finder.ts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Miro Spönemann
---
packages/sprotty/src/features/edge-junction/junction-finder.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/sprotty/src/features/edge-junction/junction-finder.ts b/packages/sprotty/src/features/edge-junction/junction-finder.ts
index abdb7b12..93c87c54 100644
--- a/packages/sprotty/src/features/edge-junction/junction-finder.ts
+++ b/packages/sprotty/src/features/edge-junction/junction-finder.ts
@@ -1,5 +1,5 @@
/********************************************************************************
- * Copyright (c) 2021-2022 EclipseSource and others.
+ * 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
From 6ea06912aa935b1725681d26b308970cab70e09d Mon Sep 17 00:00:00 2001
From: Guillaume Fontorbe
Date: Wed, 21 Feb 2024 16:31:41 +0100
Subject: [PATCH 4/8] Protected functions and simplification of routes
collection
Signed-off-by: Guillaume Fontorbe
---
.../features/edge-junction/junction-finder.ts | 34 +++++--------------
1 file changed, 9 insertions(+), 25 deletions(-)
diff --git a/packages/sprotty/src/features/edge-junction/junction-finder.ts b/packages/sprotty/src/features/edge-junction/junction-finder.ts
index 93c87c54..1ca2fa3d 100644
--- a/packages/sprotty/src/features/edge-junction/junction-finder.ts
+++ b/packages/sprotty/src/features/edge-junction/junction-finder.ts
@@ -31,7 +31,7 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
this.findJunctions(routing, parent);
}
- findJunctions(routing: EdgeRouting, parent: SParentElementImpl) {
+ protected findJunctions(routing: EdgeRouting, parent: SParentElementImpl) {
// gather all edges from the parent
const edges = parent.children.filter(child => child instanceof SEdgeImpl) as SEdgeImpl[];
@@ -46,15 +46,7 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
// 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);
// 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);
- });
-
+ const routesWithSameSource: RoutedPoint[][] = edgesWithSameSource.map(e => routing.get(e.id)).filter(r => r !== undefined) as RoutedPoint[][];
// if there are any routes with the same source, we find the junction points
if (routesWithSameSource.length > 0) {
this.findJunctionPointsWithSameSource(route, routesWithSameSource);
@@ -63,15 +55,7 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
// 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);
- });
-
+ const routesWithSameTarget: RoutedPoint[][] = edgesWithSameTarget.map(e => routing.get(e.id)).filter(r => r !== undefined) as RoutedPoint[][];
// if there are any routes with the same target, we find the junction points
if (routesWithSameTarget.length > 0) {
this.findJunctionPointsWithSameTarget(route, routesWithSameTarget);
@@ -84,7 +68,7 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
/**
* Finds the junction points of routes with the same source
*/
- findJunctionPointsWithSameSource(route: RoutedPoint[], otherRoutes: RoutedPoint[][]) {
+ protected 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);
@@ -103,7 +87,7 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
/**
* Finds the junction points of routes with the same target
*/
- findJunctionPointsWithSameTarget(route: RoutedPoint[], otherRoutes: RoutedPoint[][]) {
+ protected 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) {
@@ -131,7 +115,7 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
* 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) {
+ protected 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]);
@@ -178,7 +162,7 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
* 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) {
+ protected setPreviousPointAsJunction(route: RoutedPoint[], sameSourceRoute: RoutedPoint[], junctionIndex: number) {
route[junctionIndex - 1].isJunction = true;
sameSourceRoute[junctionIndex - 1].isJunction = true;
}
@@ -187,7 +171,7 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
* 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) {
+ protected getSegmentDirection(firstPoint: RoutedPoint, secondPoint: RoutedPoint) {
const dX = secondPoint.x - firstPoint.x;
const dY = secondPoint.y - firstPoint.y;
@@ -215,7 +199,7 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
* Finds the index where two routes diverge.
* Returns -1 if no divergence can be found.
*/
- getJunctionIndex(firstRoute: RoutedPoint[], secondRoute: RoutedPoint[]): number {
+ protected getJunctionIndex(firstRoute: RoutedPoint[], secondRoute: RoutedPoint[]): number {
let idx = 0;
while (idx < firstRoute.length && idx < secondRoute.length) {
if (firstRoute[idx].x !== secondRoute[idx].x
From 1c2f3b76751c6710f799711a1c4d78ef2bea68de Mon Sep 17 00:00:00 2001
From: Guillaume Fontorbe
Date: Thu, 22 Feb 2024 09:47:27 +0100
Subject: [PATCH 5/8] Use maps for lookup
Signed-off-by: Guillaume Fontorbe
---
.../features/edge-junction/junction-finder.ts | 80 +++++++++++++------
.../sprotty/src/features/routing/routing.ts | 2 +-
2 files changed, 55 insertions(+), 27 deletions(-)
diff --git a/packages/sprotty/src/features/edge-junction/junction-finder.ts b/packages/sprotty/src/features/edge-junction/junction-finder.ts
index 1ca2fa3d..e1011548 100644
--- a/packages/sprotty/src/features/edge-junction/junction-finder.ts
+++ b/packages/sprotty/src/features/edge-junction/junction-finder.ts
@@ -27,48 +27,67 @@ import { EdgeRouting, IEdgeRoutePostprocessor, RoutedPoint } from "../routing/ro
*/
@injectable()
export class JunctionFinder implements IEdgeRoutePostprocessor {
+ /** Map of edges as SEdgeImpl for faster lookup by id */
+ protected edgesMap: Map = new Map();
+ /** Map of unique edges ids with the same source */
+ protected sourcesMap: Map> = new Map();
+ /** Map of unique edges ids with the same target */
+ protected targetsMap: Map> = new Map();
+
apply(routing: EdgeRouting, parent: SParentElementImpl): void {
this.findJunctions(routing, parent);
}
protected findJunctions(routing: EdgeRouting, parent: SParentElementImpl) {
// gather all edges from the parent
- const edges = parent.children.filter(child => child instanceof SEdgeImpl) as SEdgeImpl[];
+ const edges = Array.from(parent.index.all().filter(child => child instanceof SEdgeImpl)) as SEdgeImpl[];
+
+ // populate the maps for faster lookup
+ edges.forEach(edge => {
+ this.edgesMap.set(edge.id, edge);
+ const sameSources = this.sourcesMap.get(edge.sourceId);
+ if (sameSources) {
+ sameSources.add(edge.id);
+ } else {
+ this.sourcesMap.set(edge.sourceId, new Set([edge.id]));
+ }
- 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);
+ const sameTargets = this.targetsMap.get(edge.targetId);
+ if (sameTargets) {
+ sameTargets.add(edge.id);
+ } else {
+ this.targetsMap.set(edge.targetId, new Set([edge.id]));
+ }
+ });
+ routing.routes.forEach((route, routeId) => {
+ // for each route we find the corresponding edge from the edges map by matching the route id and the edge id
+ const edge = this.edgesMap.get(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);
- // for each edge with the same source we find the corresponding route from the routing
- const routesWithSameSource: RoutedPoint[][] = edgesWithSameSource.map(e => routing.get(e.id)).filter(r => r !== undefined) as RoutedPoint[][];
- // if there are any routes with the same source, we find the junction points
- if (routesWithSameSource.length > 0) {
- this.findJunctionPointsWithSameSource(route, routesWithSameSource);
- }
+ // find the junction points for edges with the same source
+ this.findJunctionPointsWithSameSource(edge, route, routing);
- // 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.map(e => routing.get(e.id)).filter(r => r !== undefined) as RoutedPoint[][];
- // if there are any routes with the same target, we find the junction points
- if (routesWithSameTarget.length > 0) {
- this.findJunctionPointsWithSameTarget(route, routesWithSameTarget);
- }
+ // find the junction points for edges with the same target
+ this.findJunctionPointsWithSameTarget(edge, route, routing);
});
-
-
}
/**
* Finds the junction points of routes with the same source
*/
- protected findJunctionPointsWithSameSource(route: RoutedPoint[], otherRoutes: RoutedPoint[][]) {
+ protected findJunctionPointsWithSameSource(edge: SEdgeImpl, route: RoutedPoint[], routing: EdgeRouting) {
+ // get an array of edge/route ids with the same source as the current edge, excluding the current edge
+ const sourcesSet = this.sourcesMap.get(edge.sourceId);
+ if (!sourcesSet) {
+ return;
+ }
+ const otherRoutesIds = Array.from(sourcesSet).filter(id => id !== edge.id);
+ const otherRoutes = otherRoutesIds.map(id => routing.get(id)).filter(r => r !== undefined) as RoutedPoint[][];
+
+
for (const otherRoute of otherRoutes) {
// finds the index where the two routes diverge
const junctionIndex: number = this.getJunctionIndex(route, otherRoute);
@@ -87,7 +106,16 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
/**
* Finds the junction points of routes with the same target
*/
- protected findJunctionPointsWithSameTarget(route: RoutedPoint[], otherRoutes: RoutedPoint[][]) {
+ protected findJunctionPointsWithSameTarget(edge: SEdgeImpl, route: RoutedPoint[], routing: EdgeRouting) {
+ // get an array of edge/route ids with the same target as the current edge, excluding the current edge
+ const targetsSet = this.targetsMap.get(edge.targetId);
+ if (!targetsSet) {
+ return;
+ }
+ const otherRoutesIds = Array.from(targetsSet).filter(id => id !== edge.id);
+ const otherRoutes = otherRoutesIds.map(id => routing.get(id)).filter(r => r !== undefined) as RoutedPoint[][];
+
+
// we reverse the route so that the target is considered the source for the algorithm
route.reverse();
for (const otherRoute of otherRoutes) {
@@ -132,8 +160,8 @@ export class JunctionFinder implements IEdgeRoutePostprocessor {
}
// 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;
+ route[junctionIndex].x > otherRoute[junctionIndex].x
+ : route[junctionIndex].x < otherRoute[junctionIndex].x;
otherRoute[junctionIndex].isJunction = firstSegmentDirection === 'left' ?
otherRoute[junctionIndex].x > route[junctionIndex].x
diff --git a/packages/sprotty/src/features/routing/routing.ts b/packages/sprotty/src/features/routing/routing.ts
index 49ed0ac6..925f2f7c 100644
--- a/packages/sprotty/src/features/routing/routing.ts
+++ b/packages/sprotty/src/features/routing/routing.ts
@@ -148,7 +148,7 @@ function isMultipleEdgesRouter(
/** A postprocessor that is applied to all routes, once they are computed. */
export interface IEdgeRoutePostprocessor {
- apply(routing: EdgeRouting, parent?: SParentElementImpl): void;
+ apply(routing: EdgeRouting, parent: SParentElementImpl): void;
}
@injectable()
From f57928c1e0b98ea2c33fbc2dadecbe33aa97c7e2 Mon Sep 17 00:00:00 2001
From: Guillaume Fontorbe
Date: Thu, 22 Feb 2024 10:10:22 +0100
Subject: [PATCH 6/8] Add junction points in random example
Signed-off-by: Guillaume Fontorbe
---
examples/random-graph/random-graph.html | 4 ++
examples/random-graph/src/di.config.ts | 3 +-
examples/random-graph/src/standalone.ts | 71 +++++++++++++++++++++++++
packages/sprotty/css/sprotty.css | 4 ++
packages/sprotty/src/graph/views.tsx | 3 +-
5 files changed, 83 insertions(+), 2 deletions(-)
diff --git a/examples/random-graph/random-graph.html b/examples/random-graph/random-graph.html
index 7b71700e..45db8c05 100644
--- a/examples/random-graph/random-graph.html
+++ b/examples/random-graph/random-graph.html
@@ -23,6 +23,10 @@
Sprotty Random Graph Example
+
+
+
+
Help
diff --git a/examples/random-graph/src/di.config.ts b/examples/random-graph/src/di.config.ts
index 991777c3..3f8aba9a 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 { Container, ContainerModule } from 'inversify';
import {
Animation, CommandExecutionContext, configureModelElement, configureViewerOptions, ConsoleLogger,
- edgeIntersectionModule, isSelectable, isViewport, loadDefaultModules, LocalModelSource, LogLevel, PolylineEdgeViewWithGapsOnIntersections,
+ edgeIntersectionModule, edgeJunctionModule, isSelectable, isViewport, loadDefaultModules, LocalModelSource, LogLevel, PolylineEdgeViewWithGapsOnIntersections,
RectangularNodeView, SEdgeImpl, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SModelRootImpl, SNodeImpl, SPortImpl,
TYPES, UpdateAnimationData, UpdateModelCommand, ViewportAnimation
} from 'sprotty';
@@ -64,6 +64,7 @@ export default (containerId: string) => {
const container = new Container();
loadDefaultModules(container);
container.load(edgeIntersectionModule);
+ container.load(edgeJunctionModule);
container.load(elkLayoutModule, randomGraphModule);
return container;
};
diff --git a/examples/random-graph/src/standalone.ts b/examples/random-graph/src/standalone.ts
index 6c2b250c..d9410eae 100644
--- a/examples/random-graph/src/standalone.ts
+++ b/examples/random-graph/src/standalone.ts
@@ -30,6 +30,14 @@ export default function runRandomGraph() {
layoutConfigurator.setDirection((event.target as any)?.value ?? 'LEFT');
modelSource.updateModel();
});
+
+ document.getElementById('junction')!.addEventListener('change', async (event) => {
+ if ((event.target as any).checked) {
+ modelSource.updateModel(createRandomGraphWithJunction());
+ } else {
+ modelSource.updateModel(createRandomGraph());
+ }
+ });
}
const NODES = 50;
@@ -101,3 +109,66 @@ function createRandomGraph(): SGraph {
}
return graph;
}
+
+function createRandomGraphWithJunction(): SGraph {
+ const graph: SGraph = {
+ type: 'graph',
+ id: 'root',
+ children: []
+ };
+
+ for (let i = 0; i < NODES; i++) {
+ const node: SNode = {
+ type: 'node',
+ id: `node${i}`,
+ children: [
+ {
+ type: 'label:node',
+ id: `node${i}_label`,
+ text: i.toString()
+ },
+ {
+ type: 'port',
+ id: `port${i}-in`,
+ size: { width: 8, height: 8 },
+ children: [
+ {
+ type: 'label:port',
+ id: `port${i}-in-label`,
+ text: `in`
+ }
+ ]
+ },
+ {
+ type: 'port',
+ id: `port${i}-out`,
+ size: { width: 8, height: 8 },
+ children: [
+ {
+ type: 'label:port',
+ id: `port${i}-out-label`,
+ text: `out`
+ }
+ ]
+ }
+ ]
+ };
+ graph.children.push(node);
+ }
+
+ for (let i = 0; i < EDGES; i++) {
+ const sourceNo = Math.floor(Math.random() * NODES);
+ const targetNo = Math.floor(Math.random() * NODES);
+ if (sourceNo === targetNo) {
+ continue;
+ }
+ const edge: SEdge = {
+ type: 'edge',
+ id: `edge${i}`,
+ sourceId: `port${sourceNo}-out`,
+ targetId: `port${targetNo}-in`
+ };
+ graph.children.push(edge);
+ }
+ return graph;
+}
diff --git a/packages/sprotty/css/sprotty.css b/packages/sprotty/css/sprotty.css
index 002795d9..1092de67 100644
--- a/packages/sprotty/css/sprotty.css
+++ b/packages/sprotty/css/sprotty.css
@@ -100,4 +100,8 @@
fill: #f00;
font-size: 14pt;
text-anchor: start;
+}
+
+.sprotty-junction {
+ fill: white;
}
\ No newline at end of file
diff --git a/packages/sprotty/src/graph/views.tsx b/packages/sprotty/src/graph/views.tsx
index 05498fa4..1c5d92c3 100644
--- a/packages/sprotty/src/graph/views.tsx
+++ b/packages/sprotty/src/graph/views.tsx
@@ -87,10 +87,11 @@ export class PolylineEdgeView extends RoutableView {
}
protected renderJunctionPoints(edge: Readonly, route: RoutedPoint[], context: RenderingContext, args: IViewArgs | undefined) {
+ const radius = 5;
const junctionPoints = [];
for (let i = 1; i < route.length; i++) {
if (route[i].isJunction) {
- junctionPoints.push();
+ junctionPoints.push();
}
}
if (junctionPoints.length > 0) {
From 2202decd8ae3aad6fcb0b76d73072ab11ca8a27d Mon Sep 17 00:00:00 2001
From: Guillaume Fontorbe
Date: Thu, 22 Feb 2024 12:56:03 +0100
Subject: [PATCH 7/8] Add junction postprocessor
Signed-off-by: Guillaume Fontorbe
---
packages/sprotty/css/sprotty.css | 4 +-
.../src/features/edge-junction/di.config.ts | 3 ++
.../edge-junction/junction-postprocessor.ts | 40 +++++++++++++++++++
packages/sprotty/src/graph/views.tsx | 4 +-
4 files changed, 48 insertions(+), 3 deletions(-)
create mode 100644 packages/sprotty/src/features/edge-junction/junction-postprocessor.ts
diff --git a/packages/sprotty/css/sprotty.css b/packages/sprotty/css/sprotty.css
index 1092de67..e21dbba8 100644
--- a/packages/sprotty/css/sprotty.css
+++ b/packages/sprotty/css/sprotty.css
@@ -103,5 +103,7 @@
}
.sprotty-junction {
- fill: white;
+ stroke: #000;
+ stroke-width: 1;
+ fill: #fff;
}
\ No newline at end of file
diff --git a/packages/sprotty/src/features/edge-junction/di.config.ts b/packages/sprotty/src/features/edge-junction/di.config.ts
index e4bc367e..370ff64f 100644
--- a/packages/sprotty/src/features/edge-junction/di.config.ts
+++ b/packages/sprotty/src/features/edge-junction/di.config.ts
@@ -17,10 +17,13 @@
import { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import { JunctionFinder } from "./junction-finder";
+import { JunctionPostProcessor } from "./junction-postprocessor";
const edgeJunctionModule = new ContainerModule(bind => {
bind(JunctionFinder).toSelf().inSingletonScope();
bind(TYPES.IEdgeRoutePostprocessor).toService(JunctionFinder);
+ bind(JunctionPostProcessor).toSelf().inSingletonScope();
+ bind(TYPES.IVNodePostprocessor).toService(JunctionPostProcessor);
});
export default edgeJunctionModule;
diff --git a/packages/sprotty/src/features/edge-junction/junction-postprocessor.ts b/packages/sprotty/src/features/edge-junction/junction-postprocessor.ts
new file mode 100644
index 00000000..97bcc9a1
--- /dev/null
+++ b/packages/sprotty/src/features/edge-junction/junction-postprocessor.ts
@@ -0,0 +1,40 @@
+/********************************************************************************
+ * 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 { injectable } from "inversify";
+import { IVNodePostprocessor } from "../../base/views/vnode-postprocessor";
+import { VNode } from "snabbdom";
+import { Action } from "sprotty-protocol";
+import { SModelElementImpl } from "../../base/model/smodel";
+
+@injectable()
+export class JunctionPostProcessor implements IVNodePostprocessor {
+ decorate(vnode: VNode, element: SModelElementImpl): VNode {
+ return vnode;
+ }
+ postUpdate(cause?: Action | undefined): void {
+ const svg = document.querySelector('svg#sprotty_root > g');
+ if (svg) {
+ const junctionGroups = Array.from(document.querySelectorAll('g.sprotty-junction'));
+
+ junctionGroups.forEach(junctionGroup => {
+ junctionGroup.remove();
+ });
+
+ svg.append(...junctionGroups);
+ }
+ }
+}
diff --git a/packages/sprotty/src/graph/views.tsx b/packages/sprotty/src/graph/views.tsx
index 1c5d92c3..14ddba8e 100644
--- a/packages/sprotty/src/graph/views.tsx
+++ b/packages/sprotty/src/graph/views.tsx
@@ -91,11 +91,11 @@ export class PolylineEdgeView extends RoutableView {
const junctionPoints = [];
for (let i = 1; i < route.length; i++) {
if (route[i].isJunction) {
- junctionPoints.push();
+ junctionPoints.push();
}
}
if (junctionPoints.length > 0) {
- return {junctionPoints};
+ return {junctionPoints};
}
return undefined;
From 215ef93fbe63f71f4f28336b2403dbab5003097b Mon Sep 17 00:00:00 2001
From: Guillaume Fontorbe
Date: Wed, 28 Feb 2024 15:40:37 +0100
Subject: [PATCH 8/8] Add unit tests
Signed-off-by: Guillaume Fontorbe
---
.../edge-junction/junction-finder.spec.ts | 264 ++++++++++++++++++
.../edge-junction/junction-postprocessor.ts | 13 +-
2 files changed, 275 insertions(+), 2 deletions(-)
create mode 100644 packages/sprotty/src/features/edge-junction/junction-finder.spec.ts
diff --git a/packages/sprotty/src/features/edge-junction/junction-finder.spec.ts b/packages/sprotty/src/features/edge-junction/junction-finder.spec.ts
new file mode 100644
index 00000000..5ab99151
--- /dev/null
+++ b/packages/sprotty/src/features/edge-junction/junction-finder.spec.ts
@@ -0,0 +1,264 @@
+/********************************************************************************
+ * 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 'reflect-metadata';
+
+import { Point } from 'sprotty-protocol';
+import { assert, describe, expect, it } from 'vitest';
+import { SEdgeImpl, SGraphImpl } from '../../graph/sgraph';
+import { EdgeRouting, RoutedPoint } from '../routing/routing';
+import { JunctionFinder } from './junction-finder';
+
+
+describe('JunctionFinder', () => {
+ it('should find no junction points on two identical paths', () => {
+ const model = new SGraphImpl();
+ const edge1 = createEdge('edge1', 'A', 'B');
+ const edge2 = createEdge('edge2', 'A', 'B');
+ model.add(edge1);
+ model.add(edge2);
+
+ const routing = new EdgeRouting();
+ const route1 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 0}, {x: 100, y: 100}]);
+ const route2 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 0}, {x: 100, y: 100}]);
+
+ routing.routes.set(edge1.id, route1);
+ routing.routes.set(edge2.id, route2);
+
+ const finder = new JunctionFinder();
+ finder.apply(routing, model);
+
+ const allJunctionPoints = getAllJunctionPoints(routing);
+
+ checkAllJunctionPoints([], allJunctionPoints);
+ });
+
+ it('should find single junction point on diverging edge with same source', () => {
+ const model = new SGraphImpl();
+ const edge1 = createEdge('edge1', 'A', 'B');
+ const edge2 = createEdge('edge2', 'A', 'C');
+ model.add(edge1);
+ model.add(edge2);
+
+ const routing = new EdgeRouting();
+ const route1 = createRouting([{x: 0, y: 0}, {x: 100, y: 0}]);
+ const route2 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 100}, {x: 100, y: 100}]);
+
+ routing.routes.set(edge1.id, route1);
+ routing.routes.set(edge2.id, route2);
+
+ const finder = new JunctionFinder();
+ finder.apply(routing, model);
+
+ const allJunctionPoints = getAllJunctionPoints(routing);
+
+ const expectedJunctionPoint: RoutedPoint = {kind: 'linear', x: 50, y: 0, isJunction: true};
+
+ checkAllJunctionPoints([expectedJunctionPoint], allJunctionPoints);
+ });
+
+ it('should find two junction points on splitting edges with same source', () => {
+ const model = new SGraphImpl();
+ const edge1 = createEdge('edge1', 'A', 'B');
+ const edge2 = createEdge('edge2', 'A', 'C');
+ model.add(edge1);
+ model.add(edge2);
+
+ const routing = new EdgeRouting();
+ const route1 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 100}, {x: 100, y: 100}]);
+ const route2 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: -100}, {x: 100, y: -100}]);
+
+ routing.routes.set(edge1.id, route1);
+ routing.routes.set(edge2.id, route2);
+
+ const finder = new JunctionFinder();
+ finder.apply(routing, model);
+
+ const allJunctionPoints = getAllJunctionPoints(routing);
+
+ const expectedJunctionPoint: RoutedPoint[] = [
+ {kind: 'linear', x: 50, y: 0, isJunction: true},
+ {kind: 'linear', x: 50, y: 0, isJunction: true}
+ ];
+
+ checkAllJunctionPoints(expectedJunctionPoint, allJunctionPoints);
+ });
+
+ it('should find single junction point on diverging edge with same target', () => {
+ const model = new SGraphImpl();
+ const edge1 = createEdge('edge1', 'A', 'C');
+ const edge2 = createEdge('edge2', 'B', 'C');
+ model.add(edge1);
+ model.add(edge2);
+
+ const routing = new EdgeRouting();
+ const route1 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 100}, {x: 100, y: 100}]);
+ const route2 = createRouting([{x: 0, y: 100}, {x: 100, y: 100}]);
+
+ routing.routes.set(edge1.id, route1);
+ routing.routes.set(edge2.id, route2);
+
+ const finder = new JunctionFinder();
+ finder.apply(routing, model);
+
+ const allJunctionPoints = getAllJunctionPoints(routing);
+
+ const expectedJunctionPoint: RoutedPoint[] = [{kind: 'linear', x: 50, y: 100, isJunction: true}];
+
+ checkAllJunctionPoints(expectedJunctionPoint, allJunctionPoints);
+ });
+
+ it('should find two junction points on splitting edges with same target', () => {
+ const model = new SGraphImpl();
+ const edge1 = createEdge('edge1', 'A', 'C');
+ const edge2 = createEdge('edge2', 'B', 'C');
+ model.add(edge1);
+ model.add(edge2);
+
+ const routing = new EdgeRouting();
+ const route1 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 100}, {x: 100, y: 100}]);
+ const route2 = createRouting([{x: 0, y: 200}, {x: 50, y: 200}, {x: 50, y: 100}, {x: 100, y: 100}]);
+
+ routing.routes.set(edge1.id, route1);
+ routing.routes.set(edge2.id, route2);
+
+ const finder = new JunctionFinder();
+ finder.apply(routing, model);
+
+ const allJunctionPoints = getAllJunctionPoints(routing);
+
+ const expectedJunctionPoint: RoutedPoint[] = [
+ {kind: 'linear', x: 50, y: 100, isJunction: true},
+ {kind: 'linear', x: 50, y: 100, isJunction: true}
+ ];
+
+ checkAllJunctionPoints(expectedJunctionPoint, allJunctionPoints);
+ });
+
+ it('should find three junction points on three diverging edges with same source', () => {
+ const model = new SGraphImpl();
+ const edge1 = createEdge('edge1', 'A', 'B');
+ const edge2 = createEdge('edge2', 'A', 'C');
+ const edge3 = createEdge('edge3', 'A', 'D');
+ model.add(edge1);
+ model.add(edge2);
+ model.add(edge3);
+
+ const routing = new EdgeRouting();
+ const route1 = createRouting([{x: 0, y: 0}, {x: 100, y: 0}]);
+ const route2 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 100}, {x: 100, y: 100}]);
+ const route3 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 200}, {x: 100, y: 200}]);
+
+ routing.routes.set(edge1.id, route1);
+ routing.routes.set(edge2.id, route2);
+ routing.routes.set(edge3.id, route3);
+
+ const finder = new JunctionFinder();
+ finder.apply(routing, model);
+
+ const allJunctionPoints = getAllJunctionPoints(routing);
+
+ const expectedJunctionPoint: RoutedPoint[] = [
+ {kind: 'linear', x: 50, y: 0, isJunction: true},
+ {kind: 'linear', x: 50, y: 100, isJunction: true},
+ {kind: 'linear', x: 50, y: 0, isJunction: true},
+ ];
+
+ checkAllJunctionPoints(expectedJunctionPoint, allJunctionPoints);
+ });
+
+ it('should find four junction points on three diverging and splitting edges with same source', () => {
+ const model = new SGraphImpl();
+ const edge1 = createEdge('edge1', 'A', 'B');
+ const edge2 = createEdge('edge2', 'A', 'C');
+ const edge3 = createEdge('edge3', 'A', 'D');
+ model.add(edge1);
+ model.add(edge2);
+ model.add(edge3);
+
+ const routing = new EdgeRouting();
+ const route1 = createRouting([{x: 0, y: 0}, {x: 150, y: 0}]);
+ const route2 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 100}, {x: 100, y: 100}, {x: 100, y: 50}, {x: 150, y: 50}]);
+ const route3 = createRouting([{x: 0, y: 0}, {x: 50, y: 0}, {x: 50, y: 100}, {x: 100, y: 100}, {x: 100, y: 150}, {x: 150, y: 150}]);
+
+ routing.routes.set(edge1.id, route1);
+ routing.routes.set(edge2.id, route2);
+ routing.routes.set(edge3.id, route3);
+
+ const finder = new JunctionFinder();
+ finder.apply(routing, model);
+
+ const allJunctionPoints = getAllJunctionPoints(routing);
+
+ const expectedJunctionPoint: RoutedPoint[] = [
+ {kind: 'linear', x: 50, y: 0, isJunction: true},
+ {kind: 'linear', x: 50, y: 0, isJunction: true},
+ {kind: 'linear', x: 100, y: 100, isJunction: true},
+ {kind: 'linear', x: 100, y: 100, isJunction: true},
+ ];
+
+ checkAllJunctionPoints(expectedJunctionPoint, allJunctionPoints);
+ });
+
+});
+
+function createEdge(id: string, source: string, target: string): SEdgeImpl {
+ const edge = new SEdgeImpl();
+ edge.id = id;
+ edge.type = 'edge';
+ edge.sourceId = source;
+ edge.targetId = target;
+ return edge;
+}
+
+function createRouting(points: Point[]): RoutedPoint[] {
+
+ return points.map((p, idx) => {
+ if (idx === 0) {
+ return {kind: 'source', x: p.x, y: p.y};
+ } else if (idx === points.length - 1) {
+ return {kind: 'target', x: p.x, y: p.y};
+ } else {
+ return {kind: 'linear', x: p.x, y: p.y};
+ }
+ });
+}
+
+function getAllJunctionPoints(routing: EdgeRouting): RoutedPoint[] {
+ const junctionPoints: RoutedPoint[] = [];
+
+ routing.routes.forEach(route => {
+ route.forEach(point => {
+ if (point.isJunction) {
+ junctionPoints.push(point);
+ }
+ });
+ });
+
+ return junctionPoints;
+}
+
+function checkAllJunctionPoints(expected: RoutedPoint[], actual: RoutedPoint[]): void {
+ expect(actual.length).toBe(expected.length);
+ for (let i = 0; i < expected.length; i++) {
+ const idx = actual.findIndex(p => p.x === expected[i].x && p.y === expected[i].y);
+ if (idx === -1) {
+ assert.fail('Could not find expected junction point');
+ }
+ actual.splice(idx, 1);
+ }
+ expect(actual.length).toBe(0);
+}
diff --git a/packages/sprotty/src/features/edge-junction/junction-postprocessor.ts b/packages/sprotty/src/features/edge-junction/junction-postprocessor.ts
index 97bcc9a1..a15b2ae9 100644
--- a/packages/sprotty/src/features/edge-junction/junction-postprocessor.ts
+++ b/packages/sprotty/src/features/edge-junction/junction-postprocessor.ts
@@ -14,19 +14,28 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
-import { injectable } from "inversify";
+import { injectable, inject } from "inversify";
import { IVNodePostprocessor } from "../../base/views/vnode-postprocessor";
import { VNode } from "snabbdom";
import { Action } from "sprotty-protocol";
import { SModelElementImpl } from "../../base/model/smodel";
+import { TYPES } from "../../base/types";
+import { ViewerOptions } from "../../base/views/viewer-options";
+/**
+ * Finds all junction points in the first SVG group element (diagram root level) and moves them to the end of the SVG.
+ * This ensures that junction points are rendered on top of all other elements.
+ */
@injectable()
export class JunctionPostProcessor implements IVNodePostprocessor {
+ @inject(TYPES.ViewerOptions) private viewerOptions: ViewerOptions;
+
decorate(vnode: VNode, element: SModelElementImpl): VNode {
return vnode;
}
postUpdate(cause?: Action | undefined): void {
- const svg = document.querySelector('svg#sprotty_root > g');
+ const baseDiv = this.viewerOptions.baseDiv;
+ const svg = document.querySelector(`#${baseDiv} > svg > g`);
if (svg) {
const junctionGroups = Array.from(document.querySelectorAll('g.sprotty-junction'));