UNPKG

sprotty

Version:

A next-gen framework for graphical views

249 lines (217 loc) 8.72 kB
/******************************************************************************** * Copyright (c) 2017-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 { SModelElement } from 'sprotty-protocol/lib/model'; import { Bounds, isBounds, Point } from 'sprotty-protocol/lib/utils/geometry'; import { FluentIterable, mapIterable } from '../../utils/iterable'; /** * Base class for all elements of the internal diagram model. * Each model element must have a unique ID and a type that is used to look up its view. */ export class SModelElementImpl { type: string; id: string; features?: FeatureSet; cssClasses?: string[]; get root(): SModelRootImpl { let current: SModelElementImpl | undefined = this; while (current) { if (current instanceof SModelRootImpl) return current; else if (current instanceof SChildElementImpl) current = current.parent; else current = undefined; } throw new Error('Element has no root'); } get index(): ModelIndexImpl { return this.root.index; } /** * A feature is a symbol identifying some functionality that can be enabled or disabled for * a model element. The set of supported features is determined by the `features` property. */ hasFeature(feature: symbol): boolean { return this.features !== undefined && this.features.has(feature); } } export interface FeatureSet { has(feature: symbol): boolean } export function isParent(element: SModelElement | SModelElementImpl): element is SModelElement & { children: SModelElement[] } { const children = (element as any).children; return children !== undefined && children.constructor === Array; } /** * A parent element may contain child elements, thus the diagram model forms a tree. */ export class SParentElementImpl extends SModelElementImpl { readonly children: ReadonlyArray<SChildElementImpl> = []; add(child: SChildElementImpl, index?: number) { const children = this.children as SChildElementImpl[]; if (index === undefined) { children.push(child); } else { if (index < 0 || index > this.children.length) { throw new Error(`Child index ${index} out of bounds (0..${children.length})`); } children.splice(index, 0, child); } (child as {parent: SParentElementImpl}).parent = this; this.index.add(child); } remove(child: SChildElementImpl) { const children = this.children as SChildElementImpl[]; const i = children.indexOf(child); if (i < 0) { throw new Error(`No such child ${child.id}`); } children.splice(i, 1); this.index.remove(child); } removeAll(filter?: (e: SChildElementImpl) => boolean) { const children = this.children as SChildElementImpl[]; if (filter !== undefined) { for (let i = children.length - 1; i >= 0; i--) { if (filter(children[i])) { const child = children.splice(i, 1)[0]; this.index.remove(child); } } } else { children.forEach(child => { this.index.remove(child); }); children.splice(0, children.length); } } move(child: SChildElementImpl, newIndex: number) { const children = this.children as SChildElementImpl[]; const i = children.indexOf(child); if (i === -1) { throw new Error(`No such child ${child.id}`); } else { if (newIndex < 0 || newIndex > children.length - 1) { throw new Error(`Child index ${newIndex} out of bounds (0..${children.length})`); } children.splice(i, 1); children.splice(newIndex, 0, child); } } /** * Transform the given bounds from the local coordinate system of this element to the coordinate * system of its parent. This function should consider any transformation that is applied to the * view of this element and its contents. * The base implementation assumes that this element does not define a local coordinate system, * so it leaves the bounds unchanged. */ localToParent(point: Point | Bounds): Bounds { return isBounds(point) ? point : { x: point.x, y: point.y, width: -1, height: -1 }; } /** * Transform the given bounds from the coordinate system of this element's parent to its local * coordinate system. This function should consider any transformation that is applied to the * view of this element and its contents. * The base implementation assumes that this element does not define a local coordinate system, * so it leaves the bounds unchanged. */ parentToLocal(point: Point | Bounds): Bounds { return isBounds(point) ? point : { x: point.x, y: point.y, width: -1, height: -1 }; } } /** * A child element is contained in a parent element. All elements except the model root are child * elements. In order to keep the model class hierarchy simple, every child element is also a * parent element, although for many elements the array of children is empty (i.e. they are * leafs in the model element tree). */ export class SChildElementImpl extends SParentElementImpl { readonly parent: SParentElementImpl; } /** * Base class for the root element of the diagram model tree. */ export class SModelRootImpl extends SParentElementImpl { revision?: number; canvasBounds: Bounds = Bounds.EMPTY; constructor(index = new ModelIndexImpl()) { super(); // Override the index property from SModelElement, which has a getter, with a data property Object.defineProperty(this, 'index', { value: index, writable: false }); } } const ID_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; export function createRandomId(length: number = 8): string { let id = ''; for (let i = 0; i < length; i++) { id += ID_CHARS.charAt(Math.floor(Math.random() * ID_CHARS.length)); } return id; } /** * Used to speed up model element lookup by id. */ export interface IModelIndex { add(element: SModelElement): void remove(element: SModelElement): void contains(element: SModelElement): boolean getById(id: string): SModelElement | undefined } /** * This index implementation is for the _internal model_ that is used for rendering. */ export class ModelIndexImpl implements IModelIndex { private readonly id2element: Map<string, SModelElementImpl> = new Map(); add(element: SModelElementImpl): void { if (!element.id) { do { element.id = createRandomId(); } while (this.contains(element)); } else if (this.contains(element)) { throw new Error('Duplicate ID in model: ' + element.id); } this.id2element.set(element.id, element); if (element instanceof SParentElementImpl) { for (const child of element.children) { this.add(child as any); } } } remove(element: SModelElementImpl): void { this.id2element.delete(element.id); if (element instanceof SParentElementImpl) { for (const child of element.children) { this.remove(child as any); } } } contains(element: SModelElementImpl): boolean { return this.id2element.has(element.id); } getById(id: string): SModelElementImpl | undefined { return this.id2element.get(id); } getAttachedElements(element: SModelElementImpl): FluentIterable<SModelElementImpl> { return []; } all(): FluentIterable<SModelElementImpl> { return mapIterable(this.id2element, ([key, value]: [string, SModelElementImpl]) => value); } }