sprotty
Version:
A next-gen framework for graphical views
392 lines (343 loc) • 17.1 kB
text/typescript
/********************************************************************************
* Copyright (c) 2021 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 { inject, injectable } from 'inversify';
import { Action } from 'sprotty-protocol/lib/actions';
import { centerOfLine, Point } from 'sprotty-protocol/lib/utils/geometry';
import { ResolvedHandleMove } from '../move/move';
import { SDanglingAnchorImpl, SRoutableElementImpl, SRoutingHandleImpl } from './model';
import { SModelElementImpl } from '../../base/model/smodel';
import { EdgeRouterRegistry, RoutedPoint } from './routing';
import { AbstractEdgeRouter, LinearRouteOptions } from './abstract-edge-router';
import { MouseListener } from '../../base/views/mouse-tool';
import { Command, CommandExecutionContext, CommandReturn } from '../../base/commands/command';
import { TYPES } from "../../base/types";
import { SEdgeImpl } from '../../graph/sgraph';
export class BezierEdgeRouter extends AbstractEdgeRouter {
static readonly KIND = 'bezier';
static readonly DEFAULT_BEZIER_HANDLE_OFFSET = 25;
get kind() {
return BezierEdgeRouter.KIND;
}
route(edge: SRoutableElementImpl): RoutedPoint[] {
if (!edge.source || !edge.target)
return [];
const rpCount = edge.routingPoints.length;
const source = edge.source;
const target = edge.target;
const result: RoutedPoint[] = [];
result.push({ kind: 'source', x: 0, y: 0 });
if (rpCount === 0) {
// initial values
const [h1, h2] = this.createDefaultBezierHandles(source.position, target.position);
result.push( { kind: 'bezier-control-after', x: h1.x, y: h1.y, pointIndex: 0 } );
result.push( { kind: 'bezier-control-before', x: h2.x, y: h2.y, pointIndex: 1 } );
edge.routingPoints.push(h1);
edge.routingPoints.push(h2);
} else if (rpCount >= 2) {
for (let i = 0; i < rpCount; i++) {
const p = edge.routingPoints[i];
if (i % 3 === 0) {
result.push({ kind: 'bezier-control-after', x: p.x, y: p.y, pointIndex: i });
}
if ((i + 1) % 3 === 0) {
result.push({ kind: 'bezier-junction', x: p.x, y: p.y, pointIndex: i });
} else if ((i + 2) % 3 === 0) {
result.push({ kind: 'bezier-control-before', x: p.x, y: p.y, pointIndex: i });
}
}
}
result.push({ kind: 'target', x: 0, y: 0 });
// use "ends" of edge as reference or next bezier-junction
const p0 = rpCount > 2 ? edge.routingPoints[2] : target.position;
const pn = rpCount > 2 ? edge.routingPoints[edge.routingPoints.length - 3] : source.position;
const sourceAnchor = this.getTranslatedAnchor(source, p0, target.parent, edge, edge.sourceAnchorCorrection);
const targetAnchor = this.getTranslatedAnchor(target, pn, source.parent, edge, edge.targetAnchorCorrection);
result[0] = { kind: 'source', x: sourceAnchor.x, y: sourceAnchor.y };
result[result.length - 1] = { kind: 'target', x: targetAnchor.x, y: targetAnchor.y };
return result;
}
private createDefaultBezierHandles(relH1: Point, relH2: Point): [Point, Point] {
const h1 = {
x: relH1.x - BezierEdgeRouter.DEFAULT_BEZIER_HANDLE_OFFSET,
y: relH1.y
};
const h2 = {
x: relH2.x + BezierEdgeRouter.DEFAULT_BEZIER_HANDLE_OFFSET,
y: relH2.y
};
return [ h1, h2 ];
}
createRoutingHandles(edge: SRoutableElementImpl): void {
// route ensure there are at least 2 routed points
this.route(edge);
this.rebuildHandles(edge);
}
private rebuildHandles(edge: SRoutableElementImpl) {
this.addHandle(edge, 'source', 'routing-point', -2);
this.addHandle(edge, 'bezier-control-after', 'bezier-routing-point', 0);
this.addHandle(edge, 'bezier-add', 'bezier-create-routing-point', 0);
const rpCount = edge.routingPoints.length;
if (rpCount > 2) {
for (let i = 1; i < rpCount - 1; i += 3) {
this.addHandle(edge, 'bezier-control-before', 'bezier-routing-point', i);
// Add two circle for add/remove segments
this.addHandle(edge, 'bezier-add', 'bezier-create-routing-point', i + 1);
this.addHandle(edge, 'bezier-junction', 'routing-point', i + 1);
this.addHandle(edge, 'bezier-remove', 'bezier-remove-routing-point', i + 1);
this.addHandle(edge, 'bezier-control-after', 'bezier-routing-point', i + 2);
// re-position control-pairs
this.moveBezierControlPair(edge.routingPoints[i], i, edge);
}
}
this.addHandle(edge, 'bezier-control-before', 'bezier-routing-point', rpCount - 1);
this.addHandle(edge, 'target', 'routing-point', -1);
}
getInnerHandlePosition(edge: SRoutableElementImpl, route: RoutedPoint[], handle: SRoutingHandleImpl) {
if (handle.kind === 'bezier-control-before' || handle.kind === 'bezier-junction' || handle.kind === 'bezier-control-after') {
for (let i = 0; i < route.length; i++) {
const p = route[i];
if (p.pointIndex === handle.pointIndex && p.kind === handle.kind)
return p;
}
} else if (handle.kind === 'bezier-add') {
const ctrlPoint = this.findBezierControl(edge, route, handle.pointIndex);
return { x: ctrlPoint.x, y: ctrlPoint.y + 12.5};
} else if (handle.kind === 'bezier-remove') {
const ctrlPoint = this.findBezierControl(edge, route, handle.pointIndex);
return { x: ctrlPoint.x, y: ctrlPoint.y - 12.5};
}
return undefined;
}
protected findBezierControl(edge: SRoutableElementImpl, route: RoutedPoint[], handleIndex: number): Point {
let result: Point = { x: route[0].x, y: route[0].y };
if (handleIndex > 0) {
for (const rp of route) {
if (rp.pointIndex !== undefined && rp.pointIndex === handleIndex && rp.kind === 'bezier-junction') {
result = { x: rp.x, y: rp.y };
break;
}
}
}
return result;
}
override applyHandleMoves(edge: SRoutableElementImpl, moves: ResolvedHandleMove[]): void {
moves.forEach(move => {
const handle = move.handle;
let orgPosition = { x: 0, y: 0 };
let relativePos, newControlPos, ctrlPointIndex;
const moveToPos = move.toPosition;
switch (handle.kind) {
case 'bezier-control-before':
case 'bezier-control-after':
// find potential other handle/rp and move
this.moveBezierControlPair(moveToPos, move.handle.pointIndex, edge);
break;
case 'bezier-junction':
const index = handle.pointIndex;
if (index >= 0 && index < edge.routingPoints.length) {
ctrlPointIndex = index - 1;
orgPosition = edge.routingPoints[index];
relativePos = edge.routingPoints[ctrlPointIndex];
newControlPos = this.calcRelativeMove(orgPosition, moveToPos, relativePos);
edge.routingPoints[index] = moveToPos;
this.moveBezierControlPair(newControlPos, ctrlPointIndex, edge);
}
break;
case 'source':
ctrlPointIndex = 0;
relativePos = edge.routingPoints[ctrlPointIndex];
if (!(edge.source instanceof SDanglingAnchorImpl)) {
// detach source
const anchor = new SDanglingAnchorImpl();
anchor.id = edge.id + '_dangling-source';
anchor.original = edge.source;
anchor.position = move.toPosition;
handle.root.add(anchor);
handle.danglingAnchor = anchor;
edge.sourceId = anchor.id;
if (edge.source) orgPosition = edge.source.position;
} else if (handle.danglingAnchor) {
orgPosition = handle.danglingAnchor.position;
handle.danglingAnchor.position = moveToPos;
}
newControlPos = this.calcRelativeMove(orgPosition, moveToPos, relativePos);
this.moveBezierControlPair(newControlPos, ctrlPointIndex, edge);
break;
case 'target':
ctrlPointIndex = edge.routingPoints.length - 1;
relativePos = edge.routingPoints[ctrlPointIndex];
if (!(edge.target instanceof SDanglingAnchorImpl)) {
// detach target
const anchor = new SDanglingAnchorImpl();
anchor.id = edge.id + '_dangling-target';
anchor.original = edge.target;
anchor.position = moveToPos;
handle.root.add(anchor);
handle.danglingAnchor = anchor;
edge.targetId = anchor.id;
if (edge.target) orgPosition = edge.target.position;
} else if (handle.danglingAnchor) {
orgPosition = handle.danglingAnchor.position;
handle.danglingAnchor.position = moveToPos;
}
newControlPos = this.calcRelativeMove(orgPosition, moveToPos, relativePos);
this.moveBezierControlPair(newControlPos, ctrlPointIndex, edge);
break;
default:
break;
}
});
}
protected applyInnerHandleMoves(edge: SRoutableElementImpl, moves: ResolvedHandleMove[]): void {
// not required
}
protected getOptions(edge: SRoutableElementImpl): LinearRouteOptions {
return {
minimalPointDistance: 2,
standardDistance: 0.1,
selfEdgeOffset: 20
};
}
private calcRelativeMove(oldPos: Point, newPos: Point, relativePoint: Point): Point {
return {
x: relativePoint.x - (oldPos.x - newPos.x),
y: relativePoint.y - (oldPos.y - newPos.y)
};
}
public createNewBezierSegment(index: number, edge: SRoutableElementImpl): void {
const routingPoints = edge.routingPoints;
let bezierJunctionPos, start, end: Point;
if (routingPoints.length === 2) {
start = routingPoints[index < 0 ? 0 : index];
end = routingPoints[routingPoints.length - 1];
bezierJunctionPos = centerOfLine(start, end);
} else {
start = routingPoints[index];
end = routingPoints[index + 2];
bezierJunctionPos = centerOfLine(start, end);
}
const [h1, h2] = this.createDefaultBezierHandles(bezierJunctionPos, bezierJunctionPos);
routingPoints.splice(index + 1, 0, h1);
routingPoints.splice(index + 2, 0, bezierJunctionPos);
routingPoints.splice(index + 3, 0, h2);
// ensure handles are correctly positioned
this.moveBezierControlPair(h1, index + 1, edge);
// simple solution for now: just rebuildHandles
edge.removeAll(c => c instanceof SRoutingHandleImpl);
this.rebuildHandles(edge);
}
public removeBezierSegment(index: number, edge: SRoutableElementImpl): void {
const routingPoints = edge.routingPoints;
routingPoints.splice(index - 1, 3);
// simple solution for now: just rebuildHandles
edge.removeAll(c => c instanceof SRoutingHandleImpl);
this.rebuildHandles(edge);
}
private moveBezierControlPair(newPos: Point, ctrlPointIndex: number, edge: SRoutableElementImpl) {
if (ctrlPointIndex >= 0 && ctrlPointIndex < edge.routingPoints.length) {
// find neighbors
const before = ctrlPointIndex - 1;
const after = ctrlPointIndex + 1;
// this is the first control or the last control => nothing to do further
if (before < 0 || after === edge.routingPoints.length) {
edge.routingPoints[ctrlPointIndex] = newPos;
} else {
// behind bezier-junction
if (ctrlPointIndex % 3 === 0) {
this.setBezierMirror(edge, newPos, ctrlPointIndex, false);
// before bezier-junction
} else if ((ctrlPointIndex + 2) % 3 === 0) {
this.setBezierMirror(edge, newPos, ctrlPointIndex, true);
}
}
}
}
private setBezierMirror(edge: SRoutableElementImpl, newPos: Point, pointIndex: number, before: boolean) {
edge.routingPoints[pointIndex] = newPos;
const jct = edge.routingPoints[before ? (pointIndex + 1) : (pointIndex - 1)];
edge.routingPoints[before ? (pointIndex + 2) : (pointIndex - 2)] = {
x: jct.x - (newPos.x - jct.x),
y: jct.y - (newPos.y - jct.y)
};
}
}
/**
* Reacts on mouseDown events if the target kind is bezier-add or bezier-remove
*/
export class BezierMouseListener extends MouseListener {
override mouseDown(target: SModelElementImpl, event: MouseEvent): Action[] {
const result = [];
if (target instanceof SRoutingHandleImpl && (target.kind === 'bezier-add' || target.kind === 'bezier-remove')) {
if (target.type === 'bezier-create-routing-point') {
result.push(AddRemoveBezierSegmentAction.create('add', target.id));
} else if (target.type === 'bezier-remove-routing-point') {
result.push(AddRemoveBezierSegmentAction.create('remove', target.id));
}
}
return result;
}
};
export interface AddRemoveBezierSegmentAction extends Action {
kind: typeof AddRemoveBezierSegmentAction.KIND
targetId: string
actionTask: 'add' | 'remove'
}
export namespace AddRemoveBezierSegmentAction {
export const KIND = 'addRemoveBezierSegment';
export function create(actionTask: 'add' | 'remove', targetId: string): AddRemoveBezierSegmentAction {
return {
kind: KIND,
actionTask,
targetId
};
}
}
export class AddRemoveBezierSegmentCommand extends Command {
static readonly KIND = AddRemoveBezierSegmentAction.KIND;
constructor( protected readonly action: AddRemoveBezierSegmentAction,
protected readonly edgeRouterRegistry?: EdgeRouterRegistry) {
super();
}
execute(context: CommandExecutionContext): CommandReturn {
const index = context.root.index;
const target = index.getById(this.action.targetId);
if (this.edgeRouterRegistry && target instanceof SRoutingHandleImpl) {
const raw = this.edgeRouterRegistry.get((target.parent as SRoutableElementImpl).routerKind);
if (raw instanceof BezierEdgeRouter) {
const router = raw;
for (const child of context.root.children) {
if (child.id === target.parent.id) {
if (this.action.actionTask === 'add') {
router.createNewBezierSegment(target.pointIndex, child as SEdgeImpl);
} else if (this.action.actionTask === 'remove') {
router.removeBezierSegment(target.pointIndex, child as SEdgeImpl);
}
break;
}
}
}
}
return context.root;
}
undo(context: CommandExecutionContext): CommandReturn {
throw new Error('Method not implemented.');
}
redo(context: CommandExecutionContext): CommandReturn {
throw new Error('Method not implemented.');
}
}