Skip to content

Commit

Permalink
impl
Browse files Browse the repository at this point in the history
issue #365
  • Loading branch information
rsoika committed Dec 6, 2024
1 parent 3bad6a2 commit 73fd4ea
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 12 deletions.
288 changes: 288 additions & 0 deletions open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-manhattan-router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
/********************************************************************************
* Copyright (c) 2019-2020 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 {
Action,
Bounds,
GLSPManhattanEdgeRouter,
GModelElement,
GRoutableElement,
isConnectable,
MouseListener,
Point,
RoutedPoint,
translatePoint
} from '@eclipse-glsp/client';
import { inject, injectable } from 'inversify';

/**
* Enhanced Manhattan Edge Router for BPMN diagrams that preserves user-defined routing points
* when moving connected elements. This router extends the default Manhattan router to handle
* element movements while maintaining the Manhattan-style routing (right angles) and respecting
* existing routing points.
*/
export class BPMNManhattanRouter extends GLSPManhattanEdgeRouter {
private debug: boolean = false;

/**
* Stores the current movement information including the delta (x,y) and the ID of the moving element.
* This is updated by the MouseListener and consumed in the next routing calculation.
*/
private currentDelta?: { x: number; y: number; elementId: string };

/**
* Updates the current movement delta. Called by the MouseListener when an element is being dragged.
* @param delta The x,y movement delta
* @param elementId ID of the element being moved
*/
public updateDelta(delta: { x: number; y: number }, elementId: string): void {
this.currentDelta = { ...delta, elementId };
if (this.debug) {
console.log('update delta = ' + delta.x + "," + delta.y + ' for element ' + elementId);
}
}

/**
* Main routing method that calculates the path for an edge. This override handles the following cases:
* 1. Default routing when no movement is happening
* 2. Adjusting the route when the source element is moved
* 3. Adjusting the route when the target element is moved
*
* The method preserves the Manhattan-style routing by ensuring that segments remain either
* horizontal or vertical after adjustments.
*
* @param edge - the edge to be routed
* @returns Array of points describing the complete route
*/
override route(edge: GRoutableElement): RoutedPoint[] {
if (!edge.source || !edge.target) {
return [];
}

const routedCorners = this.createRoutedCorners(edge);
const sourceRefPoint = routedCorners[0] ||
translatePoint(Bounds.center(edge.target.bounds), edge.target.parent, edge.parent);
const sourceAnchor = this.getTranslatedAnchor(
edge.source,
sourceRefPoint,
edge.parent,
edge,
edge.sourceAnchorCorrection
);

const targetRefPoint = routedCorners[routedCorners.length - 1] ||
translatePoint(Bounds.center(edge.source.bounds), edge.source.parent, edge.parent);
const targetAnchor = this.getTranslatedAnchor(
edge.target,
targetRefPoint,
edge.parent,
edge,
edge.targetAnchorCorrection
);

if (!sourceAnchor || !targetAnchor) {
return [];
}

// Build complete route
const completeRoute: RoutedPoint[] = [];
completeRoute.push({ kind: 'source', ...sourceAnchor });
routedCorners.forEach(corner => completeRoute.push(corner));
completeRoute.push({ kind: 'target', ...targetAnchor });

if (this.currentDelta && completeRoute.length >= 3) {
if (this.debug) {
this.printPoints('├── origin route:', completeRoute);
}

const delta = this.currentDelta;
this.currentDelta = undefined;

// Check if source or target is being moved
const isSourceMoving = delta.elementId === edge.source.id;
const isTargetMoving = delta.elementId === edge.target.id;

if (isSourceMoving) {
// Adjust source anchor point
completeRoute[0] = {
kind: 'source',
x: completeRoute[0].x + delta.x,
y: completeRoute[0].y + delta.y
};

// Determine if the first segment is horizontal or vertical
const firstRoutingPoint = completeRoute[1];
const sourcePoint = completeRoute[0];

const isHorizontalSegment = Math.abs(firstRoutingPoint.x - sourcePoint.x) >
Math.abs(firstRoutingPoint.y - sourcePoint.y);

// Adjust the first routing point based on segment type
if (isHorizontalSegment) {
completeRoute[1] = {
kind: 'linear',
x: firstRoutingPoint.x,
y: sourcePoint.y
};
} else {
completeRoute[1] = {
kind: 'linear',
x: sourcePoint.x,
y: firstRoutingPoint.y
};
}
} else if (isTargetMoving) {
// Adjust target anchor point
completeRoute[completeRoute.length - 1] = {
kind: 'target',
x: completeRoute[completeRoute.length - 1].x + delta.x,
y: completeRoute[completeRoute.length - 1].y + delta.y
};
// Determine if the last segment is horizontal or vertical
const lastRoutingPointIndex = completeRoute.length - 2;
const lastRoutingPoint = completeRoute[lastRoutingPointIndex];
const targetPoint = completeRoute[completeRoute.length - 1];

const isHorizontalSegment = Math.abs(targetPoint.x - lastRoutingPoint.x) >
Math.abs(targetPoint.y - lastRoutingPoint.y);

// Adjust the last routing point based on segment type
if (isHorizontalSegment) {
completeRoute[lastRoutingPointIndex] = {
kind: 'linear',
x: lastRoutingPoint.x,
y: targetPoint.y
};
} else {
completeRoute[lastRoutingPointIndex] = {
kind: 'linear',
x: targetPoint.x,
y: lastRoutingPoint.y
};
}
}

// Update the edge's routing points
const newRoutingPoints = completeRoute.slice(1, -1).map(point => ({
x: point.x,
y: point.y
}));
edge.routingPoints = newRoutingPoints;
if (this.debug) {
console.log(`Adjusted route after ${isSourceMoving ? 'source' : 'target'} movement:`, delta);
this.printPoints('├── adjusted route:', completeRoute);
}
return completeRoute;
}

if (this.debug) {
console.log('No adjustment needed, returning original route');
}
return completeRoute;
}

/**
* Helper Debug Method
*
* @param routedPoints
*/
private printPoints(message: string, points: Point[]) {
console.log(message);
points.forEach(point => {
console.log('│ ├── x=' + point.x + ' y=' + point.y);
});
}
}

