UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

873 lines (842 loc) 58.6 kB
import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter"; import { Metrics } from "@devexpress/utils/lib/geometry/metrics"; import { Point } from "@devexpress/utils/lib/geometry/point"; import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle"; import { Size } from "@devexpress/utils/lib/geometry/size"; import { Vector } from "@devexpress/utils/lib/geometry/vector"; import { MathUtils } from "@devexpress/utils/lib/utils/math"; import { DiagramUnit } from "../Enums"; import { AddConnectionHistoryItem, SetConnectionPointIndexHistoryItem } from "../History/Common/AddConnectionHistoryItem"; import { AddConnectorHistoryItem } from "../History/Common/AddConnectorHistoryItem"; import { AddConnectorPointHistoryItem } from "../History/Common/AddConnectorPointHistoryItem"; import { AddShapeHistoryItem } from "../History/Common/AddShapeHistoryItem"; import { ChangeConnectorPointsHistoryItem, ReplaceConnectorPointsHistoryItem } from "../History/Common/ChangeConnectorPointsHistoryItem"; import { ChangeShapeParametersHistoryItem } from "../History/Common/ChangeShapeParametersHistoryItem"; import { DeleteConnectionHistoryItem } from "../History/Common/DeleteConnectionHistoryItem"; import { DeleteConnectorHistoryItem } from "../History/Common/DeleteConnectorHistoryItem"; import { DeleteShapeHistoryItem } from "../History/Common/DeleteShapeHistoryItem"; import { InsertToContainerHistoryItem } from "../History/Common/InsertToContainerHistoryItem"; import { MoveConnectorPointHistoryItem, MoveConnectorRightAnglePointsHistoryItem } from "../History/Common/MoveConnectorPointHistoryItem"; import { MoveShapeHistoryItem } from "../History/Common/MoveShapeHistoryItem"; import { RemoveFromContainerHistoryItem } from "../History/Common/RemoveFromContainerHistoryItem"; import { ResizeShapeHistoryItem } from "../History/Common/ResizeShapeHistoryItem"; import { SetSelectionHistoryItem } from "../History/Common/SetSelectionHistoryItem"; import { History } from "../History/History"; import { ModelResizeHistoryItem } from "../History/Page/ModelResizeHistoryItem"; import { UpdatePositionsOnPageResizeHistoryItem } from "../History/Page/UpdatePositionsOnPageResizeHistoryItem"; import { ChangeConnectorPropertyHistoryItem } from "../History/Properties/ChangeConnectorPropertyHistoryItem"; import { ChangeConnectorTextHistoryItem } from "../History/Properties/ChangeConnectorTextHistoryItem"; import { ChangeCustomDataHistoryItem } from "../History/Properties/ChangeCustomDataHistoryItem"; import { ChangeLockedHistoryItem } from "../History/Properties/ChangeLockedHistoryItem"; import { ChangeStyleHistoryItem } from "../History/StyleProperties/ChangeStyleHistoryItem"; import { ChangeStyleTextHistoryItem } from "../History/StyleProperties/ChangeStyleTextHistoryItem"; import { Graph } from "../Layout/Graph"; import { GraphInfo } from "../Layout/GraphInfo"; import { GraphLayout } from "../Layout/GraphLayout"; import { LayoutSettings } from "../Layout/LayoutSettings"; import { NodeInfo } from "../Layout/NodeLayout"; import { Edge } from "../Layout/Structures"; import { Selection } from "../Selection/Selection"; import { GeometryUtils, ObjectUtils } from "../Utils"; import { Connector, ConnectorPosition } from "./Connectors/Connector"; import { ConnectorLineOption } from "./Connectors/ConnectorProperties"; import { ConnectorRenderPoint } from "./Connectors/ConnectorRenderPoint"; import { ConnectorRenderPointsContext } from "./Connectors/Routing/ConnectorRenderPointsContext"; import { DiagramItem, ItemKey } from "./DiagramItem"; import { DiagramModel } from "./Model"; import { Shape } from "./Shapes/Shape"; export class ModelUtils { static setShapePosition(history: History, model: DiagramModel, shape: Shape, newPosition: Point, includeChildren: boolean = true): void { if(!shape.position.equals(newPosition)) { const delta = newPosition.clone().offset(-shape.position.x, -shape.position.y); history.addAndRedo(new MoveShapeHistoryItem(shape.key, newPosition)); if(includeChildren) shape.children.forEach(child => { if(child instanceof Shape) { const childPosition = child.position.clone().offset(delta.x, delta.y); ModelUtils.setShapePosition(history, model, child, childPosition); } }); } } static setShapeSize(history: History, model: DiagramModel, shape: Shape, newPosition: Point, newSize: Size): void { if(!shape.size.equals(newSize) || !shape.position.equals(newPosition)) history.addAndRedo(new ResizeShapeHistoryItem(shape.key, newPosition, newSize)); } static addConnectorPoint(history: History, connectorKey: string, pointIndex: number, position: Point): void { history.addAndRedo(new AddConnectorPointHistoryItem(connectorKey, pointIndex, position)); } static deleteConnectorCustomPoints(history: History, connector: Connector): void { if(connector.points.length > 2) { const oldContext = connector.createRenderPointsContext(); if(connector.properties.lineOption === ConnectorLineOption.Straight || !oldContext) history.addAndRedo(new ReplaceConnectorPointsHistoryItem(connector.key, [ connector.points[0].clone(), connector.points[connector.points.length - 1].clone() ])); else { const beginPoint = connector.points[0].clone(); const lastPoint = connector.points[connector.points.length - 1].clone(); history.addAndRedo(new ChangeConnectorPointsHistoryItem(connector.key, [beginPoint, lastPoint], new ConnectorRenderPointsContext( [ new ConnectorRenderPoint(beginPoint.x, beginPoint.y, 0), new ConnectorRenderPoint(lastPoint.x, lastPoint.y, 1) ], false, oldContext.actualRoutingMode))); } } } static deleteConnectorUnnecessaryPoints(history: History, connector: Connector): void { const oldRenderPoints = connector.getRenderPoints(true).map(p => p.clone()); if(connector.properties.lineOption === ConnectorLineOption.Straight) { const unnecessaryPoints = ModelUtils.createUnnecessaryRenderPoints( oldRenderPoints.filter(p => !p.skipped).map(p => p.clone()), connector.skippedRenderPoints, removedPoint => ModelUtils.findFirstPointIndex(oldRenderPoints, p => p.equals(removedPoint))); if(Object.keys(unnecessaryPoints).length) history.addAndRedo(new ReplaceConnectorPointsHistoryItem( connector.key, ModelUtils.createNecessaryPoints( connector.points.map(p => p.clone()), unnecessaryPoints))); } else { const oldContext = connector.createRenderPointsContext(); const newRenderPoints = oldRenderPoints.filter(p => !p.skipped).map(p => p.clone()); const unnecessaryPoints = ModelUtils.createUnnecessaryRightAngleRenderPoints( newRenderPoints, connector.skippedRenderPoints, removedPoint => ModelUtils.findFirstPointIndex(oldRenderPoints, p => p.equals(removedPoint))); if(Object.keys(unnecessaryPoints).length) { const newPoints = ModelUtils.createNecessaryPoints( connector.points.map(p => p.clone()), unnecessaryPoints); const newRenderContext = new ConnectorRenderPointsContext( ModelUtils.validateRenderPointIndexes(newPoints, newRenderPoints, 0), oldContext.lockCreateRenderPoints, oldContext.actualRoutingMode); history.addAndRedo(new ChangeConnectorPointsHistoryItem(connector.key, newPoints, newRenderContext)); } } } static skipUnnecessaryRenderPoints(points: ConnectorRenderPoint[]) { const clonedPoints = points.map(p => p.clone()); ModelUtils.removeUnnecessaryRenderPoints(clonedPoints); points.forEach(p => p.skipped = clonedPoints.some(cp => cp.skipped && cp.equals(p))); points[0].skipped = false; points[points.length - 1].skipped = false; } static skipUnnecessaryRightAngleRenderPoints(points: ConnectorRenderPoint[]) { const clonedPoints = points.map(p => p.clone()); ModelUtils.removeUnnecessaryRightAngleRenderPoints(clonedPoints); points.forEach(p => p.skipped = clonedPoints.some(cp => cp.skipped && cp.equals(p))); points[0].skipped = false; points[points.length - 1].skipped = false; } static removeUnnecessaryRenderPoints(points: ConnectorRenderPoint[]) : void { GeometryUtils.removeUnnecessaryPoints(points, (pt, index) => ModelUtils.removeUnnecessaryPoint(points, pt, index), pt => pt !== undefined && !pt.skipped ); points[0].skipped = false; points[points.length - 1].skipped = false; } static removeUnnecessaryRightAngleRenderPoints(points: ConnectorRenderPoint[]) : void { GeometryUtils.removeUnnecessaryRightAnglePoints(points, (p, index) => ModelUtils.removeUnnecessaryPoint(points, p, index), p => p !== undefined && !p.skipped ); points[0].skipped = false; points[points.length - 1].skipped = false; } static createUnnecessaryRenderPoints(renderPointsWithoutSkipped: ConnectorRenderPoint[], skippedRenderPoints: ConnectorRenderPoint[], getPosition : (removedPoint : ConnectorRenderPoint) => number, predicate : (p: ConnectorRenderPoint) => boolean = _ => true) : {[key: number] : ConnectorRenderPoint} { const result : {[key: number] : ConnectorRenderPoint} = {}; GeometryUtils.removeUnnecessaryPoints(renderPointsWithoutSkipped, (removedPoint, removedIndex) => { return ModelUtils.collectNotSkippedRenderPoints(result, renderPointsWithoutSkipped, removedPoint, removedIndex, getPosition, predicate); }); ModelUtils.collectSkippedRenderPoints(result, skippedRenderPoints, getPosition, predicate); return result; } static createUnnecessaryRightAngleRenderPoints(renderPointsWithoutSkipped: ConnectorRenderPoint[], skippedRenderPoints: ConnectorRenderPoint[], getPosition : (removedPoint : ConnectorRenderPoint) => number, predicate : (p: ConnectorRenderPoint) => boolean = _ => true) : {[key: number] : ConnectorRenderPoint} { const result : {[key: number] : ConnectorRenderPoint} = {}; GeometryUtils.removeUnnecessaryRightAnglePoints(renderPointsWithoutSkipped, (removedPoint, removedIndex) => { return ModelUtils.collectNotSkippedRenderPoints(result, renderPointsWithoutSkipped, removedPoint, removedIndex, getPosition, predicate); }); ModelUtils.collectSkippedRenderPoints(result, skippedRenderPoints, getPosition, predicate); return result; } static createNecessaryPoints(points: Point[], unnecessaryPoints: {[key: number] : ConnectorRenderPoint}) : Point[] { const result: Point[] = []; const lastPointIndex = points.length - 1; points.forEach((p, index) => { if(index === 0 || index === lastPointIndex || this.isNecessaryPoint(p, index, unnecessaryPoints)) result.push(p.clone()); }); return result; } static isNecessaryPoint(point: Point, pointIndex: number, unnecessaryPoints: {[key: number] : ConnectorRenderPoint}) : boolean { return !Object.keys(unnecessaryPoints).some(key => { const unnecessaryPoint = unnecessaryPoints[key]; return unnecessaryPoint.pointIndex === pointIndex && GeometryUtils.areDuplicatedPoints(point, unnecessaryPoint); }); } private static collectSkippedRenderPoints(targetRenderPoints : {[key: number] : ConnectorRenderPoint}, skippedRenderPoints: ConnectorRenderPoint[], getPosition : (removedPoint : Point) => number, predicate : (p: ConnectorRenderPoint) => boolean = _ => true) : void { skippedRenderPoints && skippedRenderPoints.forEach(skippedPoint => { if(predicate(skippedPoint)) { const positionIndex = getPosition(skippedPoint); if(targetRenderPoints[positionIndex] === undefined) targetRenderPoints[positionIndex] = skippedPoint; } }); } private static collectNotSkippedRenderPoints( targetRenderPoints : {[key: number] : ConnectorRenderPoint}, sourceRenderPoints : ConnectorRenderPoint[], removedPoint: ConnectorRenderPoint, removedIndex: number, getPosition : (removedPoint : Point) => number, predicate : (p: Point) => boolean = _ => true) : boolean { if(!predicate(removedPoint)) return false; const positionIndex = getPosition(removedPoint); if(targetRenderPoints[positionIndex] === undefined) { targetRenderPoints[positionIndex] = removedPoint; removedPoint.skipped = true; sourceRenderPoints.splice(removedIndex, 1); } return true; } private static removeUnnecessaryPoint(points: ConnectorRenderPoint[], point: ConnectorRenderPoint, removedIndex: number) : boolean { if(point.pointIndex === -1) { points.splice(removedIndex, 1); return true; } point.skipped = true; return false; } static validateRenderPointIndexes(points: Point[], renderPoints: ConnectorRenderPoint[], startIndex: number): ConnectorRenderPoint[] { const result = renderPoints.map((rp, i) => new ConnectorRenderPoint( rp.x, rp.y, i >= startIndex && rp.pointIndex >= 0 ? this.findFirstPointIndex(points, p => p.equals(rp)) : rp.pointIndex, rp.skipped)); result[0].skipped = false; result[result.length - 1].skipped = false; return result; } static findFirstPointIndex<T>(points: T[], predicate: (point: T) => boolean) : number { if(!points || !predicate) return -1; for(let i = 0; i < points.length; i++) if(predicate(points[i])) return i; return -1; } static moveConnectorRightAnglePoints(history: History, connector: Connector, firstPoint: Point, firstPointIndex: number, lastPoint: Point, lastPointIndex: number): void { if(!GeometryUtils.areDuplicatedPoints(connector.points[firstPointIndex], firstPoint) || !GeometryUtils.areDuplicatedPoints(connector.points[lastPointIndex], lastPoint)) history.addAndRedo(new MoveConnectorRightAnglePointsHistoryItem( connector.key, firstPointIndex, firstPoint, lastPointIndex, lastPoint)); } static moveConnectorPoint(history: History, connector: Connector, pointIndex: number, newPosition: Point): void { if(!connector.points[pointIndex].equals(newPosition)) history.addAndRedo(new MoveConnectorPointHistoryItem(connector.key, pointIndex, newPosition)); } static updateConnectorAttachedPoints(history: History, model: DiagramModel, connector: Connector): void { history.beginTransaction(); const beginContainer = connector.beginItem && model.findItemCollapsedContainer(connector.beginItem); const beginAttachedToContainer = beginContainer && (!connector.endItem || !model.isContainerItem(beginContainer, connector.endItem)); const endContainer = connector.endItem && model.findItemCollapsedContainer(connector.endItem); const endAttachedToContainer = endContainer && (!connector.beginItem || !model.isContainerItem(endContainer, connector.beginItem)); if(beginAttachedToContainer) this.updateConnectorBeginPoint(history, connector, beginContainer, (endAttachedToContainer && endContainer) || connector.endItem, index => beginContainer.getConnectionPointIndexForItem(connector.beginItem, index) ); else this.updateConnectorBeginPoint(history, connector, connector.beginItem, (endAttachedToContainer && endContainer) || connector.endItem); if(endAttachedToContainer) this.updateConnectorEndPoint(history, connector, endContainer, index => endContainer.getConnectionPointIndexForItem(connector.beginItem, index) ); else this.updateConnectorEndPoint(history, connector, connector.endItem); history.endTransaction(); } private static updateConnectorBeginPoint(history: History, connector: Connector, beginItem: DiagramItem, endItem: DiagramItem, getConnectionPointIndex?: (index: number) => number) { if(beginItem) { const connectionPointIndex = getConnectionPointIndex !== undefined ? getConnectionPointIndex(connector.beginConnectionPointIndex) : connector.beginConnectionPointIndex; let targetPoint = connector.points[1]; if(endItem && connector.points.length === 2) if(connector.endConnectionPointIndex !== -1) targetPoint = endItem.getConnectionPointPosition(connector.endConnectionPointIndex, Point.zero()); else targetPoint = endItem.rectangle.center; const newPoint = beginItem.getConnectionPointPosition(connectionPointIndex, targetPoint); this.moveConnectorPoint(history, connector, 0, newPoint.clone()); } } private static updateConnectorEndPoint(history: History, connector: Connector, endItem: DiagramItem, getConnectionPointIndex?: (index: number) => number) { if(endItem) { const connectionPointIndex = getConnectionPointIndex !== undefined ? getConnectionPointIndex(connector.endConnectionPointIndex) : connector.endConnectionPointIndex; const newPoint = endItem.getConnectionPointPosition(connectionPointIndex, connector.points[connector.points.length - 2]); this.moveConnectorPoint(history, connector, connector.points.length - 1, newPoint); } } static updateContainerConnectorsAttachedPoints(history: History, model: DiagramModel, rootContainer: Shape, container: Shape = rootContainer): void { history.beginTransaction(); const children = model.getChildren(container); children.forEach(child => { if(child instanceof Shape) { child.attachedConnectors.forEach(connector => { const beginItemInContainer = connector.beginItem && model.isContainerItem(container, connector.beginItem); const endItemInContainer = connector.endItem && model.isContainerItem(container, connector.endItem); if(beginItemInContainer && !endItemInContainer) { const collapsedContainer = model.findItemTopCollapsedContainer(connector.beginItem); const endCollapsedContainer = connector.endItem && model.findItemTopCollapsedContainer(connector.endItem); if(!collapsedContainer) this.updateConnectorBeginPoint(history, connector, connector.beginItem, endCollapsedContainer || connector.endItem); else this.updateConnectorBeginPoint(history, connector, collapsedContainer, endCollapsedContainer || connector.endItem, index => rootContainer.getConnectionPointIndexForItem(connector.beginItem, index) ); } if(endItemInContainer && !beginItemInContainer) { const collapsedContainer = model.findItemTopCollapsedContainer(connector.endItem); if(!collapsedContainer) this.updateConnectorEndPoint(history, connector, connector.endItem); else this.updateConnectorEndPoint(history, connector, collapsedContainer, index => rootContainer.getConnectionPointIndexForItem(connector.endItem, index) ); } }); this.updateContainerConnectorsAttachedPoints(history, model, rootContainer, child); } }); history.endTransaction(); } static getConnectorsWithoutBeginItemInfo(model: DiagramModel): { point: Point, connector: Connector }[] { const connectors = model.findConnectorsWithoutBeginItem(); return connectors.map(c => { return { connector: c, point: c.points[0].clone() }; }); } static getConnectorsWithoutEndItemInfo(model: DiagramModel): { point: Point, connector: Connector }[] { const connectors = model.findConnectorsWithoutEndItem(); return connectors.map(c => { return { connector: c, point: c.points[c.points.length - 1].clone() }; }); } static updateShapeAttachedConnectors(history: History, model: DiagramModel, shape: Shape): void { shape.attachedConnectors.forEach(connector => { this.removeConnectorIntermediatePoints(history, connector); this.updateConnectorAttachedPoints(history, model, connector); }); } static updateMovingShapeConnections(history: History, shape: Shape, beginPointsInfo: { point: Point, connector: Connector }[], endPointsInfo: { point: Point, connector: Connector }[], resetTargetCallback: () => void, updateTargetCallback: (shape: Shape, connectionPointIndex: number) => void, beforeAttachConnectorCallback: (connector: Connector) => void): void { resetTargetCallback(); beginPointsInfo.forEach(pi => { const connectionPointIndex = this.getMovingShapeConnectionPointIndex(shape, pi.point); if(shape.rectangle.containsPoint(pi.point) || connectionPointIndex > -1) { updateTargetCallback(shape, connectionPointIndex); if(connectionPointIndex !== pi.connector.beginConnectionPointIndex && pi.connector.beginItem) history.addAndRedo(new DeleteConnectionHistoryItem(pi.connector, ConnectorPosition.Begin)); beforeAttachConnectorCallback(pi.connector); history.addAndRedo(new AddConnectionHistoryItem(pi.connector, shape, connectionPointIndex, ConnectorPosition.Begin)); } else if(pi.connector.beginItem) { history.addAndRedo(new DeleteConnectionHistoryItem(pi.connector, ConnectorPosition.Begin)); history.addAndRedo(new MoveConnectorPointHistoryItem(pi.connector.key, 0, pi.point)); } }); endPointsInfo.forEach(pi => { const connectionPointIndex = this.getMovingShapeConnectionPointIndex(shape, pi.point); if(shape.rectangle.containsPoint(pi.point) || connectionPointIndex > -1) { updateTargetCallback(shape, connectionPointIndex); if(connectionPointIndex !== pi.connector.endConnectionPointIndex && pi.connector.endItem) history.addAndRedo(new DeleteConnectionHistoryItem(pi.connector, ConnectorPosition.End)); beforeAttachConnectorCallback(pi.connector); history.addAndRedo(new AddConnectionHistoryItem(pi.connector, shape, connectionPointIndex, ConnectorPosition.End)); } else if(pi.connector.endItem) { history.addAndRedo(new DeleteConnectionHistoryItem(pi.connector, ConnectorPosition.End)); history.addAndRedo(new MoveConnectorPointHistoryItem(pi.connector.key, pi.connector.points.length - 1, pi.point)); } }); } private static connectionPointActionSize: number = UnitConverter.pixelsToTwips(8); private static getMovingShapeConnectionPointIndex(shape: Shape, point: Point): number { let connectionPointIndex = -1; shape.getConnectionPoints().forEach((pt, index) => { if(Metrics.euclideanDistance(point, pt) < this.connectionPointActionSize) connectionPointIndex = index; }); return connectionPointIndex; } static shouldRemoveConnectorIntermediatePoints(connector: Connector, shapes: DiagramItem[]) : boolean { if(connector.properties.lineOption !== ConnectorLineOption.Orthogonal || connector.points.length === 2 || !shapes || !shapes.length) return false; let index = 0; let shape; while(shape = shapes[index]) { if(this.isShapeIntersectConnectorCustomPoints(shape, connector)) return true; index++; } return false; } static removeConnectorIntermediatePoints(history: History, connector: Connector): void { if(this.shouldRemoveConnectorIntermediatePoints(connector, [connector.beginItem, connector.endItem])) this.deleteConnectorCustomPoints(history, connector); } static isShapeIntersectConnectorCustomPoints(shape: DiagramItem, connector: Connector) : boolean { if(!shape) return false; const customRenderPoints = connector.getCustomRenderPoints(true); if(!customRenderPoints.length) return false; const offset = Connector.minOffset - UnitConverter.pixelsToTwips(1); return GeometryUtils.areIntersectedSegments( GeometryUtils.createSegments(customRenderPoints), GeometryUtils.createSegmentsFromRectangle(shape.rectangle.clone().inflate(offset, offset)) ); } static getSnappedPos(model: DiagramModel, gridSize: number, pos: number, isHorizontal: boolean): number { const snapOffset = isHorizontal ? model.snapStartPoint.x : model.snapStartPoint.y; return Math.round((pos - snapOffset) / gridSize) * gridSize + snapOffset; } static tryUpdateModelRectangle(history: History, processPoints?: (offsetLeft: number, offsetTop: number) => void): void { const offset = history.modelManipulator.getModelSizeUpdateOffset(); if(!offset.isEmpty()) { history.addAndRedo(new ModelResizeHistoryItem(offset)); if(offset.left || offset.top) { history.addAndRedo(new UpdatePositionsOnPageResizeHistoryItem(new Vector(offset.left, offset.top))); if(processPoints !== undefined) processPoints(offset.left, offset.top); } history.modelManipulator.raiseModelRectangleChanged(history.modelManipulator.model.getRectangle(true)); } } static deleteItems(history: History, model: DiagramModel, selection: Selection, items: DiagramItem[], deleteLocked?: boolean): void { history.beginTransaction(); const itemsHash = {}; items.forEach(item => itemsHash[item.key] = item); const selectionKeys = selection.getKeys().filter(key => !itemsHash[key]); history.addAndRedo(new SetSelectionHistoryItem(selection, selectionKeys)); this.deleteItemsCore(history, model, items, deleteLocked); this.tryUpdateModelRectangle(history); history.endTransaction(); } private static deleteItemsCore(history: History, model: DiagramModel, items: DiagramItem[], deleteLocked?: boolean) { items.sort(function(a, b) { const v1 = (a instanceof Connector) ? 0 : 1; const v2 = (b instanceof Connector) ? 0 : 1; return v1 - v2; }); items.forEach(item => { if(item.container) this.removeFromContainer(history, model, item); if(item instanceof Shape) { const children = model.getChildren(item); if(children.length) { children.forEach(child => { history.addAndRedo(new RemoveFromContainerHistoryItem(child)); this.updateAttachedConnectorsContainer(history, model, child); }); this.deleteItemsCore(history, model, children.filter(child => !child.locked || deleteLocked), deleteLocked); } if(model.findItem(item.key)) this.deleteShape(history, item); } if(item instanceof Connector) if(model.findItem(item.key)) this.deleteConnector(history, item); }); } static detachConnectors(history: History, shape: Shape): void { history.beginTransaction(); while(shape.attachedConnectors.length > 0) { const connector = shape.attachedConnectors[0]; history.addAndRedo(new DeleteConnectionHistoryItem(connector, connector.beginItem === shape ? ConnectorPosition.Begin : ConnectorPosition.End)); } history.endTransaction(); } static deleteShape(history: History, shape: Shape): void { const allowed = history.modelManipulator.permissionsProvider.canDeleteItems([shape]); history.beginTransaction(); this.detachConnectors(history, shape); history.addAndRedo(new DeleteShapeHistoryItem(shape.key, allowed)); history.endTransaction(); } static deleteConnector(history: History, connector: Connector): void { history.beginTransaction(); if(connector.beginItem) history.addAndRedo(new DeleteConnectionHistoryItem(connector, ConnectorPosition.Begin)); if(connector.endItem) history.addAndRedo(new DeleteConnectionHistoryItem(connector, ConnectorPosition.End)); history.addAndRedo(new DeleteConnectorHistoryItem(connector.key)); history.endTransaction(); } static deleteAllItems(history: History, model: DiagramModel, selection: Selection): void { this.deleteItems(history, model, selection, model.items.slice(), true); } static deleteSelection(history: History, model: DiagramModel, selection: Selection): void { this.deleteItems(history, model, selection, selection.getSelectedItems()); } static changeSelectionLocked(history: History, model: DiagramModel, selection: Selection, locked: boolean): void { history.beginTransaction(); const items = selection.getSelectedItems(true); items.forEach(item => { history.addAndRedo(new ChangeLockedHistoryItem(item, locked)); }); ModelUtils.updateSelection(history, selection); history.endTransaction(); } private static copyStylesToItem(history: History, model: DiagramModel, fromItem: DiagramItem, newItemKey: ItemKey): void { const toItem = model.findItem(newItemKey); fromItem.styleText.forEach(propertyName => { if(fromItem.styleText[propertyName] !== toItem.styleText[propertyName]) history.addAndRedo( new ChangeStyleTextHistoryItem(newItemKey, propertyName, fromItem.styleText[propertyName]) ); }); fromItem.style.forEach(propertyName => { if(fromItem.style[propertyName] !== toItem.style[propertyName]) history.addAndRedo( new ChangeStyleHistoryItem(newItemKey, propertyName, fromItem.style[propertyName]) ); }); } static updateSelection(history: History, selection: Selection): void { history.addAndRedo(new SetSelectionHistoryItem(selection, selection.getKeys(), true)); } private static cloneShapeToOffset(history: History, model: DiagramModel, shape: Shape, dx: number, dy: number): ItemKey { history.beginTransaction(); const newPosition = shape.position.clone().offset(dx, dy); const addHistoryItem = new AddShapeHistoryItem(shape.description, newPosition, shape.text); history.addAndRedo(addHistoryItem); const newKey = addHistoryItem.shapeKey; history.addAndRedo(new ResizeShapeHistoryItem(newKey, newPosition, shape.size.clone())); history.addAndRedo(new ChangeCustomDataHistoryItem(newKey, ObjectUtils.cloneObject(shape.customData))); history.addAndRedo(new ChangeShapeParametersHistoryItem(newKey, shape.parameters.clone())); this.copyStylesToItem(history, model, shape, newKey); history.endTransaction(); return newKey; } private static applyOffsetToConnectorRenderPointsContext(context: ConnectorRenderPointsContext, dx: number, dy: number) : ConnectorRenderPointsContext { return context && context.renderPoints ? new ConnectorRenderPointsContext(context.renderPoints.map(p => p.clone().offset(dx, dy)), true, context.actualRoutingMode) : undefined; } private static cloneConnectorToOffset(history: History, model: DiagramModel, connector: Connector, beginItemKey: ItemKey, endItemKey: ItemKey, dx: number, dy: number): ItemKey { history.beginTransaction(); const newPoints = connector.points.map(p => p.clone().offset(dx, dy)); const addHistoryItem = new AddConnectorHistoryItem(newPoints, undefined, this.applyOffsetToConnectorRenderPointsContext(connector.createRenderPointsContext(), dx, dy)); history.addAndRedo(addHistoryItem); const newKey = addHistoryItem.connectorKey; const newConnector = model.findConnector(newKey); connector.properties.forEach(propertyName => { if(connector.properties[propertyName] !== newConnector.properties[propertyName]) history.addAndRedo(new ChangeConnectorPropertyHistoryItem(newKey, propertyName, connector.properties[propertyName])); }); if(beginItemKey) { const from = model.findShape(beginItemKey); history.addAndRedo(new AddConnectionHistoryItem(newConnector, from, connector.beginConnectionPointIndex, ConnectorPosition.Begin)); } if(endItemKey) { const to = model.findShape(endItemKey); history.addAndRedo(new AddConnectionHistoryItem(newConnector, to, connector.endConnectionPointIndex, ConnectorPosition.End)); } const newTexts = connector.texts.clone(); newTexts.forEach(connectorText => { history.addAndRedo( new ChangeConnectorTextHistoryItem(newConnector, connectorText.position, connectorText.value) ); }); this.copyStylesToItem(history, model, connector, newKey); history.endTransaction(); return newKey; } static cloneSelectionToOffset(history: History, model: DiagramModel, onItemAdded: (itemKey: ItemKey) => void, selection: Selection, dx: number, dy: number): void { history.beginTransaction(); const newShapes: { [key: string]: ItemKey } = {}; const ids = []; selection.getSelectedShapes().forEach(shape => { const newKey = this.cloneShapeToOffset(history, model, shape, dx, dy); newShapes[shape.key] = newKey; ids.push(newKey); if(onItemAdded) onItemAdded(newKey); }); selection.getSelectedConnectors().forEach(connector => { const beginItemKey = connector.beginItem ? newShapes[connector.beginItem.key] : null; const endItemKey = connector.endItem ? newShapes[connector.endItem.key] : null; const newKey = this.cloneConnectorToOffset(history, model, connector, beginItemKey, endItemKey, dx, dy); ids.push(newKey); if(onItemAdded) onItemAdded(newKey); }); history.addAndRedo(new SetSelectionHistoryItem(selection, ids)); ModelUtils.tryUpdateModelRectangle(history); history.endTransaction(); } static findContainerByEventKey(model: DiagramModel, selection: Selection, key: ItemKey): Shape { const container = model.findContainer(key); if(container && !container.isLocked) return container; else { const shape = model.findShape(key); if(shape && shape.container && !selection.hasKey(shape.key)) return ModelUtils.findContainerByEventKey(model, selection, shape.container.key); } } static canInsertToContainer(model: DiagramModel, item: DiagramItem, container: Shape): boolean { if(item === container) return false; if(item instanceof Shape) if(model.findChild(item, container.key)) return false; return true; } static canInsertSelectionToContainer(model: DiagramModel, selection: Selection, container: Shape): boolean { let result = true; selection.getSelectedItems().forEach(item => { if(item === container) { result = false; return; } if(item instanceof Shape) if(model.findChild(item, container.key)) { result = false; return; } }); return result; } static insertToContainer(history: History, model: DiagramModel, item: DiagramItem, container: Shape): void { if(!container.enableChildren) throw Error("Inpossible to add children to non-container shape."); if(!this.canInsertToContainer(model, item, container)) return; const oldContainer = item.container; if(oldContainer !== container) { history.beginTransaction(); if(oldContainer) { history.addAndRedo(new RemoveFromContainerHistoryItem(item)); item.attachedConnectors.forEach(connector => { if(connector.container) history.addAndRedo(new RemoveFromContainerHistoryItem(connector)); }); } history.addAndRedo(new InsertToContainerHistoryItem(item, container)); this.updateAttachedConnectorsContainer(history, model, item); history.endTransaction(); } } static removeFromContainer(history: History, model: DiagramModel, item: DiagramItem): void { if(item.container) { history.beginTransaction(); history.addAndRedo(new RemoveFromContainerHistoryItem(item)); this.updateAttachedConnectorsContainer(history, model, item); history.endTransaction(); } } static insertSelectionToContainer(history: History, model: DiagramModel, selection: Selection, container: Shape): void { history.beginTransaction(); const selectedItems = selection.getSelectedItems(); const items = selectedItems.filter(item => !item.container || selectedItems.indexOf(item.container) === -1); items.forEach(item => { this.insertToContainer(history, model, item, container); }); history.endTransaction(); } static removeSelectionFromContainer(history: History, model: DiagramModel, selection: Selection): void { history.beginTransaction(); selection.getSelectedItems().forEach(item => { if(item.container && !selection.hasKey(item.container.key)) { history.addAndRedo(new RemoveFromContainerHistoryItem(item)); this.updateAttachedConnectorsContainer(history, model, item); } }); history.endTransaction(); } static getConnectorContainer(connector: Connector): Shape { if(connector.beginItem && connector.endItem) { const beginItemContainers = {}; let containerForBeginItem = connector.beginItem.container; while(containerForBeginItem) { beginItemContainers[containerForBeginItem.key] = true; containerForBeginItem = containerForBeginItem.container; } let containerForEndItem = connector.endItem.container; while(containerForEndItem) { if(beginItemContainers[containerForEndItem.key] !== undefined) return containerForEndItem; containerForEndItem = containerForEndItem.container; } } } static updateAttachedConnectorsContainer(history: History, model: DiagramModel, item: DiagramItem): void { history.beginTransaction(); item.attachedConnectors.forEach(connector => { this.updateConnectorContainer(history, model, connector); }); history.endTransaction(); } static updateConnectorContainer(history: History, model: DiagramModel, connector: Connector): void { const container = this.getConnectorContainer(connector); if(container) history.addAndRedo( new InsertToContainerHistoryItem(connector, container) ); else if(connector.container) history.addAndRedo( new RemoveFromContainerHistoryItem(connector) ); } static updateNewShapeProperties(history: History, selection: Selection, itemKey: ItemKey): void { const style = selection.inputPosition.getDefaultStyle(); style.forEach(propertyName => { history.addAndRedo( new ChangeStyleHistoryItem(itemKey, propertyName, selection.inputPosition.getDefaultStylePropertyValue(propertyName)) ); }); const textStyle = selection.inputPosition.getDefaultTextStyle(); textStyle.forEach(propertyName => { history.addAndRedo( new ChangeStyleTextHistoryItem(itemKey, propertyName, selection.inputPosition.getDefaultTextStylePropertyValue(propertyName)) ); }); } static updateNewConnectorProperties(history: History, selection: Selection, itemKey: ItemKey): void { const connectorProperties = selection.inputPosition.getDefaultConnectorProperties(); connectorProperties.forEach(propertyName => { history.addAndRedo( new ChangeConnectorPropertyHistoryItem(itemKey, propertyName, selection.inputPosition.getDefaultConnectorPropertyValue(propertyName)) ); }); const style = selection.inputPosition.getDefaultStyle(); style.forEach(propertyName => { history.addAndRedo( new ChangeStyleHistoryItem(itemKey, propertyName, selection.inputPosition.getDefaultStylePropertyValue(propertyName)) ); }); const textStyle = selection.inputPosition.getDefaultTextStyle(); textStyle.forEach(propertyName => { history.addAndRedo( new ChangeStyleTextHistoryItem(itemKey, propertyName, selection.inputPosition.getDefaultTextStylePropertyValue(propertyName)) ); }); } static applyLayout(history: History, model: DiagramModel, container: Shape, graph: Graph<NodeInfo>, layout: GraphLayout, nonGraphItems: DiagramItem[], settings: LayoutSettings, snapToGrid: boolean, gridSize: number, skipPointIndices: boolean): Rectangle { history.beginTransaction(); const occupiedRectangles = this.getOccupiedRectangles(nonGraphItems, container); layout = this.offsetLayoutToFreeSpace(layout, container && container.clientRectangle, occupiedRectangles, settings.containerPadding); if(snapToGrid) this.adjustLayoutToSnapGrid(model, layout, gridSize); if(container) this.resizeContainerOnLayout(history, model, layout, container, settings.containerPadding); this.applyLayoutToNodes(history, model, layout, graph.edges.map(e => model.findConnector(e.key))); this.applyLayoutToConnectors(history, model, layout, graph.edges.map(e => model.findConnector(e.key)), skipPointIndices); history.endTransaction(); return layout.getRectangle(true); } static getNonGraphItems(model: DiagramModel, container: Shape, nodeKeyMap: {[nodeKey: string]: any}, shapes: DiagramItem[], connectors: DiagramItem[]): DiagramItem[] { const allItems = container ? model.getChildren(container) : model.items.filter(item => !item.container); return allItems.filter(item => { if(item instanceof Connector) return (!item.beginItem || !nodeKeyMap[item.beginItem.key]) && (!item.endItem || !nodeKeyMap[item.endItem.key]) && connectors.indexOf(item) === -1; if(item instanceof Shape) return !nodeKeyMap[item.key] && shapes.indexOf(item) === -1; }); } static getOccupiedRectangles(nonGraphItems: DiagramItem[], container: Shape): Rectangle[] { const occupiedRectangles = nonGraphItems.map(i => i.rectangle); if(container && occupiedRectangles.length) { const rect = container.clientRectangle; occupiedRectangles.push(new Rectangle(rect.right, rect.y, 1, 1)); occupiedRectangles.push(new Rectangle(rect.right, rect.bottom, 1, 1)); } return occupiedRectangles; } static offsetLayoutToFreeSpace(layout: GraphLayout, containerRect: Rectangle, occupiedRectangles: Rectangle[], spacing: number): GraphLayout { const graphItemRect = layout.getRectangle(true); const freePoint = GeometryUtils.findFreeSpace(occupiedRectangles, graphItemRect.createSize().offset(spacing, spacing).nonNegativeSize(), false, containerRect); if(freePoint) { const x = freePoint.x + spacing; const y = freePoint.y + spacing; return layout.offsetNodes(x, y); } const maxX = occupiedRectangles && occupiedRectangles.length ? occupiedRectangles.reduce((max, rect) => rect.right > max ? rect.right : max, 0) : (containerRect ? containerRect.x : 0); const minY = containerRect ? containerRect.y : Math.max(0, graphItemRect.y); return layout.offsetNodes(maxX + spacing, minY + spacing); } static resizeContainerOnLayout(history: History, model: DiagramModel, layout: GraphLayout, container: Shape, spacing: number): void { const layoutRect = layout.getRectangle(true); const nonLayoutRectangles = container.children .filter(item => { if(item instanceof Shape) return layout.nodeKeys.indexOf(item.key) === -1; if(item instanceof Connector && item.beginItem && item.endItem) return layout.nodeKeys.indexOf(item.beginItem.key) === -1 && layout.nodeKeys.indexOf(item.endItem.key) === -1; return false; }) .map(item => item.rectangle); const right = nonLayoutRectangles.map(rect => rect.right).reduce((prev, cur) => Math.max(prev, cur), layoutRect.right); const bottom = nonLayoutRectangles.map(rect => rect.bottom).reduce((prev, cur) => Math.max(prev, cur), layoutRect.bottom); const width = container.rectangle.width + right + spacing - container.rectangle.right; const height = container.rectangle.height + bottom + spacing - container.rectangle.bottom; ModelUtils.setShapeSize(history, model, container, container.position, new Size(width, height)); ModelUtils.updateShapeAttachedConnectors(history, model, container); } private static applyLayoutToNodes(history: History, model: DiagramModel, layout: GraphLayout, connectors: Connector[]) { const connectorsSet = connectors.reduce((acc, c) => acc[c.key] = true && acc, {}); layout.forEachNode((nl, nk) => { const shape = model.findShape(nk); this.applyLayoutToNode(history, model, shape, nl.position, connectorsSet); }); } private static applyLayoutToNode(history: History, model: DiagramModel, shape: Shape, position: Point, connectorsSet: {[key: string]: any}) { const delta = position.clone().offset(-shape.position.x, -shape.position.y); ModelUtils.setShapePosition(history, model, shape, position, false); if(delta.x !== 0 || delta.y !== 0) { shape.attachedConnectors .filter(c => !connectorsSet[c.key]) .forEach(connector => { this.updateConnectorAttachedPoints(history, model, connector); const beginPointIndex = connector.beginItem ? 1 : 0; const endPointIndex = connector.endItem ? (connector.points.length - 2) : (connector.points.length - 1); for(let i = beginPoi