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'));