/**
* Mouse listener that tracks the movement of connectable elements (nodes that can have edges)
* and communicates these movements to the BPMNManhattanRouter. This enables the router to
* adjust edge routes in real-time as elements are being moved.
*/
@injectable()
export class BPMNRouterMoveListener extends MouseListener {
protected isDragging = false;
protected lastPosition?: Point;
protected elementId?: string;

@inject(BPMNManhattanRouter)
protected router: BPMNManhattanRouter;

/**
* Handles mouse button press. Starts tracking if:
* 1. It's a left mouse button press
* 2. The target is a connectable element
*
* @param target The element under the mouse
* @param event Mouse event
*/
override mouseDown(target: GModelElement, event: MouseEvent): Action[] {
if (event.button === 0) { // left mouse button
// Only track connectable elements
if (isConnectable(target)) {
this.isDragging = true;
this.elementId = target.id;
this.lastPosition = {
x: event.clientX,
y: event.clientY
};
}
}
return [];
}

/**
* Handles mouse movement during drag operations. Calculates the movement delta
* and informs the router about the movement.
*
* @param target Current element under the mouse
* @param event Mouse event
*/
override mouseMove(target: GModelElement, event: MouseEvent): Action[] {
if (this.isDragging && this.lastPosition && this.elementId) {
const newPosition = {
x: event.clientX,
y: event.clientY
};

// Calculate movement delta
const delta = {
x: newPosition.x - this.lastPosition.x,
y: newPosition.y - this.lastPosition.y
};

// Inform router about new move event with element ID
this.router.updateDelta(delta, this.elementId);
this.lastPosition = newPosition;
}
return [];
}

/**
* Handles mouse button release. Cleans up tracking state.
*
* @param target The element under the mouse
* @param event Mouse event
*/
override mouseUp(target: GModelElement, event: MouseEvent): Action[] {
if (this.isDragging) {
this.isDragging = false;
this.lastPosition = undefined;
this.elementId = undefined;
}
return [];
}
}
31 changes: 31 additions & 0 deletions open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-router-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/********************************************************************************
* Copyright (c) 2022 Imixs Software Solutions GmbH 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 {
FeatureModule,
GLSPManhattanEdgeRouter,
TYPES
} from '@eclipse-glsp/client';
import { BPMNManhattanRouter, BPMNRouterMoveListener } from './bpmn-manhattan-router';

export * from './bpmn-manhattan-router';
export const BPMNRouterModule = new FeatureModule((bind, unbind, isBound, rebind) => {
// Bind BPMNManhattanRouter as single service
bind(BPMNManhattanRouter).toSelf().inSingletonScope();
// Rebind GLSPManhattanEdgeRouter to BPMNManhattanRouter
rebind(GLSPManhattanEdgeRouter).toService(BPMNManhattanRouter);
// Bind the BPMNMouseListener
bind(TYPES.MouseListener).to(BPMNRouterMoveListener);
});
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,6 @@ import { inject, injectable } from 'inversify';
export class BPMNElementSnapper implements ISnapper {
constructor(public grid: { x: number; y: number } = { x: 1, y: 1 }) { }
snap(position: Point, element: GModelElement): Point {

// move routing-points by 5x5
// if ('volatile-routing-point' === element.type) {
// return {
// x: Math.round(position.x / 5) * 5,
// y: Math.round(position.y / 5) * 5
// };
// }

// default move 1x1...
return {
x: Math.round(position.x),
Expand Down
12 changes: 9 additions & 3 deletions open-bpmn.glsp-client/open-bpmn-glsp/src/di.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
import 'balloon-css/balloon.min.css';
import { Container, ContainerModule } from 'inversify';
import 'sprotty/css/edit-label.css';

import '../css/diagram.css';
import {
BPMNGridView,
Expand All @@ -76,13 +77,13 @@ import {
TaskNodeView,
TextAnnotationNodeView
} from './bpmn-element-views';
import { BPMNRouterModule } from './bpmn-router-module';
import { BPMNEdgeView } from './bpmn-routing-views';
import {
BPMNElementSnapper,
BPMNMultiNodeSelectionListener,
BPMNSelectionHelper
} from './bpmn-select-listeners';

const bpmnDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => {
const context = { bind, unbind, isBound, rebind };

Expand All @@ -102,7 +103,6 @@ const bpmnDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) =>
bind(TYPES.ISelectionListener).to(BPMNSelectionHelper);
bind(TYPES.ISelectionListener).to(BPMNMultiNodeSelectionListener);
bind(TYPES.MouseListener).to(BPMNPropertiesMouseListener);

bind(TYPES.IContextMenuItemProvider).to(DeleteElementContextMenuItemProvider);

// Configure BMW View Elements
Expand Down Expand Up @@ -165,5 +165,11 @@ export function createBPMNDiagramContainer(...containerConfiguration: ContainerC

export function initializeBPMNDiagramContainer(container: Container,
...containerConfiguration: ContainerConfiguration): Container {
return initializeDiagramContainer(container, bpmnDiagramModule, helperLineModule, BPMNPropertyModule, ...containerConfiguration);
return initializeDiagramContainer(
container,
bpmnDiagramModule,
helperLineModule,
BPMNPropertyModule,
BPMNRouterModule,
...containerConfiguration);
}

0 comments on commit 73fd4ea

Please sign in to comment.