UNPKG

sprotty

Version:

A next-gen framework for graphical views

184 lines (156 loc) 6.95 kB
/******************************************************************************** * Copyright (c) 2018 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 { Hoverable, Selectable } from 'sprotty-protocol/lib/model'; import { Bounds, Point } from 'sprotty-protocol/lib/utils/geometry'; import { SChildElementImpl, SModelElementImpl } from '../../base/model/smodel'; import { FluentIterable } from '../../utils/iterable'; import { SShapeElementImpl } from '../bounds/model'; import { deletableFeature } from '../edit/delete'; import { selectFeature } from '../select/model'; import { hoverFeedbackFeature } from '../hover/model'; import { moveFeature } from '../move/model'; export abstract class SRoutableElementImpl extends SChildElementImpl { routerKind?: string; routingPoints: Point[] = []; sourceId: string; targetId: string; sourceAnchorCorrection?: number; targetAnchorCorrection?: number; get source(): SConnectableElementImpl | undefined { return this.index.getById(this.sourceId) as SConnectableElementImpl; } get target(): SConnectableElementImpl | undefined { return this.index.getById(this.targetId) as SConnectableElementImpl; } get bounds(): Bounds { // this should also work for splines, which have the convex hull property return this.routingPoints.reduce<Bounds>((bounds, routingPoint) => Bounds.combine(bounds, { x: routingPoint.x, y: routingPoint.y, width: 0, height: 0 }), Bounds.EMPTY); } } export const connectableFeature = Symbol('connectableFeature'); /** * Feature extension interface for {@link connectableFeature}. */ export interface Connectable { canConnect(routable: SRoutableElementImpl, role: 'source' | 'target'): boolean; } export function isConnectable<T extends SModelElementImpl>(element: T): element is Connectable & T { return element.hasFeature(connectableFeature) && (element as any).canConnect; } export function getAbsoluteRouteBounds(model: Readonly<SRoutableElementImpl>, route: Point[] = model.routingPoints): Bounds { let bounds = getRouteBounds(route); let current: SModelElementImpl = model; while (current instanceof SChildElementImpl) { const parent = current.parent; bounds = parent.localToParent(bounds); current = parent; } return bounds; } export function getRouteBounds(route: Point[]): Bounds { const bounds = { x: NaN, y: NaN, width: 0, height: 0 }; for (const point of route) { if (isNaN(bounds.x)) { bounds.x = point.x; bounds.y = point.y; } else { if (point.x < bounds.x) { bounds.width += bounds.x - point.x; bounds.x = point.x; } else if (point.x > bounds.x + bounds.width) { bounds.width = point.x - bounds.x; } if (point.y < bounds.y) { bounds.height += bounds.y - point.y; bounds.y = point.y; } else if (point.y > bounds.y + bounds.height) { bounds.height = point.y - bounds.y; } } } return bounds; } /** * A connectable element is one that can have outgoing and incoming edges, i.e. it can be the source * or target element of an edge. There are two kinds of connectable elements: nodes (`SNode`) and * ports (`SPort`). A node represents a main entity, while a port is a connection point inside a node. */ export abstract class SConnectableElementImpl extends SShapeElementImpl implements Connectable { get anchorKind(): string | undefined { return undefined; } strokeWidth: number = 0; /** * The incoming edges of this connectable element. They are resolved by the index, which must * be an `SGraphIndex` for efficient lookup. */ get incomingEdges(): FluentIterable<SRoutableElementImpl> { const allEdges = this.index.all().filter(e => e instanceof SRoutableElementImpl) as FluentIterable<SRoutableElementImpl>; return allEdges.filter(e => e.targetId === this.id); } /** * The outgoing edges of this connectable element. They are resolved by the index, which must * be an `SGraphIndex` for efficient lookup. */ get outgoingEdges(): FluentIterable<SRoutableElementImpl> { const allEdges = this.index.all().filter(e => e instanceof SRoutableElementImpl) as FluentIterable<SRoutableElementImpl>; return allEdges.filter(e => e.sourceId === this.id); } canConnect(routable: SRoutableElementImpl, role: 'source' | 'target') { return true; } } export type RoutingHandleKind = 'junction' | 'line' | 'source' | 'target' | 'manhattan-50%' | 'bezier-control-after' | 'bezier-control-before' | 'bezier-junction' | 'bezier-add' | 'bezier-remove'; export class SRoutingHandleImpl extends SChildElementImpl implements Selectable, Hoverable { static readonly DEFAULT_FEATURES = [selectFeature, moveFeature, hoverFeedbackFeature]; /** * 'junction' is a point where two line segments meet, * 'line' is a volatile handle placed on a line segment, * 'source' and 'target' are the respective anchors. */ kind: RoutingHandleKind; /** The actual routing point index (junction) or the previous point index (line). */ pointIndex: number; /** Whether the routing point is being dragged. */ editMode: boolean = false; hoverFeedback: boolean = false; selected: boolean = false; danglingAnchor?: SDanglingAnchorImpl; /** * SRoutingHandles are created using the constructor, so we hard-wire the * default features */ override hasFeature(feature: symbol): boolean { return SRoutingHandleImpl.DEFAULT_FEATURES.indexOf(feature) !== -1; } } export class SDanglingAnchorImpl extends SConnectableElementImpl { static readonly DEFAULT_FEATURES = [deletableFeature]; original?: SModelElementImpl; override type = 'dangling-anchor'; constructor() { super(); this.size = { width: 0, height: 0 }; } } export const edgeInProgressID = 'edge-in-progress'; export const edgeInProgressTargetHandleID = edgeInProgressID + '-target-anchor';