UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

896 lines (855 loc) 56.6 kB
import { PAGE_BG_TEXTFLOOR_FILTER_IDPREFIX } from "./CanvasManagerBase"; import { GroupPrimitive } from "./Primitives/GroupPrimitive"; import { RenderUtils } from "./Utils"; import { IVisualizersListener } from "../Events/EventManager"; import { Selection, ISelectionChangesListener } from "../Selection/Selection"; import { IMouseOperationsListener, MouseEventElementType, ITextInputOperationListener, ResizeEventSource } from "../Events/Event"; import { ItemKey, ConnectionPointSide, DiagramItem, ItemsMap } from "../Model/DiagramItem"; import { GeometryUtils } from "../Utils"; import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle"; import { Size } from "@devexpress/utils/lib/geometry/size"; import { Point } from "@devexpress/utils/lib/geometry/point"; import { ConnectionPointInfo } from "../Events/Visualizers/ConnectionPointsVisualizer"; import { ExtensionLine, ExtensionLineType } from "../Events/Visualizers/ExtensionLinesVisualizer"; import { Shape } from "../Model/Shapes/Shape"; import { Connector } from "../Model/Connectors/Connector"; import { ConnectorLineOption } from "../Model/Connectors/ConnectorProperties"; import { RectanglePrimitive } from "./Primitives/RectaglePrimitive"; import { SvgPrimitive } from "./Primitives/Primitive"; import { PathPrimitive, PathPrimitiveMoveToCommand, PathPrimitiveLineToCommand, PathPrimitiveCommand } from "./Primitives/PathPrimitive"; import { TextPrimitive } from "./Primitives/TextPrimitive"; import { EllipsePrimitive } from "./Primitives/EllipsePrimitive"; import { IZoomChangesListener, IReadOnlyChangesListener } from "../Settings"; import { ShapeParameterPoint } from "../Model/Shapes/ShapeParameterPoint"; import { ConnectorRenderPoint } from "../Model/Connectors/ConnectorRenderPoint"; import { TextStyle, Style, StrokeStyle } from "../Model/Style"; import { ItemChange } from "../Model/ModelChange"; import { ICanvasViewListener } from "./CanvasViewManager"; import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter"; import { TextOwner } from "./Measurer/ITextMeasurer"; import { DOMManipulator } from "./DOMManipulator"; import { Browser } from "@devexpress/utils/lib/browser"; import { DomUtils } from "@devexpress/utils/lib/utils/dom"; import { NOT_VALID_CSSCLASS } from "./CanvasItemsManager"; import { CanvasManager } from "./CanvasManager"; import { ConnectionTargetInfo } from "../Events/Visualizers/ConnectionTargetVisualizer"; import { ContainerTargetInfo } from "../Events/Visualizers/ContainerTargetVisualizer"; import { Metrics } from "@devexpress/utils/lib/geometry/metrics"; import { MathUtils } from "@devexpress/utils/lib/utils/math"; import { ModelUtils } from "../Model/ModelUtils"; const MULTIPLE_SELECTION_KEY = "-1"; export const SELECTION_ELEMENT_CLASSNAMES = { SELECTION_RECTANGLE: "selection-rect", CONNECTION_POINT: "connection-point", ACTIVE: "active", CONTAINER_TARGET: "container-target", CONNECTION_TARGET: "connection-target", EXTENSION_LINE: "extension-line", CONNECTION_MARK: "connection-mark", SELECTION_MARK: "selection-mark", LOCKED_SELECTION_MARK: "locked-selection-mark", ITEMS_SELECTION_RECT: "items-selection-rect", CONNECTOR_MULTI_SELECTION: "connector-multi-selection", CONNECTOR_SELECTION: "connector-selection", CONNECTOR_POINT_MARK: "connector-point-mark", CONNECTOR_SELECTION_MASK: "connector-selection-mask", CONNECTOR_SIDE_MARK: "connector-side-mark", ITEM_SELECTION_RECT: "item-selection-rect", ITEM_MULTI_SELECTION: "item-multi-selection-rect" }; export const ACTIVE_SELECTION_CSSCLASS = "dxdi-active-selection"; export class CanvasSelectionManager extends CanvasManager implements IVisualizersListener, IMouseOperationsListener, ITextInputOperationListener, ISelectionChangesListener, ICanvasViewListener, IReadOnlyChangesListener { private itemSelectionContainer: SVGElement; private selectionMarksContainer: SVGElement; private visualizersContainer: SVGElement; private selectionRectElement: SVGElement; private resizeInfoElement: SVGElement; private connectionPointElements: SVGElement[] = []; private connectionTargetElement: SVGElement; private containerTargetElement: SVGElement; private extensionLineElements: SVGElement[] = []; private parentContainer: SVGElement; private selectionMap: { [shapeKey: string]: CanvasElement } = {}; static selectionMarkSize: number = UnitConverter.pixelsToTwips(10); static lockedSelectionMarkSize: number = UnitConverter.pixelsToTwips(8); static selectionOffset: number = UnitConverter.pixelsToTwips(2); static selectionRectLineWidth: number = UnitConverter.pixelsToTwips(1); static multiSelectionRectLineWidth: number = UnitConverter.pixelsToTwips(1); static connectionPointSmallSize: number = UnitConverter.pixelsToTwips(5); static connectionPointLargeSize: number = UnitConverter.pixelsToTwips(12); static connectionPointShift: number = UnitConverter.pixelsToTwips(16); static connectionTargetBorderWidth: number = UnitConverter.pixelsToTwips(2); static geomertyMarkSize: number = UnitConverter.pixelsToTwips(8); static connectorPointMarkSize: number = UnitConverter.pixelsToTwips(6); static connectorSideMarkSize: number = UnitConverter.pixelsToTwips(6); static extensionLineWidth: number = UnitConverter.pixelsToTwips(1); static extensionLineOffset: number = UnitConverter.pixelsToTwips(1); static extensionLineEndingSize: number = UnitConverter.pixelsToTwips(6); static resizeInfoOffset: number = UnitConverter.pixelsToTwips(16); static resizeInfoTextOffset: number = UnitConverter.pixelsToTwips(2); static resizeInfoLineWidth: number = UnitConverter.pixelsToTwips(1); static evenOddSelectionCorrection = UnitConverter.pixelsToTwips(1); constructor(parent: SVGElement, zoomLevel: number, private readOnly: boolean, dom: DOMManipulator, instanceId: string) { super(zoomLevel, dom, instanceId); this.parentContainer = parent; this.initializeContainerElements(parent); } private initializeContainerElements(parent: SVGElement) { this.itemSelectionContainer = this.createAndChangePrimitiveElement( new GroupPrimitive([], null), parent ); this.visualizersContainer = this.createAndChangePrimitiveElement( new GroupPrimitive([], null), parent ); this.selectionMarksContainer = this.createAndChangePrimitiveElement( new GroupPrimitive([], null), parent ); } clear() { RenderUtils.removeContent(this.itemSelectionContainer); RenderUtils.removeContent(this.selectionMarksContainer); RenderUtils.removeContent(this.visualizersContainer); this.selectionRectElement = undefined; this.resizeInfoElement = undefined; this.connectionPointElements = []; this.connectionTargetElement = undefined; this.containerTargetElement = undefined; this.extensionLineElements = []; this.selectionMap = {}; } private showSelectionRect(rect: Rectangle) { DomUtils.addClassName(this.parentContainer, ACTIVE_SELECTION_CSSCLASS); const primitive = new RectanglePrimitive(rect.x, rect.y, rect.width, rect.height, StrokeStyle.default1pxInstance, SELECTION_ELEMENT_CLASSNAMES.SELECTION_RECTANGLE); const rectEl = this.getSelectionRectElement(primitive); this.changePrimitiveElement(primitive, rectEl); } private hideSelectionRect() { DomUtils.removeClassName(this.parentContainer, ACTIVE_SELECTION_CSSCLASS); if(this.selectionRectElement !== undefined) this.dom.changeByFunc(this.selectionRectElement, e => e.style.display = "none"); } private getSelectionRectElement(primitive: SvgPrimitive<SVGElement>) { if(this.selectionRectElement !== undefined) this.dom.changeByFunc(this.selectionRectElement, e => e.style.display = ""); else this.selectionRectElement = this.createPrimitiveElement(primitive, this.visualizersContainer); return this.selectionRectElement; } showResizeInfo(point: Point, text: string) { const rectPrimitive = new RectanglePrimitive(point.x, point.y, 0, 0, StrokeStyle.default1pxInstance); const primitive = new GroupPrimitive([ rectPrimitive, new TextPrimitive(point.x, point.y, text, TextOwner.Resize) ], "resize-info"); const groupEl = this.getResizeInfoElement(primitive); this.changePrimitiveElement(primitive, groupEl); const textSize = this.dom.measurer.measureTextLine(text, null, TextOwner.Resize).applyConverter(UnitConverter.pixelsToTwips); rectPrimitive.width = textSize.width + CanvasSelectionManager.resizeInfoTextOffset * 2; rectPrimitive.height = textSize.height + CanvasSelectionManager.resizeInfoTextOffset * 2; rectPrimitive.x = point.x - textSize.width / 2 - CanvasSelectionManager.resizeInfoTextOffset; rectPrimitive.y = point.y - textSize.height / 2 - CanvasSelectionManager.resizeInfoTextOffset; this.changePrimitiveElement(primitive, groupEl); } hideResizeInfo() { if(this.resizeInfoElement !== undefined) this.dom.changeByFunc(this.resizeInfoElement, e => e.style.display = "none"); } private getResizeInfoElement(primitive: SvgPrimitive<SVGElement>) { if(this.resizeInfoElement !== undefined) this.dom.changeByFunc(this.resizeInfoElement, e => e.style.display = ""); else this.resizeInfoElement = this.createPrimitiveElement(primitive, this.visualizersContainer); return this.resizeInfoElement; } getConnectionPointClassName(active: boolean, allowed: boolean): string { let className = SELECTION_ELEMENT_CLASSNAMES.CONNECTION_POINT; if(active) className += " " + SELECTION_ELEMENT_CLASSNAMES.ACTIVE; if(!allowed) className += " " + NOT_VALID_CSSCLASS; return className; } showConnectionPoint(index: number, point: Point, key: ItemKey, value: any, active: boolean, allowed: boolean): void { this.showConnectionPointCore(index * 2, point.x, point.y, CanvasSelectionManager.connectionPointLargeSize, CanvasSelectionManager.connectionPointLargeSize, MouseEventElementType.ShapeConnectionPoint, key, value, SELECTION_ELEMENT_CLASSNAMES.CONNECTION_POINT + " selector" + (!allowed ? " " + NOT_VALID_CSSCLASS : "")); this.showConnectionPointCore(index * 2 + 1, point.x, point.y, CanvasSelectionManager.connectionPointSmallSize, CanvasSelectionManager.connectionPointSmallSize, MouseEventElementType.ShapeConnectionPoint, key, value, this.getConnectionPointClassName(active, allowed)); } showConnectionPointCore(index: number, cx: number, cy: number, rx: number, ry: number, type: MouseEventElementType, key: ItemKey, value: any, className: string) { const primitive = new EllipsePrimitive(cx, cy, rx, ry, null, className, e => RenderUtils.setElementEventData(e, type, key, value)); const ellEl = this.getConnectionPointElement(primitive, index); this.changePrimitiveElement(primitive, ellEl); } private hideConnectionPoints() { for(let i = 0; i < this.connectionPointElements.length; i++) this.dom.changeByFunc(this.connectionPointElements[i], e => e.style.display = "none"); } private getConnectionPointElement(primitive: SvgPrimitive<SVGElement>, index: number) { let ellEl = this.connectionPointElements[index]; if(ellEl !== undefined) this.dom.changeByFunc(ellEl, e => e.style.display = ""); else { ellEl = this.createPrimitiveElement(primitive, this.visualizersContainer); this.connectionPointElements[index] = ellEl; } return ellEl; } private showContainerTarget(index: number, targetRect: Rectangle) { const primitive = new RectanglePrimitive(targetRect.x, targetRect.y, targetRect.width, targetRect.height, null, SELECTION_ELEMENT_CLASSNAMES.CONTAINER_TARGET); const rectEl = this.getContainerTargetElement(primitive); this.changePrimitiveElement(primitive, rectEl); } private hideContainerTarget() { if(this.containerTargetElement) this.dom.changeByFunc(this.containerTargetElement, e => e.style.display = "none"); } private getContainerTargetElement(primitive: SvgPrimitive<SVGElement>) { if(this.containerTargetElement !== undefined) this.dom.changeByFunc(this.containerTargetElement, e => e.style.display = ""); else this.containerTargetElement = this.createPrimitiveElement(primitive, this.itemSelectionContainer); return this.containerTargetElement; } private showConnectionTarget(index: number, targetRect: Rectangle) { const primitive = new RectanglePrimitive(targetRect.x, targetRect.y, targetRect.width, targetRect.height, null, SELECTION_ELEMENT_CLASSNAMES.CONNECTION_TARGET); const rectEl = this.getConnectionTargetElement(primitive); this.changePrimitiveElement(primitive, rectEl); } private hideConnectionTarget() { if(this.connectionTargetElement) this.dom.changeByFunc(this.connectionTargetElement, e => e.style.display = "none"); } private getConnectionTargetElement(primitive: SvgPrimitive<SVGElement>) { if(this.connectionTargetElement !== undefined) this.dom.changeByFunc(this.connectionTargetElement, e => e.style.display = ""); else this.connectionTargetElement = this.createPrimitiveElement(primitive, this.itemSelectionContainer); return this.connectionTargetElement; } private showExtensionLine(index: number, type: ExtensionLineType, startPoint: Point, endPoint: Point, text: string) { let className = SELECTION_ELEMENT_CLASSNAMES.EXTENSION_LINE; if(type === ExtensionLineType.VerticalCenterAfter || type === ExtensionLineType.VerticalCenterBefore || type === ExtensionLineType.HorizontalCenterAbove || type === ExtensionLineType.HorizontalCenterBelow) className += " center"; if(type === ExtensionLineType.VerticalCenterToPageCenter || type === ExtensionLineType.HorizontalCenterToPageCenter || type === ExtensionLineType.LeftToPageCenter || type === ExtensionLineType.RightToPageCenter || type === ExtensionLineType.TopToPageCenter || type === ExtensionLineType.BottomToPageCenter) className += " page"; let x1_1 = 0; let y1_1 = 0; let x1_2 = 0; let y1_2 = 0; let x2_1 = 0; let y2_1 = 0; let x2_2 = 0; let y2_2 = 0; if(startPoint.y === endPoint.y) { x1_1 = startPoint.x - CanvasSelectionManager.extensionLineWidth; y1_1 = startPoint.y - CanvasSelectionManager.extensionLineEndingSize; x1_2 = startPoint.x - CanvasSelectionManager.extensionLineWidth; y1_2 = startPoint.y + CanvasSelectionManager.extensionLineEndingSize; x2_1 = endPoint.x - CanvasSelectionManager.extensionLineWidth; y2_1 = startPoint.y - CanvasSelectionManager.extensionLineEndingSize; x2_2 = endPoint.x - CanvasSelectionManager.extensionLineWidth; y2_2 = startPoint.y + CanvasSelectionManager.extensionLineEndingSize; } else if(startPoint.x === endPoint.x) { x1_1 = startPoint.x - CanvasSelectionManager.extensionLineEndingSize; y1_1 = startPoint.y - CanvasSelectionManager.extensionLineWidth; x1_2 = startPoint.x + CanvasSelectionManager.extensionLineEndingSize; y1_2 = startPoint.y - CanvasSelectionManager.extensionLineWidth; x2_1 = startPoint.x - CanvasSelectionManager.extensionLineEndingSize; y2_1 = endPoint.y - CanvasSelectionManager.extensionLineWidth; x2_2 = startPoint.x + CanvasSelectionManager.extensionLineEndingSize; y2_2 = endPoint.y - CanvasSelectionManager.extensionLineWidth; } let sizeLineXCorr = 0; let sizeLineYCorr = 0; if(type === ExtensionLineType.RightToRightAbove || type === ExtensionLineType.RightToRightBelow) sizeLineXCorr -= CanvasSelectionManager.extensionLineWidth; if(type === ExtensionLineType.BottomToBottomAfter || type === ExtensionLineType.BottomToBottomBefore) sizeLineYCorr -= CanvasSelectionManager.extensionLineWidth; const children = [ new PathPrimitive([ PathPrimitiveMoveToCommand.fromPoint(startPoint.clone().offset(sizeLineXCorr, sizeLineYCorr)), PathPrimitiveLineToCommand.fromPoint(endPoint.clone().offset(sizeLineXCorr, sizeLineYCorr)) ], StrokeStyle.default1pxInstance, "size-line"), new PathPrimitive([ new PathPrimitiveMoveToCommand(x1_1, y1_1), new PathPrimitiveLineToCommand(x1_2, y1_2), new PathPrimitiveMoveToCommand(x2_1, y2_1), new PathPrimitiveLineToCommand(x2_2, y2_2) ], StrokeStyle.default1pxInstance), new TextPrimitive( (endPoint.x + startPoint.x) / 2, (endPoint.y + startPoint.y) / 2, text, TextOwner.ExtensionLine, undefined, undefined, undefined, null, undefined, null, PAGE_BG_TEXTFLOOR_FILTER_IDPREFIX + this.instanceId ) ]; const primitive = new GroupPrimitive(children, className); const ellEl = this.getExtensionLineElement(primitive, index); this.changePrimitiveElement(primitive, ellEl); } private hideExtensionLines() { for(let i = 0; i < this.extensionLineElements.length; i++) if(this.extensionLineElements[i]) this.dom.changeByFunc(this.extensionLineElements[i], e => e.style.display = "none"); } private getExtensionLineElement(primitive: SvgPrimitive<SVGElement>, index: number) { let ellEl = this.extensionLineElements[index]; if(ellEl !== undefined) this.dom.changeByFunc(ellEl, e => e.style.display = ""); else { ellEl = this.createPrimitiveElement(primitive, this.visualizersContainer); this.extensionLineElements[index] = (ellEl); } return ellEl; } protected getOrCreateShapeSelection(shape: Shape, usedItems?: ItemsMap): ShapeSelectionElement { let element = <ShapeSelectionElement> this.selectionMap[shape.key]; if(!element) { element = new ShapeSelectionElement(this.itemSelectionContainer, this.selectionMarksContainer, this.actualZoom, this.readOnly, this.dom, shape.key, shape.isLocked, shape.rectangle, shape.style, shape.allowResizeHorizontally, shape.allowResizeVertically, shape.description.getParameterPoints(shape)); this.selectionMap[shape.key] = element; } usedItems && (usedItems[shape.key] = true); return element; } protected getOrCreateConnectorSelection(connector: Connector, usedItems?: ItemsMap): ConnectorSelectionElement { let element = <ConnectorSelectionElement> this.selectionMap[connector.key]; const points = connector.getRenderPoints(true); const pointsNonSkipped = connector.getRenderPoints(false); if(!element) { element = new ConnectorSelectionElement(this.itemSelectionContainer, this.selectionMarksContainer, this.actualZoom, this.readOnly, this.dom, connector.key, connector.isLocked, connector.rectangle, points, connector.style, connector.styleText, connector.enableText, connector.texts.map(t => { const textInfo = GeometryUtils.getPathPointByPosition(pointsNonSkipped, t.position); return { text: connector.getText(t.position), point: textInfo[0], pointIndex: textInfo[1], pos: t.position }; }).sort((a, b) => a.pos - b.pos), connector.points, connector.properties.lineOption); this.selectionMap[connector.key] = element; } usedItems && (usedItems[connector.key] = true); return element; } protected getOrCreateMultipleSelection(usedItems: ItemsMap) { let element = <MultipleSelectionElement> this.selectionMap[MULTIPLE_SELECTION_KEY]; if(!element) { element = new MultipleSelectionElement(this.itemSelectionContainer, this.selectionMarksContainer, this.actualZoom, this.readOnly, this.dom); this.selectionMap[MULTIPLE_SELECTION_KEY] = element; } usedItems[MULTIPLE_SELECTION_KEY] = true; return element; } protected getMultipleSelection(): MultipleSelectionElement { return <MultipleSelectionElement> this.selectionMap[MULTIPLE_SELECTION_KEY]; } protected updateShapeSelection(shape: Shape, multipleSelection: MultipleSelectionElement) { if(shape.key in this.selectionMap) { this.getOrCreateShapeSelection(shape).onModelChanged(shape.isLocked, shape.rectangle, shape.style, shape.allowResizeHorizontally, shape.allowResizeVertically, shape.description.getParameterPoints(shape)); multipleSelection && multipleSelection.onModelItemChanged(shape.key, shape.rectangle); } } protected updateConnectorSelection(connector: Connector, multipleSelection: MultipleSelectionElement) { if(connector.key in this.selectionMap) { const renderPoints = connector.getRenderPoints(true); const renderPointsNonSkipped = connector.getRenderPoints(false); this.getOrCreateConnectorSelection(connector) .onModelChanged(connector.isLocked, connector.rectangle, renderPoints, connector.style, connector.styleText, connector.enableText, connector.texts.map(t => { const textPos = GeometryUtils.getPathPointByPosition(renderPointsNonSkipped, t.position); return { text: connector.getText(t.position), pointIndex: textPos[1], pos: t.position, point: textPos[0] }; }).sort((a, b) => a.pos - b.pos), connector.points, connector.properties.lineOption); multipleSelection && multipleSelection.onModelItemChanged(connector.key, connector.rectangle); } } private hideOutdatedSelection(updated: ItemsMap) { Object.keys(this.selectionMap) .filter(k => !updated[k]) .forEach(k => { this.selectionMap[k].destroy(); delete this.selectionMap[k]; }); } private selectionCanBeDrawn(item: DiagramItem) { return !item.container || (item.container.expanded && this.selectionCanBeDrawn(item.container)); } notifySelectionChanged(selection: Selection) { const items = selection.getSelectedItems(true).filter(item => this.selectionCanBeDrawn(item)); const changedItems: ItemsMap = {}; const isMultipleSelection = items.length > 1; const shapes = selection.getSelectedShapes(true).filter(shape => this.selectionCanBeDrawn(shape)); const connectors = selection.getSelectedConnectors(true).filter(connector => this.selectionCanBeDrawn(connector)); shapes.forEach(shape => this.getOrCreateShapeSelection(shape, changedItems).onSelectionChanged(isMultipleSelection)); connectors.forEach(connector => this.getOrCreateConnectorSelection(connector, changedItems).onSelectionChanged(isMultipleSelection)); if(isMultipleSelection) { const strokeWidth = items.length > 0 ? items[0].strokeWidth : 0; const rectangles = {}; items.filter(i => !i.isLocked).forEach(item => rectangles[item.key] = item.rectangle); this.getOrCreateMultipleSelection(changedItems).onSelectionChanged(!!shapes.filter(i => !i.isLocked).length, strokeWidth, rectangles); } this.hideOutdatedSelection(changedItems); } applyChangesCore(changes: ItemChange[]) { super.applyChangesCore(changes); const multipleSelection = this.getMultipleSelection(); multipleSelection && multipleSelection.onModelChanged(); } applyChange(change: ItemChange) { const multipleSelection = this.getMultipleSelection(); if(change.item instanceof Shape) this.updateShapeSelection(change.item, multipleSelection); else if(change.item instanceof Connector) this.updateConnectorSelection(change.item, multipleSelection); } notifyPageColorChanged(color: number) { } notifyPageSizeChanged(pageSize: Size, pageLandscape: boolean) { } notifyActualZoomChanged(actualZoom: number) { Object.keys(this.selectionMap).forEach(k => this.selectionMap[k].notifyZoomChanged(actualZoom)); this.actualZoom = actualZoom; } notifyViewAdjusted(canvasOffset: Point) { } notifyReadOnlyChanged(readOnly: boolean) { this.readOnly = readOnly; Object.keys(this.selectionMap).forEach(k => this.selectionMap[k].notifyReadOnlyChanged(readOnly)); } notifySelectionRectShow(rect: Rectangle) { this.showSelectionRect(rect.clone().multiply(this.actualZoom, this.actualZoom)); } notifySelectionRectHide() { this.hideSelectionRect(); } notifyResizeInfoShow(point: Point, text: string) { this.showResizeInfo(point.clone().multiply(this.actualZoom, this.actualZoom), text); } notifyResizeInfoHide() { this.hideResizeInfo(); } notifyConnectionPointsShow(key: ItemKey, points: ConnectionPointInfo[], activePointIndex: number, outsideRectangle: Rectangle) { this.hideConnectionPoints(); points.forEach((p, index) => { const point = p.point.clone().multiply(this.actualZoom, this.actualZoom); if(outsideRectangle) switch(p.side) { case ConnectionPointSide.North: point.y = outsideRectangle.y * this.actualZoom - CanvasSelectionManager.connectionPointShift; break; case ConnectionPointSide.South: point.y = outsideRectangle.bottom * this.actualZoom + CanvasSelectionManager.connectionPointShift; break; case ConnectionPointSide.West: point.x = outsideRectangle.x * this.actualZoom - CanvasSelectionManager.connectionPointShift; break; case ConnectionPointSide.East: point.x = outsideRectangle.right * this.actualZoom + CanvasSelectionManager.connectionPointShift; break; } this.showConnectionPoint(index, point, key, index, index === activePointIndex, p.allowed); }); } notifyConnectionPointsHide() { this.hideConnectionPoints(); } notifyConnectionTargetShow(key: ItemKey, info: ConnectionTargetInfo) { if(!info.allowed) return; const rect = CanvasSelectionManager.correctSelectionRect(info.rect.clone().multiply(this.actualZoom, this.actualZoom), info.strokeWidth, CanvasSelectionManager.connectionTargetBorderWidth, this.actualZoom, 0); this.showConnectionTarget(0, rect); } notifyConnectionTargetHide() { this.hideConnectionTarget(); } notifyContainerTargetShow(key: ItemKey, info: ContainerTargetInfo) { const rect = CanvasSelectionManager.correctSelectionRect(info.rect.clone().multiply(this.actualZoom, this.actualZoom), info.strokeWidth, CanvasSelectionManager.connectionTargetBorderWidth, this.actualZoom, 0); this.showContainerTarget(0, rect); } notifyContainerTargetHide() { this.hideContainerTarget(); } notifyExtensionLinesShow(lines: ExtensionLine[]) { this.hideExtensionLines(); lines.forEach((line, index) => { this.showExtensionLine(index, line.type, line.segment.startPoint.clone().multiply(this.actualZoom, this.actualZoom), line.segment.endPoint.clone().multiply(this.actualZoom, this.actualZoom), line.text ); }); } notifyExtensionLinesHide() { this.hideExtensionLines(); } notifyDragStart(itemKeys: ItemKey[]) { this.dom.changeByFunc(this.selectionMarksContainer, e => e.style.display = "none"); } notifyDragEnd(itemKeys: ItemKey[]) { this.dom.changeByFunc(this.selectionMarksContainer, e => e.style.display = ""); } notifyDragScrollStart() { } notifyDragScrollEnd() { } notifyTextInputStart(item: DiagramItem, text: string, position: Point, size?: Size): void { this.dom.changeByFunc(this.visualizersContainer, e => e.style.display = "none"); } notifyTextInputEnd(item: DiagramItem, captureFocus?: boolean): void { this.dom.changeByFunc(this.visualizersContainer, e => e.style.display = ""); } notifyTextInputPermissionsCheck(item: DiagramItem, allowed: boolean): void {} static correctSelectionRect(rect: Rectangle, targetLineWidth: number, selectionLineWidth: number, zoomLevel: number, outsideOffset: number = CanvasSelectionManager.selectionOffset): Rectangle { const evenOddWidth = UnitConverter.twipsToPixels(targetLineWidth) % 2 !== UnitConverter.twipsToPixels(selectionLineWidth) % 2; const corr = Math.ceil(targetLineWidth / 2 * zoomLevel); rect = rect.clone().inflate(corr, corr); const lwCorr = Math.floor(selectionLineWidth / 2); rect.x -= lwCorr; rect.y -= lwCorr; rect.width += selectionLineWidth; rect.height += selectionLineWidth; if(evenOddWidth) { const correction = CanvasSelectionManager.evenOddSelectionCorrection * (UnitConverter.twipsToPixels(selectionLineWidth) % 2 === 1 ? -1 : 1); rect = rect.clone().moveRectangle(correction, correction); } return rect.clone().inflate(outsideOffset, outsideOffset); } } abstract class CanvasElement implements IZoomChangesListener, IReadOnlyChangesListener { private elements: { [key: string]: SVGElement } = {}; private updatedElements: { [key: string]: boolean } = {}; constructor( protected rectsContainer: SVGElement, protected marksContainer: SVGElement, protected key: ItemKey, protected zoomLevel: number, protected readOnly: boolean, protected dom: DOMManipulator) { } notifyZoomChanged(zoom: number) { if(this.zoomLevel !== zoom) { this.zoomLevel = zoom; this.redraw(); } } notifyReadOnlyChanged(readOnly: boolean) { this.readOnly = readOnly; this.redraw(); } destroy() { Object.keys(this.elements) .forEach(key => { this.elements[key].parentNode.removeChild(this.elements[key]); delete this.elements[key]; }); } protected redraw() { this.updatedElements = {}; this.redrawCore(); Object.keys(this.elements) .filter(key => !this.updatedElements[key]) .forEach(key => { this.elements[key].parentNode.removeChild(this.elements[key]); delete this.elements[key]; }); this.updatedElements = {}; } protected abstract redrawCore(); protected drawSelectionMarks(rect: Rectangle, allowResizeHorizontally: boolean, allowResizeVertically: boolean) { if(this.readOnly) return; const hasEWMarks = allowResizeHorizontally && rect.height > CanvasSelectionManager.selectionMarkSize * 3; const hasNSMarks = allowResizeVertically && rect.width > CanvasSelectionManager.selectionMarkSize * 3; const hasCornerMarks = allowResizeHorizontally || allowResizeVertically; if(hasCornerMarks) this.drawSelectionMark(0, new Point(rect.x, rect.y), CanvasSelectionManager.selectionMarkSize, MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_NW, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK); if(hasNSMarks && !Browser.TouchUI) this.drawSelectionMark(1, new Point(rect.x + rect.width / 2, rect.y), CanvasSelectionManager.selectionMarkSize, MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_N, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK); if(hasCornerMarks) this.drawSelectionMark(2, new Point(rect.right, rect.y), CanvasSelectionManager.selectionMarkSize, MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_NE, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK); if(hasEWMarks && !Browser.TouchUI) this.drawSelectionMark(3, new Point(rect.right, rect.y + rect.height / 2), CanvasSelectionManager.selectionMarkSize, MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_E, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK); if(hasCornerMarks) this.drawSelectionMark(4, new Point(rect.right, rect.bottom), CanvasSelectionManager.selectionMarkSize, MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_SE, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK); if(hasNSMarks && !Browser.TouchUI) this.drawSelectionMark(5, new Point(rect.x + rect.width / 2, rect.bottom), CanvasSelectionManager.selectionMarkSize, MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_S, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK); if(hasCornerMarks) this.drawSelectionMark(6, new Point(rect.x, rect.bottom), CanvasSelectionManager.selectionMarkSize, MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_SW, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK); if(hasEWMarks && !Browser.TouchUI) this.drawSelectionMark(7, new Point(rect.x, rect.y + rect.height / 2), CanvasSelectionManager.selectionMarkSize, MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_W, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK); } protected drawSelectionMark(index: number, point: Point, size: number, type: MouseEventElementType, value: any, className: string) { this.getOrCreateElement("SM" + index, new RectanglePrimitive(point.x - size / 2, point.y - size / 2, size, size, null, className, undefined, el => { RenderUtils.setElementEventData(el, type, this.key, value); }), this.marksContainer); } protected drawSelectionRect(rectangle: Rectangle, type: MouseEventElementType, className: string) { const primitive = new RectanglePrimitive(rectangle.x, rectangle.y, rectangle.width, rectangle.height, StrokeStyle.default1pxInstance, className, undefined, el => { RenderUtils.setElementEventData(el, type, "-1", -1); } ); this.getOrCreateElement("shapeSelection", primitive, this.rectsContainer); } protected getOrCreateElement(cacheKey: string, primitive: SvgPrimitive<SVGElement>, container: SVGElement) { let element = this.elements[cacheKey]; if(!element) { element = primitive.createElement(el => container.appendChild(el)); this.elements[cacheKey] = element; } this.updatedElements[cacheKey] = true; this.dom.changeByPrimitive(element, primitive); return element; } } abstract class ItemSelectionElement extends CanvasElement { protected isMultipleSelection: boolean; constructor(rectsContainer: SVGElement, marksContainer: SVGElement, key: ItemKey, zoomLevel: number, readOnly: boolean, dom: DOMManipulator, protected isLocked: boolean, protected rectangle: Rectangle) { super(rectsContainer, marksContainer, key, zoomLevel, readOnly, dom); } onSelectionChanged(isMultipleSelection: boolean) { if(this.isMultipleSelection !== isMultipleSelection) { this.isMultipleSelection = isMultipleSelection; this.redraw(); } } protected isLockedRender() { return this.isLocked && !this.readOnly; } protected drawLockedSelectionMark(index: number, point: Point, size: number, className: string) { const primitive = new PathPrimitive([ new PathPrimitiveMoveToCommand(point.x - size / 2, point.y - size / 2), new PathPrimitiveLineToCommand(point.x + size / 2, point.y + size / 2), new PathPrimitiveMoveToCommand(point.x + size / 2, point.y - size / 2), new PathPrimitiveLineToCommand(point.x - size / 2, point.y + size / 2) ], null, className); this.getOrCreateElement("LSM" + index, primitive, this.marksContainer); } } class MultipleSelectionElement extends CanvasElement { private needDrawSelectionMarks: boolean; private strokeWidth: number; private rectangles: {[key: string]: Rectangle} = {}; constructor(rectsContainer: SVGElement, marksContainer: SVGElement, zoomLevel: number, readOnly: boolean, dom: DOMManipulator) { super(rectsContainer, marksContainer, "-1", zoomLevel, readOnly, dom); } onModelItemChanged(key: ItemKey, rectangle: Rectangle) { if(key in this.rectangles) this.rectangles[key] = rectangle; } onModelChanged() { this.redraw(); } onSelectionChanged(needDrawSelectionMarks: boolean, strokeWidth: number, rectangles: {[key: string]: Rectangle}) { this.needDrawSelectionMarks = needDrawSelectionMarks; this.strokeWidth = strokeWidth; this.rectangles = rectangles; this.redraw(); } protected redrawCore() { const rectKeys = Object.keys(this.rectangles); if(!rectKeys.length) return; const rect = GeometryUtils.getCommonRectangle(rectKeys.map(key => this.rectangles[key])).clone().multiply(this.zoomLevel, this.zoomLevel); const selRect = CanvasSelectionManager.correctSelectionRect(rect, this.strokeWidth, CanvasSelectionManager.selectionRectLineWidth, this.zoomLevel); this.drawSelectionRect(selRect, MouseEventElementType.SelectionRect, SELECTION_ELEMENT_CLASSNAMES.ITEMS_SELECTION_RECT); if(this.needDrawSelectionMarks) this.drawSelectionMarks(rect, true, true); } } class ShapeSelectionElement extends ItemSelectionElement { constructor(rectsContainer: SVGElement, marksContainer: SVGElement, zoomLevel: number, readOnly: boolean, dom: DOMManipulator, key: ItemKey, isLocked: boolean, rectangle: Rectangle, protected style: Style, protected allowResizeHorizontally: boolean, protected allowResizeVertically: boolean, protected shapeParameterPoints: ShapeParameterPoint[]) { super(rectsContainer, marksContainer, key, zoomLevel, readOnly, dom, isLocked, rectangle); } onModelChanged(isLocked: boolean, rectangle: Rectangle, style: Style, allowResizeHorizontally: boolean, allowResizeVertically: boolean, shapeParameterPoints: ShapeParameterPoint[]) { this.isLocked = isLocked; this.rectangle = rectangle; this.style = style; this.allowResizeHorizontally = allowResizeHorizontally; this.allowResizeVertically = allowResizeVertically; this.shapeParameterPoints = shapeParameterPoints; this.redraw(); } protected redrawCore() { const rect = this.rectangle.clone().multiply(this.zoomLevel, this.zoomLevel); if(this.isLockedRender()) this.drawLockedSelection(rect); else this.drawUnlockedSelection(rect); } private drawLockedSelection(rect: Rectangle) { this.drawLockedSelectionMark(0, new Point(rect.x, rect.y), CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK); this.drawLockedSelectionMark(1, new Point(rect.right, rect.y), CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK); this.drawLockedSelectionMark(2, new Point(rect.right, rect.bottom), CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK); this.drawLockedSelectionMark(3, new Point(rect.x, rect.bottom), CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK); } private drawUnlockedSelection(rect: Rectangle) { const selRect = CanvasSelectionManager.correctSelectionRect(rect, this.style.strokeWidth, CanvasSelectionManager.selectionRectLineWidth, this.zoomLevel); this.drawSelectionRect(selRect, MouseEventElementType.SelectionRect, this.isMultipleSelection ? SELECTION_ELEMENT_CLASSNAMES.ITEM_MULTI_SELECTION : SELECTION_ELEMENT_CLASSNAMES.ITEM_SELECTION_RECT); if(!this.isMultipleSelection) this.drawSelectionMarks(rect, this.allowResizeHorizontally, this.allowResizeVertically); this.drawShapeParameterPoints(); } private drawShapeParameterPoints() { if(this.readOnly) return; this.shapeParameterPoints.forEach((pp, index) => { const point = pp.point.clone().multiply(this.zoomLevel, this.zoomLevel); this.drawShapeParameterPoint(point, index, pp.key); }); } private drawShapeParameterPoint(point: Point, index: number, pointKey: string) { const size = CanvasSelectionManager.geomertyMarkSize; const primitive = new RectanglePrimitive(point.x - size / 2, point.y - size / 2, size, size, null, "geometry-mark", undefined, el => { RenderUtils.setElementEventData(el, MouseEventElementType.ShapeParameterBox, this.key, pointKey); }); this.getOrCreateElement("pp" + index.toString(), primitive, this.marksContainer); } } class ConnectorSelectionElement extends ItemSelectionElement { constructor(rectsContainer: SVGElement, marksContainer: SVGElement, zoomLevel: number, readOnly: boolean, dom: DOMManipulator, key: string, isLocked: boolean, rectangle: Rectangle, protected renderPoints: ConnectorRenderPoint[], protected style: Style, protected styleText: TextStyle, protected enableText: boolean, protected texts: {text: string, point: Point, pointIndex: number}[], protected points: Point[], protected lineType: ConnectorLineOption) { super(rectsContainer, marksContainer, key, zoomLevel, readOnly, dom, isLocked, rectangle); } onModelChanged(isLocked: boolean, rectangle: Rectangle, renderPoints: ConnectorRenderPoint[], style: Style, styleText: TextStyle, enableText: boolean, texts: {text: string, point: Point, pointIndex: number}[], points: Point[], lineType: ConnectorLineOption) { this.isLocked = isLocked; this.rectangle = rectangle; this.renderPoints = renderPoints; this.style = style; this.styleText = styleText; this.enableText = enableText; this.texts = texts; this.points = points; this.lineType = lineType; this.redraw(); } protected redrawCore() { if(this.isLockedRender()) this.drawLockedSelection(); else this.drawUnlockedSelection(); } protected drawLockedSelection() { this.renderPoints.forEach((pt, index) => { this.drawLockedSelectionMark(index, pt, CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK); }); } protected drawUnlockedSelection() { this.drawConnectorSelection(); if(!this.isMultipleSelection && !this.readOnly) this.drawConnectorSelectionMarks(); } private drawConnectorSelection() { const commands: PathPrimitiveCommand[] = []; const commandsWB: PathPrimitiveCommand[] = []; const className = this.isMultipleSelection ? SELECTION_ELEMENT_CLASSNAMES.CONNECTOR_MULTI_SELECTION : SELECTION_ELEMENT_CLASSNAMES.CONNECTOR_SELECTION; this.populateSelectionPrimitiveCommands(commands, commandsWB); const primitive = new PathPrimitive(commands.concat(commandsWB.reverse()), StrokeStyle.default1pxInstance, className); this.getOrCreateElement("CS", primitive, this.rectsContainer); } populateSelectionPrimitiveCommands(commands: PathPrimitiveCommand[], commandsWB: PathPrimitiveCommand[]) { const texts = this.texts; const txtAlign = this.styleText.getAlignment(); const points = this.createNotSkippedRenderPoints(); const zoomLevel = this.zoomLevel; const strokeWidthPx = this.style.strokeWidthPx; const selectionOffset = this.getSelectionOffset(strokeWidthPx); const strokeWidthPxIsEvenOdd = strokeWidthPx % 2 === 0; let prevPt: Point = points[0]; let textIndex = 0; let offset: Point; let distance: number; let nextOffset: Point; let nextDistance: number; for(let i = 1, pt: ConnectorRenderPoint; pt = points[i]; i++) { const nextPt: Point = points[i + 1]; if(offset === undefined) { distance = Metrics.euclideanDistance(prevPt, pt); if(MathUtils.numberCloseTo(distance, 0)) continue; offset = GeometryUtils.getSelectionOffsetPoint(prevPt, pt, distance).multiply(selectionOffset, selectionOffset); } if(nextPt) { nextDistance = Metrics.euclideanDistance(pt, nextPt); if(MathUtils.numberCloseTo(nextDistance, 0)) continue; nextOffset = GeometryUtils.getSelectionOffsetPoint(pt, nextPt, nextDistance).multiply(selectionOffset, selectionOffset); } let offsetX = offset.x; let offsetY = offset.y; let offsetXNegative = -offsetX; let offsetYNegative = -offsetY; let nextOffsetX = nextOffset && nextOffset.x; let nextOffsetY = nextOffset && nextOffset.y; let nextOffsetXNegative = nextOffset && -nextOffset.x; let nextOffsetYNegative = nextOffset && -nextOffset.y; if(strokeWidthPxIsEvenOdd) { if(offsetXNegative > 0) offsetXNegative -= CanvasSelectionManager.evenOddSelectionCorrection; else if(offsetX > 0) offsetX -= CanvasSelectionManager.evenOddSelectionCorrection; if(offsetYNegative > 0) offsetYNegative -= CanvasSelectionManager.evenOddSelectionCorrection; else if(offsetY > 0) offsetY -= CanvasSelectionManager.evenOddSelectionCorrection; if(nextOffsetXNegative > 0) nextOffsetXNegative -= CanvasSelectionManager.evenOddSelectionCorrection; else if(nextOffsetX > 0) nextOffsetX -= CanvasSelectionManager.evenOddSelectionCorrection; if(nextOffsetYNegative > 0) nextOffsetYNegative -= CanvasSelectionManager.evenOddSelectionCorrection; else if(nextOffsetY > 0) nextOffsetY -= CanvasSelectionManager.evenOddSelectionCorrection; } while(texts[textIndex] && texts[textIndex].pointIndex <= i) { const text = texts[textIndex]; const size = this.getConnectorSelectionTextSize(text.text, selectionOffset); const textPts = GeometryUtils.getSelectionTextStartEndPoints(prevPt, pt, distance, text.point, size, txtAlign); if(texts[textIndex].pointIndex < i) { prevPt = textPts[1]; commands.push(PathPrimitiveMoveToCommand.fromPoint(prevPt.clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel))); commandsWB.push(PathPrimitiveLineToCommand.fromPoint(prevPt.clone().offset(offsetXNegative, offsetYNegative).multiply(zoomLevel, zoomLevel))); } else { if(!commands.length) { commands.push(PathPrimitiveMoveToCommand.fromPoint(prevPt.clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel))); commandsWB.push(PathPrimitiveLineToCommand.fromPoint(prevPt.clone().offset(offsetXNegative, offsetYNegative).