From 7609e82ef40a320f1baf5435717b7b07a8d2a599 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Sun, 9 Jun 2024 11:50:43 +0200 Subject: [PATCH 1/5] START simulate no VertexHandler --- packages/core/src/index.ts | 2 +- packages/core/src/view/Graph.ts | 3 +- .../core/src/view/handler/VertexHandler.ts | 4069 +++++++++-------- 3 files changed, 2046 insertions(+), 2028 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 617deba664..d3ab383827 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -82,7 +82,7 @@ export { default as PopupMenuHandler } from './view/handler/PopupMenuHandler'; export { default as RubberBandHandler } from './view/handler/RubberBandHandler'; export { default as SelectionCellsHandler } from './view/handler/SelectionCellsHandler'; export { default as TooltipHandler } from './view/handler/TooltipHandler'; -export { default as VertexHandler } from './view/handler/VertexHandler'; +export type { default as VertexHandler } from './view/handler/VertexHandler'; export { default as CircleLayout } from './view/layout/CircleLayout'; export { default as CompactTreeLayout } from './view/layout/CompactTreeLayout'; diff --git a/packages/core/src/view/Graph.ts b/packages/core/src/view/Graph.ts index 77a16ba575..ab792851df 100644 --- a/packages/core/src/view/Graph.ts +++ b/packages/core/src/view/Graph.ts @@ -485,7 +485,8 @@ class Graph extends EventSource { * @param state {@link CellState} to create the handler for. */ createVertexHandler(state: CellState): VertexHandler { - return new VertexHandler(state); + // return new VertexHandler(state); + return null!; } // =================================================================================================================== diff --git a/packages/core/src/view/handler/VertexHandler.ts b/packages/core/src/view/handler/VertexHandler.ts index bafe886d16..8773f91b18 100644 --- a/packages/core/src/view/handler/VertexHandler.ts +++ b/packages/core/src/view/handler/VertexHandler.ts @@ -55,2032 +55,2049 @@ import SelectionCellsHandler from './SelectionCellsHandler'; * * This handler is automatically created in {@link Graph#createHandler}. */ -class VertexHandler { - escapeHandler: (sender: Listenable, evt: Event) => void; - selectionBounds: Rectangle; - bounds: Rectangle; - selectionBorder: RectangleShape; - - /** - * Reference to the enclosing {@link Graph}. - */ - graph: Graph; - - /** - * Reference to the {@link CellState} being modified. - */ +// class VertexHandler { +// escapeHandler: (sender: Listenable, evt: Event) => void; +// selectionBounds: Rectangle; +// bounds: Rectangle; +// selectionBorder: RectangleShape; +// +// /** +// * Reference to the enclosing {@link Graph}. +// */ +// graph: Graph; +// +// /** +// * Reference to the {@link CellState} being modified. +// */ +// state: CellState; +// +// sizers: Shape[] = []; +// +// /** +// * Specifies if only one sizer handle at the bottom, right corner should be used. +// * @default false +// */ +// singleSizer = false; +// +// /** +// * Holds the index of the current handle. +// */ +// index: number | null = null; +// +// /** +// * Specifies if the bounds of handles should be used for hit-detection in IE or if {@link tolerance} > 0. +// * @default true +// */ +// allowHandleBoundsCheck = true; +// +// /** +// * Optional {@link Image} to be used as handles. +// * @default null +// */ +// handleImage: Image | null = null; +// +// /** +// * If handles are currently visible. +// * @default true +// */ +// handlesVisible = true; +// +// /** +// * Optional tolerance for hit-detection in {@link getHandleForEvent}. +// * @default 0 +// */ +// tolerance = 0; +// +// /** +// * Specifies if a rotation handle should be visible. +// * @default false +// */ +// rotationEnabled = false; +// +// /** +// * Specifies if the parent should be highlighted if a child cell is selected. +// * @default false +// */ +// parentHighlightEnabled = false; +// +// /** +// * Specifies if rotation steps should be "rasterized" depending on the distance to the handle. +// * @default true +// */ +// rotationRaster = true; +// +// /** +// * Specifies the cursor for the rotation handle. +// * @default 'crosshair'. +// */ +// rotationCursor = 'crosshair'; +// +// /** +// * Specifies if resize should change the cell in-place. This is an experimental +// * feature for non-touch devices. +// * @default false +// */ +// livePreview = false; +// +// /** +// * Specifies if the live preview should be moved to the front. +// * @default false +// */ +// movePreviewToFront = false; +// +// /** +// * Specifies if sizers should be hidden and spaced if the vertex is small. +// * @default false +// */ +// manageSizers = false; +// +// /** +// * Specifies if the size of groups should be constrained by the children. +// * @default false +// */ +// constrainGroupByChildren = false; +// +// /** +// * Vertical spacing for rotation icon. +// * @default -16 +// */ +// rotationHandleVSpacing = -16; +// +// /** +// * The horizontal offset for the handles. This is updated in {@link redrawHandles} +// * if {@link manageSizers} is `true` and the sizers are offset horizontally. +// */ +// horizontalOffset = 0; +// +// /** +// * The horizontal offset for the handles. This is updated in +// * if {@link manageSizers} is true and the sizers are offset vertically. +// */ +// verticalOffset = 0; +// +// minBounds: Rectangle | null = null; +// +// x0 = 0; +// y0 = 0; +// +// customHandles: CellHandle[] = []; +// +// inTolerance = false; +// +// startX = 0; +// startY = 0; +// +// rotationShape: Shape | null = null; +// +// currentAlpha = 100; +// startAngle = 0; +// startDist = 0; +// +// ghostPreview: Shape | null = null; +// +// livePreviewActive = false; +// +// childOffsetX = 0; +// childOffsetY = 0; +// +// parentState: CellState | null = null; +// parentHighlight: RectangleShape | null = null; +// +// unscaledBounds: Rectangle | null = null; +// +// preview: Shape | null = null; +// +// labelShape: Shape | null = null; +// +// edgeHandlers: EdgeHandler[] = []; +// +// EMPTY_POINT = new Point(); +// +// /** +// * Constructs an event handler that allows to resize vertices and groups. +// * +// * @param state {@link CellState} of the cell to be resized. +// */ +// constructor(state: CellState) { +// this.state = state; +// this.graph = this.state.view.graph; +// this.selectionBounds = this.getSelectionBounds(this.state); +// this.bounds = new Rectangle( +// this.selectionBounds.x, +// this.selectionBounds.y, +// this.selectionBounds.width, +// this.selectionBounds.height +// ); +// this.selectionBorder = this.createSelectionShape(this.bounds); +// // VML dialect required here for event transparency in IE +// this.selectionBorder.dialect = DIALECT.SVG; +// this.selectionBorder.pointerEvents = false; +// this.selectionBorder.rotation = this.state.style.rotation ?? 0; +// this.selectionBorder.init(this.graph.getView().getOverlayPane()); +// InternalEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state); +// +// if (this.graph.isCellMovable(this.state.cell)) { +// this.selectionBorder.setCursor(CURSOR.MOVABLE_VERTEX); +// } +// +// const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; +// +// // Adds the sizer handles +// if ( +// selectionHandler && +// (selectionHandler.maxCells <= 0 || +// this.graph.getSelectionCount() < selectionHandler.maxCells) +// ) { +// const resizable = this.graph.isCellResizable(this.state.cell); +// this.sizers = []; +// +// if ( +// resizable || +// (this.graph.isLabelMovable(this.state.cell) && +// this.state.width >= 2 && +// this.state.height >= 2) +// ) { +// let i = 0; +// +// if (resizable) { +// if (!this.singleSizer) { +// this.sizers.push(this.createSizer('nw-resize', i++)); +// this.sizers.push(this.createSizer('n-resize', i++)); +// this.sizers.push(this.createSizer('ne-resize', i++)); +// this.sizers.push(this.createSizer('w-resize', i++)); +// this.sizers.push(this.createSizer('e-resize', i++)); +// this.sizers.push(this.createSizer('sw-resize', i++)); +// this.sizers.push(this.createSizer('s-resize', i++)); +// } +// +// this.sizers.push(this.createSizer('se-resize', i++)); +// } +// +// const geo = this.state.cell.getGeometry(); +// +// if ( +// geo != null && +// !geo.relative && +// //!this.graph.isSwimlane(this.state.cell) && disable for now +// this.graph.isLabelMovable(this.state.cell) +// ) { +// // Marks this as the label handle for getHandleForEvent +// this.labelShape = this.createSizer( +// CURSOR.LABEL_HANDLE, +// InternalEvent.LABEL_HANDLE, +// LABEL_HANDLE_SIZE, +// LABEL_HANDLE_FILLCOLOR +// ); +// this.sizers.push(this.labelShape); +// } +// } else if ( +// this.graph.isCellMovable(this.state.cell) && +// !this.graph.isCellResizable(this.state.cell) && +// this.state.width < 2 && +// this.state.height < 2 +// ) { +// this.labelShape = this.createSizer( +// CURSOR.MOVABLE_VERTEX, +// InternalEvent.LABEL_HANDLE, +// undefined, +// LABEL_HANDLE_FILLCOLOR +// ); +// this.sizers.push(this.labelShape); +// } +// } +// +// // Adds the rotation handler +// if (this.isRotationHandleVisible()) { +// this.rotationShape = this.createSizer( +// this.rotationCursor, +// InternalEvent.ROTATION_HANDLE, +// HANDLE_SIZE + 3, +// HANDLE_FILLCOLOR +// ); +// this.sizers.push(this.rotationShape); +// } +// +// this.customHandles = this.createCustomHandles(); +// this.redraw(); +// +// if (this.constrainGroupByChildren) { +// this.updateMinBounds(); +// } +// +// // Handles escape keystrokes +// this.escapeHandler = (sender: Listenable, evt: Event) => { +// if (this.livePreview && this.index != null) { +// // Redraws the live preview +// (this.state.view.graph).cellRenderer.redraw(this.state, true); +// +// // Redraws connected edges +// this.state.view.invalidate(this.state.cell); +// this.state.invalid = false; +// this.state.view.validate(); +// } +// +// this.reset(); +// }; +// +// (this.state.view.graph).addListener(InternalEvent.ESCAPE, this.escapeHandler); +// } +// +// /** +// * Returns `true` if the rotation handle should be showing. +// */ +// isRotationHandleVisible() { +// const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; +// const selectionHandlerCheck = selectionHandler +// ? selectionHandler.maxCells <= 0 || +// this.graph.getSelectionCount() < selectionHandler.maxCells +// : true; +// +// return ( +// this.graph.isEnabled() && +// this.rotationEnabled && +// this.graph.isCellRotatable(this.state.cell) && +// selectionHandlerCheck +// ); +// } +// +// /** +// * Returns `true` if the aspect ratio if the cell should be maintained. +// */ +// isConstrainedEvent(me: InternalMouseEvent) { +// return isShiftDown(me.getEvent()) || this.state.style.aspect === 'fixed'; +// } +// +// /** +// * Returns `true` if the center of the vertex should be maintained during the resize. +// */ +// isCenteredEvent(state: CellState, me: InternalMouseEvent) { +// return false; +// } +// +// /** +// * Returns an array of custom handles. This implementation returns null. +// */ +// createCustomHandles() { +// return []; +// } +// +// /** +// * Initializes the shapes required for this vertex handler. +// */ +// updateMinBounds() { +// const children = this.graph.getChildCells(this.state.cell); +// +// if (children.length > 0) { +// this.minBounds = this.graph.view.getBounds(children); +// +// if (this.minBounds) { +// const s = this.state.view.scale; +// const t = this.state.view.translate; +// +// this.minBounds.x -= this.state.x; +// this.minBounds.y -= this.state.y; +// this.minBounds.x /= s; +// this.minBounds.y /= s; +// this.minBounds.width /= s; +// this.minBounds.height /= s; +// this.x0 = this.state.x / s - t.x; +// this.y0 = this.state.y / s - t.y; +// } +// } +// } +// +// /** +// * Returns the mxRectangle that defines the bounds of the selection border. +// */ +// getSelectionBounds(state: CellState) { +// return new Rectangle( +// Math.round(state.x), +// Math.round(state.y), +// Math.round(state.width), +// Math.round(state.height) +// ); +// } +// +// /** +// * Creates the shape used to draw the selection border. +// */ +// createParentHighlightShape(bounds: Rectangle) { +// return this.createSelectionShape(bounds); +// } +// +// /** +// * Creates the shape used to draw the selection border. +// */ +// createSelectionShape(bounds: Rectangle) { +// const shape = new RectangleShape( +// Rectangle.fromRectangle(bounds), +// NONE, +// this.getSelectionColor() +// ); +// shape.strokeWidth = this.getSelectionStrokeWidth(); +// shape.isDashed = this.isSelectionDashed(); +// +// return shape; +// } +// +// /** +// * Returns {@link VERTEX_SELECTION_COLOR}. +// */ +// getSelectionColor() { +// return VERTEX_SELECTION_COLOR; +// } +// +// /** +// * Returns {@link VERTEX_SELECTION_STROKEWIDTH}. +// */ +// getSelectionStrokeWidth() { +// return VERTEX_SELECTION_STROKEWIDTH; +// } +// +// /** +// * Returns {@link VERTEX_SELECTION_DASHED}. +// */ +// isSelectionDashed() { +// return VERTEX_SELECTION_DASHED; +// } +// +// /** +// * Creates a sizer handle for the specified cursor and index and returns +// * the new {@link RectangleShape} that represents the handle. +// */ +// createSizer( +// cursor: string, +// index: number, +// size = HANDLE_SIZE, +// fillColor = HANDLE_FILLCOLOR +// ) { +// const bounds = new Rectangle(0, 0, size, size); +// const sizer = this.createSizerShape(bounds, index, fillColor); +// +// if ( +// sizer.bounds && +// sizer.isHtmlAllowed() && +// this.state.text && +// this.state.text.node.parentNode === this.graph.container +// ) { +// sizer.bounds.height -= 1; +// sizer.bounds.width -= 1; +// sizer.dialect = DIALECT.STRICTHTML; +// sizer.init(this.graph.container); +// } else { +// sizer.dialect = +// this.graph.dialect !== DIALECT.SVG ? DIALECT.MIXEDHTML : DIALECT.SVG; +// sizer.init(this.graph.getView().getOverlayPane()); +// } +// +// InternalEvent.redirectMouseEvents(sizer.node, this.graph, this.state); +// +// if (this.graph.isEnabled()) { +// sizer.setCursor(cursor); +// } +// +// if (!this.isSizerVisible(index)) { +// sizer.visible = false; +// } +// +// return sizer; +// } +// +// /** +// * Returns `true` if the sizer for the given index is visible. +// * +// * This implementation returns `true` for all given indices. +// */ +// isSizerVisible(index: number) { +// return true; +// } +// +// /** +// * Creates the shape used for the sizer handle for the specified bounds an +// * index. Only images and rectangles should be returned if support for HTML +// * labels with not foreign objects is required. +// */ +// createSizerShape(bounds: Rectangle, index: number, fillColor = HANDLE_FILLCOLOR) { +// if (this.handleImage) { +// bounds = new Rectangle( +// bounds.x, +// bounds.y, +// this.handleImage.width, +// this.handleImage.height +// ); +// const shape = new ImageShape(bounds, this.handleImage.src); +// +// // Allows HTML rendering of the images +// shape.preserveImageAspect = false; +// +// return shape; +// } +// if (index === InternalEvent.ROTATION_HANDLE) { +// return new EllipseShape(bounds, fillColor, HANDLE_STROKECOLOR); +// } +// return new RectangleShape(bounds, fillColor, HANDLE_STROKECOLOR); +// } +// +// /** +// * Helper method to create an {@link Rectangle} around the given center point +// * with a width and height of 2*s or 6, if no s is given. +// */ +// moveSizerTo(shape: Shape, x: number, y: number) { +// if (shape && shape.bounds) { +// shape.bounds.x = Math.floor(x - shape.bounds.width / 2); +// shape.bounds.y = Math.floor(y - shape.bounds.height / 2); +// +// // Fixes visible inactive handles in VML +// if (shape.node && shape.node.style.display !== 'none') { +// shape.redraw(); +// } +// } +// } +// +// /** +// * Returns the index of the handle for the given event. This returns the index +// * of the sizer from where the event originated or {@link Event.LABEL_HANDLE}. +// */ +// getHandleForEvent(me: InternalMouseEvent) { +// // Connection highlight may consume events before they reach sizer handle +// const tol = !isMouseEvent(me.getEvent()) ? this.tolerance : 1; +// const hit = +// this.allowHandleBoundsCheck && tol > 0 +// ? new Rectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) +// : null; +// +// const checkShape = (shape: Shape | null) => { +// const st = +// shape && shape.constructor !== ImageShape && this.allowHandleBoundsCheck +// ? shape.strokeWidth + shape.svgStrokeTolerance +// : null; +// const real = st +// ? new Rectangle( +// me.getGraphX() - Math.floor(st / 2), +// me.getGraphY() - Math.floor(st / 2), +// st, +// st +// ) +// : hit; +// +// return ( +// shape && +// shape.bounds && +// (me.isSource(shape) || +// (real && +// intersects(shape.bounds, real) && +// shape.node.style.display !== 'none' && +// shape.node.style.visibility !== 'hidden')) +// ); +// }; +// +// if (checkShape(this.rotationShape)) { +// return InternalEvent.ROTATION_HANDLE; +// } +// if (checkShape(this.labelShape)) { +// return InternalEvent.LABEL_HANDLE; +// } +// +// for (let i = 0; i < this.sizers.length; i += 1) { +// if (checkShape(this.sizers[i])) { +// return i; +// } +// } +// +// if (this.customHandles != null && this.isCustomHandleEvent(me)) { +// // Inverse loop order to match display order +// for (let i = this.customHandles.length - 1; i >= 0; i--) { +// if (checkShape(this.customHandles[i].shape)) { +// // LATER: Return reference to active shape +// return InternalEvent.CUSTOM_HANDLE - i; +// } +// } +// } +// +// return null; +// } +// +// /** +// * Returns `true` if the given event allows custom handles to be changed. +// * +// * This implementation returns `true`. +// */ +// isCustomHandleEvent(me: InternalMouseEvent) { +// return true; +// } +// +// /** +// * Handles the event if a handle has been clicked. By consuming the +// * event all subsequent events of the gesture are redirected to this +// * handler. +// */ +// mouseDown(sender: EventSource, me: InternalMouseEvent) { +// if (!me.isConsumed() && this.graph.isEnabled()) { +// const handle = this.getHandleForEvent(me); +// +// if (handle) { +// this.start(me.getGraphX(), me.getGraphY(), handle); +// me.consume(); +// } +// } +// } +// +// /** +// * Called if {@link livePreview} is enabled to check if a border should be painted. +// * +// * This implementation returns `true` if the shape is transparent. +// */ +// isLivePreviewBorder() { +// return ( +// this.state.shape && +// this.state.shape.fill === NONE && +// this.state.shape.stroke === NONE +// ); +// } +// +// /** +// * Starts the handling of the mouse gesture. +// */ +// start(x: number, y: number, index: number) { +// this.livePreviewActive = this.livePreview && this.state.cell.getChildCount() === 0; +// this.inTolerance = true; +// this.childOffsetX = 0; +// this.childOffsetY = 0; +// this.index = index; +// this.startX = x; +// this.startY = y; +// +// if (this.index <= InternalEvent.CUSTOM_HANDLE && this.isGhostPreview()) { +// this.ghostPreview = this.createGhostPreview(); +// } else { +// // Saves reference to parent state +// const { model } = this.state.view.graph; +// const parent = this.state.cell.getParent(); +// +// if ( +// this.state.view.currentRoot !== parent && +// parent && +// (parent.isVertex() || parent.isEdge()) +// ) { +// this.parentState = (this.state.view.graph).view.getState(parent); +// } +// +// // Creates a preview that can be on top of any HTML label +// this.selectionBorder.node.style.display = +// index === InternalEvent.ROTATION_HANDLE ? 'inline' : 'none'; +// +// // Creates the border that represents the new bounds +// if (!this.livePreviewActive || this.isLivePreviewBorder()) { +// this.preview = this.createSelectionShape(this.bounds); +// +// if ( +// !(Client.IS_SVG && Number(this.state.style.rotation || '0') !== 0) && +// this.state.text != null && +// this.state.text.node.parentNode === this.graph.container +// ) { +// this.preview.dialect = DIALECT.STRICTHTML; +// this.preview.init(this.graph.container); +// } else { +// this.preview.dialect = DIALECT.SVG; +// this.preview.init(this.graph.view.getOverlayPane()); +// } +// } +// +// if (index === InternalEvent.ROTATION_HANDLE) { +// // With the rotation handle in a corner, need the angle and distance +// const pos = this.getRotationHandlePosition(); +// +// const dx = pos.x - this.state.getCenterX(); +// const dy = pos.y - this.state.getCenterY(); +// +// this.startAngle = dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : 0; +// this.startDist = Math.sqrt(dx * dx + dy * dy); +// } +// +// // Prepares the handles for live preview +// if (this.livePreviewActive) { +// this.hideSizers(); +// +// if (index === InternalEvent.ROTATION_HANDLE && this.rotationShape) { +// this.rotationShape.node.style.display = ''; +// } else if (index === InternalEvent.LABEL_HANDLE && this.labelShape) { +// this.labelShape.node.style.display = ''; +// } else if (this.sizers[index]) { +// this.sizers[index].node.style.display = ''; +// } else if (index <= InternalEvent.CUSTOM_HANDLE) { +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].setVisible(true); +// } +// +// // Gets the array of connected edge handlers for redrawing +// const edges = this.state.cell.getEdges(); +// this.edgeHandlers = []; +// +// const selectionCellsHandler = this.graph.getPlugin( +// 'SelectionCellsHandler' +// ) as SelectionCellsHandler; +// +// for (let i = 0; i < edges.length; i += 1) { +// const handler = selectionCellsHandler?.getHandler(edges[i]); +// if (handler) { +// this.edgeHandlers.push(handler as EdgeHandler); +// } +// } +// } +// } +// } +// +// /** +// * Starts the handling of the mouse gesture. +// */ +// createGhostPreview() { +// const shape = this.graph.cellRenderer.createShape(this.state); +// shape.init(this.graph.view.getOverlayPane()); +// shape.scale = this.state.view.scale; +// shape.bounds = this.bounds; +// shape.outline = true; +// +// return shape; +// } +// +// /** +// * Shortcut to {@link hideSizers}. +// */ +// setHandlesVisible(visible: boolean) { +// this.handlesVisible = visible; +// +// for (let i = 0; i < this.sizers.length; i += 1) { +// this.sizers[i].node.style.display = visible ? '' : 'none'; +// } +// +// for (let i = 0; i < this.customHandles.length; i += 1) { +// this.customHandles[i].setVisible(visible); +// } +// } +// +// /** +// * Hides all sizers except. +// * +// * Starts the handling of the mouse gesture. +// */ +// hideSizers() { +// this.setHandlesVisible(false); +// } +// +// /** +// * Checks if the coordinates for the given event are within the +// * {@link Graph#tolerance}. If the event is a mouse event then the tolerance is +// * ignored. +// */ +// checkTolerance(me: InternalMouseEvent) { +// if (this.inTolerance && this.startX !== null && this.startY !== null) { +// if ( +// isMouseEvent(me.getEvent()) || +// Math.abs(me.getGraphX() - this.startX) > this.graph.getEventTolerance() || +// Math.abs(me.getGraphY() - this.startY) > this.graph.getEventTolerance() +// ) { +// this.inTolerance = false; +// } +// } +// } +// +// /** +// * Hook for subclasses do show details while the handler is active. +// */ +// updateHint(me: InternalMouseEvent) { +// return; +// } +// +// /** +// * Hooks for subclasses to hide details when the handler gets inactive. +// */ +// removeHint() { +// return; +// } +// +// /** +// * Hook for rounding the angle. This uses {@link Math.round}. +// */ +// roundAngle(angle: number) { +// return Math.round(angle * 10) / 10; +// } +// +// /** +// * Hook for rounding the unscaled width or height. This uses {@link Math.round}. +// */ +// roundLength(length: number) { +// return Math.round(length * 100) / 100; +// } +// +// /** +// * Handles the event by updating the preview. +// */ +// mouseMove(sender: EventSource, me: InternalMouseEvent) { +// if (!me.isConsumed() && this.index != null) { +// // Checks tolerance for ignoring single clicks +// this.checkTolerance(me); +// +// if (!this.inTolerance) { +// if (this.index <= InternalEvent.CUSTOM_HANDLE) { +// if (this.customHandles != null) { +// this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].processEvent(me); +// this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].active = true; +// +// if (this.ghostPreview != null) { +// this.ghostPreview.apply(this.state); +// this.ghostPreview.strokeWidth = +// this.getSelectionStrokeWidth() / +// this.ghostPreview.scale / +// this.ghostPreview.scale; +// this.ghostPreview.isDashed = this.isSelectionDashed(); +// this.ghostPreview.stroke = this.getSelectionColor(); +// this.ghostPreview.redraw(); +// +// if (this.selectionBounds != null) { +// this.selectionBorder.node.style.display = 'none'; +// } +// } else { +// if (this.movePreviewToFront) { +// this.moveToFront(); +// } +// +// this.customHandles[ +// InternalEvent.CUSTOM_HANDLE - this.index +// ].positionChanged(); +// } +// } +// } else if (this.index === InternalEvent.LABEL_HANDLE) { +// this.moveLabel(me); +// } else { +// if (this.index === InternalEvent.ROTATION_HANDLE) { +// this.rotateVertex(me); +// } else { +// this.resizeVertex(me); +// } +// +// this.updateHint(me); +// } +// } +// +// me.consume(); +// } +// // Workaround for disabling the connect highlight when over handle +// else if (!this.graph.isMouseDown && this.getHandleForEvent(me)) { +// me.consume(false); +// } +// } +// +// /** +// * Returns `true` if a ghost preview should be used for custom handles. +// */ +// isGhostPreview() { +// return this.state.cell.getChildCount() > 0; +// } +// +// /** +// * Moves the vertex. +// */ +// moveLabel(me: InternalMouseEvent) { +// const point = new Point(me.getGraphX(), me.getGraphY()); +// const tr = this.graph.view.translate; +// const { scale } = this.graph.view; +// +// if (this.graph.isGridEnabledEvent(me.getEvent())) { +// point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale; +// point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale; +// } +// +// const index = this.rotationShape ? this.sizers.length - 2 : this.sizers.length - 1; +// this.moveSizerTo(this.sizers[index], point.x, point.y); +// } +// +// /** +// * Rotates the vertex. +// */ +// rotateVertex(me: InternalMouseEvent) { +// const point = new Point(me.getGraphX(), me.getGraphY()); +// let dx = this.state.x + this.state.width / 2 - point.x; +// let dy = this.state.y + this.state.height / 2 - point.y; +// this.currentAlpha = +// dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : dy < 0 ? 180 : 0; +// +// if (dx > 0) { +// this.currentAlpha -= 180; +// } +// +// this.currentAlpha -= this.startAngle; +// +// // Rotation raster +// if (this.rotationRaster && this.graph.isGridEnabledEvent(me.getEvent())) { +// let raster; +// dx = point.x - this.state.getCenterX(); +// dy = point.y - this.state.getCenterY(); +// const dist = Math.sqrt(dx * dx + dy * dy); +// +// if (dist - this.startDist < 2) { +// raster = 15; +// } else if (dist - this.startDist < 25) { +// raster = 5; +// } else { +// raster = 1; +// } +// +// this.currentAlpha = Math.round(this.currentAlpha / raster) * raster; +// } else { +// this.currentAlpha = this.roundAngle(this.currentAlpha); +// } +// +// this.selectionBorder.rotation = this.currentAlpha; +// this.selectionBorder.redraw(); +// +// if (this.livePreviewActive) { +// this.redrawHandles(); +// } +// } +// +// /** +// * Resizes the vertex. +// */ +// resizeVertex(me: InternalMouseEvent) { +// const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); +// const alpha = toRadians(this.state.style.rotation ?? 0); +// const point = new Point(me.getGraphX(), me.getGraphY()); +// const tr = this.graph.view.translate; +// const { scale } = this.graph.view; +// let cos = Math.cos(-alpha); +// let sin = Math.sin(-alpha); +// +// let dx = point.x - this.startX; +// let dy = point.y - this.startY; +// +// // Rotates vector for mouse gesture +// const tx = cos * dx - sin * dy; +// const ty = sin * dx + cos * dy; +// +// dx = tx; +// dy = ty; +// +// const geo = this.state.cell.getGeometry(); +// if (geo && this.index !== null) { +// this.unscaledBounds = this.union( +// geo, +// dx / scale, +// dy / scale, +// this.index, +// this.graph.isGridEnabledEvent(me.getEvent()), +// 1, +// new Point(0, 0), +// this.isConstrainedEvent(me), +// this.isCenteredEvent(this.state, me) +// ); +// } +// +// // Keeps vertex within maximum graph or parent bounds +// if (geo && !geo.relative) { +// let max = this.graph.getMaximumGraphBounds(); +// +// // Handles child cells +// if (max != null && this.parentState != null) { +// max = Rectangle.fromRectangle(max); +// +// max.x -= (this.parentState.x - tr.x * scale) / scale; +// max.y -= (this.parentState.y - tr.y * scale) / scale; +// } +// +// if (this.graph.isConstrainChild(this.state.cell)) { +// let tmp = this.graph.getCellContainmentArea(this.state.cell); +// +// if (tmp != null) { +// const overlap = this.graph.getOverlap(this.state.cell); +// +// if (overlap > 0) { +// tmp = Rectangle.fromRectangle(tmp); +// +// tmp.x -= tmp.width * overlap; +// tmp.y -= tmp.height * overlap; +// tmp.width += 2 * tmp.width * overlap; +// tmp.height += 2 * tmp.height * overlap; +// } +// +// if (!max) { +// max = tmp; +// } else { +// max = Rectangle.fromRectangle(max); +// max.intersect(tmp); +// } +// } +// } +// +// if (max && this.unscaledBounds) { +// if (this.unscaledBounds.x < max.x) { +// this.unscaledBounds.width -= max.x - this.unscaledBounds.x; +// this.unscaledBounds.x = max.x; +// } +// +// if (this.unscaledBounds.y < max.y) { +// this.unscaledBounds.height -= max.y - this.unscaledBounds.y; +// this.unscaledBounds.y = max.y; +// } +// +// if (this.unscaledBounds.x + this.unscaledBounds.width > max.x + max.width) { +// this.unscaledBounds.width -= +// this.unscaledBounds.x + this.unscaledBounds.width - max.x - max.width; +// } +// +// if (this.unscaledBounds.y + this.unscaledBounds.height > max.y + max.height) { +// this.unscaledBounds.height -= +// this.unscaledBounds.y + this.unscaledBounds.height - max.y - max.height; +// } +// } +// } +// +// if (this.unscaledBounds) { +// const old = this.bounds; +// +// this.bounds = new Rectangle( +// (this.parentState ? this.parentState.x : tr.x * scale) + +// this.unscaledBounds.x * scale, +// (this.parentState ? this.parentState.y : tr.y * scale) + +// this.unscaledBounds.y * scale, +// this.unscaledBounds.width * scale, +// this.unscaledBounds.height * scale +// ); +// +// if (geo && geo.relative && this.parentState) { +// this.bounds.x += this.state.x - this.parentState.x; +// this.bounds.y += this.state.y - this.parentState.y; +// } +// +// cos = Math.cos(alpha); +// sin = Math.sin(alpha); +// +// const c2 = new Point(this.bounds.getCenterX(), this.bounds.getCenterY()); +// +// dx = c2.x - ct.x; +// dy = c2.y - ct.y; +// +// const dx2 = cos * dx - sin * dy; +// const dy2 = sin * dx + cos * dy; +// +// const dx3 = dx2 - dx; +// const dy3 = dy2 - dy; +// +// const dx4 = this.bounds.x - this.state.x; +// const dy4 = this.bounds.y - this.state.y; +// +// const dx5 = cos * dx4 - sin * dy4; +// const dy5 = sin * dx4 + cos * dy4; +// +// this.bounds.x += dx3; +// this.bounds.y += dy3; +// +// // Rounds unscaled bounds to int +// this.unscaledBounds.x = this.roundLength(this.unscaledBounds.x + dx3 / scale); +// this.unscaledBounds.y = this.roundLength(this.unscaledBounds.y + dy3 / scale); +// this.unscaledBounds.width = this.roundLength(this.unscaledBounds.width); +// this.unscaledBounds.height = this.roundLength(this.unscaledBounds.height); +// +// // Shifts the children according to parent offset +// if (!this.state.cell.isCollapsed() && (dx3 !== 0 || dy3 !== 0)) { +// this.childOffsetX = this.state.x - this.bounds.x + dx5; +// this.childOffsetY = this.state.y - this.bounds.y + dy5; +// } else { +// this.childOffsetX = 0; +// this.childOffsetY = 0; +// } +// +// if (!old.equals(this.bounds)) { +// if (this.livePreviewActive) { +// this.updateLivePreview(me); +// } +// +// if (this.preview != null) { +// this.drawPreview(); +// } else { +// this.updateParentHighlight(); +// } +// } +// } +// } +// +// /** +// * Repaints the live preview. +// */ +// updateLivePreview(me: InternalMouseEvent) { +// // TODO: Apply child offset to children in live preview +// const { scale } = this.graph.view; +// const tr = this.graph.view.translate; +// +// // Saves current state +// const tempState = this.state.clone(); +// +// // Temporarily changes size and origin +// this.state.x = this.bounds.x; +// this.state.y = this.bounds.y; +// this.state.origin = new Point( +// this.state.x / scale - tr.x, +// this.state.y / scale - tr.y +// ); +// this.state.width = this.bounds.width; +// this.state.height = this.bounds.height; +// +// // Redraws cell and handles +// let off = this.state.absoluteOffset; +// off = new Point(off.x, off.y); +// +// // Required to store and reset absolute offset for updating label position +// this.state.absoluteOffset.x = 0; +// this.state.absoluteOffset.y = 0; +// const geo = this.state.cell.getGeometry(); +// +// if (geo != null) { +// const offset = geo.offset || this.EMPTY_POINT; +// +// if (offset != null && !geo.relative) { +// this.state.absoluteOffset.x = this.state.view.scale * offset.x; +// this.state.absoluteOffset.y = this.state.view.scale * offset.y; +// } +// +// this.state.view.updateVertexLabelOffset(this.state); +// } +// +// // Draws the live preview +// (this.state.view.graph).cellRenderer.redraw(this.state, true); +// +// // Redraws connected edges TODO: Include child edges +// this.state.view.invalidate(this.state.cell); +// this.state.invalid = false; +// this.state.view.validate(); +// this.redrawHandles(); +// +// // Moves live preview to front +// if (this.movePreviewToFront) { +// this.moveToFront(); +// } +// +// // Hides folding icon +// if (this.state.control != null && this.state.control.node != null) { +// this.state.control.node.style.visibility = 'hidden'; +// } +// +// // Restores current state +// this.state.setState(tempState); +// } +// +// /** +// * Handles the event by applying the changes to the geometry. +// */ +// moveToFront() { +// if ( +// (this.state.text && this.state.text.node && this.state.text.node.nextSibling) || +// (this.state.shape && +// this.state.shape.node && +// this.state.shape.node.nextSibling && +// (!this.state.text || this.state.shape.node.nextSibling !== this.state.text.node)) +// ) { +// if (this.state.shape && this.state.shape.node && this.state.shape.node.parentNode) { +// this.state.shape.node.parentNode.appendChild(this.state.shape.node); +// } +// +// if (this.state.text && this.state.text.node && this.state.text.node.parentNode) { +// this.state.text.node.parentNode.appendChild(this.state.text.node); +// } +// } +// } +// +// /** +// * Handles the event by applying the changes to the geometry. +// */ +// mouseUp(sender: EventSource, me: InternalMouseEvent) { +// if (this.index != null && this.state != null) { +// const point = new Point(me.getGraphX(), me.getGraphY()); +// const { index } = this; +// this.index = null; +// +// if (this.ghostPreview == null) { +// // Required to restore order in case of no change +// this.state.view.invalidate(this.state.cell, false, false); +// this.state.view.validate(); +// } +// +// this.graph.batchUpdate(() => { +// if (index <= InternalEvent.CUSTOM_HANDLE) { +// if (this.customHandles != null) { +// // Creates style before changing cell state +// const style = (this.state.view.graph).getCellStyle(this.state.cell); +// +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].active = false; +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].execute(me); +// +// // Sets style and apply on shape to force repaint and +// // check if execute has removed custom handles +// if ( +// this.customHandles != null && +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index] != null +// ) { +// this.state.style = style; +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].positionChanged(); +// } +// } +// } else if (index === InternalEvent.ROTATION_HANDLE) { +// if (this.currentAlpha != null) { +// const delta = this.currentAlpha - (this.state.style.rotation ?? 0); +// +// if (delta !== 0) { +// this.rotateCell(this.state.cell, delta); +// } +// } else { +// this.rotateClick(); +// } +// } else { +// const gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); +// const alpha = toRadians(this.state.style.rotation ?? 0); +// const cos = Math.cos(-alpha); +// const sin = Math.sin(-alpha); +// +// let dx = point.x - this.startX; +// let dy = point.y - this.startY; +// +// // Rotates vector for mouse gesture +// const tx = cos * dx - sin * dy; +// const ty = sin * dx + cos * dy; +// +// dx = tx; +// dy = ty; +// +// const s = this.graph.view.scale; +// const recurse = this.isRecursiveResize(this.state, me); +// +// this.resizeCell( +// this.state.cell, +// this.roundLength(dx / s), +// this.roundLength(dy / s), +// index, +// gridEnabled, +// this.isConstrainedEvent(me), +// recurse +// ); +// } +// }); +// +// me.consume(); +// this.reset(); +// this.redrawHandles(); +// } +// } +// +// /** +// * Returns the `recursiveResize` status of the given state. +// * @param state the given {@link CellState}. This implementation takes the value of this state. +// * @param me the mouse event. +// */ +// isRecursiveResize(state: CellState, me: InternalMouseEvent) { +// return this.graph.isRecursiveResize(this.state); +// } +// +// /** +// * Hook for subclasses to implement a single click on the rotation handle. +// * This code is executed as part of the model transaction. This implementation +// * is empty. +// */ +// rotateClick() { +// return; +// } +// +// /** +// * Rotates the given cell and its children by the given angle in degrees. +// * +// * @param cell {@link Cell} to be rotated. +// * @param angle Angle in degrees. +// * @param parent if set, consider the parent in the rotation computation. +// */ +// rotateCell(cell: Cell, angle: number, parent?: Cell) { +// if (angle !== 0) { +// const model = this.graph.getDataModel(); +// +// if (cell.isVertex() || cell.isEdge()) { +// if (!cell.isEdge()) { +// const style = this.graph.getCurrentCellStyle(cell); +// const total = (style.rotation || 0) + angle; +// this.graph.setCellStyles('rotation', total, [cell]); +// } +// +// let geo = cell.getGeometry(); +// +// if (geo && parent) { +// const pgeo = parent.getGeometry(); +// +// if (pgeo != null && !parent.isEdge()) { +// geo = geo.clone(); +// geo.rotate(angle, new Point(pgeo.width / 2, pgeo.height / 2)); +// model.setGeometry(cell, geo); +// } +// +// if ((cell.isVertex() && !geo.relative) || cell.isEdge()) { +// // Recursive rotation +// const childCount = cell.getChildCount(); +// +// for (let i = 0; i < childCount; i += 1) { +// this.rotateCell(cell.getChildAt(i), angle, cell); +// } +// } +// } +// } +// } +// } +// +// /** +// * Resets the state of this handler. +// */ +// reset() { +// if (this.index !== null && this.sizers[this.index].node.style.display === 'none') { +// this.sizers[this.index].node.style.display = ''; +// } +// +// this.index = null; +// +// // TODO: Reset and redraw cell states for live preview +// if (this.preview) { +// this.preview.destroy(); +// this.preview = null; +// } +// +// if (this.ghostPreview) { +// this.ghostPreview.destroy(); +// this.ghostPreview = null; +// } +// +// if (this.livePreviewActive) { +// for (let i = 0; i < this.sizers.length; i += 1) { +// this.sizers[i].node.style.display = ''; +// } +// +// // Shows folding icon +// if (this.state.control && this.state.control.node) { +// this.state.control.node.style.visibility = ''; +// } +// } +// +// for (let i = 0; i < this.customHandles.length; i += 1) { +// if (this.customHandles[i].active) { +// this.customHandles[i].active = false; +// this.customHandles[i].reset(); +// } else { +// this.customHandles[i].setVisible(true); +// } +// } +// +// // Checks if handler has been destroyed +// this.selectionBorder.node.style.display = 'inline'; +// this.selectionBounds = this.getSelectionBounds(this.state); +// this.bounds = new Rectangle( +// this.selectionBounds.x, +// this.selectionBounds.y, +// this.selectionBounds.width, +// this.selectionBounds.height +// ); +// this.drawPreview(); +// +// this.removeHint(); +// this.redrawHandles(); +// this.edgeHandlers = []; +// this.handlesVisible = true; +// this.unscaledBounds = null; +// } +// +// /** +// * Uses the given vector to change the bounds of the given cell +// * in the graph using {@link Graph#resizeCell}. +// */ +// resizeCell( +// cell: Cell, +// dx: number, +// dy: number, +// index: number, +// gridEnabled: boolean, +// constrained: boolean, +// recurse: boolean +// ) { +// let geo = cell.getGeometry(); +// +// if (geo) { +// if ( +// index === InternalEvent.LABEL_HANDLE && +// this.labelShape && +// this.labelShape.bounds +// ) { +// const alpha = -toRadians(this.state.style.rotation ?? 0); +// const cos = Math.cos(alpha); +// const sin = Math.sin(alpha); +// const { scale } = this.graph.view; +// const pt = getRotatedPoint( +// new Point( +// Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale), +// Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale) +// ), +// cos, +// sin +// ); +// +// geo = geo.clone(); +// +// if (geo.offset == null) { +// geo.offset = pt; +// } else { +// geo.offset.x += pt.x; +// geo.offset.y += pt.y; +// } +// +// this.graph.model.setGeometry(cell, geo); +// } else if (this.unscaledBounds) { +// const { scale } = this.graph.view; +// +// if (this.childOffsetX !== 0 || this.childOffsetY !== 0) { +// this.moveChildren( +// cell, +// Math.round(this.childOffsetX / scale), +// Math.round(this.childOffsetY / scale) +// ); +// } +// +// this.graph.resizeCell(cell, this.unscaledBounds, recurse); +// } +// } +// } +// +// /** +// * Moves the children of the given cell by the given vector. +// */ +// moveChildren(cell: Cell, dx: number, dy: number) { +// const model = this.graph.getDataModel(); +// const childCount = cell.getChildCount(); +// +// for (let i = 0; i < childCount; i += 1) { +// const child = cell.getChildAt(i); +// let geo = child.getGeometry(); +// +// if (geo != null) { +// geo = geo.clone(); +// geo.translate(dx, dy); +// model.setGeometry(child, geo); +// } +// } +// } +// +// /** +// * Returns the union of the given bounds and location for the specified +// * handle index. +// * +// * To override this to limit the size of vertex via a minWidth/-Height style, +// * the following code can be used. +// * +// * ```javascript +// * let vertexHandlerUnion = union; +// * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> +// * { +// * let result = vertexHandlerUnion.apply(this, arguments); +// * +// * result.width = Math.max(result.width, mxUtils.getNumber(this.state.style, 'minWidth', 0)); +// * result.height = Math.max(result.height, mxUtils.getNumber(this.state.style, 'minHeight', 0)); +// * +// * return result; +// * }; +// * ``` +// * +// * The minWidth/-Height style can then be used as follows: +// * +// * ```javascript +// * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'minWidth=100;minHeight=100;'); +// * ``` +// * +// * To override this to update the height for a wrapped text if the width of a vertex is +// * changed, the following can be used. +// * +// * ```javascript +// * let mxVertexHandlerUnion = union; +// * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> +// * { +// * let result = mxVertexHandlerUnion.apply(this, arguments); +// * let s = this.state; +// * +// * if (this.graph.isHtmlLabel(s.cell) && (index == 3 || index == 4) && +// * s.text != null && s.style.whiteSpace == 'wrap') +// * { +// * let label = this.graph.getLabel(s.cell); +// * let fontSize = mxUtils.getNumber(s.style, 'fontSize', mxConstants.DEFAULT_FONTSIZE); +// * let ww = result.width / s.view.scale - s.text.spacingRight - s.text.spacingLeft +// * +// * result.height = mxUtils.getSizeForString(label, fontSize, s.style.fontFamily, ww).height; +// * } +// * +// * return result; +// * }; +// * ``` +// */ +// union( +// bounds: Rectangle, +// dx: number, +// dy: number, +// index: number, +// gridEnabled: boolean, +// scale: number, +// tr: Point, +// constrained: boolean, +// centered: boolean +// ) { +// gridEnabled = gridEnabled && this.graph.isGridEnabled(); +// +// if (this.singleSizer) { +// let x = bounds.x + bounds.width + dx; +// let y = bounds.y + bounds.height + dy; +// +// if (gridEnabled) { +// x = this.graph.snap(x / scale) * scale; +// y = this.graph.snap(y / scale) * scale; +// } +// +// const rect = new Rectangle(bounds.x, bounds.y, 0, 0); +// rect.add(new Rectangle(x, y, 0, 0)); +// +// return rect; +// } +// const w0 = bounds.width; +// const h0 = bounds.height; +// let left = bounds.x - tr.x * scale; +// let right = left + w0; +// let top = bounds.y - tr.y * scale; +// let bottom = top + h0; +// +// const cx = left + w0 / 2; +// const cy = top + h0 / 2; +// +// if (index > 4 /* Bottom Row */) { +// bottom += dy; +// +// if (gridEnabled) { +// bottom = this.graph.snap(bottom / scale) * scale; +// } else { +// bottom = Math.round(bottom / scale) * scale; +// } +// } else if (index < 3 /* Top Row */) { +// top += dy; +// +// if (gridEnabled) { +// top = this.graph.snap(top / scale) * scale; +// } else { +// top = Math.round(top / scale) * scale; +// } +// } +// +// if (index === 0 || index === 3 || index === 5 /* Left */) { +// left += dx; +// +// if (gridEnabled) { +// left = this.graph.snap(left / scale) * scale; +// } else { +// left = Math.round(left / scale) * scale; +// } +// } else if (index === 2 || index === 4 || index === 7 /* Right */) { +// right += dx; +// +// if (gridEnabled) { +// right = this.graph.snap(right / scale) * scale; +// } else { +// right = Math.round(right / scale) * scale; +// } +// } +// +// let width = right - left; +// let height = bottom - top; +// +// if (constrained) { +// const geo = this.state.cell.getGeometry(); +// +// if (geo != null) { +// const aspect = geo.width / geo.height; +// +// if (index === 1 || index === 2 || index === 7 || index === 6) { +// width = height * aspect; +// } else { +// height = width / aspect; +// } +// +// if (index === 0) { +// left = right - width; +// top = bottom - height; +// } +// } +// } +// +// if (centered) { +// width += width - w0; +// height += height - h0; +// +// const cdx = cx - (left + width / 2); +// const cdy = cy - (top + height / 2); +// +// left += cdx; +// top += cdy; +// right += cdx; +// bottom += cdy; +// } +// +// // Flips over left side +// if (width < 0) { +// left += width; +// width = Math.abs(width); +// } +// +// // Flips over top side +// if (height < 0) { +// top += height; +// height = Math.abs(height); +// } +// +// const result = new Rectangle(left + tr.x * scale, top + tr.y * scale, width, height); +// +// if (this.minBounds != null) { +// result.width = Math.max( +// result.width, +// this.minBounds.x * scale + +// this.minBounds.width * scale + +// Math.max(0, this.x0 * scale - result.x) +// ); +// result.height = Math.max( +// result.height, +// this.minBounds.y * scale + +// this.minBounds.height * scale + +// Math.max(0, this.y0 * scale - result.y) +// ); +// } +// +// return result; +// } +// +// /** +// * Redraws the handles and the preview. +// */ +// redraw(ignoreHandles?: boolean) { +// this.selectionBounds = this.getSelectionBounds(this.state); +// this.bounds = new Rectangle( +// this.selectionBounds.x, +// this.selectionBounds.y, +// this.selectionBounds.width, +// this.selectionBounds.height +// ); +// this.drawPreview(); +// +// if (!ignoreHandles) { +// this.redrawHandles(); +// } +// } +// +// /** +// * Returns the padding to be used for drawing handles for the current . +// */ +// getHandlePadding() { +// // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) +// const result = new Point(0, 0); +// let tol = this.tolerance; +// +// if ( +// this.sizers.length > 0 && +// this.sizers[0].bounds && +// (this.bounds.width < 2 * this.sizers[0].bounds.width + 2 * tol || +// this.bounds.height < 2 * this.sizers[0].bounds.height + 2 * tol) +// ) { +// tol /= 2; +// +// result.x = this.sizers[0].bounds.width + tol; +// result.y = this.sizers[0].bounds.height + tol; +// } +// +// return result; +// } +// +// /** +// * Returns the bounds used to paint the resize handles. +// */ +// getSizerBounds() { +// return this.bounds; +// } +// +// /** +// * Redraws the handles. To hide certain handles the following code can be used. +// * +// * ```javascript +// * redrawHandles() +// * { +// * mxVertexHandlerRedrawHandles.apply(this, arguments); +// * +// * if (this.sizers != null && this.sizers.length > 7) +// * { +// * this.sizers[1].node.style.display = 'none'; +// * this.sizers[6].node.style.display = 'none'; +// * } +// * }; +// * ``` +// */ +// redrawHandles() { +// let s = this.getSizerBounds(); +// const tol = this.tolerance; +// this.horizontalOffset = 0; +// this.verticalOffset = 0; +// +// for (let i = 0; i < this.customHandles.length; i += 1) { +// const shape = this.customHandles[i].shape; +// +// if (shape) { +// const temp = shape.node.style.display; +// this.customHandles[i].redraw(); +// shape.node.style.display = temp; +// +// // Hides custom handles during text editing +// shape.node.style.visibility = +// this.handlesVisible && this.isCustomHandleVisible(this.customHandles[i]) +// ? '' +// : 'hidden'; +// } +// } +// +// if (this.sizers.length > 0 && this.sizers[0]) { +// if (this.index === null && this.manageSizers && this.sizers.length >= 8) { +// // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) +// const padding = this.getHandlePadding(); +// this.horizontalOffset = padding.x; +// this.verticalOffset = padding.y; +// +// if (this.horizontalOffset !== 0 || this.verticalOffset !== 0) { +// s = new Rectangle(s.x, s.y, s.width, s.height); +// +// s.x -= this.horizontalOffset / 2; +// s.width += this.horizontalOffset; +// s.y -= this.verticalOffset / 2; +// s.height += this.verticalOffset; +// } +// +// if (this.sizers.length >= 8) { +// if ( +// this.sizers[0].bounds && +// (s.width < 2 * this.sizers[0].bounds.width + 2 * tol || +// s.height < 2 * this.sizers[0].bounds.height + 2 * tol) +// ) { +// this.sizers[0].node.style.display = 'none'; +// this.sizers[2].node.style.display = 'none'; +// this.sizers[5].node.style.display = 'none'; +// this.sizers[7].node.style.display = 'none'; +// } else if (this.handlesVisible) { +// this.sizers[0].node.style.display = ''; +// this.sizers[2].node.style.display = ''; +// this.sizers[5].node.style.display = ''; +// this.sizers[7].node.style.display = ''; +// } +// } +// } +// +// const r = s.x + s.width; +// const b = s.y + s.height; +// +// if (this.singleSizer) { +// this.moveSizerTo(this.sizers[0], r, b); +// } else { +// const cx = s.x + s.width / 2; +// const cy = s.y + s.height / 2; +// +// if (this.sizers.length >= 8) { +// const crs = [ +// 'nw-resize', +// 'n-resize', +// 'ne-resize', +// 'e-resize', +// 'se-resize', +// 's-resize', +// 'sw-resize', +// 'w-resize', +// ]; +// +// const alpha = toRadians(this.state.style.rotation ?? 0); +// const cos = Math.cos(alpha); +// const sin = Math.sin(alpha); +// const da = Math.round((alpha * 4) / Math.PI); +// +// const ct = new Point(s.getCenterX(), s.getCenterY()); +// let pt = getRotatedPoint(new Point(s.x, s.y), cos, sin, ct); +// this.moveSizerTo(this.sizers[0], pt.x, pt.y); +// this.sizers[0].setCursor(crs[mod(0 + da, crs.length)]); +// +// pt.x = cx; +// pt.y = s.y; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[1], pt.x, pt.y); +// this.sizers[1].setCursor(crs[mod(1 + da, crs.length)]); +// +// pt.x = r; +// pt.y = s.y; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[2], pt.x, pt.y); +// this.sizers[2].setCursor(crs[mod(2 + da, crs.length)]); +// +// pt.x = s.x; +// pt.y = cy; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[3], pt.x, pt.y); +// this.sizers[3].setCursor(crs[mod(7 + da, crs.length)]); +// +// pt.x = r; +// pt.y = cy; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[4], pt.x, pt.y); +// this.sizers[4].setCursor(crs[mod(3 + da, crs.length)]); +// +// pt.x = s.x; +// pt.y = b; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[5], pt.x, pt.y); +// this.sizers[5].setCursor(crs[mod(6 + da, crs.length)]); +// +// pt.x = cx; +// pt.y = b; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[6], pt.x, pt.y); +// this.sizers[6].setCursor(crs[mod(5 + da, crs.length)]); +// +// pt.x = r; +// pt.y = b; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[7], pt.x, pt.y); +// this.sizers[7].setCursor(crs[mod(4 + da, crs.length)]); +// +// pt.x = cx + this.state.absoluteOffset.x; +// pt.y = cy + this.state.absoluteOffset.y; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[8], pt.x, pt.y); +// } else if (this.state.width >= 2 && this.state.height >= 2) { +// this.moveSizerTo( +// this.sizers[0], +// cx + this.state.absoluteOffset.x, +// cy + this.state.absoluteOffset.y +// ); +// } else { +// this.moveSizerTo(this.sizers[0], this.state.x, this.state.y); +// } +// } +// } +// +// if (this.rotationShape) { +// const alpha = toRadians(this.currentAlpha); +// const cos = Math.cos(alpha); +// const sin = Math.sin(alpha); +// +// const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); +// const pt = getRotatedPoint(this.getRotationHandlePosition(), cos, sin, ct); +// +// if (this.rotationShape.node != null) { +// this.moveSizerTo(this.rotationShape, pt.x, pt.y); +// +// // Hides rotation handle during text editing +// this.rotationShape.node.style.visibility = +// (this.state.view.graph).isEditing() || !this.handlesVisible +// ? 'hidden' +// : ''; +// } +// } +// +// if (this.selectionBorder != null) { +// this.selectionBorder.rotation = this.state.style.rotation ?? 0; +// } +// +// if (this.edgeHandlers != null) { +// for (let i = 0; i < this.edgeHandlers.length; i += 1) { +// this.edgeHandlers[i].redraw(); +// } +// } +// } +// +// /** +// * Returns true if the given custom handle is visible. +// */ +// isCustomHandleVisible(handle: CellHandle) { +// return ( +// !this.graph.isEditing() && (this.state.view.graph).getSelectionCount() === 1 +// ); +// } +// +// /** +// * Returns an {@link Point} that defines the rotation handle position. +// */ +// getRotationHandlePosition() { +// return new Point( +// this.bounds.x + this.bounds.width / 2, +// this.bounds.y + this.rotationHandleVSpacing +// ); +// } +// +// /** +// * Returns `true` if the parent highlight should be visible. +// * +// * This implementation always returns `true`. +// */ +// isParentHighlightVisible() { +// const parent = this.state.cell.getParent(); +// return parent ? !this.graph.isCellSelected(parent) : false; +// } +// +// /** +// * Updates the highlight of the parent if {@link parentHighlightEnabled} is `true`. +// */ +// updateParentHighlight() { +// if (!this.isDestroyed()) { +// const visible = this.isParentHighlightVisible(); +// const parent = this.state.cell.getParent(); +// const pstate = parent ? this.graph.view.getState(parent) : null; +// +// if (this.parentHighlight) { +// if (parent && parent.isVertex() && visible) { +// const b = this.parentHighlight.bounds; +// +// if ( +// pstate && +// b && +// (b.x !== pstate.x || +// b.y !== pstate.y || +// b.width !== pstate.width || +// b.height !== pstate.height) +// ) { +// this.parentHighlight.bounds = Rectangle.fromRectangle(pstate); +// this.parentHighlight.redraw(); +// } +// } else { +// if (pstate != null && pstate.parentHighlight === this.parentHighlight) { +// pstate.parentHighlight = null; +// } +// +// this.parentHighlight.destroy(); +// this.parentHighlight = null; +// } +// } else if (this.parentHighlightEnabled && visible) { +// if ( +// parent && +// parent.isVertex() && +// pstate != null && +// pstate.parentHighlight == null +// ) { +// this.parentHighlight = this.createParentHighlightShape(pstate); +// // VML dialect required here for event transparency in IE +// this.parentHighlight.dialect = DIALECT.SVG; +// this.parentHighlight.pointerEvents = false; +// this.parentHighlight.rotation = pstate.style.rotation ?? 0; +// this.parentHighlight.init(this.graph.getView().getOverlayPane()); +// this.parentHighlight.redraw(); +// +// // Shows highlight once per parent +// pstate.parentHighlight = this.parentHighlight; +// } +// } +// } +// } +// +// /** +// * Redraws the preview. +// */ +// drawPreview() { +// if (this.preview != null) { +// this.preview.bounds = this.bounds; +// +// if (this.preview.node.parentNode === this.graph.container) { +// this.preview.bounds.width = Math.max(0, this.preview.bounds.width - 1); +// this.preview.bounds.height = Math.max(0, this.preview.bounds.height - 1); +// } +// +// this.preview.rotation = this.state.style.rotation ?? 0; +// this.preview.redraw(); +// } +// +// this.selectionBorder.bounds = this.getSelectionBorderBounds(); +// this.selectionBorder.redraw(); +// this.updateParentHighlight(); +// } +// +// /** +// * Returns the bounds for the selection border. +// */ +// getSelectionBorderBounds() { +// return this.bounds; +// } +// +// /** +// * Returns `true` if this handler was destroyed or not initialized. +// */ +// isDestroyed() { +// return this.selectionBorder == null; +// } +// +// /** +// * Destroys the handler and all its resources and DOM nodes. +// */ +// onDestroy() { +// (this.state.view.graph).removeListener(this.escapeHandler); +// this.escapeHandler = () => { +// return; +// }; +// +// if (this.preview) { +// this.preview.destroy(); +// this.preview = null; +// } +// +// if (this.parentHighlight) { +// const parent = this.state.cell.getParent(); +// const pstate = parent ? this.graph.view.getState(parent) : null; +// +// if (pstate && pstate.parentHighlight === this.parentHighlight) { +// pstate.parentHighlight = null; +// } +// +// this.parentHighlight.destroy(); +// this.parentHighlight = null; +// } +// +// if (this.ghostPreview) { +// this.ghostPreview.destroy(); +// this.ghostPreview = null; +// } +// +// if (this.selectionBorder) { +// this.selectionBorder.destroy(); +// } +// +// this.labelShape = null; +// this.removeHint(); +// +// for (let i = 0; i < this.sizers.length; i += 1) { +// this.sizers[i].destroy(); +// } +// +// this.sizers = []; +// +// for (let i = 0; i < this.customHandles.length; i += 1) { +// this.customHandles[i].destroy(); +// } +// +// this.customHandles = []; +// } +// } + +type VertexHandler = { + reset(): void; state: CellState; - - sizers: Shape[] = []; - - /** - * Specifies if only one sizer handle at the bottom, right corner should be used. - * @default false - */ - singleSizer = false; - - /** - * Holds the index of the current handle. - */ - index: number | null = null; - - /** - * Specifies if the bounds of handles should be used for hit-detection in IE or if {@link tolerance} > 0. - * @default true - */ - allowHandleBoundsCheck = true; - - /** - * Optional {@link Image} to be used as handles. - * @default null - */ - handleImage: Image | null = null; - - /** - * If handles are currently visible. - * @default true - */ - handlesVisible = true; - - /** - * Optional tolerance for hit-detection in {@link getHandleForEvent}. - * @default 0 - */ - tolerance = 0; - - /** - * Specifies if a rotation handle should be visible. - * @default false - */ - rotationEnabled = false; - - /** - * Specifies if the parent should be highlighted if a child cell is selected. - * @default false - */ - parentHighlightEnabled = false; - - /** - * Specifies if rotation steps should be "rasterized" depending on the distance to the handle. - * @default true - */ - rotationRaster = true; - - /** - * Specifies the cursor for the rotation handle. - * @default 'crosshair'. - */ - rotationCursor = 'crosshair'; - - /** - * Specifies if resize should change the cell in-place. This is an experimental - * feature for non-touch devices. - * @default false - */ - livePreview = false; - - /** - * Specifies if the live preview should be moved to the front. - * @default false - */ - movePreviewToFront = false; - - /** - * Specifies if sizers should be hidden and spaced if the vertex is small. - * @default false - */ - manageSizers = false; - - /** - * Specifies if the size of groups should be constrained by the children. - * @default false - */ - constrainGroupByChildren = false; - - /** - * Vertical spacing for rotation icon. - * @default -16 - */ - rotationHandleVSpacing = -16; - - /** - * The horizontal offset for the handles. This is updated in {@link redrawHandles} - * if {@link manageSizers} is `true` and the sizers are offset horizontally. - */ - horizontalOffset = 0; - - /** - * The horizontal offset for the handles. This is updated in - * if {@link manageSizers} is true and the sizers are offset vertically. - */ - verticalOffset = 0; - - minBounds: Rectangle | null = null; - - x0 = 0; - y0 = 0; - - customHandles: CellHandle[] = []; - - inTolerance = false; - - startX = 0; - startY = 0; - - rotationShape: Shape | null = null; - - currentAlpha = 100; - startAngle = 0; - startDist = 0; - - ghostPreview: Shape | null = null; - - livePreviewActive = false; - - childOffsetX = 0; - childOffsetY = 0; - - parentState: CellState | null = null; - parentHighlight: RectangleShape | null = null; - - unscaledBounds: Rectangle | null = null; - - preview: Shape | null = null; - - labelShape: Shape | null = null; - - edgeHandlers: EdgeHandler[] = []; - - EMPTY_POINT = new Point(); - - /** - * Constructs an event handler that allows to resize vertices and groups. - * - * @param state {@link CellState} of the cell to be resized. - */ - constructor(state: CellState) { - this.state = state; - this.graph = this.state.view.graph; - this.selectionBounds = this.getSelectionBounds(this.state); - this.bounds = new Rectangle( - this.selectionBounds.x, - this.selectionBounds.y, - this.selectionBounds.width, - this.selectionBounds.height - ); - this.selectionBorder = this.createSelectionShape(this.bounds); - // VML dialect required here for event transparency in IE - this.selectionBorder.dialect = DIALECT.SVG; - this.selectionBorder.pointerEvents = false; - this.selectionBorder.rotation = this.state.style.rotation ?? 0; - this.selectionBorder.init(this.graph.getView().getOverlayPane()); - InternalEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state); - - if (this.graph.isCellMovable(this.state.cell)) { - this.selectionBorder.setCursor(CURSOR.MOVABLE_VERTEX); - } - - const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; - - // Adds the sizer handles - if ( - selectionHandler && - (selectionHandler.maxCells <= 0 || - this.graph.getSelectionCount() < selectionHandler.maxCells) - ) { - const resizable = this.graph.isCellResizable(this.state.cell); - this.sizers = []; - - if ( - resizable || - (this.graph.isLabelMovable(this.state.cell) && - this.state.width >= 2 && - this.state.height >= 2) - ) { - let i = 0; - - if (resizable) { - if (!this.singleSizer) { - this.sizers.push(this.createSizer('nw-resize', i++)); - this.sizers.push(this.createSizer('n-resize', i++)); - this.sizers.push(this.createSizer('ne-resize', i++)); - this.sizers.push(this.createSizer('w-resize', i++)); - this.sizers.push(this.createSizer('e-resize', i++)); - this.sizers.push(this.createSizer('sw-resize', i++)); - this.sizers.push(this.createSizer('s-resize', i++)); - } - - this.sizers.push(this.createSizer('se-resize', i++)); - } - - const geo = this.state.cell.getGeometry(); - - if ( - geo != null && - !geo.relative && - //!this.graph.isSwimlane(this.state.cell) && disable for now - this.graph.isLabelMovable(this.state.cell) - ) { - // Marks this as the label handle for getHandleForEvent - this.labelShape = this.createSizer( - CURSOR.LABEL_HANDLE, - InternalEvent.LABEL_HANDLE, - LABEL_HANDLE_SIZE, - LABEL_HANDLE_FILLCOLOR - ); - this.sizers.push(this.labelShape); - } - } else if ( - this.graph.isCellMovable(this.state.cell) && - !this.graph.isCellResizable(this.state.cell) && - this.state.width < 2 && - this.state.height < 2 - ) { - this.labelShape = this.createSizer( - CURSOR.MOVABLE_VERTEX, - InternalEvent.LABEL_HANDLE, - undefined, - LABEL_HANDLE_FILLCOLOR - ); - this.sizers.push(this.labelShape); - } - } - - // Adds the rotation handler - if (this.isRotationHandleVisible()) { - this.rotationShape = this.createSizer( - this.rotationCursor, - InternalEvent.ROTATION_HANDLE, - HANDLE_SIZE + 3, - HANDLE_FILLCOLOR - ); - this.sizers.push(this.rotationShape); - } - - this.customHandles = this.createCustomHandles(); - this.redraw(); - - if (this.constrainGroupByChildren) { - this.updateMinBounds(); - } - - // Handles escape keystrokes - this.escapeHandler = (sender: Listenable, evt: Event) => { - if (this.livePreview && this.index != null) { - // Redraws the live preview - (this.state.view.graph).cellRenderer.redraw(this.state, true); - - // Redraws connected edges - this.state.view.invalidate(this.state.cell); - this.state.invalid = false; - this.state.view.validate(); - } - - this.reset(); - }; - - (this.state.view.graph).addListener(InternalEvent.ESCAPE, this.escapeHandler); - } - - /** - * Returns `true` if the rotation handle should be showing. - */ - isRotationHandleVisible() { - const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; - const selectionHandlerCheck = selectionHandler - ? selectionHandler.maxCells <= 0 || - this.graph.getSelectionCount() < selectionHandler.maxCells - : true; - - return ( - this.graph.isEnabled() && - this.rotationEnabled && - this.graph.isCellRotatable(this.state.cell) && - selectionHandlerCheck - ); - } - - /** - * Returns `true` if the aspect ratio if the cell should be maintained. - */ - isConstrainedEvent(me: InternalMouseEvent) { - return isShiftDown(me.getEvent()) || this.state.style.aspect === 'fixed'; - } - - /** - * Returns `true` if the center of the vertex should be maintained during the resize. - */ - isCenteredEvent(state: CellState, me: InternalMouseEvent) { - return false; - } - - /** - * Returns an array of custom handles. This implementation returns null. - */ - createCustomHandles() { - return []; - } - - /** - * Initializes the shapes required for this vertex handler. - */ - updateMinBounds() { - const children = this.graph.getChildCells(this.state.cell); - - if (children.length > 0) { - this.minBounds = this.graph.view.getBounds(children); - - if (this.minBounds) { - const s = this.state.view.scale; - const t = this.state.view.translate; - - this.minBounds.x -= this.state.x; - this.minBounds.y -= this.state.y; - this.minBounds.x /= s; - this.minBounds.y /= s; - this.minBounds.width /= s; - this.minBounds.height /= s; - this.x0 = this.state.x / s - t.x; - this.y0 = this.state.y / s - t.y; - } - } - } - - /** - * Returns the mxRectangle that defines the bounds of the selection border. - */ - getSelectionBounds(state: CellState) { - return new Rectangle( - Math.round(state.x), - Math.round(state.y), - Math.round(state.width), - Math.round(state.height) - ); - } - - /** - * Creates the shape used to draw the selection border. - */ - createParentHighlightShape(bounds: Rectangle) { - return this.createSelectionShape(bounds); - } - - /** - * Creates the shape used to draw the selection border. - */ - createSelectionShape(bounds: Rectangle) { - const shape = new RectangleShape( - Rectangle.fromRectangle(bounds), - NONE, - this.getSelectionColor() - ); - shape.strokeWidth = this.getSelectionStrokeWidth(); - shape.isDashed = this.isSelectionDashed(); - - return shape; - } - - /** - * Returns {@link VERTEX_SELECTION_COLOR}. - */ - getSelectionColor() { - return VERTEX_SELECTION_COLOR; - } - - /** - * Returns {@link VERTEX_SELECTION_STROKEWIDTH}. - */ - getSelectionStrokeWidth() { - return VERTEX_SELECTION_STROKEWIDTH; - } - - /** - * Returns {@link VERTEX_SELECTION_DASHED}. - */ - isSelectionDashed() { - return VERTEX_SELECTION_DASHED; - } - - /** - * Creates a sizer handle for the specified cursor and index and returns - * the new {@link RectangleShape} that represents the handle. - */ - createSizer( - cursor: string, - index: number, - size = HANDLE_SIZE, - fillColor = HANDLE_FILLCOLOR - ) { - const bounds = new Rectangle(0, 0, size, size); - const sizer = this.createSizerShape(bounds, index, fillColor); - - if ( - sizer.bounds && - sizer.isHtmlAllowed() && - this.state.text && - this.state.text.node.parentNode === this.graph.container - ) { - sizer.bounds.height -= 1; - sizer.bounds.width -= 1; - sizer.dialect = DIALECT.STRICTHTML; - sizer.init(this.graph.container); - } else { - sizer.dialect = - this.graph.dialect !== DIALECT.SVG ? DIALECT.MIXEDHTML : DIALECT.SVG; - sizer.init(this.graph.getView().getOverlayPane()); - } - - InternalEvent.redirectMouseEvents(sizer.node, this.graph, this.state); - - if (this.graph.isEnabled()) { - sizer.setCursor(cursor); - } - - if (!this.isSizerVisible(index)) { - sizer.visible = false; - } - - return sizer; - } - - /** - * Returns `true` if the sizer for the given index is visible. - * - * This implementation returns `true` for all given indices. - */ - isSizerVisible(index: number) { - return true; - } - - /** - * Creates the shape used for the sizer handle for the specified bounds an - * index. Only images and rectangles should be returned if support for HTML - * labels with not foreign objects is required. - */ - createSizerShape(bounds: Rectangle, index: number, fillColor = HANDLE_FILLCOLOR) { - if (this.handleImage) { - bounds = new Rectangle( - bounds.x, - bounds.y, - this.handleImage.width, - this.handleImage.height - ); - const shape = new ImageShape(bounds, this.handleImage.src); - - // Allows HTML rendering of the images - shape.preserveImageAspect = false; - - return shape; - } - if (index === InternalEvent.ROTATION_HANDLE) { - return new EllipseShape(bounds, fillColor, HANDLE_STROKECOLOR); - } - return new RectangleShape(bounds, fillColor, HANDLE_STROKECOLOR); - } - - /** - * Helper method to create an {@link Rectangle} around the given center point - * with a width and height of 2*s or 6, if no s is given. - */ - moveSizerTo(shape: Shape, x: number, y: number) { - if (shape && shape.bounds) { - shape.bounds.x = Math.floor(x - shape.bounds.width / 2); - shape.bounds.y = Math.floor(y - shape.bounds.height / 2); - - // Fixes visible inactive handles in VML - if (shape.node && shape.node.style.display !== 'none') { - shape.redraw(); - } - } - } - - /** - * Returns the index of the handle for the given event. This returns the index - * of the sizer from where the event originated or {@link Event.LABEL_HANDLE}. - */ - getHandleForEvent(me: InternalMouseEvent) { - // Connection highlight may consume events before they reach sizer handle - const tol = !isMouseEvent(me.getEvent()) ? this.tolerance : 1; - const hit = - this.allowHandleBoundsCheck && tol > 0 - ? new Rectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) - : null; - - const checkShape = (shape: Shape | null) => { - const st = - shape && shape.constructor !== ImageShape && this.allowHandleBoundsCheck - ? shape.strokeWidth + shape.svgStrokeTolerance - : null; - const real = st - ? new Rectangle( - me.getGraphX() - Math.floor(st / 2), - me.getGraphY() - Math.floor(st / 2), - st, - st - ) - : hit; - - return ( - shape && - shape.bounds && - (me.isSource(shape) || - (real && - intersects(shape.bounds, real) && - shape.node.style.display !== 'none' && - shape.node.style.visibility !== 'hidden')) - ); - }; - - if (checkShape(this.rotationShape)) { - return InternalEvent.ROTATION_HANDLE; - } - if (checkShape(this.labelShape)) { - return InternalEvent.LABEL_HANDLE; - } - - for (let i = 0; i < this.sizers.length; i += 1) { - if (checkShape(this.sizers[i])) { - return i; - } - } - - if (this.customHandles != null && this.isCustomHandleEvent(me)) { - // Inverse loop order to match display order - for (let i = this.customHandles.length - 1; i >= 0; i--) { - if (checkShape(this.customHandles[i].shape)) { - // LATER: Return reference to active shape - return InternalEvent.CUSTOM_HANDLE - i; - } - } - } - - return null; - } - - /** - * Returns `true` if the given event allows custom handles to be changed. - * - * This implementation returns `true`. - */ - isCustomHandleEvent(me: InternalMouseEvent) { - return true; - } - - /** - * Handles the event if a handle has been clicked. By consuming the - * event all subsequent events of the gesture are redirected to this - * handler. - */ - mouseDown(sender: EventSource, me: InternalMouseEvent) { - if (!me.isConsumed() && this.graph.isEnabled()) { - const handle = this.getHandleForEvent(me); - - if (handle) { - this.start(me.getGraphX(), me.getGraphY(), handle); - me.consume(); - } - } - } - - /** - * Called if {@link livePreview} is enabled to check if a border should be painted. - * - * This implementation returns `true` if the shape is transparent. - */ - isLivePreviewBorder() { - return ( - this.state.shape && - this.state.shape.fill === NONE && - this.state.shape.stroke === NONE - ); - } - - /** - * Starts the handling of the mouse gesture. - */ - start(x: number, y: number, index: number) { - this.livePreviewActive = this.livePreview && this.state.cell.getChildCount() === 0; - this.inTolerance = true; - this.childOffsetX = 0; - this.childOffsetY = 0; - this.index = index; - this.startX = x; - this.startY = y; - - if (this.index <= InternalEvent.CUSTOM_HANDLE && this.isGhostPreview()) { - this.ghostPreview = this.createGhostPreview(); - } else { - // Saves reference to parent state - const { model } = this.state.view.graph; - const parent = this.state.cell.getParent(); - - if ( - this.state.view.currentRoot !== parent && - parent && - (parent.isVertex() || parent.isEdge()) - ) { - this.parentState = (this.state.view.graph).view.getState(parent); - } - - // Creates a preview that can be on top of any HTML label - this.selectionBorder.node.style.display = - index === InternalEvent.ROTATION_HANDLE ? 'inline' : 'none'; - - // Creates the border that represents the new bounds - if (!this.livePreviewActive || this.isLivePreviewBorder()) { - this.preview = this.createSelectionShape(this.bounds); - - if ( - !(Client.IS_SVG && Number(this.state.style.rotation || '0') !== 0) && - this.state.text != null && - this.state.text.node.parentNode === this.graph.container - ) { - this.preview.dialect = DIALECT.STRICTHTML; - this.preview.init(this.graph.container); - } else { - this.preview.dialect = DIALECT.SVG; - this.preview.init(this.graph.view.getOverlayPane()); - } - } - - if (index === InternalEvent.ROTATION_HANDLE) { - // With the rotation handle in a corner, need the angle and distance - const pos = this.getRotationHandlePosition(); - - const dx = pos.x - this.state.getCenterX(); - const dy = pos.y - this.state.getCenterY(); - - this.startAngle = dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : 0; - this.startDist = Math.sqrt(dx * dx + dy * dy); - } - - // Prepares the handles for live preview - if (this.livePreviewActive) { - this.hideSizers(); - - if (index === InternalEvent.ROTATION_HANDLE && this.rotationShape) { - this.rotationShape.node.style.display = ''; - } else if (index === InternalEvent.LABEL_HANDLE && this.labelShape) { - this.labelShape.node.style.display = ''; - } else if (this.sizers[index]) { - this.sizers[index].node.style.display = ''; - } else if (index <= InternalEvent.CUSTOM_HANDLE) { - this.customHandles[InternalEvent.CUSTOM_HANDLE - index].setVisible(true); - } - - // Gets the array of connected edge handlers for redrawing - const edges = this.state.cell.getEdges(); - this.edgeHandlers = []; - - const selectionCellsHandler = this.graph.getPlugin( - 'SelectionCellsHandler' - ) as SelectionCellsHandler; - - for (let i = 0; i < edges.length; i += 1) { - const handler = selectionCellsHandler?.getHandler(edges[i]); - if (handler) { - this.edgeHandlers.push(handler as EdgeHandler); - } - } - } - } - } - - /** - * Starts the handling of the mouse gesture. - */ - createGhostPreview() { - const shape = this.graph.cellRenderer.createShape(this.state); - shape.init(this.graph.view.getOverlayPane()); - shape.scale = this.state.view.scale; - shape.bounds = this.bounds; - shape.outline = true; - - return shape; - } - - /** - * Shortcut to {@link hideSizers}. - */ - setHandlesVisible(visible: boolean) { - this.handlesVisible = visible; - - for (let i = 0; i < this.sizers.length; i += 1) { - this.sizers[i].node.style.display = visible ? '' : 'none'; - } - - for (let i = 0; i < this.customHandles.length; i += 1) { - this.customHandles[i].setVisible(visible); - } - } - - /** - * Hides all sizers except. - * - * Starts the handling of the mouse gesture. - */ - hideSizers() { - this.setHandlesVisible(false); - } - - /** - * Checks if the coordinates for the given event are within the - * {@link Graph#tolerance}. If the event is a mouse event then the tolerance is - * ignored. - */ - checkTolerance(me: InternalMouseEvent) { - if (this.inTolerance && this.startX !== null && this.startY !== null) { - if ( - isMouseEvent(me.getEvent()) || - Math.abs(me.getGraphX() - this.startX) > this.graph.getEventTolerance() || - Math.abs(me.getGraphY() - this.startY) > this.graph.getEventTolerance() - ) { - this.inTolerance = false; - } - } - } - - /** - * Hook for subclasses do show details while the handler is active. - */ - updateHint(me: InternalMouseEvent) { - return; - } - - /** - * Hooks for subclasses to hide details when the handler gets inactive. - */ - removeHint() { - return; - } - - /** - * Hook for rounding the angle. This uses {@link Math.round}. - */ - roundAngle(angle: number) { - return Math.round(angle * 10) / 10; - } - - /** - * Hook for rounding the unscaled width or height. This uses {@link Math.round}. - */ - roundLength(length: number) { - return Math.round(length * 100) / 100; - } - - /** - * Handles the event by updating the preview. - */ - mouseMove(sender: EventSource, me: InternalMouseEvent) { - if (!me.isConsumed() && this.index != null) { - // Checks tolerance for ignoring single clicks - this.checkTolerance(me); - - if (!this.inTolerance) { - if (this.index <= InternalEvent.CUSTOM_HANDLE) { - if (this.customHandles != null) { - this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].processEvent(me); - this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].active = true; - - if (this.ghostPreview != null) { - this.ghostPreview.apply(this.state); - this.ghostPreview.strokeWidth = - this.getSelectionStrokeWidth() / - this.ghostPreview.scale / - this.ghostPreview.scale; - this.ghostPreview.isDashed = this.isSelectionDashed(); - this.ghostPreview.stroke = this.getSelectionColor(); - this.ghostPreview.redraw(); - - if (this.selectionBounds != null) { - this.selectionBorder.node.style.display = 'none'; - } - } else { - if (this.movePreviewToFront) { - this.moveToFront(); - } - - this.customHandles[ - InternalEvent.CUSTOM_HANDLE - this.index - ].positionChanged(); - } - } - } else if (this.index === InternalEvent.LABEL_HANDLE) { - this.moveLabel(me); - } else { - if (this.index === InternalEvent.ROTATION_HANDLE) { - this.rotateVertex(me); - } else { - this.resizeVertex(me); - } - - this.updateHint(me); - } - } - - me.consume(); - } - // Workaround for disabling the connect highlight when over handle - else if (!this.graph.isMouseDown && this.getHandleForEvent(me)) { - me.consume(false); - } - } - - /** - * Returns `true` if a ghost preview should be used for custom handles. - */ - isGhostPreview() { - return this.state.cell.getChildCount() > 0; - } - - /** - * Moves the vertex. - */ - moveLabel(me: InternalMouseEvent) { - const point = new Point(me.getGraphX(), me.getGraphY()); - const tr = this.graph.view.translate; - const { scale } = this.graph.view; - - if (this.graph.isGridEnabledEvent(me.getEvent())) { - point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale; - point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale; - } - - const index = this.rotationShape ? this.sizers.length - 2 : this.sizers.length - 1; - this.moveSizerTo(this.sizers[index], point.x, point.y); - } - - /** - * Rotates the vertex. - */ - rotateVertex(me: InternalMouseEvent) { - const point = new Point(me.getGraphX(), me.getGraphY()); - let dx = this.state.x + this.state.width / 2 - point.x; - let dy = this.state.y + this.state.height / 2 - point.y; - this.currentAlpha = - dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : dy < 0 ? 180 : 0; - - if (dx > 0) { - this.currentAlpha -= 180; - } - - this.currentAlpha -= this.startAngle; - - // Rotation raster - if (this.rotationRaster && this.graph.isGridEnabledEvent(me.getEvent())) { - let raster; - dx = point.x - this.state.getCenterX(); - dy = point.y - this.state.getCenterY(); - const dist = Math.sqrt(dx * dx + dy * dy); - - if (dist - this.startDist < 2) { - raster = 15; - } else if (dist - this.startDist < 25) { - raster = 5; - } else { - raster = 1; - } - - this.currentAlpha = Math.round(this.currentAlpha / raster) * raster; - } else { - this.currentAlpha = this.roundAngle(this.currentAlpha); - } - - this.selectionBorder.rotation = this.currentAlpha; - this.selectionBorder.redraw(); - - if (this.livePreviewActive) { - this.redrawHandles(); - } - } - - /** - * Resizes the vertex. - */ - resizeVertex(me: InternalMouseEvent) { - const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); - const alpha = toRadians(this.state.style.rotation ?? 0); - const point = new Point(me.getGraphX(), me.getGraphY()); - const tr = this.graph.view.translate; - const { scale } = this.graph.view; - let cos = Math.cos(-alpha); - let sin = Math.sin(-alpha); - - let dx = point.x - this.startX; - let dy = point.y - this.startY; - - // Rotates vector for mouse gesture - const tx = cos * dx - sin * dy; - const ty = sin * dx + cos * dy; - - dx = tx; - dy = ty; - - const geo = this.state.cell.getGeometry(); - if (geo && this.index !== null) { - this.unscaledBounds = this.union( - geo, - dx / scale, - dy / scale, - this.index, - this.graph.isGridEnabledEvent(me.getEvent()), - 1, - new Point(0, 0), - this.isConstrainedEvent(me), - this.isCenteredEvent(this.state, me) - ); - } - - // Keeps vertex within maximum graph or parent bounds - if (geo && !geo.relative) { - let max = this.graph.getMaximumGraphBounds(); - - // Handles child cells - if (max != null && this.parentState != null) { - max = Rectangle.fromRectangle(max); - - max.x -= (this.parentState.x - tr.x * scale) / scale; - max.y -= (this.parentState.y - tr.y * scale) / scale; - } - - if (this.graph.isConstrainChild(this.state.cell)) { - let tmp = this.graph.getCellContainmentArea(this.state.cell); - - if (tmp != null) { - const overlap = this.graph.getOverlap(this.state.cell); - - if (overlap > 0) { - tmp = Rectangle.fromRectangle(tmp); - - tmp.x -= tmp.width * overlap; - tmp.y -= tmp.height * overlap; - tmp.width += 2 * tmp.width * overlap; - tmp.height += 2 * tmp.height * overlap; - } - - if (!max) { - max = tmp; - } else { - max = Rectangle.fromRectangle(max); - max.intersect(tmp); - } - } - } - - if (max && this.unscaledBounds) { - if (this.unscaledBounds.x < max.x) { - this.unscaledBounds.width -= max.x - this.unscaledBounds.x; - this.unscaledBounds.x = max.x; - } - - if (this.unscaledBounds.y < max.y) { - this.unscaledBounds.height -= max.y - this.unscaledBounds.y; - this.unscaledBounds.y = max.y; - } - - if (this.unscaledBounds.x + this.unscaledBounds.width > max.x + max.width) { - this.unscaledBounds.width -= - this.unscaledBounds.x + this.unscaledBounds.width - max.x - max.width; - } - - if (this.unscaledBounds.y + this.unscaledBounds.height > max.y + max.height) { - this.unscaledBounds.height -= - this.unscaledBounds.y + this.unscaledBounds.height - max.y - max.height; - } - } - } - - if (this.unscaledBounds) { - const old = this.bounds; - - this.bounds = new Rectangle( - (this.parentState ? this.parentState.x : tr.x * scale) + - this.unscaledBounds.x * scale, - (this.parentState ? this.parentState.y : tr.y * scale) + - this.unscaledBounds.y * scale, - this.unscaledBounds.width * scale, - this.unscaledBounds.height * scale - ); - - if (geo && geo.relative && this.parentState) { - this.bounds.x += this.state.x - this.parentState.x; - this.bounds.y += this.state.y - this.parentState.y; - } - - cos = Math.cos(alpha); - sin = Math.sin(alpha); - - const c2 = new Point(this.bounds.getCenterX(), this.bounds.getCenterY()); - - dx = c2.x - ct.x; - dy = c2.y - ct.y; - - const dx2 = cos * dx - sin * dy; - const dy2 = sin * dx + cos * dy; - - const dx3 = dx2 - dx; - const dy3 = dy2 - dy; - - const dx4 = this.bounds.x - this.state.x; - const dy4 = this.bounds.y - this.state.y; - - const dx5 = cos * dx4 - sin * dy4; - const dy5 = sin * dx4 + cos * dy4; - - this.bounds.x += dx3; - this.bounds.y += dy3; - - // Rounds unscaled bounds to int - this.unscaledBounds.x = this.roundLength(this.unscaledBounds.x + dx3 / scale); - this.unscaledBounds.y = this.roundLength(this.unscaledBounds.y + dy3 / scale); - this.unscaledBounds.width = this.roundLength(this.unscaledBounds.width); - this.unscaledBounds.height = this.roundLength(this.unscaledBounds.height); - - // Shifts the children according to parent offset - if (!this.state.cell.isCollapsed() && (dx3 !== 0 || dy3 !== 0)) { - this.childOffsetX = this.state.x - this.bounds.x + dx5; - this.childOffsetY = this.state.y - this.bounds.y + dy5; - } else { - this.childOffsetX = 0; - this.childOffsetY = 0; - } - - if (!old.equals(this.bounds)) { - if (this.livePreviewActive) { - this.updateLivePreview(me); - } - - if (this.preview != null) { - this.drawPreview(); - } else { - this.updateParentHighlight(); - } - } - } - } - - /** - * Repaints the live preview. - */ - updateLivePreview(me: InternalMouseEvent) { - // TODO: Apply child offset to children in live preview - const { scale } = this.graph.view; - const tr = this.graph.view.translate; - - // Saves current state - const tempState = this.state.clone(); - - // Temporarily changes size and origin - this.state.x = this.bounds.x; - this.state.y = this.bounds.y; - this.state.origin = new Point( - this.state.x / scale - tr.x, - this.state.y / scale - tr.y - ); - this.state.width = this.bounds.width; - this.state.height = this.bounds.height; - - // Redraws cell and handles - let off = this.state.absoluteOffset; - off = new Point(off.x, off.y); - - // Required to store and reset absolute offset for updating label position - this.state.absoluteOffset.x = 0; - this.state.absoluteOffset.y = 0; - const geo = this.state.cell.getGeometry(); - - if (geo != null) { - const offset = geo.offset || this.EMPTY_POINT; - - if (offset != null && !geo.relative) { - this.state.absoluteOffset.x = this.state.view.scale * offset.x; - this.state.absoluteOffset.y = this.state.view.scale * offset.y; - } - - this.state.view.updateVertexLabelOffset(this.state); - } - - // Draws the live preview - (this.state.view.graph).cellRenderer.redraw(this.state, true); - - // Redraws connected edges TODO: Include child edges - this.state.view.invalidate(this.state.cell); - this.state.invalid = false; - this.state.view.validate(); - this.redrawHandles(); - - // Moves live preview to front - if (this.movePreviewToFront) { - this.moveToFront(); - } - - // Hides folding icon - if (this.state.control != null && this.state.control.node != null) { - this.state.control.node.style.visibility = 'hidden'; - } - - // Restores current state - this.state.setState(tempState); - } - - /** - * Handles the event by applying the changes to the geometry. - */ - moveToFront() { - if ( - (this.state.text && this.state.text.node && this.state.text.node.nextSibling) || - (this.state.shape && - this.state.shape.node && - this.state.shape.node.nextSibling && - (!this.state.text || this.state.shape.node.nextSibling !== this.state.text.node)) - ) { - if (this.state.shape && this.state.shape.node && this.state.shape.node.parentNode) { - this.state.shape.node.parentNode.appendChild(this.state.shape.node); - } - - if (this.state.text && this.state.text.node && this.state.text.node.parentNode) { - this.state.text.node.parentNode.appendChild(this.state.text.node); - } - } - } - - /** - * Handles the event by applying the changes to the geometry. - */ - mouseUp(sender: EventSource, me: InternalMouseEvent) { - if (this.index != null && this.state != null) { - const point = new Point(me.getGraphX(), me.getGraphY()); - const { index } = this; - this.index = null; - - if (this.ghostPreview == null) { - // Required to restore order in case of no change - this.state.view.invalidate(this.state.cell, false, false); - this.state.view.validate(); - } - - this.graph.batchUpdate(() => { - if (index <= InternalEvent.CUSTOM_HANDLE) { - if (this.customHandles != null) { - // Creates style before changing cell state - const style = (this.state.view.graph).getCellStyle(this.state.cell); - - this.customHandles[InternalEvent.CUSTOM_HANDLE - index].active = false; - this.customHandles[InternalEvent.CUSTOM_HANDLE - index].execute(me); - - // Sets style and apply on shape to force repaint and - // check if execute has removed custom handles - if ( - this.customHandles != null && - this.customHandles[InternalEvent.CUSTOM_HANDLE - index] != null - ) { - this.state.style = style; - this.customHandles[InternalEvent.CUSTOM_HANDLE - index].positionChanged(); - } - } - } else if (index === InternalEvent.ROTATION_HANDLE) { - if (this.currentAlpha != null) { - const delta = this.currentAlpha - (this.state.style.rotation ?? 0); - - if (delta !== 0) { - this.rotateCell(this.state.cell, delta); - } - } else { - this.rotateClick(); - } - } else { - const gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); - const alpha = toRadians(this.state.style.rotation ?? 0); - const cos = Math.cos(-alpha); - const sin = Math.sin(-alpha); - - let dx = point.x - this.startX; - let dy = point.y - this.startY; - - // Rotates vector for mouse gesture - const tx = cos * dx - sin * dy; - const ty = sin * dx + cos * dy; - - dx = tx; - dy = ty; - - const s = this.graph.view.scale; - const recurse = this.isRecursiveResize(this.state, me); - - this.resizeCell( - this.state.cell, - this.roundLength(dx / s), - this.roundLength(dy / s), - index, - gridEnabled, - this.isConstrainedEvent(me), - recurse - ); - } - }); - - me.consume(); - this.reset(); - this.redrawHandles(); - } - } - - /** - * Returns the `recursiveResize` status of the given state. - * @param state the given {@link CellState}. This implementation takes the value of this state. - * @param me the mouse event. - */ - isRecursiveResize(state: CellState, me: InternalMouseEvent) { - return this.graph.isRecursiveResize(this.state); - } - - /** - * Hook for subclasses to implement a single click on the rotation handle. - * This code is executed as part of the model transaction. This implementation - * is empty. - */ - rotateClick() { - return; - } - - /** - * Rotates the given cell and its children by the given angle in degrees. - * - * @param cell {@link Cell} to be rotated. - * @param angle Angle in degrees. - * @param parent if set, consider the parent in the rotation computation. - */ - rotateCell(cell: Cell, angle: number, parent?: Cell) { - if (angle !== 0) { - const model = this.graph.getDataModel(); - - if (cell.isVertex() || cell.isEdge()) { - if (!cell.isEdge()) { - const style = this.graph.getCurrentCellStyle(cell); - const total = (style.rotation || 0) + angle; - this.graph.setCellStyles('rotation', total, [cell]); - } - - let geo = cell.getGeometry(); - - if (geo && parent) { - const pgeo = parent.getGeometry(); - - if (pgeo != null && !parent.isEdge()) { - geo = geo.clone(); - geo.rotate(angle, new Point(pgeo.width / 2, pgeo.height / 2)); - model.setGeometry(cell, geo); - } - - if ((cell.isVertex() && !geo.relative) || cell.isEdge()) { - // Recursive rotation - const childCount = cell.getChildCount(); - - for (let i = 0; i < childCount; i += 1) { - this.rotateCell(cell.getChildAt(i), angle, cell); - } - } - } - } - } - } - - /** - * Resets the state of this handler. - */ - reset() { - if (this.index !== null && this.sizers[this.index].node.style.display === 'none') { - this.sizers[this.index].node.style.display = ''; - } - - this.index = null; - - // TODO: Reset and redraw cell states for live preview - if (this.preview) { - this.preview.destroy(); - this.preview = null; - } - - if (this.ghostPreview) { - this.ghostPreview.destroy(); - this.ghostPreview = null; - } - - if (this.livePreviewActive) { - for (let i = 0; i < this.sizers.length; i += 1) { - this.sizers[i].node.style.display = ''; - } - - // Shows folding icon - if (this.state.control && this.state.control.node) { - this.state.control.node.style.visibility = ''; - } - } - - for (let i = 0; i < this.customHandles.length; i += 1) { - if (this.customHandles[i].active) { - this.customHandles[i].active = false; - this.customHandles[i].reset(); - } else { - this.customHandles[i].setVisible(true); - } - } - - // Checks if handler has been destroyed - this.selectionBorder.node.style.display = 'inline'; - this.selectionBounds = this.getSelectionBounds(this.state); - this.bounds = new Rectangle( - this.selectionBounds.x, - this.selectionBounds.y, - this.selectionBounds.width, - this.selectionBounds.height - ); - this.drawPreview(); - - this.removeHint(); - this.redrawHandles(); - this.edgeHandlers = []; - this.handlesVisible = true; - this.unscaledBounds = null; - } - - /** - * Uses the given vector to change the bounds of the given cell - * in the graph using {@link Graph#resizeCell}. - */ - resizeCell( - cell: Cell, - dx: number, - dy: number, - index: number, - gridEnabled: boolean, - constrained: boolean, - recurse: boolean - ) { - let geo = cell.getGeometry(); - - if (geo) { - if ( - index === InternalEvent.LABEL_HANDLE && - this.labelShape && - this.labelShape.bounds - ) { - const alpha = -toRadians(this.state.style.rotation ?? 0); - const cos = Math.cos(alpha); - const sin = Math.sin(alpha); - const { scale } = this.graph.view; - const pt = getRotatedPoint( - new Point( - Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale), - Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale) - ), - cos, - sin - ); - - geo = geo.clone(); - - if (geo.offset == null) { - geo.offset = pt; - } else { - geo.offset.x += pt.x; - geo.offset.y += pt.y; - } - - this.graph.model.setGeometry(cell, geo); - } else if (this.unscaledBounds) { - const { scale } = this.graph.view; - - if (this.childOffsetX !== 0 || this.childOffsetY !== 0) { - this.moveChildren( - cell, - Math.round(this.childOffsetX / scale), - Math.round(this.childOffsetY / scale) - ); - } - - this.graph.resizeCell(cell, this.unscaledBounds, recurse); - } - } - } - - /** - * Moves the children of the given cell by the given vector. - */ - moveChildren(cell: Cell, dx: number, dy: number) { - const model = this.graph.getDataModel(); - const childCount = cell.getChildCount(); - - for (let i = 0; i < childCount; i += 1) { - const child = cell.getChildAt(i); - let geo = child.getGeometry(); - - if (geo != null) { - geo = geo.clone(); - geo.translate(dx, dy); - model.setGeometry(child, geo); - } - } - } - - /** - * Returns the union of the given bounds and location for the specified - * handle index. - * - * To override this to limit the size of vertex via a minWidth/-Height style, - * the following code can be used. - * - * ```javascript - * let vertexHandlerUnion = union; - * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> - * { - * let result = vertexHandlerUnion.apply(this, arguments); - * - * result.width = Math.max(result.width, mxUtils.getNumber(this.state.style, 'minWidth', 0)); - * result.height = Math.max(result.height, mxUtils.getNumber(this.state.style, 'minHeight', 0)); - * - * return result; - * }; - * ``` - * - * The minWidth/-Height style can then be used as follows: - * - * ```javascript - * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'minWidth=100;minHeight=100;'); - * ``` - * - * To override this to update the height for a wrapped text if the width of a vertex is - * changed, the following can be used. - * - * ```javascript - * let mxVertexHandlerUnion = union; - * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> - * { - * let result = mxVertexHandlerUnion.apply(this, arguments); - * let s = this.state; - * - * if (this.graph.isHtmlLabel(s.cell) && (index == 3 || index == 4) && - * s.text != null && s.style.whiteSpace == 'wrap') - * { - * let label = this.graph.getLabel(s.cell); - * let fontSize = mxUtils.getNumber(s.style, 'fontSize', mxConstants.DEFAULT_FONTSIZE); - * let ww = result.width / s.view.scale - s.text.spacingRight - s.text.spacingLeft - * - * result.height = mxUtils.getSizeForString(label, fontSize, s.style.fontFamily, ww).height; - * } - * - * return result; - * }; - * ``` - */ - union( - bounds: Rectangle, - dx: number, - dy: number, - index: number, - gridEnabled: boolean, - scale: number, - tr: Point, - constrained: boolean, - centered: boolean - ) { - gridEnabled = gridEnabled && this.graph.isGridEnabled(); - - if (this.singleSizer) { - let x = bounds.x + bounds.width + dx; - let y = bounds.y + bounds.height + dy; - - if (gridEnabled) { - x = this.graph.snap(x / scale) * scale; - y = this.graph.snap(y / scale) * scale; - } - - const rect = new Rectangle(bounds.x, bounds.y, 0, 0); - rect.add(new Rectangle(x, y, 0, 0)); - - return rect; - } - const w0 = bounds.width; - const h0 = bounds.height; - let left = bounds.x - tr.x * scale; - let right = left + w0; - let top = bounds.y - tr.y * scale; - let bottom = top + h0; - - const cx = left + w0 / 2; - const cy = top + h0 / 2; - - if (index > 4 /* Bottom Row */) { - bottom += dy; - - if (gridEnabled) { - bottom = this.graph.snap(bottom / scale) * scale; - } else { - bottom = Math.round(bottom / scale) * scale; - } - } else if (index < 3 /* Top Row */) { - top += dy; - - if (gridEnabled) { - top = this.graph.snap(top / scale) * scale; - } else { - top = Math.round(top / scale) * scale; - } - } - - if (index === 0 || index === 3 || index === 5 /* Left */) { - left += dx; - - if (gridEnabled) { - left = this.graph.snap(left / scale) * scale; - } else { - left = Math.round(left / scale) * scale; - } - } else if (index === 2 || index === 4 || index === 7 /* Right */) { - right += dx; - - if (gridEnabled) { - right = this.graph.snap(right / scale) * scale; - } else { - right = Math.round(right / scale) * scale; - } - } - - let width = right - left; - let height = bottom - top; - - if (constrained) { - const geo = this.state.cell.getGeometry(); - - if (geo != null) { - const aspect = geo.width / geo.height; - - if (index === 1 || index === 2 || index === 7 || index === 6) { - width = height * aspect; - } else { - height = width / aspect; - } - - if (index === 0) { - left = right - width; - top = bottom - height; - } - } - } - - if (centered) { - width += width - w0; - height += height - h0; - - const cdx = cx - (left + width / 2); - const cdy = cy - (top + height / 2); - - left += cdx; - top += cdy; - right += cdx; - bottom += cdy; - } - - // Flips over left side - if (width < 0) { - left += width; - width = Math.abs(width); - } - - // Flips over top side - if (height < 0) { - top += height; - height = Math.abs(height); - } - - const result = new Rectangle(left + tr.x * scale, top + tr.y * scale, width, height); - - if (this.minBounds != null) { - result.width = Math.max( - result.width, - this.minBounds.x * scale + - this.minBounds.width * scale + - Math.max(0, this.x0 * scale - result.x) - ); - result.height = Math.max( - result.height, - this.minBounds.y * scale + - this.minBounds.height * scale + - Math.max(0, this.y0 * scale - result.y) - ); - } - - return result; - } - - /** - * Redraws the handles and the preview. - */ - redraw(ignoreHandles?: boolean) { - this.selectionBounds = this.getSelectionBounds(this.state); - this.bounds = new Rectangle( - this.selectionBounds.x, - this.selectionBounds.y, - this.selectionBounds.width, - this.selectionBounds.height - ); - this.drawPreview(); - - if (!ignoreHandles) { - this.redrawHandles(); - } - } - - /** - * Returns the padding to be used for drawing handles for the current . - */ - getHandlePadding() { - // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) - const result = new Point(0, 0); - let tol = this.tolerance; - - if ( - this.sizers.length > 0 && - this.sizers[0].bounds && - (this.bounds.width < 2 * this.sizers[0].bounds.width + 2 * tol || - this.bounds.height < 2 * this.sizers[0].bounds.height + 2 * tol) - ) { - tol /= 2; - - result.x = this.sizers[0].bounds.width + tol; - result.y = this.sizers[0].bounds.height + tol; - } - - return result; - } - - /** - * Returns the bounds used to paint the resize handles. - */ - getSizerBounds() { - return this.bounds; - } - - /** - * Redraws the handles. To hide certain handles the following code can be used. - * - * ```javascript - * redrawHandles() - * { - * mxVertexHandlerRedrawHandles.apply(this, arguments); - * - * if (this.sizers != null && this.sizers.length > 7) - * { - * this.sizers[1].node.style.display = 'none'; - * this.sizers[6].node.style.display = 'none'; - * } - * }; - * ``` - */ - redrawHandles() { - let s = this.getSizerBounds(); - const tol = this.tolerance; - this.horizontalOffset = 0; - this.verticalOffset = 0; - - for (let i = 0; i < this.customHandles.length; i += 1) { - const shape = this.customHandles[i].shape; - - if (shape) { - const temp = shape.node.style.display; - this.customHandles[i].redraw(); - shape.node.style.display = temp; - - // Hides custom handles during text editing - shape.node.style.visibility = - this.handlesVisible && this.isCustomHandleVisible(this.customHandles[i]) - ? '' - : 'hidden'; - } - } - - if (this.sizers.length > 0 && this.sizers[0]) { - if (this.index === null && this.manageSizers && this.sizers.length >= 8) { - // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) - const padding = this.getHandlePadding(); - this.horizontalOffset = padding.x; - this.verticalOffset = padding.y; - - if (this.horizontalOffset !== 0 || this.verticalOffset !== 0) { - s = new Rectangle(s.x, s.y, s.width, s.height); - - s.x -= this.horizontalOffset / 2; - s.width += this.horizontalOffset; - s.y -= this.verticalOffset / 2; - s.height += this.verticalOffset; - } - - if (this.sizers.length >= 8) { - if ( - this.sizers[0].bounds && - (s.width < 2 * this.sizers[0].bounds.width + 2 * tol || - s.height < 2 * this.sizers[0].bounds.height + 2 * tol) - ) { - this.sizers[0].node.style.display = 'none'; - this.sizers[2].node.style.display = 'none'; - this.sizers[5].node.style.display = 'none'; - this.sizers[7].node.style.display = 'none'; - } else if (this.handlesVisible) { - this.sizers[0].node.style.display = ''; - this.sizers[2].node.style.display = ''; - this.sizers[5].node.style.display = ''; - this.sizers[7].node.style.display = ''; - } - } - } - - const r = s.x + s.width; - const b = s.y + s.height; - - if (this.singleSizer) { - this.moveSizerTo(this.sizers[0], r, b); - } else { - const cx = s.x + s.width / 2; - const cy = s.y + s.height / 2; - - if (this.sizers.length >= 8) { - const crs = [ - 'nw-resize', - 'n-resize', - 'ne-resize', - 'e-resize', - 'se-resize', - 's-resize', - 'sw-resize', - 'w-resize', - ]; - - const alpha = toRadians(this.state.style.rotation ?? 0); - const cos = Math.cos(alpha); - const sin = Math.sin(alpha); - const da = Math.round((alpha * 4) / Math.PI); - - const ct = new Point(s.getCenterX(), s.getCenterY()); - let pt = getRotatedPoint(new Point(s.x, s.y), cos, sin, ct); - this.moveSizerTo(this.sizers[0], pt.x, pt.y); - this.sizers[0].setCursor(crs[mod(0 + da, crs.length)]); - - pt.x = cx; - pt.y = s.y; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[1], pt.x, pt.y); - this.sizers[1].setCursor(crs[mod(1 + da, crs.length)]); - - pt.x = r; - pt.y = s.y; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[2], pt.x, pt.y); - this.sizers[2].setCursor(crs[mod(2 + da, crs.length)]); - - pt.x = s.x; - pt.y = cy; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[3], pt.x, pt.y); - this.sizers[3].setCursor(crs[mod(7 + da, crs.length)]); - - pt.x = r; - pt.y = cy; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[4], pt.x, pt.y); - this.sizers[4].setCursor(crs[mod(3 + da, crs.length)]); - - pt.x = s.x; - pt.y = b; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[5], pt.x, pt.y); - this.sizers[5].setCursor(crs[mod(6 + da, crs.length)]); - - pt.x = cx; - pt.y = b; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[6], pt.x, pt.y); - this.sizers[6].setCursor(crs[mod(5 + da, crs.length)]); - - pt.x = r; - pt.y = b; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[7], pt.x, pt.y); - this.sizers[7].setCursor(crs[mod(4 + da, crs.length)]); - - pt.x = cx + this.state.absoluteOffset.x; - pt.y = cy + this.state.absoluteOffset.y; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[8], pt.x, pt.y); - } else if (this.state.width >= 2 && this.state.height >= 2) { - this.moveSizerTo( - this.sizers[0], - cx + this.state.absoluteOffset.x, - cy + this.state.absoluteOffset.y - ); - } else { - this.moveSizerTo(this.sizers[0], this.state.x, this.state.y); - } - } - } - - if (this.rotationShape) { - const alpha = toRadians(this.currentAlpha); - const cos = Math.cos(alpha); - const sin = Math.sin(alpha); - - const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); - const pt = getRotatedPoint(this.getRotationHandlePosition(), cos, sin, ct); - - if (this.rotationShape.node != null) { - this.moveSizerTo(this.rotationShape, pt.x, pt.y); - - // Hides rotation handle during text editing - this.rotationShape.node.style.visibility = - (this.state.view.graph).isEditing() || !this.handlesVisible - ? 'hidden' - : ''; - } - } - - if (this.selectionBorder != null) { - this.selectionBorder.rotation = this.state.style.rotation ?? 0; - } - - if (this.edgeHandlers != null) { - for (let i = 0; i < this.edgeHandlers.length; i += 1) { - this.edgeHandlers[i].redraw(); - } - } - } - - /** - * Returns true if the given custom handle is visible. - */ - isCustomHandleVisible(handle: CellHandle) { - return ( - !this.graph.isEditing() && (this.state.view.graph).getSelectionCount() === 1 - ); - } - - /** - * Returns an {@link Point} that defines the rotation handle position. - */ - getRotationHandlePosition() { - return new Point( - this.bounds.x + this.bounds.width / 2, - this.bounds.y + this.rotationHandleVSpacing - ); - } - - /** - * Returns `true` if the parent highlight should be visible. - * - * This implementation always returns `true`. - */ - isParentHighlightVisible() { - const parent = this.state.cell.getParent(); - return parent ? !this.graph.isCellSelected(parent) : false; - } - - /** - * Updates the highlight of the parent if {@link parentHighlightEnabled} is `true`. - */ - updateParentHighlight() { - if (!this.isDestroyed()) { - const visible = this.isParentHighlightVisible(); - const parent = this.state.cell.getParent(); - const pstate = parent ? this.graph.view.getState(parent) : null; - - if (this.parentHighlight) { - if (parent && parent.isVertex() && visible) { - const b = this.parentHighlight.bounds; - - if ( - pstate && - b && - (b.x !== pstate.x || - b.y !== pstate.y || - b.width !== pstate.width || - b.height !== pstate.height) - ) { - this.parentHighlight.bounds = Rectangle.fromRectangle(pstate); - this.parentHighlight.redraw(); - } - } else { - if (pstate != null && pstate.parentHighlight === this.parentHighlight) { - pstate.parentHighlight = null; - } - - this.parentHighlight.destroy(); - this.parentHighlight = null; - } - } else if (this.parentHighlightEnabled && visible) { - if ( - parent && - parent.isVertex() && - pstate != null && - pstate.parentHighlight == null - ) { - this.parentHighlight = this.createParentHighlightShape(pstate); - // VML dialect required here for event transparency in IE - this.parentHighlight.dialect = DIALECT.SVG; - this.parentHighlight.pointerEvents = false; - this.parentHighlight.rotation = pstate.style.rotation ?? 0; - this.parentHighlight.init(this.graph.getView().getOverlayPane()); - this.parentHighlight.redraw(); - - // Shows highlight once per parent - pstate.parentHighlight = this.parentHighlight; - } - } - } - } - - /** - * Redraws the preview. - */ - drawPreview() { - if (this.preview != null) { - this.preview.bounds = this.bounds; - - if (this.preview.node.parentNode === this.graph.container) { - this.preview.bounds.width = Math.max(0, this.preview.bounds.width - 1); - this.preview.bounds.height = Math.max(0, this.preview.bounds.height - 1); - } - - this.preview.rotation = this.state.style.rotation ?? 0; - this.preview.redraw(); - } - - this.selectionBorder.bounds = this.getSelectionBorderBounds(); - this.selectionBorder.redraw(); - this.updateParentHighlight(); - } - - /** - * Returns the bounds for the selection border. - */ - getSelectionBorderBounds() { - return this.bounds; - } - - /** - * Returns `true` if this handler was destroyed or not initialized. - */ - isDestroyed() { - return this.selectionBorder == null; - } - - /** - * Destroys the handler and all its resources and DOM nodes. - */ - onDestroy() { - (this.state.view.graph).removeListener(this.escapeHandler); - this.escapeHandler = () => { - return; - }; - - if (this.preview) { - this.preview.destroy(); - this.preview = null; - } - - if (this.parentHighlight) { - const parent = this.state.cell.getParent(); - const pstate = parent ? this.graph.view.getState(parent) : null; - - if (pstate && pstate.parentHighlight === this.parentHighlight) { - pstate.parentHighlight = null; - } - - this.parentHighlight.destroy(); - this.parentHighlight = null; - } - - if (this.ghostPreview) { - this.ghostPreview.destroy(); - this.ghostPreview = null; - } - - if (this.selectionBorder) { - this.selectionBorder.destroy(); - } - - this.labelShape = null; - this.removeHint(); - - for (let i = 0; i < this.sizers.length; i += 1) { - this.sizers[i].destroy(); - } - - this.sizers = []; - - for (let i = 0; i < this.customHandles.length; i += 1) { - this.customHandles[i].destroy(); - } - - this.customHandles = []; - } -} + onDestroy(): void; + redraw(): void; + index: number | null; + startX: number; + startY: number; + setHandlesVisible(visible: boolean): void; + start(x: number, y: number, index: number): void; + // start(x: number | null, y: number | null, index: number | null): void; + updateParentHighlight(): void; + mouseDown(sender: EventSource, me: InternalMouseEvent): void; + mouseUp(sender: EventSource, me: InternalMouseEvent): void; + mouseMove(sender: EventSource, me: InternalMouseEvent): void; +}; export default VertexHandler; From 7b9238ee973262fb193c1bc0c0624e8701403b00 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Sun, 9 Jun 2024 11:52:35 +0200 Subject: [PATCH 2/5] Revert "START simulate no VertexHandler" This reverts commit 7609e82ef40a320f1baf5435717b7b07a8d2a599. --- packages/core/src/index.ts | 2 +- packages/core/src/view/Graph.ts | 3 +- .../core/src/view/handler/VertexHandler.ts | 4069 ++++++++--------- 3 files changed, 2028 insertions(+), 2046 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d3ab383827..617deba664 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -82,7 +82,7 @@ export { default as PopupMenuHandler } from './view/handler/PopupMenuHandler'; export { default as RubberBandHandler } from './view/handler/RubberBandHandler'; export { default as SelectionCellsHandler } from './view/handler/SelectionCellsHandler'; export { default as TooltipHandler } from './view/handler/TooltipHandler'; -export type { default as VertexHandler } from './view/handler/VertexHandler'; +export { default as VertexHandler } from './view/handler/VertexHandler'; export { default as CircleLayout } from './view/layout/CircleLayout'; export { default as CompactTreeLayout } from './view/layout/CompactTreeLayout'; diff --git a/packages/core/src/view/Graph.ts b/packages/core/src/view/Graph.ts index ab792851df..77a16ba575 100644 --- a/packages/core/src/view/Graph.ts +++ b/packages/core/src/view/Graph.ts @@ -485,8 +485,7 @@ class Graph extends EventSource { * @param state {@link CellState} to create the handler for. */ createVertexHandler(state: CellState): VertexHandler { - // return new VertexHandler(state); - return null!; + return new VertexHandler(state); } // =================================================================================================================== diff --git a/packages/core/src/view/handler/VertexHandler.ts b/packages/core/src/view/handler/VertexHandler.ts index 8773f91b18..bafe886d16 100644 --- a/packages/core/src/view/handler/VertexHandler.ts +++ b/packages/core/src/view/handler/VertexHandler.ts @@ -55,2049 +55,2032 @@ import SelectionCellsHandler from './SelectionCellsHandler'; * * This handler is automatically created in {@link Graph#createHandler}. */ -// class VertexHandler { -// escapeHandler: (sender: Listenable, evt: Event) => void; -// selectionBounds: Rectangle; -// bounds: Rectangle; -// selectionBorder: RectangleShape; -// -// /** -// * Reference to the enclosing {@link Graph}. -// */ -// graph: Graph; -// -// /** -// * Reference to the {@link CellState} being modified. -// */ -// state: CellState; -// -// sizers: Shape[] = []; -// -// /** -// * Specifies if only one sizer handle at the bottom, right corner should be used. -// * @default false -// */ -// singleSizer = false; -// -// /** -// * Holds the index of the current handle. -// */ -// index: number | null = null; -// -// /** -// * Specifies if the bounds of handles should be used for hit-detection in IE or if {@link tolerance} > 0. -// * @default true -// */ -// allowHandleBoundsCheck = true; -// -// /** -// * Optional {@link Image} to be used as handles. -// * @default null -// */ -// handleImage: Image | null = null; -// -// /** -// * If handles are currently visible. -// * @default true -// */ -// handlesVisible = true; -// -// /** -// * Optional tolerance for hit-detection in {@link getHandleForEvent}. -// * @default 0 -// */ -// tolerance = 0; -// -// /** -// * Specifies if a rotation handle should be visible. -// * @default false -// */ -// rotationEnabled = false; -// -// /** -// * Specifies if the parent should be highlighted if a child cell is selected. -// * @default false -// */ -// parentHighlightEnabled = false; -// -// /** -// * Specifies if rotation steps should be "rasterized" depending on the distance to the handle. -// * @default true -// */ -// rotationRaster = true; -// -// /** -// * Specifies the cursor for the rotation handle. -// * @default 'crosshair'. -// */ -// rotationCursor = 'crosshair'; -// -// /** -// * Specifies if resize should change the cell in-place. This is an experimental -// * feature for non-touch devices. -// * @default false -// */ -// livePreview = false; -// -// /** -// * Specifies if the live preview should be moved to the front. -// * @default false -// */ -// movePreviewToFront = false; -// -// /** -// * Specifies if sizers should be hidden and spaced if the vertex is small. -// * @default false -// */ -// manageSizers = false; -// -// /** -// * Specifies if the size of groups should be constrained by the children. -// * @default false -// */ -// constrainGroupByChildren = false; -// -// /** -// * Vertical spacing for rotation icon. -// * @default -16 -// */ -// rotationHandleVSpacing = -16; -// -// /** -// * The horizontal offset for the handles. This is updated in {@link redrawHandles} -// * if {@link manageSizers} is `true` and the sizers are offset horizontally. -// */ -// horizontalOffset = 0; -// -// /** -// * The horizontal offset for the handles. This is updated in -// * if {@link manageSizers} is true and the sizers are offset vertically. -// */ -// verticalOffset = 0; -// -// minBounds: Rectangle | null = null; -// -// x0 = 0; -// y0 = 0; -// -// customHandles: CellHandle[] = []; -// -// inTolerance = false; -// -// startX = 0; -// startY = 0; -// -// rotationShape: Shape | null = null; -// -// currentAlpha = 100; -// startAngle = 0; -// startDist = 0; -// -// ghostPreview: Shape | null = null; -// -// livePreviewActive = false; -// -// childOffsetX = 0; -// childOffsetY = 0; -// -// parentState: CellState | null = null; -// parentHighlight: RectangleShape | null = null; -// -// unscaledBounds: Rectangle | null = null; -// -// preview: Shape | null = null; -// -// labelShape: Shape | null = null; -// -// edgeHandlers: EdgeHandler[] = []; -// -// EMPTY_POINT = new Point(); -// -// /** -// * Constructs an event handler that allows to resize vertices and groups. -// * -// * @param state {@link CellState} of the cell to be resized. -// */ -// constructor(state: CellState) { -// this.state = state; -// this.graph = this.state.view.graph; -// this.selectionBounds = this.getSelectionBounds(this.state); -// this.bounds = new Rectangle( -// this.selectionBounds.x, -// this.selectionBounds.y, -// this.selectionBounds.width, -// this.selectionBounds.height -// ); -// this.selectionBorder = this.createSelectionShape(this.bounds); -// // VML dialect required here for event transparency in IE -// this.selectionBorder.dialect = DIALECT.SVG; -// this.selectionBorder.pointerEvents = false; -// this.selectionBorder.rotation = this.state.style.rotation ?? 0; -// this.selectionBorder.init(this.graph.getView().getOverlayPane()); -// InternalEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state); -// -// if (this.graph.isCellMovable(this.state.cell)) { -// this.selectionBorder.setCursor(CURSOR.MOVABLE_VERTEX); -// } -// -// const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; -// -// // Adds the sizer handles -// if ( -// selectionHandler && -// (selectionHandler.maxCells <= 0 || -// this.graph.getSelectionCount() < selectionHandler.maxCells) -// ) { -// const resizable = this.graph.isCellResizable(this.state.cell); -// this.sizers = []; -// -// if ( -// resizable || -// (this.graph.isLabelMovable(this.state.cell) && -// this.state.width >= 2 && -// this.state.height >= 2) -// ) { -// let i = 0; -// -// if (resizable) { -// if (!this.singleSizer) { -// this.sizers.push(this.createSizer('nw-resize', i++)); -// this.sizers.push(this.createSizer('n-resize', i++)); -// this.sizers.push(this.createSizer('ne-resize', i++)); -// this.sizers.push(this.createSizer('w-resize', i++)); -// this.sizers.push(this.createSizer('e-resize', i++)); -// this.sizers.push(this.createSizer('sw-resize', i++)); -// this.sizers.push(this.createSizer('s-resize', i++)); -// } -// -// this.sizers.push(this.createSizer('se-resize', i++)); -// } -// -// const geo = this.state.cell.getGeometry(); -// -// if ( -// geo != null && -// !geo.relative && -// //!this.graph.isSwimlane(this.state.cell) && disable for now -// this.graph.isLabelMovable(this.state.cell) -// ) { -// // Marks this as the label handle for getHandleForEvent -// this.labelShape = this.createSizer( -// CURSOR.LABEL_HANDLE, -// InternalEvent.LABEL_HANDLE, -// LABEL_HANDLE_SIZE, -// LABEL_HANDLE_FILLCOLOR -// ); -// this.sizers.push(this.labelShape); -// } -// } else if ( -// this.graph.isCellMovable(this.state.cell) && -// !this.graph.isCellResizable(this.state.cell) && -// this.state.width < 2 && -// this.state.height < 2 -// ) { -// this.labelShape = this.createSizer( -// CURSOR.MOVABLE_VERTEX, -// InternalEvent.LABEL_HANDLE, -// undefined, -// LABEL_HANDLE_FILLCOLOR -// ); -// this.sizers.push(this.labelShape); -// } -// } -// -// // Adds the rotation handler -// if (this.isRotationHandleVisible()) { -// this.rotationShape = this.createSizer( -// this.rotationCursor, -// InternalEvent.ROTATION_HANDLE, -// HANDLE_SIZE + 3, -// HANDLE_FILLCOLOR -// ); -// this.sizers.push(this.rotationShape); -// } -// -// this.customHandles = this.createCustomHandles(); -// this.redraw(); -// -// if (this.constrainGroupByChildren) { -// this.updateMinBounds(); -// } -// -// // Handles escape keystrokes -// this.escapeHandler = (sender: Listenable, evt: Event) => { -// if (this.livePreview && this.index != null) { -// // Redraws the live preview -// (this.state.view.graph).cellRenderer.redraw(this.state, true); -// -// // Redraws connected edges -// this.state.view.invalidate(this.state.cell); -// this.state.invalid = false; -// this.state.view.validate(); -// } -// -// this.reset(); -// }; -// -// (this.state.view.graph).addListener(InternalEvent.ESCAPE, this.escapeHandler); -// } -// -// /** -// * Returns `true` if the rotation handle should be showing. -// */ -// isRotationHandleVisible() { -// const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; -// const selectionHandlerCheck = selectionHandler -// ? selectionHandler.maxCells <= 0 || -// this.graph.getSelectionCount() < selectionHandler.maxCells -// : true; -// -// return ( -// this.graph.isEnabled() && -// this.rotationEnabled && -// this.graph.isCellRotatable(this.state.cell) && -// selectionHandlerCheck -// ); -// } -// -// /** -// * Returns `true` if the aspect ratio if the cell should be maintained. -// */ -// isConstrainedEvent(me: InternalMouseEvent) { -// return isShiftDown(me.getEvent()) || this.state.style.aspect === 'fixed'; -// } -// -// /** -// * Returns `true` if the center of the vertex should be maintained during the resize. -// */ -// isCenteredEvent(state: CellState, me: InternalMouseEvent) { -// return false; -// } -// -// /** -// * Returns an array of custom handles. This implementation returns null. -// */ -// createCustomHandles() { -// return []; -// } -// -// /** -// * Initializes the shapes required for this vertex handler. -// */ -// updateMinBounds() { -// const children = this.graph.getChildCells(this.state.cell); -// -// if (children.length > 0) { -// this.minBounds = this.graph.view.getBounds(children); -// -// if (this.minBounds) { -// const s = this.state.view.scale; -// const t = this.state.view.translate; -// -// this.minBounds.x -= this.state.x; -// this.minBounds.y -= this.state.y; -// this.minBounds.x /= s; -// this.minBounds.y /= s; -// this.minBounds.width /= s; -// this.minBounds.height /= s; -// this.x0 = this.state.x / s - t.x; -// this.y0 = this.state.y / s - t.y; -// } -// } -// } -// -// /** -// * Returns the mxRectangle that defines the bounds of the selection border. -// */ -// getSelectionBounds(state: CellState) { -// return new Rectangle( -// Math.round(state.x), -// Math.round(state.y), -// Math.round(state.width), -// Math.round(state.height) -// ); -// } -// -// /** -// * Creates the shape used to draw the selection border. -// */ -// createParentHighlightShape(bounds: Rectangle) { -// return this.createSelectionShape(bounds); -// } -// -// /** -// * Creates the shape used to draw the selection border. -// */ -// createSelectionShape(bounds: Rectangle) { -// const shape = new RectangleShape( -// Rectangle.fromRectangle(bounds), -// NONE, -// this.getSelectionColor() -// ); -// shape.strokeWidth = this.getSelectionStrokeWidth(); -// shape.isDashed = this.isSelectionDashed(); -// -// return shape; -// } -// -// /** -// * Returns {@link VERTEX_SELECTION_COLOR}. -// */ -// getSelectionColor() { -// return VERTEX_SELECTION_COLOR; -// } -// -// /** -// * Returns {@link VERTEX_SELECTION_STROKEWIDTH}. -// */ -// getSelectionStrokeWidth() { -// return VERTEX_SELECTION_STROKEWIDTH; -// } -// -// /** -// * Returns {@link VERTEX_SELECTION_DASHED}. -// */ -// isSelectionDashed() { -// return VERTEX_SELECTION_DASHED; -// } -// -// /** -// * Creates a sizer handle for the specified cursor and index and returns -// * the new {@link RectangleShape} that represents the handle. -// */ -// createSizer( -// cursor: string, -// index: number, -// size = HANDLE_SIZE, -// fillColor = HANDLE_FILLCOLOR -// ) { -// const bounds = new Rectangle(0, 0, size, size); -// const sizer = this.createSizerShape(bounds, index, fillColor); -// -// if ( -// sizer.bounds && -// sizer.isHtmlAllowed() && -// this.state.text && -// this.state.text.node.parentNode === this.graph.container -// ) { -// sizer.bounds.height -= 1; -// sizer.bounds.width -= 1; -// sizer.dialect = DIALECT.STRICTHTML; -// sizer.init(this.graph.container); -// } else { -// sizer.dialect = -// this.graph.dialect !== DIALECT.SVG ? DIALECT.MIXEDHTML : DIALECT.SVG; -// sizer.init(this.graph.getView().getOverlayPane()); -// } -// -// InternalEvent.redirectMouseEvents(sizer.node, this.graph, this.state); -// -// if (this.graph.isEnabled()) { -// sizer.setCursor(cursor); -// } -// -// if (!this.isSizerVisible(index)) { -// sizer.visible = false; -// } -// -// return sizer; -// } -// -// /** -// * Returns `true` if the sizer for the given index is visible. -// * -// * This implementation returns `true` for all given indices. -// */ -// isSizerVisible(index: number) { -// return true; -// } -// -// /** -// * Creates the shape used for the sizer handle for the specified bounds an -// * index. Only images and rectangles should be returned if support for HTML -// * labels with not foreign objects is required. -// */ -// createSizerShape(bounds: Rectangle, index: number, fillColor = HANDLE_FILLCOLOR) { -// if (this.handleImage) { -// bounds = new Rectangle( -// bounds.x, -// bounds.y, -// this.handleImage.width, -// this.handleImage.height -// ); -// const shape = new ImageShape(bounds, this.handleImage.src); -// -// // Allows HTML rendering of the images -// shape.preserveImageAspect = false; -// -// return shape; -// } -// if (index === InternalEvent.ROTATION_HANDLE) { -// return new EllipseShape(bounds, fillColor, HANDLE_STROKECOLOR); -// } -// return new RectangleShape(bounds, fillColor, HANDLE_STROKECOLOR); -// } -// -// /** -// * Helper method to create an {@link Rectangle} around the given center point -// * with a width and height of 2*s or 6, if no s is given. -// */ -// moveSizerTo(shape: Shape, x: number, y: number) { -// if (shape && shape.bounds) { -// shape.bounds.x = Math.floor(x - shape.bounds.width / 2); -// shape.bounds.y = Math.floor(y - shape.bounds.height / 2); -// -// // Fixes visible inactive handles in VML -// if (shape.node && shape.node.style.display !== 'none') { -// shape.redraw(); -// } -// } -// } -// -// /** -// * Returns the index of the handle for the given event. This returns the index -// * of the sizer from where the event originated or {@link Event.LABEL_HANDLE}. -// */ -// getHandleForEvent(me: InternalMouseEvent) { -// // Connection highlight may consume events before they reach sizer handle -// const tol = !isMouseEvent(me.getEvent()) ? this.tolerance : 1; -// const hit = -// this.allowHandleBoundsCheck && tol > 0 -// ? new Rectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) -// : null; -// -// const checkShape = (shape: Shape | null) => { -// const st = -// shape && shape.constructor !== ImageShape && this.allowHandleBoundsCheck -// ? shape.strokeWidth + shape.svgStrokeTolerance -// : null; -// const real = st -// ? new Rectangle( -// me.getGraphX() - Math.floor(st / 2), -// me.getGraphY() - Math.floor(st / 2), -// st, -// st -// ) -// : hit; -// -// return ( -// shape && -// shape.bounds && -// (me.isSource(shape) || -// (real && -// intersects(shape.bounds, real) && -// shape.node.style.display !== 'none' && -// shape.node.style.visibility !== 'hidden')) -// ); -// }; -// -// if (checkShape(this.rotationShape)) { -// return InternalEvent.ROTATION_HANDLE; -// } -// if (checkShape(this.labelShape)) { -// return InternalEvent.LABEL_HANDLE; -// } -// -// for (let i = 0; i < this.sizers.length; i += 1) { -// if (checkShape(this.sizers[i])) { -// return i; -// } -// } -// -// if (this.customHandles != null && this.isCustomHandleEvent(me)) { -// // Inverse loop order to match display order -// for (let i = this.customHandles.length - 1; i >= 0; i--) { -// if (checkShape(this.customHandles[i].shape)) { -// // LATER: Return reference to active shape -// return InternalEvent.CUSTOM_HANDLE - i; -// } -// } -// } -// -// return null; -// } -// -// /** -// * Returns `true` if the given event allows custom handles to be changed. -// * -// * This implementation returns `true`. -// */ -// isCustomHandleEvent(me: InternalMouseEvent) { -// return true; -// } -// -// /** -// * Handles the event if a handle has been clicked. By consuming the -// * event all subsequent events of the gesture are redirected to this -// * handler. -// */ -// mouseDown(sender: EventSource, me: InternalMouseEvent) { -// if (!me.isConsumed() && this.graph.isEnabled()) { -// const handle = this.getHandleForEvent(me); -// -// if (handle) { -// this.start(me.getGraphX(), me.getGraphY(), handle); -// me.consume(); -// } -// } -// } -// -// /** -// * Called if {@link livePreview} is enabled to check if a border should be painted. -// * -// * This implementation returns `true` if the shape is transparent. -// */ -// isLivePreviewBorder() { -// return ( -// this.state.shape && -// this.state.shape.fill === NONE && -// this.state.shape.stroke === NONE -// ); -// } -// -// /** -// * Starts the handling of the mouse gesture. -// */ -// start(x: number, y: number, index: number) { -// this.livePreviewActive = this.livePreview && this.state.cell.getChildCount() === 0; -// this.inTolerance = true; -// this.childOffsetX = 0; -// this.childOffsetY = 0; -// this.index = index; -// this.startX = x; -// this.startY = y; -// -// if (this.index <= InternalEvent.CUSTOM_HANDLE && this.isGhostPreview()) { -// this.ghostPreview = this.createGhostPreview(); -// } else { -// // Saves reference to parent state -// const { model } = this.state.view.graph; -// const parent = this.state.cell.getParent(); -// -// if ( -// this.state.view.currentRoot !== parent && -// parent && -// (parent.isVertex() || parent.isEdge()) -// ) { -// this.parentState = (this.state.view.graph).view.getState(parent); -// } -// -// // Creates a preview that can be on top of any HTML label -// this.selectionBorder.node.style.display = -// index === InternalEvent.ROTATION_HANDLE ? 'inline' : 'none'; -// -// // Creates the border that represents the new bounds -// if (!this.livePreviewActive || this.isLivePreviewBorder()) { -// this.preview = this.createSelectionShape(this.bounds); -// -// if ( -// !(Client.IS_SVG && Number(this.state.style.rotation || '0') !== 0) && -// this.state.text != null && -// this.state.text.node.parentNode === this.graph.container -// ) { -// this.preview.dialect = DIALECT.STRICTHTML; -// this.preview.init(this.graph.container); -// } else { -// this.preview.dialect = DIALECT.SVG; -// this.preview.init(this.graph.view.getOverlayPane()); -// } -// } -// -// if (index === InternalEvent.ROTATION_HANDLE) { -// // With the rotation handle in a corner, need the angle and distance -// const pos = this.getRotationHandlePosition(); -// -// const dx = pos.x - this.state.getCenterX(); -// const dy = pos.y - this.state.getCenterY(); -// -// this.startAngle = dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : 0; -// this.startDist = Math.sqrt(dx * dx + dy * dy); -// } -// -// // Prepares the handles for live preview -// if (this.livePreviewActive) { -// this.hideSizers(); -// -// if (index === InternalEvent.ROTATION_HANDLE && this.rotationShape) { -// this.rotationShape.node.style.display = ''; -// } else if (index === InternalEvent.LABEL_HANDLE && this.labelShape) { -// this.labelShape.node.style.display = ''; -// } else if (this.sizers[index]) { -// this.sizers[index].node.style.display = ''; -// } else if (index <= InternalEvent.CUSTOM_HANDLE) { -// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].setVisible(true); -// } -// -// // Gets the array of connected edge handlers for redrawing -// const edges = this.state.cell.getEdges(); -// this.edgeHandlers = []; -// -// const selectionCellsHandler = this.graph.getPlugin( -// 'SelectionCellsHandler' -// ) as SelectionCellsHandler; -// -// for (let i = 0; i < edges.length; i += 1) { -// const handler = selectionCellsHandler?.getHandler(edges[i]); -// if (handler) { -// this.edgeHandlers.push(handler as EdgeHandler); -// } -// } -// } -// } -// } -// -// /** -// * Starts the handling of the mouse gesture. -// */ -// createGhostPreview() { -// const shape = this.graph.cellRenderer.createShape(this.state); -// shape.init(this.graph.view.getOverlayPane()); -// shape.scale = this.state.view.scale; -// shape.bounds = this.bounds; -// shape.outline = true; -// -// return shape; -// } -// -// /** -// * Shortcut to {@link hideSizers}. -// */ -// setHandlesVisible(visible: boolean) { -// this.handlesVisible = visible; -// -// for (let i = 0; i < this.sizers.length; i += 1) { -// this.sizers[i].node.style.display = visible ? '' : 'none'; -// } -// -// for (let i = 0; i < this.customHandles.length; i += 1) { -// this.customHandles[i].setVisible(visible); -// } -// } -// -// /** -// * Hides all sizers except. -// * -// * Starts the handling of the mouse gesture. -// */ -// hideSizers() { -// this.setHandlesVisible(false); -// } -// -// /** -// * Checks if the coordinates for the given event are within the -// * {@link Graph#tolerance}. If the event is a mouse event then the tolerance is -// * ignored. -// */ -// checkTolerance(me: InternalMouseEvent) { -// if (this.inTolerance && this.startX !== null && this.startY !== null) { -// if ( -// isMouseEvent(me.getEvent()) || -// Math.abs(me.getGraphX() - this.startX) > this.graph.getEventTolerance() || -// Math.abs(me.getGraphY() - this.startY) > this.graph.getEventTolerance() -// ) { -// this.inTolerance = false; -// } -// } -// } -// -// /** -// * Hook for subclasses do show details while the handler is active. -// */ -// updateHint(me: InternalMouseEvent) { -// return; -// } -// -// /** -// * Hooks for subclasses to hide details when the handler gets inactive. -// */ -// removeHint() { -// return; -// } -// -// /** -// * Hook for rounding the angle. This uses {@link Math.round}. -// */ -// roundAngle(angle: number) { -// return Math.round(angle * 10) / 10; -// } -// -// /** -// * Hook for rounding the unscaled width or height. This uses {@link Math.round}. -// */ -// roundLength(length: number) { -// return Math.round(length * 100) / 100; -// } -// -// /** -// * Handles the event by updating the preview. -// */ -// mouseMove(sender: EventSource, me: InternalMouseEvent) { -// if (!me.isConsumed() && this.index != null) { -// // Checks tolerance for ignoring single clicks -// this.checkTolerance(me); -// -// if (!this.inTolerance) { -// if (this.index <= InternalEvent.CUSTOM_HANDLE) { -// if (this.customHandles != null) { -// this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].processEvent(me); -// this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].active = true; -// -// if (this.ghostPreview != null) { -// this.ghostPreview.apply(this.state); -// this.ghostPreview.strokeWidth = -// this.getSelectionStrokeWidth() / -// this.ghostPreview.scale / -// this.ghostPreview.scale; -// this.ghostPreview.isDashed = this.isSelectionDashed(); -// this.ghostPreview.stroke = this.getSelectionColor(); -// this.ghostPreview.redraw(); -// -// if (this.selectionBounds != null) { -// this.selectionBorder.node.style.display = 'none'; -// } -// } else { -// if (this.movePreviewToFront) { -// this.moveToFront(); -// } -// -// this.customHandles[ -// InternalEvent.CUSTOM_HANDLE - this.index -// ].positionChanged(); -// } -// } -// } else if (this.index === InternalEvent.LABEL_HANDLE) { -// this.moveLabel(me); -// } else { -// if (this.index === InternalEvent.ROTATION_HANDLE) { -// this.rotateVertex(me); -// } else { -// this.resizeVertex(me); -// } -// -// this.updateHint(me); -// } -// } -// -// me.consume(); -// } -// // Workaround for disabling the connect highlight when over handle -// else if (!this.graph.isMouseDown && this.getHandleForEvent(me)) { -// me.consume(false); -// } -// } -// -// /** -// * Returns `true` if a ghost preview should be used for custom handles. -// */ -// isGhostPreview() { -// return this.state.cell.getChildCount() > 0; -// } -// -// /** -// * Moves the vertex. -// */ -// moveLabel(me: InternalMouseEvent) { -// const point = new Point(me.getGraphX(), me.getGraphY()); -// const tr = this.graph.view.translate; -// const { scale } = this.graph.view; -// -// if (this.graph.isGridEnabledEvent(me.getEvent())) { -// point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale; -// point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale; -// } -// -// const index = this.rotationShape ? this.sizers.length - 2 : this.sizers.length - 1; -// this.moveSizerTo(this.sizers[index], point.x, point.y); -// } -// -// /** -// * Rotates the vertex. -// */ -// rotateVertex(me: InternalMouseEvent) { -// const point = new Point(me.getGraphX(), me.getGraphY()); -// let dx = this.state.x + this.state.width / 2 - point.x; -// let dy = this.state.y + this.state.height / 2 - point.y; -// this.currentAlpha = -// dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : dy < 0 ? 180 : 0; -// -// if (dx > 0) { -// this.currentAlpha -= 180; -// } -// -// this.currentAlpha -= this.startAngle; -// -// // Rotation raster -// if (this.rotationRaster && this.graph.isGridEnabledEvent(me.getEvent())) { -// let raster; -// dx = point.x - this.state.getCenterX(); -// dy = point.y - this.state.getCenterY(); -// const dist = Math.sqrt(dx * dx + dy * dy); -// -// if (dist - this.startDist < 2) { -// raster = 15; -// } else if (dist - this.startDist < 25) { -// raster = 5; -// } else { -// raster = 1; -// } -// -// this.currentAlpha = Math.round(this.currentAlpha / raster) * raster; -// } else { -// this.currentAlpha = this.roundAngle(this.currentAlpha); -// } -// -// this.selectionBorder.rotation = this.currentAlpha; -// this.selectionBorder.redraw(); -// -// if (this.livePreviewActive) { -// this.redrawHandles(); -// } -// } -// -// /** -// * Resizes the vertex. -// */ -// resizeVertex(me: InternalMouseEvent) { -// const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); -// const alpha = toRadians(this.state.style.rotation ?? 0); -// const point = new Point(me.getGraphX(), me.getGraphY()); -// const tr = this.graph.view.translate; -// const { scale } = this.graph.view; -// let cos = Math.cos(-alpha); -// let sin = Math.sin(-alpha); -// -// let dx = point.x - this.startX; -// let dy = point.y - this.startY; -// -// // Rotates vector for mouse gesture -// const tx = cos * dx - sin * dy; -// const ty = sin * dx + cos * dy; -// -// dx = tx; -// dy = ty; -// -// const geo = this.state.cell.getGeometry(); -// if (geo && this.index !== null) { -// this.unscaledBounds = this.union( -// geo, -// dx / scale, -// dy / scale, -// this.index, -// this.graph.isGridEnabledEvent(me.getEvent()), -// 1, -// new Point(0, 0), -// this.isConstrainedEvent(me), -// this.isCenteredEvent(this.state, me) -// ); -// } -// -// // Keeps vertex within maximum graph or parent bounds -// if (geo && !geo.relative) { -// let max = this.graph.getMaximumGraphBounds(); -// -// // Handles child cells -// if (max != null && this.parentState != null) { -// max = Rectangle.fromRectangle(max); -// -// max.x -= (this.parentState.x - tr.x * scale) / scale; -// max.y -= (this.parentState.y - tr.y * scale) / scale; -// } -// -// if (this.graph.isConstrainChild(this.state.cell)) { -// let tmp = this.graph.getCellContainmentArea(this.state.cell); -// -// if (tmp != null) { -// const overlap = this.graph.getOverlap(this.state.cell); -// -// if (overlap > 0) { -// tmp = Rectangle.fromRectangle(tmp); -// -// tmp.x -= tmp.width * overlap; -// tmp.y -= tmp.height * overlap; -// tmp.width += 2 * tmp.width * overlap; -// tmp.height += 2 * tmp.height * overlap; -// } -// -// if (!max) { -// max = tmp; -// } else { -// max = Rectangle.fromRectangle(max); -// max.intersect(tmp); -// } -// } -// } -// -// if (max && this.unscaledBounds) { -// if (this.unscaledBounds.x < max.x) { -// this.unscaledBounds.width -= max.x - this.unscaledBounds.x; -// this.unscaledBounds.x = max.x; -// } -// -// if (this.unscaledBounds.y < max.y) { -// this.unscaledBounds.height -= max.y - this.unscaledBounds.y; -// this.unscaledBounds.y = max.y; -// } -// -// if (this.unscaledBounds.x + this.unscaledBounds.width > max.x + max.width) { -// this.unscaledBounds.width -= -// this.unscaledBounds.x + this.unscaledBounds.width - max.x - max.width; -// } -// -// if (this.unscaledBounds.y + this.unscaledBounds.height > max.y + max.height) { -// this.unscaledBounds.height -= -// this.unscaledBounds.y + this.unscaledBounds.height - max.y - max.height; -// } -// } -// } -// -// if (this.unscaledBounds) { -// const old = this.bounds; -// -// this.bounds = new Rectangle( -// (this.parentState ? this.parentState.x : tr.x * scale) + -// this.unscaledBounds.x * scale, -// (this.parentState ? this.parentState.y : tr.y * scale) + -// this.unscaledBounds.y * scale, -// this.unscaledBounds.width * scale, -// this.unscaledBounds.height * scale -// ); -// -// if (geo && geo.relative && this.parentState) { -// this.bounds.x += this.state.x - this.parentState.x; -// this.bounds.y += this.state.y - this.parentState.y; -// } -// -// cos = Math.cos(alpha); -// sin = Math.sin(alpha); -// -// const c2 = new Point(this.bounds.getCenterX(), this.bounds.getCenterY()); -// -// dx = c2.x - ct.x; -// dy = c2.y - ct.y; -// -// const dx2 = cos * dx - sin * dy; -// const dy2 = sin * dx + cos * dy; -// -// const dx3 = dx2 - dx; -// const dy3 = dy2 - dy; -// -// const dx4 = this.bounds.x - this.state.x; -// const dy4 = this.bounds.y - this.state.y; -// -// const dx5 = cos * dx4 - sin * dy4; -// const dy5 = sin * dx4 + cos * dy4; -// -// this.bounds.x += dx3; -// this.bounds.y += dy3; -// -// // Rounds unscaled bounds to int -// this.unscaledBounds.x = this.roundLength(this.unscaledBounds.x + dx3 / scale); -// this.unscaledBounds.y = this.roundLength(this.unscaledBounds.y + dy3 / scale); -// this.unscaledBounds.width = this.roundLength(this.unscaledBounds.width); -// this.unscaledBounds.height = this.roundLength(this.unscaledBounds.height); -// -// // Shifts the children according to parent offset -// if (!this.state.cell.isCollapsed() && (dx3 !== 0 || dy3 !== 0)) { -// this.childOffsetX = this.state.x - this.bounds.x + dx5; -// this.childOffsetY = this.state.y - this.bounds.y + dy5; -// } else { -// this.childOffsetX = 0; -// this.childOffsetY = 0; -// } -// -// if (!old.equals(this.bounds)) { -// if (this.livePreviewActive) { -// this.updateLivePreview(me); -// } -// -// if (this.preview != null) { -// this.drawPreview(); -// } else { -// this.updateParentHighlight(); -// } -// } -// } -// } -// -// /** -// * Repaints the live preview. -// */ -// updateLivePreview(me: InternalMouseEvent) { -// // TODO: Apply child offset to children in live preview -// const { scale } = this.graph.view; -// const tr = this.graph.view.translate; -// -// // Saves current state -// const tempState = this.state.clone(); -// -// // Temporarily changes size and origin -// this.state.x = this.bounds.x; -// this.state.y = this.bounds.y; -// this.state.origin = new Point( -// this.state.x / scale - tr.x, -// this.state.y / scale - tr.y -// ); -// this.state.width = this.bounds.width; -// this.state.height = this.bounds.height; -// -// // Redraws cell and handles -// let off = this.state.absoluteOffset; -// off = new Point(off.x, off.y); -// -// // Required to store and reset absolute offset for updating label position -// this.state.absoluteOffset.x = 0; -// this.state.absoluteOffset.y = 0; -// const geo = this.state.cell.getGeometry(); -// -// if (geo != null) { -// const offset = geo.offset || this.EMPTY_POINT; -// -// if (offset != null && !geo.relative) { -// this.state.absoluteOffset.x = this.state.view.scale * offset.x; -// this.state.absoluteOffset.y = this.state.view.scale * offset.y; -// } -// -// this.state.view.updateVertexLabelOffset(this.state); -// } -// -// // Draws the live preview -// (this.state.view.graph).cellRenderer.redraw(this.state, true); -// -// // Redraws connected edges TODO: Include child edges -// this.state.view.invalidate(this.state.cell); -// this.state.invalid = false; -// this.state.view.validate(); -// this.redrawHandles(); -// -// // Moves live preview to front -// if (this.movePreviewToFront) { -// this.moveToFront(); -// } -// -// // Hides folding icon -// if (this.state.control != null && this.state.control.node != null) { -// this.state.control.node.style.visibility = 'hidden'; -// } -// -// // Restores current state -// this.state.setState(tempState); -// } -// -// /** -// * Handles the event by applying the changes to the geometry. -// */ -// moveToFront() { -// if ( -// (this.state.text && this.state.text.node && this.state.text.node.nextSibling) || -// (this.state.shape && -// this.state.shape.node && -// this.state.shape.node.nextSibling && -// (!this.state.text || this.state.shape.node.nextSibling !== this.state.text.node)) -// ) { -// if (this.state.shape && this.state.shape.node && this.state.shape.node.parentNode) { -// this.state.shape.node.parentNode.appendChild(this.state.shape.node); -// } -// -// if (this.state.text && this.state.text.node && this.state.text.node.parentNode) { -// this.state.text.node.parentNode.appendChild(this.state.text.node); -// } -// } -// } -// -// /** -// * Handles the event by applying the changes to the geometry. -// */ -// mouseUp(sender: EventSource, me: InternalMouseEvent) { -// if (this.index != null && this.state != null) { -// const point = new Point(me.getGraphX(), me.getGraphY()); -// const { index } = this; -// this.index = null; -// -// if (this.ghostPreview == null) { -// // Required to restore order in case of no change -// this.state.view.invalidate(this.state.cell, false, false); -// this.state.view.validate(); -// } -// -// this.graph.batchUpdate(() => { -// if (index <= InternalEvent.CUSTOM_HANDLE) { -// if (this.customHandles != null) { -// // Creates style before changing cell state -// const style = (this.state.view.graph).getCellStyle(this.state.cell); -// -// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].active = false; -// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].execute(me); -// -// // Sets style and apply on shape to force repaint and -// // check if execute has removed custom handles -// if ( -// this.customHandles != null && -// this.customHandles[InternalEvent.CUSTOM_HANDLE - index] != null -// ) { -// this.state.style = style; -// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].positionChanged(); -// } -// } -// } else if (index === InternalEvent.ROTATION_HANDLE) { -// if (this.currentAlpha != null) { -// const delta = this.currentAlpha - (this.state.style.rotation ?? 0); -// -// if (delta !== 0) { -// this.rotateCell(this.state.cell, delta); -// } -// } else { -// this.rotateClick(); -// } -// } else { -// const gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); -// const alpha = toRadians(this.state.style.rotation ?? 0); -// const cos = Math.cos(-alpha); -// const sin = Math.sin(-alpha); -// -// let dx = point.x - this.startX; -// let dy = point.y - this.startY; -// -// // Rotates vector for mouse gesture -// const tx = cos * dx - sin * dy; -// const ty = sin * dx + cos * dy; -// -// dx = tx; -// dy = ty; -// -// const s = this.graph.view.scale; -// const recurse = this.isRecursiveResize(this.state, me); -// -// this.resizeCell( -// this.state.cell, -// this.roundLength(dx / s), -// this.roundLength(dy / s), -// index, -// gridEnabled, -// this.isConstrainedEvent(me), -// recurse -// ); -// } -// }); -// -// me.consume(); -// this.reset(); -// this.redrawHandles(); -// } -// } -// -// /** -// * Returns the `recursiveResize` status of the given state. -// * @param state the given {@link CellState}. This implementation takes the value of this state. -// * @param me the mouse event. -// */ -// isRecursiveResize(state: CellState, me: InternalMouseEvent) { -// return this.graph.isRecursiveResize(this.state); -// } -// -// /** -// * Hook for subclasses to implement a single click on the rotation handle. -// * This code is executed as part of the model transaction. This implementation -// * is empty. -// */ -// rotateClick() { -// return; -// } -// -// /** -// * Rotates the given cell and its children by the given angle in degrees. -// * -// * @param cell {@link Cell} to be rotated. -// * @param angle Angle in degrees. -// * @param parent if set, consider the parent in the rotation computation. -// */ -// rotateCell(cell: Cell, angle: number, parent?: Cell) { -// if (angle !== 0) { -// const model = this.graph.getDataModel(); -// -// if (cell.isVertex() || cell.isEdge()) { -// if (!cell.isEdge()) { -// const style = this.graph.getCurrentCellStyle(cell); -// const total = (style.rotation || 0) + angle; -// this.graph.setCellStyles('rotation', total, [cell]); -// } -// -// let geo = cell.getGeometry(); -// -// if (geo && parent) { -// const pgeo = parent.getGeometry(); -// -// if (pgeo != null && !parent.isEdge()) { -// geo = geo.clone(); -// geo.rotate(angle, new Point(pgeo.width / 2, pgeo.height / 2)); -// model.setGeometry(cell, geo); -// } -// -// if ((cell.isVertex() && !geo.relative) || cell.isEdge()) { -// // Recursive rotation -// const childCount = cell.getChildCount(); -// -// for (let i = 0; i < childCount; i += 1) { -// this.rotateCell(cell.getChildAt(i), angle, cell); -// } -// } -// } -// } -// } -// } -// -// /** -// * Resets the state of this handler. -// */ -// reset() { -// if (this.index !== null && this.sizers[this.index].node.style.display === 'none') { -// this.sizers[this.index].node.style.display = ''; -// } -// -// this.index = null; -// -// // TODO: Reset and redraw cell states for live preview -// if (this.preview) { -// this.preview.destroy(); -// this.preview = null; -// } -// -// if (this.ghostPreview) { -// this.ghostPreview.destroy(); -// this.ghostPreview = null; -// } -// -// if (this.livePreviewActive) { -// for (let i = 0; i < this.sizers.length; i += 1) { -// this.sizers[i].node.style.display = ''; -// } -// -// // Shows folding icon -// if (this.state.control && this.state.control.node) { -// this.state.control.node.style.visibility = ''; -// } -// } -// -// for (let i = 0; i < this.customHandles.length; i += 1) { -// if (this.customHandles[i].active) { -// this.customHandles[i].active = false; -// this.customHandles[i].reset(); -// } else { -// this.customHandles[i].setVisible(true); -// } -// } -// -// // Checks if handler has been destroyed -// this.selectionBorder.node.style.display = 'inline'; -// this.selectionBounds = this.getSelectionBounds(this.state); -// this.bounds = new Rectangle( -// this.selectionBounds.x, -// this.selectionBounds.y, -// this.selectionBounds.width, -// this.selectionBounds.height -// ); -// this.drawPreview(); -// -// this.removeHint(); -// this.redrawHandles(); -// this.edgeHandlers = []; -// this.handlesVisible = true; -// this.unscaledBounds = null; -// } -// -// /** -// * Uses the given vector to change the bounds of the given cell -// * in the graph using {@link Graph#resizeCell}. -// */ -// resizeCell( -// cell: Cell, -// dx: number, -// dy: number, -// index: number, -// gridEnabled: boolean, -// constrained: boolean, -// recurse: boolean -// ) { -// let geo = cell.getGeometry(); -// -// if (geo) { -// if ( -// index === InternalEvent.LABEL_HANDLE && -// this.labelShape && -// this.labelShape.bounds -// ) { -// const alpha = -toRadians(this.state.style.rotation ?? 0); -// const cos = Math.cos(alpha); -// const sin = Math.sin(alpha); -// const { scale } = this.graph.view; -// const pt = getRotatedPoint( -// new Point( -// Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale), -// Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale) -// ), -// cos, -// sin -// ); -// -// geo = geo.clone(); -// -// if (geo.offset == null) { -// geo.offset = pt; -// } else { -// geo.offset.x += pt.x; -// geo.offset.y += pt.y; -// } -// -// this.graph.model.setGeometry(cell, geo); -// } else if (this.unscaledBounds) { -// const { scale } = this.graph.view; -// -// if (this.childOffsetX !== 0 || this.childOffsetY !== 0) { -// this.moveChildren( -// cell, -// Math.round(this.childOffsetX / scale), -// Math.round(this.childOffsetY / scale) -// ); -// } -// -// this.graph.resizeCell(cell, this.unscaledBounds, recurse); -// } -// } -// } -// -// /** -// * Moves the children of the given cell by the given vector. -// */ -// moveChildren(cell: Cell, dx: number, dy: number) { -// const model = this.graph.getDataModel(); -// const childCount = cell.getChildCount(); -// -// for (let i = 0; i < childCount; i += 1) { -// const child = cell.getChildAt(i); -// let geo = child.getGeometry(); -// -// if (geo != null) { -// geo = geo.clone(); -// geo.translate(dx, dy); -// model.setGeometry(child, geo); -// } -// } -// } -// -// /** -// * Returns the union of the given bounds and location for the specified -// * handle index. -// * -// * To override this to limit the size of vertex via a minWidth/-Height style, -// * the following code can be used. -// * -// * ```javascript -// * let vertexHandlerUnion = union; -// * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> -// * { -// * let result = vertexHandlerUnion.apply(this, arguments); -// * -// * result.width = Math.max(result.width, mxUtils.getNumber(this.state.style, 'minWidth', 0)); -// * result.height = Math.max(result.height, mxUtils.getNumber(this.state.style, 'minHeight', 0)); -// * -// * return result; -// * }; -// * ``` -// * -// * The minWidth/-Height style can then be used as follows: -// * -// * ```javascript -// * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'minWidth=100;minHeight=100;'); -// * ``` -// * -// * To override this to update the height for a wrapped text if the width of a vertex is -// * changed, the following can be used. -// * -// * ```javascript -// * let mxVertexHandlerUnion = union; -// * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> -// * { -// * let result = mxVertexHandlerUnion.apply(this, arguments); -// * let s = this.state; -// * -// * if (this.graph.isHtmlLabel(s.cell) && (index == 3 || index == 4) && -// * s.text != null && s.style.whiteSpace == 'wrap') -// * { -// * let label = this.graph.getLabel(s.cell); -// * let fontSize = mxUtils.getNumber(s.style, 'fontSize', mxConstants.DEFAULT_FONTSIZE); -// * let ww = result.width / s.view.scale - s.text.spacingRight - s.text.spacingLeft -// * -// * result.height = mxUtils.getSizeForString(label, fontSize, s.style.fontFamily, ww).height; -// * } -// * -// * return result; -// * }; -// * ``` -// */ -// union( -// bounds: Rectangle, -// dx: number, -// dy: number, -// index: number, -// gridEnabled: boolean, -// scale: number, -// tr: Point, -// constrained: boolean, -// centered: boolean -// ) { -// gridEnabled = gridEnabled && this.graph.isGridEnabled(); -// -// if (this.singleSizer) { -// let x = bounds.x + bounds.width + dx; -// let y = bounds.y + bounds.height + dy; -// -// if (gridEnabled) { -// x = this.graph.snap(x / scale) * scale; -// y = this.graph.snap(y / scale) * scale; -// } -// -// const rect = new Rectangle(bounds.x, bounds.y, 0, 0); -// rect.add(new Rectangle(x, y, 0, 0)); -// -// return rect; -// } -// const w0 = bounds.width; -// const h0 = bounds.height; -// let left = bounds.x - tr.x * scale; -// let right = left + w0; -// let top = bounds.y - tr.y * scale; -// let bottom = top + h0; -// -// const cx = left + w0 / 2; -// const cy = top + h0 / 2; -// -// if (index > 4 /* Bottom Row */) { -// bottom += dy; -// -// if (gridEnabled) { -// bottom = this.graph.snap(bottom / scale) * scale; -// } else { -// bottom = Math.round(bottom / scale) * scale; -// } -// } else if (index < 3 /* Top Row */) { -// top += dy; -// -// if (gridEnabled) { -// top = this.graph.snap(top / scale) * scale; -// } else { -// top = Math.round(top / scale) * scale; -// } -// } -// -// if (index === 0 || index === 3 || index === 5 /* Left */) { -// left += dx; -// -// if (gridEnabled) { -// left = this.graph.snap(left / scale) * scale; -// } else { -// left = Math.round(left / scale) * scale; -// } -// } else if (index === 2 || index === 4 || index === 7 /* Right */) { -// right += dx; -// -// if (gridEnabled) { -// right = this.graph.snap(right / scale) * scale; -// } else { -// right = Math.round(right / scale) * scale; -// } -// } -// -// let width = right - left; -// let height = bottom - top; -// -// if (constrained) { -// const geo = this.state.cell.getGeometry(); -// -// if (geo != null) { -// const aspect = geo.width / geo.height; -// -// if (index === 1 || index === 2 || index === 7 || index === 6) { -// width = height * aspect; -// } else { -// height = width / aspect; -// } -// -// if (index === 0) { -// left = right - width; -// top = bottom - height; -// } -// } -// } -// -// if (centered) { -// width += width - w0; -// height += height - h0; -// -// const cdx = cx - (left + width / 2); -// const cdy = cy - (top + height / 2); -// -// left += cdx; -// top += cdy; -// right += cdx; -// bottom += cdy; -// } -// -// // Flips over left side -// if (width < 0) { -// left += width; -// width = Math.abs(width); -// } -// -// // Flips over top side -// if (height < 0) { -// top += height; -// height = Math.abs(height); -// } -// -// const result = new Rectangle(left + tr.x * scale, top + tr.y * scale, width, height); -// -// if (this.minBounds != null) { -// result.width = Math.max( -// result.width, -// this.minBounds.x * scale + -// this.minBounds.width * scale + -// Math.max(0, this.x0 * scale - result.x) -// ); -// result.height = Math.max( -// result.height, -// this.minBounds.y * scale + -// this.minBounds.height * scale + -// Math.max(0, this.y0 * scale - result.y) -// ); -// } -// -// return result; -// } -// -// /** -// * Redraws the handles and the preview. -// */ -// redraw(ignoreHandles?: boolean) { -// this.selectionBounds = this.getSelectionBounds(this.state); -// this.bounds = new Rectangle( -// this.selectionBounds.x, -// this.selectionBounds.y, -// this.selectionBounds.width, -// this.selectionBounds.height -// ); -// this.drawPreview(); -// -// if (!ignoreHandles) { -// this.redrawHandles(); -// } -// } -// -// /** -// * Returns the padding to be used for drawing handles for the current . -// */ -// getHandlePadding() { -// // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) -// const result = new Point(0, 0); -// let tol = this.tolerance; -// -// if ( -// this.sizers.length > 0 && -// this.sizers[0].bounds && -// (this.bounds.width < 2 * this.sizers[0].bounds.width + 2 * tol || -// this.bounds.height < 2 * this.sizers[0].bounds.height + 2 * tol) -// ) { -// tol /= 2; -// -// result.x = this.sizers[0].bounds.width + tol; -// result.y = this.sizers[0].bounds.height + tol; -// } -// -// return result; -// } -// -// /** -// * Returns the bounds used to paint the resize handles. -// */ -// getSizerBounds() { -// return this.bounds; -// } -// -// /** -// * Redraws the handles. To hide certain handles the following code can be used. -// * -// * ```javascript -// * redrawHandles() -// * { -// * mxVertexHandlerRedrawHandles.apply(this, arguments); -// * -// * if (this.sizers != null && this.sizers.length > 7) -// * { -// * this.sizers[1].node.style.display = 'none'; -// * this.sizers[6].node.style.display = 'none'; -// * } -// * }; -// * ``` -// */ -// redrawHandles() { -// let s = this.getSizerBounds(); -// const tol = this.tolerance; -// this.horizontalOffset = 0; -// this.verticalOffset = 0; -// -// for (let i = 0; i < this.customHandles.length; i += 1) { -// const shape = this.customHandles[i].shape; -// -// if (shape) { -// const temp = shape.node.style.display; -// this.customHandles[i].redraw(); -// shape.node.style.display = temp; -// -// // Hides custom handles during text editing -// shape.node.style.visibility = -// this.handlesVisible && this.isCustomHandleVisible(this.customHandles[i]) -// ? '' -// : 'hidden'; -// } -// } -// -// if (this.sizers.length > 0 && this.sizers[0]) { -// if (this.index === null && this.manageSizers && this.sizers.length >= 8) { -// // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) -// const padding = this.getHandlePadding(); -// this.horizontalOffset = padding.x; -// this.verticalOffset = padding.y; -// -// if (this.horizontalOffset !== 0 || this.verticalOffset !== 0) { -// s = new Rectangle(s.x, s.y, s.width, s.height); -// -// s.x -= this.horizontalOffset / 2; -// s.width += this.horizontalOffset; -// s.y -= this.verticalOffset / 2; -// s.height += this.verticalOffset; -// } -// -// if (this.sizers.length >= 8) { -// if ( -// this.sizers[0].bounds && -// (s.width < 2 * this.sizers[0].bounds.width + 2 * tol || -// s.height < 2 * this.sizers[0].bounds.height + 2 * tol) -// ) { -// this.sizers[0].node.style.display = 'none'; -// this.sizers[2].node.style.display = 'none'; -// this.sizers[5].node.style.display = 'none'; -// this.sizers[7].node.style.display = 'none'; -// } else if (this.handlesVisible) { -// this.sizers[0].node.style.display = ''; -// this.sizers[2].node.style.display = ''; -// this.sizers[5].node.style.display = ''; -// this.sizers[7].node.style.display = ''; -// } -// } -// } -// -// const r = s.x + s.width; -// const b = s.y + s.height; -// -// if (this.singleSizer) { -// this.moveSizerTo(this.sizers[0], r, b); -// } else { -// const cx = s.x + s.width / 2; -// const cy = s.y + s.height / 2; -// -// if (this.sizers.length >= 8) { -// const crs = [ -// 'nw-resize', -// 'n-resize', -// 'ne-resize', -// 'e-resize', -// 'se-resize', -// 's-resize', -// 'sw-resize', -// 'w-resize', -// ]; -// -// const alpha = toRadians(this.state.style.rotation ?? 0); -// const cos = Math.cos(alpha); -// const sin = Math.sin(alpha); -// const da = Math.round((alpha * 4) / Math.PI); -// -// const ct = new Point(s.getCenterX(), s.getCenterY()); -// let pt = getRotatedPoint(new Point(s.x, s.y), cos, sin, ct); -// this.moveSizerTo(this.sizers[0], pt.x, pt.y); -// this.sizers[0].setCursor(crs[mod(0 + da, crs.length)]); -// -// pt.x = cx; -// pt.y = s.y; -// pt = getRotatedPoint(pt, cos, sin, ct); -// this.moveSizerTo(this.sizers[1], pt.x, pt.y); -// this.sizers[1].setCursor(crs[mod(1 + da, crs.length)]); -// -// pt.x = r; -// pt.y = s.y; -// pt = getRotatedPoint(pt, cos, sin, ct); -// this.moveSizerTo(this.sizers[2], pt.x, pt.y); -// this.sizers[2].setCursor(crs[mod(2 + da, crs.length)]); -// -// pt.x = s.x; -// pt.y = cy; -// pt = getRotatedPoint(pt, cos, sin, ct); -// this.moveSizerTo(this.sizers[3], pt.x, pt.y); -// this.sizers[3].setCursor(crs[mod(7 + da, crs.length)]); -// -// pt.x = r; -// pt.y = cy; -// pt = getRotatedPoint(pt, cos, sin, ct); -// this.moveSizerTo(this.sizers[4], pt.x, pt.y); -// this.sizers[4].setCursor(crs[mod(3 + da, crs.length)]); -// -// pt.x = s.x; -// pt.y = b; -// pt = getRotatedPoint(pt, cos, sin, ct); -// this.moveSizerTo(this.sizers[5], pt.x, pt.y); -// this.sizers[5].setCursor(crs[mod(6 + da, crs.length)]); -// -// pt.x = cx; -// pt.y = b; -// pt = getRotatedPoint(pt, cos, sin, ct); -// this.moveSizerTo(this.sizers[6], pt.x, pt.y); -// this.sizers[6].setCursor(crs[mod(5 + da, crs.length)]); -// -// pt.x = r; -// pt.y = b; -// pt = getRotatedPoint(pt, cos, sin, ct); -// this.moveSizerTo(this.sizers[7], pt.x, pt.y); -// this.sizers[7].setCursor(crs[mod(4 + da, crs.length)]); -// -// pt.x = cx + this.state.absoluteOffset.x; -// pt.y = cy + this.state.absoluteOffset.y; -// pt = getRotatedPoint(pt, cos, sin, ct); -// this.moveSizerTo(this.sizers[8], pt.x, pt.y); -// } else if (this.state.width >= 2 && this.state.height >= 2) { -// this.moveSizerTo( -// this.sizers[0], -// cx + this.state.absoluteOffset.x, -// cy + this.state.absoluteOffset.y -// ); -// } else { -// this.moveSizerTo(this.sizers[0], this.state.x, this.state.y); -// } -// } -// } -// -// if (this.rotationShape) { -// const alpha = toRadians(this.currentAlpha); -// const cos = Math.cos(alpha); -// const sin = Math.sin(alpha); -// -// const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); -// const pt = getRotatedPoint(this.getRotationHandlePosition(), cos, sin, ct); -// -// if (this.rotationShape.node != null) { -// this.moveSizerTo(this.rotationShape, pt.x, pt.y); -// -// // Hides rotation handle during text editing -// this.rotationShape.node.style.visibility = -// (this.state.view.graph).isEditing() || !this.handlesVisible -// ? 'hidden' -// : ''; -// } -// } -// -// if (this.selectionBorder != null) { -// this.selectionBorder.rotation = this.state.style.rotation ?? 0; -// } -// -// if (this.edgeHandlers != null) { -// for (let i = 0; i < this.edgeHandlers.length; i += 1) { -// this.edgeHandlers[i].redraw(); -// } -// } -// } -// -// /** -// * Returns true if the given custom handle is visible. -// */ -// isCustomHandleVisible(handle: CellHandle) { -// return ( -// !this.graph.isEditing() && (this.state.view.graph).getSelectionCount() === 1 -// ); -// } -// -// /** -// * Returns an {@link Point} that defines the rotation handle position. -// */ -// getRotationHandlePosition() { -// return new Point( -// this.bounds.x + this.bounds.width / 2, -// this.bounds.y + this.rotationHandleVSpacing -// ); -// } -// -// /** -// * Returns `true` if the parent highlight should be visible. -// * -// * This implementation always returns `true`. -// */ -// isParentHighlightVisible() { -// const parent = this.state.cell.getParent(); -// return parent ? !this.graph.isCellSelected(parent) : false; -// } -// -// /** -// * Updates the highlight of the parent if {@link parentHighlightEnabled} is `true`. -// */ -// updateParentHighlight() { -// if (!this.isDestroyed()) { -// const visible = this.isParentHighlightVisible(); -// const parent = this.state.cell.getParent(); -// const pstate = parent ? this.graph.view.getState(parent) : null; -// -// if (this.parentHighlight) { -// if (parent && parent.isVertex() && visible) { -// const b = this.parentHighlight.bounds; -// -// if ( -// pstate && -// b && -// (b.x !== pstate.x || -// b.y !== pstate.y || -// b.width !== pstate.width || -// b.height !== pstate.height) -// ) { -// this.parentHighlight.bounds = Rectangle.fromRectangle(pstate); -// this.parentHighlight.redraw(); -// } -// } else { -// if (pstate != null && pstate.parentHighlight === this.parentHighlight) { -// pstate.parentHighlight = null; -// } -// -// this.parentHighlight.destroy(); -// this.parentHighlight = null; -// } -// } else if (this.parentHighlightEnabled && visible) { -// if ( -// parent && -// parent.isVertex() && -// pstate != null && -// pstate.parentHighlight == null -// ) { -// this.parentHighlight = this.createParentHighlightShape(pstate); -// // VML dialect required here for event transparency in IE -// this.parentHighlight.dialect = DIALECT.SVG; -// this.parentHighlight.pointerEvents = false; -// this.parentHighlight.rotation = pstate.style.rotation ?? 0; -// this.parentHighlight.init(this.graph.getView().getOverlayPane()); -// this.parentHighlight.redraw(); -// -// // Shows highlight once per parent -// pstate.parentHighlight = this.parentHighlight; -// } -// } -// } -// } -// -// /** -// * Redraws the preview. -// */ -// drawPreview() { -// if (this.preview != null) { -// this.preview.bounds = this.bounds; -// -// if (this.preview.node.parentNode === this.graph.container) { -// this.preview.bounds.width = Math.max(0, this.preview.bounds.width - 1); -// this.preview.bounds.height = Math.max(0, this.preview.bounds.height - 1); -// } -// -// this.preview.rotation = this.state.style.rotation ?? 0; -// this.preview.redraw(); -// } -// -// this.selectionBorder.bounds = this.getSelectionBorderBounds(); -// this.selectionBorder.redraw(); -// this.updateParentHighlight(); -// } -// -// /** -// * Returns the bounds for the selection border. -// */ -// getSelectionBorderBounds() { -// return this.bounds; -// } -// -// /** -// * Returns `true` if this handler was destroyed or not initialized. -// */ -// isDestroyed() { -// return this.selectionBorder == null; -// } -// -// /** -// * Destroys the handler and all its resources and DOM nodes. -// */ -// onDestroy() { -// (this.state.view.graph).removeListener(this.escapeHandler); -// this.escapeHandler = () => { -// return; -// }; -// -// if (this.preview) { -// this.preview.destroy(); -// this.preview = null; -// } -// -// if (this.parentHighlight) { -// const parent = this.state.cell.getParent(); -// const pstate = parent ? this.graph.view.getState(parent) : null; -// -// if (pstate && pstate.parentHighlight === this.parentHighlight) { -// pstate.parentHighlight = null; -// } -// -// this.parentHighlight.destroy(); -// this.parentHighlight = null; -// } -// -// if (this.ghostPreview) { -// this.ghostPreview.destroy(); -// this.ghostPreview = null; -// } -// -// if (this.selectionBorder) { -// this.selectionBorder.destroy(); -// } -// -// this.labelShape = null; -// this.removeHint(); -// -// for (let i = 0; i < this.sizers.length; i += 1) { -// this.sizers[i].destroy(); -// } -// -// this.sizers = []; -// -// for (let i = 0; i < this.customHandles.length; i += 1) { -// this.customHandles[i].destroy(); -// } -// -// this.customHandles = []; -// } -// } - -type VertexHandler = { - reset(): void; +class VertexHandler { + escapeHandler: (sender: Listenable, evt: Event) => void; + selectionBounds: Rectangle; + bounds: Rectangle; + selectionBorder: RectangleShape; + + /** + * Reference to the enclosing {@link Graph}. + */ + graph: Graph; + + /** + * Reference to the {@link CellState} being modified. + */ state: CellState; - onDestroy(): void; - redraw(): void; - index: number | null; - startX: number; - startY: number; - setHandlesVisible(visible: boolean): void; - start(x: number, y: number, index: number): void; - // start(x: number | null, y: number | null, index: number | null): void; - updateParentHighlight(): void; - mouseDown(sender: EventSource, me: InternalMouseEvent): void; - mouseUp(sender: EventSource, me: InternalMouseEvent): void; - mouseMove(sender: EventSource, me: InternalMouseEvent): void; -}; + + sizers: Shape[] = []; + + /** + * Specifies if only one sizer handle at the bottom, right corner should be used. + * @default false + */ + singleSizer = false; + + /** + * Holds the index of the current handle. + */ + index: number | null = null; + + /** + * Specifies if the bounds of handles should be used for hit-detection in IE or if {@link tolerance} > 0. + * @default true + */ + allowHandleBoundsCheck = true; + + /** + * Optional {@link Image} to be used as handles. + * @default null + */ + handleImage: Image | null = null; + + /** + * If handles are currently visible. + * @default true + */ + handlesVisible = true; + + /** + * Optional tolerance for hit-detection in {@link getHandleForEvent}. + * @default 0 + */ + tolerance = 0; + + /** + * Specifies if a rotation handle should be visible. + * @default false + */ + rotationEnabled = false; + + /** + * Specifies if the parent should be highlighted if a child cell is selected. + * @default false + */ + parentHighlightEnabled = false; + + /** + * Specifies if rotation steps should be "rasterized" depending on the distance to the handle. + * @default true + */ + rotationRaster = true; + + /** + * Specifies the cursor for the rotation handle. + * @default 'crosshair'. + */ + rotationCursor = 'crosshair'; + + /** + * Specifies if resize should change the cell in-place. This is an experimental + * feature for non-touch devices. + * @default false + */ + livePreview = false; + + /** + * Specifies if the live preview should be moved to the front. + * @default false + */ + movePreviewToFront = false; + + /** + * Specifies if sizers should be hidden and spaced if the vertex is small. + * @default false + */ + manageSizers = false; + + /** + * Specifies if the size of groups should be constrained by the children. + * @default false + */ + constrainGroupByChildren = false; + + /** + * Vertical spacing for rotation icon. + * @default -16 + */ + rotationHandleVSpacing = -16; + + /** + * The horizontal offset for the handles. This is updated in {@link redrawHandles} + * if {@link manageSizers} is `true` and the sizers are offset horizontally. + */ + horizontalOffset = 0; + + /** + * The horizontal offset for the handles. This is updated in + * if {@link manageSizers} is true and the sizers are offset vertically. + */ + verticalOffset = 0; + + minBounds: Rectangle | null = null; + + x0 = 0; + y0 = 0; + + customHandles: CellHandle[] = []; + + inTolerance = false; + + startX = 0; + startY = 0; + + rotationShape: Shape | null = null; + + currentAlpha = 100; + startAngle = 0; + startDist = 0; + + ghostPreview: Shape | null = null; + + livePreviewActive = false; + + childOffsetX = 0; + childOffsetY = 0; + + parentState: CellState | null = null; + parentHighlight: RectangleShape | null = null; + + unscaledBounds: Rectangle | null = null; + + preview: Shape | null = null; + + labelShape: Shape | null = null; + + edgeHandlers: EdgeHandler[] = []; + + EMPTY_POINT = new Point(); + + /** + * Constructs an event handler that allows to resize vertices and groups. + * + * @param state {@link CellState} of the cell to be resized. + */ + constructor(state: CellState) { + this.state = state; + this.graph = this.state.view.graph; + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new Rectangle( + this.selectionBounds.x, + this.selectionBounds.y, + this.selectionBounds.width, + this.selectionBounds.height + ); + this.selectionBorder = this.createSelectionShape(this.bounds); + // VML dialect required here for event transparency in IE + this.selectionBorder.dialect = DIALECT.SVG; + this.selectionBorder.pointerEvents = false; + this.selectionBorder.rotation = this.state.style.rotation ?? 0; + this.selectionBorder.init(this.graph.getView().getOverlayPane()); + InternalEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state); + + if (this.graph.isCellMovable(this.state.cell)) { + this.selectionBorder.setCursor(CURSOR.MOVABLE_VERTEX); + } + + const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; + + // Adds the sizer handles + if ( + selectionHandler && + (selectionHandler.maxCells <= 0 || + this.graph.getSelectionCount() < selectionHandler.maxCells) + ) { + const resizable = this.graph.isCellResizable(this.state.cell); + this.sizers = []; + + if ( + resizable || + (this.graph.isLabelMovable(this.state.cell) && + this.state.width >= 2 && + this.state.height >= 2) + ) { + let i = 0; + + if (resizable) { + if (!this.singleSizer) { + this.sizers.push(this.createSizer('nw-resize', i++)); + this.sizers.push(this.createSizer('n-resize', i++)); + this.sizers.push(this.createSizer('ne-resize', i++)); + this.sizers.push(this.createSizer('w-resize', i++)); + this.sizers.push(this.createSizer('e-resize', i++)); + this.sizers.push(this.createSizer('sw-resize', i++)); + this.sizers.push(this.createSizer('s-resize', i++)); + } + + this.sizers.push(this.createSizer('se-resize', i++)); + } + + const geo = this.state.cell.getGeometry(); + + if ( + geo != null && + !geo.relative && + //!this.graph.isSwimlane(this.state.cell) && disable for now + this.graph.isLabelMovable(this.state.cell) + ) { + // Marks this as the label handle for getHandleForEvent + this.labelShape = this.createSizer( + CURSOR.LABEL_HANDLE, + InternalEvent.LABEL_HANDLE, + LABEL_HANDLE_SIZE, + LABEL_HANDLE_FILLCOLOR + ); + this.sizers.push(this.labelShape); + } + } else if ( + this.graph.isCellMovable(this.state.cell) && + !this.graph.isCellResizable(this.state.cell) && + this.state.width < 2 && + this.state.height < 2 + ) { + this.labelShape = this.createSizer( + CURSOR.MOVABLE_VERTEX, + InternalEvent.LABEL_HANDLE, + undefined, + LABEL_HANDLE_FILLCOLOR + ); + this.sizers.push(this.labelShape); + } + } + + // Adds the rotation handler + if (this.isRotationHandleVisible()) { + this.rotationShape = this.createSizer( + this.rotationCursor, + InternalEvent.ROTATION_HANDLE, + HANDLE_SIZE + 3, + HANDLE_FILLCOLOR + ); + this.sizers.push(this.rotationShape); + } + + this.customHandles = this.createCustomHandles(); + this.redraw(); + + if (this.constrainGroupByChildren) { + this.updateMinBounds(); + } + + // Handles escape keystrokes + this.escapeHandler = (sender: Listenable, evt: Event) => { + if (this.livePreview && this.index != null) { + // Redraws the live preview + (this.state.view.graph).cellRenderer.redraw(this.state, true); + + // Redraws connected edges + this.state.view.invalidate(this.state.cell); + this.state.invalid = false; + this.state.view.validate(); + } + + this.reset(); + }; + + (this.state.view.graph).addListener(InternalEvent.ESCAPE, this.escapeHandler); + } + + /** + * Returns `true` if the rotation handle should be showing. + */ + isRotationHandleVisible() { + const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; + const selectionHandlerCheck = selectionHandler + ? selectionHandler.maxCells <= 0 || + this.graph.getSelectionCount() < selectionHandler.maxCells + : true; + + return ( + this.graph.isEnabled() && + this.rotationEnabled && + this.graph.isCellRotatable(this.state.cell) && + selectionHandlerCheck + ); + } + + /** + * Returns `true` if the aspect ratio if the cell should be maintained. + */ + isConstrainedEvent(me: InternalMouseEvent) { + return isShiftDown(me.getEvent()) || this.state.style.aspect === 'fixed'; + } + + /** + * Returns `true` if the center of the vertex should be maintained during the resize. + */ + isCenteredEvent(state: CellState, me: InternalMouseEvent) { + return false; + } + + /** + * Returns an array of custom handles. This implementation returns null. + */ + createCustomHandles() { + return []; + } + + /** + * Initializes the shapes required for this vertex handler. + */ + updateMinBounds() { + const children = this.graph.getChildCells(this.state.cell); + + if (children.length > 0) { + this.minBounds = this.graph.view.getBounds(children); + + if (this.minBounds) { + const s = this.state.view.scale; + const t = this.state.view.translate; + + this.minBounds.x -= this.state.x; + this.minBounds.y -= this.state.y; + this.minBounds.x /= s; + this.minBounds.y /= s; + this.minBounds.width /= s; + this.minBounds.height /= s; + this.x0 = this.state.x / s - t.x; + this.y0 = this.state.y / s - t.y; + } + } + } + + /** + * Returns the mxRectangle that defines the bounds of the selection border. + */ + getSelectionBounds(state: CellState) { + return new Rectangle( + Math.round(state.x), + Math.round(state.y), + Math.round(state.width), + Math.round(state.height) + ); + } + + /** + * Creates the shape used to draw the selection border. + */ + createParentHighlightShape(bounds: Rectangle) { + return this.createSelectionShape(bounds); + } + + /** + * Creates the shape used to draw the selection border. + */ + createSelectionShape(bounds: Rectangle) { + const shape = new RectangleShape( + Rectangle.fromRectangle(bounds), + NONE, + this.getSelectionColor() + ); + shape.strokeWidth = this.getSelectionStrokeWidth(); + shape.isDashed = this.isSelectionDashed(); + + return shape; + } + + /** + * Returns {@link VERTEX_SELECTION_COLOR}. + */ + getSelectionColor() { + return VERTEX_SELECTION_COLOR; + } + + /** + * Returns {@link VERTEX_SELECTION_STROKEWIDTH}. + */ + getSelectionStrokeWidth() { + return VERTEX_SELECTION_STROKEWIDTH; + } + + /** + * Returns {@link VERTEX_SELECTION_DASHED}. + */ + isSelectionDashed() { + return VERTEX_SELECTION_DASHED; + } + + /** + * Creates a sizer handle for the specified cursor and index and returns + * the new {@link RectangleShape} that represents the handle. + */ + createSizer( + cursor: string, + index: number, + size = HANDLE_SIZE, + fillColor = HANDLE_FILLCOLOR + ) { + const bounds = new Rectangle(0, 0, size, size); + const sizer = this.createSizerShape(bounds, index, fillColor); + + if ( + sizer.bounds && + sizer.isHtmlAllowed() && + this.state.text && + this.state.text.node.parentNode === this.graph.container + ) { + sizer.bounds.height -= 1; + sizer.bounds.width -= 1; + sizer.dialect = DIALECT.STRICTHTML; + sizer.init(this.graph.container); + } else { + sizer.dialect = + this.graph.dialect !== DIALECT.SVG ? DIALECT.MIXEDHTML : DIALECT.SVG; + sizer.init(this.graph.getView().getOverlayPane()); + } + + InternalEvent.redirectMouseEvents(sizer.node, this.graph, this.state); + + if (this.graph.isEnabled()) { + sizer.setCursor(cursor); + } + + if (!this.isSizerVisible(index)) { + sizer.visible = false; + } + + return sizer; + } + + /** + * Returns `true` if the sizer for the given index is visible. + * + * This implementation returns `true` for all given indices. + */ + isSizerVisible(index: number) { + return true; + } + + /** + * Creates the shape used for the sizer handle for the specified bounds an + * index. Only images and rectangles should be returned if support for HTML + * labels with not foreign objects is required. + */ + createSizerShape(bounds: Rectangle, index: number, fillColor = HANDLE_FILLCOLOR) { + if (this.handleImage) { + bounds = new Rectangle( + bounds.x, + bounds.y, + this.handleImage.width, + this.handleImage.height + ); + const shape = new ImageShape(bounds, this.handleImage.src); + + // Allows HTML rendering of the images + shape.preserveImageAspect = false; + + return shape; + } + if (index === InternalEvent.ROTATION_HANDLE) { + return new EllipseShape(bounds, fillColor, HANDLE_STROKECOLOR); + } + return new RectangleShape(bounds, fillColor, HANDLE_STROKECOLOR); + } + + /** + * Helper method to create an {@link Rectangle} around the given center point + * with a width and height of 2*s or 6, if no s is given. + */ + moveSizerTo(shape: Shape, x: number, y: number) { + if (shape && shape.bounds) { + shape.bounds.x = Math.floor(x - shape.bounds.width / 2); + shape.bounds.y = Math.floor(y - shape.bounds.height / 2); + + // Fixes visible inactive handles in VML + if (shape.node && shape.node.style.display !== 'none') { + shape.redraw(); + } + } + } + + /** + * Returns the index of the handle for the given event. This returns the index + * of the sizer from where the event originated or {@link Event.LABEL_HANDLE}. + */ + getHandleForEvent(me: InternalMouseEvent) { + // Connection highlight may consume events before they reach sizer handle + const tol = !isMouseEvent(me.getEvent()) ? this.tolerance : 1; + const hit = + this.allowHandleBoundsCheck && tol > 0 + ? new Rectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) + : null; + + const checkShape = (shape: Shape | null) => { + const st = + shape && shape.constructor !== ImageShape && this.allowHandleBoundsCheck + ? shape.strokeWidth + shape.svgStrokeTolerance + : null; + const real = st + ? new Rectangle( + me.getGraphX() - Math.floor(st / 2), + me.getGraphY() - Math.floor(st / 2), + st, + st + ) + : hit; + + return ( + shape && + shape.bounds && + (me.isSource(shape) || + (real && + intersects(shape.bounds, real) && + shape.node.style.display !== 'none' && + shape.node.style.visibility !== 'hidden')) + ); + }; + + if (checkShape(this.rotationShape)) { + return InternalEvent.ROTATION_HANDLE; + } + if (checkShape(this.labelShape)) { + return InternalEvent.LABEL_HANDLE; + } + + for (let i = 0; i < this.sizers.length; i += 1) { + if (checkShape(this.sizers[i])) { + return i; + } + } + + if (this.customHandles != null && this.isCustomHandleEvent(me)) { + // Inverse loop order to match display order + for (let i = this.customHandles.length - 1; i >= 0; i--) { + if (checkShape(this.customHandles[i].shape)) { + // LATER: Return reference to active shape + return InternalEvent.CUSTOM_HANDLE - i; + } + } + } + + return null; + } + + /** + * Returns `true` if the given event allows custom handles to be changed. + * + * This implementation returns `true`. + */ + isCustomHandleEvent(me: InternalMouseEvent) { + return true; + } + + /** + * Handles the event if a handle has been clicked. By consuming the + * event all subsequent events of the gesture are redirected to this + * handler. + */ + mouseDown(sender: EventSource, me: InternalMouseEvent) { + if (!me.isConsumed() && this.graph.isEnabled()) { + const handle = this.getHandleForEvent(me); + + if (handle) { + this.start(me.getGraphX(), me.getGraphY(), handle); + me.consume(); + } + } + } + + /** + * Called if {@link livePreview} is enabled to check if a border should be painted. + * + * This implementation returns `true` if the shape is transparent. + */ + isLivePreviewBorder() { + return ( + this.state.shape && + this.state.shape.fill === NONE && + this.state.shape.stroke === NONE + ); + } + + /** + * Starts the handling of the mouse gesture. + */ + start(x: number, y: number, index: number) { + this.livePreviewActive = this.livePreview && this.state.cell.getChildCount() === 0; + this.inTolerance = true; + this.childOffsetX = 0; + this.childOffsetY = 0; + this.index = index; + this.startX = x; + this.startY = y; + + if (this.index <= InternalEvent.CUSTOM_HANDLE && this.isGhostPreview()) { + this.ghostPreview = this.createGhostPreview(); + } else { + // Saves reference to parent state + const { model } = this.state.view.graph; + const parent = this.state.cell.getParent(); + + if ( + this.state.view.currentRoot !== parent && + parent && + (parent.isVertex() || parent.isEdge()) + ) { + this.parentState = (this.state.view.graph).view.getState(parent); + } + + // Creates a preview that can be on top of any HTML label + this.selectionBorder.node.style.display = + index === InternalEvent.ROTATION_HANDLE ? 'inline' : 'none'; + + // Creates the border that represents the new bounds + if (!this.livePreviewActive || this.isLivePreviewBorder()) { + this.preview = this.createSelectionShape(this.bounds); + + if ( + !(Client.IS_SVG && Number(this.state.style.rotation || '0') !== 0) && + this.state.text != null && + this.state.text.node.parentNode === this.graph.container + ) { + this.preview.dialect = DIALECT.STRICTHTML; + this.preview.init(this.graph.container); + } else { + this.preview.dialect = DIALECT.SVG; + this.preview.init(this.graph.view.getOverlayPane()); + } + } + + if (index === InternalEvent.ROTATION_HANDLE) { + // With the rotation handle in a corner, need the angle and distance + const pos = this.getRotationHandlePosition(); + + const dx = pos.x - this.state.getCenterX(); + const dy = pos.y - this.state.getCenterY(); + + this.startAngle = dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : 0; + this.startDist = Math.sqrt(dx * dx + dy * dy); + } + + // Prepares the handles for live preview + if (this.livePreviewActive) { + this.hideSizers(); + + if (index === InternalEvent.ROTATION_HANDLE && this.rotationShape) { + this.rotationShape.node.style.display = ''; + } else if (index === InternalEvent.LABEL_HANDLE && this.labelShape) { + this.labelShape.node.style.display = ''; + } else if (this.sizers[index]) { + this.sizers[index].node.style.display = ''; + } else if (index <= InternalEvent.CUSTOM_HANDLE) { + this.customHandles[InternalEvent.CUSTOM_HANDLE - index].setVisible(true); + } + + // Gets the array of connected edge handlers for redrawing + const edges = this.state.cell.getEdges(); + this.edgeHandlers = []; + + const selectionCellsHandler = this.graph.getPlugin( + 'SelectionCellsHandler' + ) as SelectionCellsHandler; + + for (let i = 0; i < edges.length; i += 1) { + const handler = selectionCellsHandler?.getHandler(edges[i]); + if (handler) { + this.edgeHandlers.push(handler as EdgeHandler); + } + } + } + } + } + + /** + * Starts the handling of the mouse gesture. + */ + createGhostPreview() { + const shape = this.graph.cellRenderer.createShape(this.state); + shape.init(this.graph.view.getOverlayPane()); + shape.scale = this.state.view.scale; + shape.bounds = this.bounds; + shape.outline = true; + + return shape; + } + + /** + * Shortcut to {@link hideSizers}. + */ + setHandlesVisible(visible: boolean) { + this.handlesVisible = visible; + + for (let i = 0; i < this.sizers.length; i += 1) { + this.sizers[i].node.style.display = visible ? '' : 'none'; + } + + for (let i = 0; i < this.customHandles.length; i += 1) { + this.customHandles[i].setVisible(visible); + } + } + + /** + * Hides all sizers except. + * + * Starts the handling of the mouse gesture. + */ + hideSizers() { + this.setHandlesVisible(false); + } + + /** + * Checks if the coordinates for the given event are within the + * {@link Graph#tolerance}. If the event is a mouse event then the tolerance is + * ignored. + */ + checkTolerance(me: InternalMouseEvent) { + if (this.inTolerance && this.startX !== null && this.startY !== null) { + if ( + isMouseEvent(me.getEvent()) || + Math.abs(me.getGraphX() - this.startX) > this.graph.getEventTolerance() || + Math.abs(me.getGraphY() - this.startY) > this.graph.getEventTolerance() + ) { + this.inTolerance = false; + } + } + } + + /** + * Hook for subclasses do show details while the handler is active. + */ + updateHint(me: InternalMouseEvent) { + return; + } + + /** + * Hooks for subclasses to hide details when the handler gets inactive. + */ + removeHint() { + return; + } + + /** + * Hook for rounding the angle. This uses {@link Math.round}. + */ + roundAngle(angle: number) { + return Math.round(angle * 10) / 10; + } + + /** + * Hook for rounding the unscaled width or height. This uses {@link Math.round}. + */ + roundLength(length: number) { + return Math.round(length * 100) / 100; + } + + /** + * Handles the event by updating the preview. + */ + mouseMove(sender: EventSource, me: InternalMouseEvent) { + if (!me.isConsumed() && this.index != null) { + // Checks tolerance for ignoring single clicks + this.checkTolerance(me); + + if (!this.inTolerance) { + if (this.index <= InternalEvent.CUSTOM_HANDLE) { + if (this.customHandles != null) { + this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].processEvent(me); + this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].active = true; + + if (this.ghostPreview != null) { + this.ghostPreview.apply(this.state); + this.ghostPreview.strokeWidth = + this.getSelectionStrokeWidth() / + this.ghostPreview.scale / + this.ghostPreview.scale; + this.ghostPreview.isDashed = this.isSelectionDashed(); + this.ghostPreview.stroke = this.getSelectionColor(); + this.ghostPreview.redraw(); + + if (this.selectionBounds != null) { + this.selectionBorder.node.style.display = 'none'; + } + } else { + if (this.movePreviewToFront) { + this.moveToFront(); + } + + this.customHandles[ + InternalEvent.CUSTOM_HANDLE - this.index + ].positionChanged(); + } + } + } else if (this.index === InternalEvent.LABEL_HANDLE) { + this.moveLabel(me); + } else { + if (this.index === InternalEvent.ROTATION_HANDLE) { + this.rotateVertex(me); + } else { + this.resizeVertex(me); + } + + this.updateHint(me); + } + } + + me.consume(); + } + // Workaround for disabling the connect highlight when over handle + else if (!this.graph.isMouseDown && this.getHandleForEvent(me)) { + me.consume(false); + } + } + + /** + * Returns `true` if a ghost preview should be used for custom handles. + */ + isGhostPreview() { + return this.state.cell.getChildCount() > 0; + } + + /** + * Moves the vertex. + */ + moveLabel(me: InternalMouseEvent) { + const point = new Point(me.getGraphX(), me.getGraphY()); + const tr = this.graph.view.translate; + const { scale } = this.graph.view; + + if (this.graph.isGridEnabledEvent(me.getEvent())) { + point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale; + point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale; + } + + const index = this.rotationShape ? this.sizers.length - 2 : this.sizers.length - 1; + this.moveSizerTo(this.sizers[index], point.x, point.y); + } + + /** + * Rotates the vertex. + */ + rotateVertex(me: InternalMouseEvent) { + const point = new Point(me.getGraphX(), me.getGraphY()); + let dx = this.state.x + this.state.width / 2 - point.x; + let dy = this.state.y + this.state.height / 2 - point.y; + this.currentAlpha = + dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : dy < 0 ? 180 : 0; + + if (dx > 0) { + this.currentAlpha -= 180; + } + + this.currentAlpha -= this.startAngle; + + // Rotation raster + if (this.rotationRaster && this.graph.isGridEnabledEvent(me.getEvent())) { + let raster; + dx = point.x - this.state.getCenterX(); + dy = point.y - this.state.getCenterY(); + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist - this.startDist < 2) { + raster = 15; + } else if (dist - this.startDist < 25) { + raster = 5; + } else { + raster = 1; + } + + this.currentAlpha = Math.round(this.currentAlpha / raster) * raster; + } else { + this.currentAlpha = this.roundAngle(this.currentAlpha); + } + + this.selectionBorder.rotation = this.currentAlpha; + this.selectionBorder.redraw(); + + if (this.livePreviewActive) { + this.redrawHandles(); + } + } + + /** + * Resizes the vertex. + */ + resizeVertex(me: InternalMouseEvent) { + const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); + const alpha = toRadians(this.state.style.rotation ?? 0); + const point = new Point(me.getGraphX(), me.getGraphY()); + const tr = this.graph.view.translate; + const { scale } = this.graph.view; + let cos = Math.cos(-alpha); + let sin = Math.sin(-alpha); + + let dx = point.x - this.startX; + let dy = point.y - this.startY; + + // Rotates vector for mouse gesture + const tx = cos * dx - sin * dy; + const ty = sin * dx + cos * dy; + + dx = tx; + dy = ty; + + const geo = this.state.cell.getGeometry(); + if (geo && this.index !== null) { + this.unscaledBounds = this.union( + geo, + dx / scale, + dy / scale, + this.index, + this.graph.isGridEnabledEvent(me.getEvent()), + 1, + new Point(0, 0), + this.isConstrainedEvent(me), + this.isCenteredEvent(this.state, me) + ); + } + + // Keeps vertex within maximum graph or parent bounds + if (geo && !geo.relative) { + let max = this.graph.getMaximumGraphBounds(); + + // Handles child cells + if (max != null && this.parentState != null) { + max = Rectangle.fromRectangle(max); + + max.x -= (this.parentState.x - tr.x * scale) / scale; + max.y -= (this.parentState.y - tr.y * scale) / scale; + } + + if (this.graph.isConstrainChild(this.state.cell)) { + let tmp = this.graph.getCellContainmentArea(this.state.cell); + + if (tmp != null) { + const overlap = this.graph.getOverlap(this.state.cell); + + if (overlap > 0) { + tmp = Rectangle.fromRectangle(tmp); + + tmp.x -= tmp.width * overlap; + tmp.y -= tmp.height * overlap; + tmp.width += 2 * tmp.width * overlap; + tmp.height += 2 * tmp.height * overlap; + } + + if (!max) { + max = tmp; + } else { + max = Rectangle.fromRectangle(max); + max.intersect(tmp); + } + } + } + + if (max && this.unscaledBounds) { + if (this.unscaledBounds.x < max.x) { + this.unscaledBounds.width -= max.x - this.unscaledBounds.x; + this.unscaledBounds.x = max.x; + } + + if (this.unscaledBounds.y < max.y) { + this.unscaledBounds.height -= max.y - this.unscaledBounds.y; + this.unscaledBounds.y = max.y; + } + + if (this.unscaledBounds.x + this.unscaledBounds.width > max.x + max.width) { + this.unscaledBounds.width -= + this.unscaledBounds.x + this.unscaledBounds.width - max.x - max.width; + } + + if (this.unscaledBounds.y + this.unscaledBounds.height > max.y + max.height) { + this.unscaledBounds.height -= + this.unscaledBounds.y + this.unscaledBounds.height - max.y - max.height; + } + } + } + + if (this.unscaledBounds) { + const old = this.bounds; + + this.bounds = new Rectangle( + (this.parentState ? this.parentState.x : tr.x * scale) + + this.unscaledBounds.x * scale, + (this.parentState ? this.parentState.y : tr.y * scale) + + this.unscaledBounds.y * scale, + this.unscaledBounds.width * scale, + this.unscaledBounds.height * scale + ); + + if (geo && geo.relative && this.parentState) { + this.bounds.x += this.state.x - this.parentState.x; + this.bounds.y += this.state.y - this.parentState.y; + } + + cos = Math.cos(alpha); + sin = Math.sin(alpha); + + const c2 = new Point(this.bounds.getCenterX(), this.bounds.getCenterY()); + + dx = c2.x - ct.x; + dy = c2.y - ct.y; + + const dx2 = cos * dx - sin * dy; + const dy2 = sin * dx + cos * dy; + + const dx3 = dx2 - dx; + const dy3 = dy2 - dy; + + const dx4 = this.bounds.x - this.state.x; + const dy4 = this.bounds.y - this.state.y; + + const dx5 = cos * dx4 - sin * dy4; + const dy5 = sin * dx4 + cos * dy4; + + this.bounds.x += dx3; + this.bounds.y += dy3; + + // Rounds unscaled bounds to int + this.unscaledBounds.x = this.roundLength(this.unscaledBounds.x + dx3 / scale); + this.unscaledBounds.y = this.roundLength(this.unscaledBounds.y + dy3 / scale); + this.unscaledBounds.width = this.roundLength(this.unscaledBounds.width); + this.unscaledBounds.height = this.roundLength(this.unscaledBounds.height); + + // Shifts the children according to parent offset + if (!this.state.cell.isCollapsed() && (dx3 !== 0 || dy3 !== 0)) { + this.childOffsetX = this.state.x - this.bounds.x + dx5; + this.childOffsetY = this.state.y - this.bounds.y + dy5; + } else { + this.childOffsetX = 0; + this.childOffsetY = 0; + } + + if (!old.equals(this.bounds)) { + if (this.livePreviewActive) { + this.updateLivePreview(me); + } + + if (this.preview != null) { + this.drawPreview(); + } else { + this.updateParentHighlight(); + } + } + } + } + + /** + * Repaints the live preview. + */ + updateLivePreview(me: InternalMouseEvent) { + // TODO: Apply child offset to children in live preview + const { scale } = this.graph.view; + const tr = this.graph.view.translate; + + // Saves current state + const tempState = this.state.clone(); + + // Temporarily changes size and origin + this.state.x = this.bounds.x; + this.state.y = this.bounds.y; + this.state.origin = new Point( + this.state.x / scale - tr.x, + this.state.y / scale - tr.y + ); + this.state.width = this.bounds.width; + this.state.height = this.bounds.height; + + // Redraws cell and handles + let off = this.state.absoluteOffset; + off = new Point(off.x, off.y); + + // Required to store and reset absolute offset for updating label position + this.state.absoluteOffset.x = 0; + this.state.absoluteOffset.y = 0; + const geo = this.state.cell.getGeometry(); + + if (geo != null) { + const offset = geo.offset || this.EMPTY_POINT; + + if (offset != null && !geo.relative) { + this.state.absoluteOffset.x = this.state.view.scale * offset.x; + this.state.absoluteOffset.y = this.state.view.scale * offset.y; + } + + this.state.view.updateVertexLabelOffset(this.state); + } + + // Draws the live preview + (this.state.view.graph).cellRenderer.redraw(this.state, true); + + // Redraws connected edges TODO: Include child edges + this.state.view.invalidate(this.state.cell); + this.state.invalid = false; + this.state.view.validate(); + this.redrawHandles(); + + // Moves live preview to front + if (this.movePreviewToFront) { + this.moveToFront(); + } + + // Hides folding icon + if (this.state.control != null && this.state.control.node != null) { + this.state.control.node.style.visibility = 'hidden'; + } + + // Restores current state + this.state.setState(tempState); + } + + /** + * Handles the event by applying the changes to the geometry. + */ + moveToFront() { + if ( + (this.state.text && this.state.text.node && this.state.text.node.nextSibling) || + (this.state.shape && + this.state.shape.node && + this.state.shape.node.nextSibling && + (!this.state.text || this.state.shape.node.nextSibling !== this.state.text.node)) + ) { + if (this.state.shape && this.state.shape.node && this.state.shape.node.parentNode) { + this.state.shape.node.parentNode.appendChild(this.state.shape.node); + } + + if (this.state.text && this.state.text.node && this.state.text.node.parentNode) { + this.state.text.node.parentNode.appendChild(this.state.text.node); + } + } + } + + /** + * Handles the event by applying the changes to the geometry. + */ + mouseUp(sender: EventSource, me: InternalMouseEvent) { + if (this.index != null && this.state != null) { + const point = new Point(me.getGraphX(), me.getGraphY()); + const { index } = this; + this.index = null; + + if (this.ghostPreview == null) { + // Required to restore order in case of no change + this.state.view.invalidate(this.state.cell, false, false); + this.state.view.validate(); + } + + this.graph.batchUpdate(() => { + if (index <= InternalEvent.CUSTOM_HANDLE) { + if (this.customHandles != null) { + // Creates style before changing cell state + const style = (this.state.view.graph).getCellStyle(this.state.cell); + + this.customHandles[InternalEvent.CUSTOM_HANDLE - index].active = false; + this.customHandles[InternalEvent.CUSTOM_HANDLE - index].execute(me); + + // Sets style and apply on shape to force repaint and + // check if execute has removed custom handles + if ( + this.customHandles != null && + this.customHandles[InternalEvent.CUSTOM_HANDLE - index] != null + ) { + this.state.style = style; + this.customHandles[InternalEvent.CUSTOM_HANDLE - index].positionChanged(); + } + } + } else if (index === InternalEvent.ROTATION_HANDLE) { + if (this.currentAlpha != null) { + const delta = this.currentAlpha - (this.state.style.rotation ?? 0); + + if (delta !== 0) { + this.rotateCell(this.state.cell, delta); + } + } else { + this.rotateClick(); + } + } else { + const gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); + const alpha = toRadians(this.state.style.rotation ?? 0); + const cos = Math.cos(-alpha); + const sin = Math.sin(-alpha); + + let dx = point.x - this.startX; + let dy = point.y - this.startY; + + // Rotates vector for mouse gesture + const tx = cos * dx - sin * dy; + const ty = sin * dx + cos * dy; + + dx = tx; + dy = ty; + + const s = this.graph.view.scale; + const recurse = this.isRecursiveResize(this.state, me); + + this.resizeCell( + this.state.cell, + this.roundLength(dx / s), + this.roundLength(dy / s), + index, + gridEnabled, + this.isConstrainedEvent(me), + recurse + ); + } + }); + + me.consume(); + this.reset(); + this.redrawHandles(); + } + } + + /** + * Returns the `recursiveResize` status of the given state. + * @param state the given {@link CellState}. This implementation takes the value of this state. + * @param me the mouse event. + */ + isRecursiveResize(state: CellState, me: InternalMouseEvent) { + return this.graph.isRecursiveResize(this.state); + } + + /** + * Hook for subclasses to implement a single click on the rotation handle. + * This code is executed as part of the model transaction. This implementation + * is empty. + */ + rotateClick() { + return; + } + + /** + * Rotates the given cell and its children by the given angle in degrees. + * + * @param cell {@link Cell} to be rotated. + * @param angle Angle in degrees. + * @param parent if set, consider the parent in the rotation computation. + */ + rotateCell(cell: Cell, angle: number, parent?: Cell) { + if (angle !== 0) { + const model = this.graph.getDataModel(); + + if (cell.isVertex() || cell.isEdge()) { + if (!cell.isEdge()) { + const style = this.graph.getCurrentCellStyle(cell); + const total = (style.rotation || 0) + angle; + this.graph.setCellStyles('rotation', total, [cell]); + } + + let geo = cell.getGeometry(); + + if (geo && parent) { + const pgeo = parent.getGeometry(); + + if (pgeo != null && !parent.isEdge()) { + geo = geo.clone(); + geo.rotate(angle, new Point(pgeo.width / 2, pgeo.height / 2)); + model.setGeometry(cell, geo); + } + + if ((cell.isVertex() && !geo.relative) || cell.isEdge()) { + // Recursive rotation + const childCount = cell.getChildCount(); + + for (let i = 0; i < childCount; i += 1) { + this.rotateCell(cell.getChildAt(i), angle, cell); + } + } + } + } + } + } + + /** + * Resets the state of this handler. + */ + reset() { + if (this.index !== null && this.sizers[this.index].node.style.display === 'none') { + this.sizers[this.index].node.style.display = ''; + } + + this.index = null; + + // TODO: Reset and redraw cell states for live preview + if (this.preview) { + this.preview.destroy(); + this.preview = null; + } + + if (this.ghostPreview) { + this.ghostPreview.destroy(); + this.ghostPreview = null; + } + + if (this.livePreviewActive) { + for (let i = 0; i < this.sizers.length; i += 1) { + this.sizers[i].node.style.display = ''; + } + + // Shows folding icon + if (this.state.control && this.state.control.node) { + this.state.control.node.style.visibility = ''; + } + } + + for (let i = 0; i < this.customHandles.length; i += 1) { + if (this.customHandles[i].active) { + this.customHandles[i].active = false; + this.customHandles[i].reset(); + } else { + this.customHandles[i].setVisible(true); + } + } + + // Checks if handler has been destroyed + this.selectionBorder.node.style.display = 'inline'; + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new Rectangle( + this.selectionBounds.x, + this.selectionBounds.y, + this.selectionBounds.width, + this.selectionBounds.height + ); + this.drawPreview(); + + this.removeHint(); + this.redrawHandles(); + this.edgeHandlers = []; + this.handlesVisible = true; + this.unscaledBounds = null; + } + + /** + * Uses the given vector to change the bounds of the given cell + * in the graph using {@link Graph#resizeCell}. + */ + resizeCell( + cell: Cell, + dx: number, + dy: number, + index: number, + gridEnabled: boolean, + constrained: boolean, + recurse: boolean + ) { + let geo = cell.getGeometry(); + + if (geo) { + if ( + index === InternalEvent.LABEL_HANDLE && + this.labelShape && + this.labelShape.bounds + ) { + const alpha = -toRadians(this.state.style.rotation ?? 0); + const cos = Math.cos(alpha); + const sin = Math.sin(alpha); + const { scale } = this.graph.view; + const pt = getRotatedPoint( + new Point( + Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale), + Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale) + ), + cos, + sin + ); + + geo = geo.clone(); + + if (geo.offset == null) { + geo.offset = pt; + } else { + geo.offset.x += pt.x; + geo.offset.y += pt.y; + } + + this.graph.model.setGeometry(cell, geo); + } else if (this.unscaledBounds) { + const { scale } = this.graph.view; + + if (this.childOffsetX !== 0 || this.childOffsetY !== 0) { + this.moveChildren( + cell, + Math.round(this.childOffsetX / scale), + Math.round(this.childOffsetY / scale) + ); + } + + this.graph.resizeCell(cell, this.unscaledBounds, recurse); + } + } + } + + /** + * Moves the children of the given cell by the given vector. + */ + moveChildren(cell: Cell, dx: number, dy: number) { + const model = this.graph.getDataModel(); + const childCount = cell.getChildCount(); + + for (let i = 0; i < childCount; i += 1) { + const child = cell.getChildAt(i); + let geo = child.getGeometry(); + + if (geo != null) { + geo = geo.clone(); + geo.translate(dx, dy); + model.setGeometry(child, geo); + } + } + } + + /** + * Returns the union of the given bounds and location for the specified + * handle index. + * + * To override this to limit the size of vertex via a minWidth/-Height style, + * the following code can be used. + * + * ```javascript + * let vertexHandlerUnion = union; + * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> + * { + * let result = vertexHandlerUnion.apply(this, arguments); + * + * result.width = Math.max(result.width, mxUtils.getNumber(this.state.style, 'minWidth', 0)); + * result.height = Math.max(result.height, mxUtils.getNumber(this.state.style, 'minHeight', 0)); + * + * return result; + * }; + * ``` + * + * The minWidth/-Height style can then be used as follows: + * + * ```javascript + * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'minWidth=100;minHeight=100;'); + * ``` + * + * To override this to update the height for a wrapped text if the width of a vertex is + * changed, the following can be used. + * + * ```javascript + * let mxVertexHandlerUnion = union; + * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> + * { + * let result = mxVertexHandlerUnion.apply(this, arguments); + * let s = this.state; + * + * if (this.graph.isHtmlLabel(s.cell) && (index == 3 || index == 4) && + * s.text != null && s.style.whiteSpace == 'wrap') + * { + * let label = this.graph.getLabel(s.cell); + * let fontSize = mxUtils.getNumber(s.style, 'fontSize', mxConstants.DEFAULT_FONTSIZE); + * let ww = result.width / s.view.scale - s.text.spacingRight - s.text.spacingLeft + * + * result.height = mxUtils.getSizeForString(label, fontSize, s.style.fontFamily, ww).height; + * } + * + * return result; + * }; + * ``` + */ + union( + bounds: Rectangle, + dx: number, + dy: number, + index: number, + gridEnabled: boolean, + scale: number, + tr: Point, + constrained: boolean, + centered: boolean + ) { + gridEnabled = gridEnabled && this.graph.isGridEnabled(); + + if (this.singleSizer) { + let x = bounds.x + bounds.width + dx; + let y = bounds.y + bounds.height + dy; + + if (gridEnabled) { + x = this.graph.snap(x / scale) * scale; + y = this.graph.snap(y / scale) * scale; + } + + const rect = new Rectangle(bounds.x, bounds.y, 0, 0); + rect.add(new Rectangle(x, y, 0, 0)); + + return rect; + } + const w0 = bounds.width; + const h0 = bounds.height; + let left = bounds.x - tr.x * scale; + let right = left + w0; + let top = bounds.y - tr.y * scale; + let bottom = top + h0; + + const cx = left + w0 / 2; + const cy = top + h0 / 2; + + if (index > 4 /* Bottom Row */) { + bottom += dy; + + if (gridEnabled) { + bottom = this.graph.snap(bottom / scale) * scale; + } else { + bottom = Math.round(bottom / scale) * scale; + } + } else if (index < 3 /* Top Row */) { + top += dy; + + if (gridEnabled) { + top = this.graph.snap(top / scale) * scale; + } else { + top = Math.round(top / scale) * scale; + } + } + + if (index === 0 || index === 3 || index === 5 /* Left */) { + left += dx; + + if (gridEnabled) { + left = this.graph.snap(left / scale) * scale; + } else { + left = Math.round(left / scale) * scale; + } + } else if (index === 2 || index === 4 || index === 7 /* Right */) { + right += dx; + + if (gridEnabled) { + right = this.graph.snap(right / scale) * scale; + } else { + right = Math.round(right / scale) * scale; + } + } + + let width = right - left; + let height = bottom - top; + + if (constrained) { + const geo = this.state.cell.getGeometry(); + + if (geo != null) { + const aspect = geo.width / geo.height; + + if (index === 1 || index === 2 || index === 7 || index === 6) { + width = height * aspect; + } else { + height = width / aspect; + } + + if (index === 0) { + left = right - width; + top = bottom - height; + } + } + } + + if (centered) { + width += width - w0; + height += height - h0; + + const cdx = cx - (left + width / 2); + const cdy = cy - (top + height / 2); + + left += cdx; + top += cdy; + right += cdx; + bottom += cdy; + } + + // Flips over left side + if (width < 0) { + left += width; + width = Math.abs(width); + } + + // Flips over top side + if (height < 0) { + top += height; + height = Math.abs(height); + } + + const result = new Rectangle(left + tr.x * scale, top + tr.y * scale, width, height); + + if (this.minBounds != null) { + result.width = Math.max( + result.width, + this.minBounds.x * scale + + this.minBounds.width * scale + + Math.max(0, this.x0 * scale - result.x) + ); + result.height = Math.max( + result.height, + this.minBounds.y * scale + + this.minBounds.height * scale + + Math.max(0, this.y0 * scale - result.y) + ); + } + + return result; + } + + /** + * Redraws the handles and the preview. + */ + redraw(ignoreHandles?: boolean) { + this.selectionBounds = this.getSelectionBounds(this.state); + this.bounds = new Rectangle( + this.selectionBounds.x, + this.selectionBounds.y, + this.selectionBounds.width, + this.selectionBounds.height + ); + this.drawPreview(); + + if (!ignoreHandles) { + this.redrawHandles(); + } + } + + /** + * Returns the padding to be used for drawing handles for the current . + */ + getHandlePadding() { + // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) + const result = new Point(0, 0); + let tol = this.tolerance; + + if ( + this.sizers.length > 0 && + this.sizers[0].bounds && + (this.bounds.width < 2 * this.sizers[0].bounds.width + 2 * tol || + this.bounds.height < 2 * this.sizers[0].bounds.height + 2 * tol) + ) { + tol /= 2; + + result.x = this.sizers[0].bounds.width + tol; + result.y = this.sizers[0].bounds.height + tol; + } + + return result; + } + + /** + * Returns the bounds used to paint the resize handles. + */ + getSizerBounds() { + return this.bounds; + } + + /** + * Redraws the handles. To hide certain handles the following code can be used. + * + * ```javascript + * redrawHandles() + * { + * mxVertexHandlerRedrawHandles.apply(this, arguments); + * + * if (this.sizers != null && this.sizers.length > 7) + * { + * this.sizers[1].node.style.display = 'none'; + * this.sizers[6].node.style.display = 'none'; + * } + * }; + * ``` + */ + redrawHandles() { + let s = this.getSizerBounds(); + const tol = this.tolerance; + this.horizontalOffset = 0; + this.verticalOffset = 0; + + for (let i = 0; i < this.customHandles.length; i += 1) { + const shape = this.customHandles[i].shape; + + if (shape) { + const temp = shape.node.style.display; + this.customHandles[i].redraw(); + shape.node.style.display = temp; + + // Hides custom handles during text editing + shape.node.style.visibility = + this.handlesVisible && this.isCustomHandleVisible(this.customHandles[i]) + ? '' + : 'hidden'; + } + } + + if (this.sizers.length > 0 && this.sizers[0]) { + if (this.index === null && this.manageSizers && this.sizers.length >= 8) { + // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) + const padding = this.getHandlePadding(); + this.horizontalOffset = padding.x; + this.verticalOffset = padding.y; + + if (this.horizontalOffset !== 0 || this.verticalOffset !== 0) { + s = new Rectangle(s.x, s.y, s.width, s.height); + + s.x -= this.horizontalOffset / 2; + s.width += this.horizontalOffset; + s.y -= this.verticalOffset / 2; + s.height += this.verticalOffset; + } + + if (this.sizers.length >= 8) { + if ( + this.sizers[0].bounds && + (s.width < 2 * this.sizers[0].bounds.width + 2 * tol || + s.height < 2 * this.sizers[0].bounds.height + 2 * tol) + ) { + this.sizers[0].node.style.display = 'none'; + this.sizers[2].node.style.display = 'none'; + this.sizers[5].node.style.display = 'none'; + this.sizers[7].node.style.display = 'none'; + } else if (this.handlesVisible) { + this.sizers[0].node.style.display = ''; + this.sizers[2].node.style.display = ''; + this.sizers[5].node.style.display = ''; + this.sizers[7].node.style.display = ''; + } + } + } + + const r = s.x + s.width; + const b = s.y + s.height; + + if (this.singleSizer) { + this.moveSizerTo(this.sizers[0], r, b); + } else { + const cx = s.x + s.width / 2; + const cy = s.y + s.height / 2; + + if (this.sizers.length >= 8) { + const crs = [ + 'nw-resize', + 'n-resize', + 'ne-resize', + 'e-resize', + 'se-resize', + 's-resize', + 'sw-resize', + 'w-resize', + ]; + + const alpha = toRadians(this.state.style.rotation ?? 0); + const cos = Math.cos(alpha); + const sin = Math.sin(alpha); + const da = Math.round((alpha * 4) / Math.PI); + + const ct = new Point(s.getCenterX(), s.getCenterY()); + let pt = getRotatedPoint(new Point(s.x, s.y), cos, sin, ct); + this.moveSizerTo(this.sizers[0], pt.x, pt.y); + this.sizers[0].setCursor(crs[mod(0 + da, crs.length)]); + + pt.x = cx; + pt.y = s.y; + pt = getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[1], pt.x, pt.y); + this.sizers[1].setCursor(crs[mod(1 + da, crs.length)]); + + pt.x = r; + pt.y = s.y; + pt = getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[2], pt.x, pt.y); + this.sizers[2].setCursor(crs[mod(2 + da, crs.length)]); + + pt.x = s.x; + pt.y = cy; + pt = getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[3], pt.x, pt.y); + this.sizers[3].setCursor(crs[mod(7 + da, crs.length)]); + + pt.x = r; + pt.y = cy; + pt = getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[4], pt.x, pt.y); + this.sizers[4].setCursor(crs[mod(3 + da, crs.length)]); + + pt.x = s.x; + pt.y = b; + pt = getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[5], pt.x, pt.y); + this.sizers[5].setCursor(crs[mod(6 + da, crs.length)]); + + pt.x = cx; + pt.y = b; + pt = getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[6], pt.x, pt.y); + this.sizers[6].setCursor(crs[mod(5 + da, crs.length)]); + + pt.x = r; + pt.y = b; + pt = getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[7], pt.x, pt.y); + this.sizers[7].setCursor(crs[mod(4 + da, crs.length)]); + + pt.x = cx + this.state.absoluteOffset.x; + pt.y = cy + this.state.absoluteOffset.y; + pt = getRotatedPoint(pt, cos, sin, ct); + this.moveSizerTo(this.sizers[8], pt.x, pt.y); + } else if (this.state.width >= 2 && this.state.height >= 2) { + this.moveSizerTo( + this.sizers[0], + cx + this.state.absoluteOffset.x, + cy + this.state.absoluteOffset.y + ); + } else { + this.moveSizerTo(this.sizers[0], this.state.x, this.state.y); + } + } + } + + if (this.rotationShape) { + const alpha = toRadians(this.currentAlpha); + const cos = Math.cos(alpha); + const sin = Math.sin(alpha); + + const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); + const pt = getRotatedPoint(this.getRotationHandlePosition(), cos, sin, ct); + + if (this.rotationShape.node != null) { + this.moveSizerTo(this.rotationShape, pt.x, pt.y); + + // Hides rotation handle during text editing + this.rotationShape.node.style.visibility = + (this.state.view.graph).isEditing() || !this.handlesVisible + ? 'hidden' + : ''; + } + } + + if (this.selectionBorder != null) { + this.selectionBorder.rotation = this.state.style.rotation ?? 0; + } + + if (this.edgeHandlers != null) { + for (let i = 0; i < this.edgeHandlers.length; i += 1) { + this.edgeHandlers[i].redraw(); + } + } + } + + /** + * Returns true if the given custom handle is visible. + */ + isCustomHandleVisible(handle: CellHandle) { + return ( + !this.graph.isEditing() && (this.state.view.graph).getSelectionCount() === 1 + ); + } + + /** + * Returns an {@link Point} that defines the rotation handle position. + */ + getRotationHandlePosition() { + return new Point( + this.bounds.x + this.bounds.width / 2, + this.bounds.y + this.rotationHandleVSpacing + ); + } + + /** + * Returns `true` if the parent highlight should be visible. + * + * This implementation always returns `true`. + */ + isParentHighlightVisible() { + const parent = this.state.cell.getParent(); + return parent ? !this.graph.isCellSelected(parent) : false; + } + + /** + * Updates the highlight of the parent if {@link parentHighlightEnabled} is `true`. + */ + updateParentHighlight() { + if (!this.isDestroyed()) { + const visible = this.isParentHighlightVisible(); + const parent = this.state.cell.getParent(); + const pstate = parent ? this.graph.view.getState(parent) : null; + + if (this.parentHighlight) { + if (parent && parent.isVertex() && visible) { + const b = this.parentHighlight.bounds; + + if ( + pstate && + b && + (b.x !== pstate.x || + b.y !== pstate.y || + b.width !== pstate.width || + b.height !== pstate.height) + ) { + this.parentHighlight.bounds = Rectangle.fromRectangle(pstate); + this.parentHighlight.redraw(); + } + } else { + if (pstate != null && pstate.parentHighlight === this.parentHighlight) { + pstate.parentHighlight = null; + } + + this.parentHighlight.destroy(); + this.parentHighlight = null; + } + } else if (this.parentHighlightEnabled && visible) { + if ( + parent && + parent.isVertex() && + pstate != null && + pstate.parentHighlight == null + ) { + this.parentHighlight = this.createParentHighlightShape(pstate); + // VML dialect required here for event transparency in IE + this.parentHighlight.dialect = DIALECT.SVG; + this.parentHighlight.pointerEvents = false; + this.parentHighlight.rotation = pstate.style.rotation ?? 0; + this.parentHighlight.init(this.graph.getView().getOverlayPane()); + this.parentHighlight.redraw(); + + // Shows highlight once per parent + pstate.parentHighlight = this.parentHighlight; + } + } + } + } + + /** + * Redraws the preview. + */ + drawPreview() { + if (this.preview != null) { + this.preview.bounds = this.bounds; + + if (this.preview.node.parentNode === this.graph.container) { + this.preview.bounds.width = Math.max(0, this.preview.bounds.width - 1); + this.preview.bounds.height = Math.max(0, this.preview.bounds.height - 1); + } + + this.preview.rotation = this.state.style.rotation ?? 0; + this.preview.redraw(); + } + + this.selectionBorder.bounds = this.getSelectionBorderBounds(); + this.selectionBorder.redraw(); + this.updateParentHighlight(); + } + + /** + * Returns the bounds for the selection border. + */ + getSelectionBorderBounds() { + return this.bounds; + } + + /** + * Returns `true` if this handler was destroyed or not initialized. + */ + isDestroyed() { + return this.selectionBorder == null; + } + + /** + * Destroys the handler and all its resources and DOM nodes. + */ + onDestroy() { + (this.state.view.graph).removeListener(this.escapeHandler); + this.escapeHandler = () => { + return; + }; + + if (this.preview) { + this.preview.destroy(); + this.preview = null; + } + + if (this.parentHighlight) { + const parent = this.state.cell.getParent(); + const pstate = parent ? this.graph.view.getState(parent) : null; + + if (pstate && pstate.parentHighlight === this.parentHighlight) { + pstate.parentHighlight = null; + } + + this.parentHighlight.destroy(); + this.parentHighlight = null; + } + + if (this.ghostPreview) { + this.ghostPreview.destroy(); + this.ghostPreview = null; + } + + if (this.selectionBorder) { + this.selectionBorder.destroy(); + } + + this.labelShape = null; + this.removeHint(); + + for (let i = 0; i < this.sizers.length; i += 1) { + this.sizers[i].destroy(); + } + + this.sizers = []; + + for (let i = 0; i < this.customHandles.length; i += 1) { + this.customHandles[i].destroy(); + } + + this.customHandles = []; + } +} export default VertexHandler; From 536416ab43915fb4affb6dfe7c65de08b6d7c585 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Sun, 9 Jun 2024 12:25:48 +0200 Subject: [PATCH 3/5] START simulate no EdgeHandler --- packages/core/src/view/Graph.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/core/src/view/Graph.ts b/packages/core/src/view/Graph.ts index 77a16ba575..1061426e95 100644 --- a/packages/core/src/view/Graph.ts +++ b/packages/core/src/view/Graph.ts @@ -430,7 +430,8 @@ class Graph extends EventSource { createEdgeHandlerInstance(state: CellState): EdgeHandler { // Note this method not being called createEdgeHandler to keep compatibility // with older code which overrides/calls createEdgeHandler - return new EdgeHandler(state); + // return new EdgeHandler(state); + return null!; } /** @@ -439,7 +440,8 @@ class Graph extends EventSource { * @param state {@link CellState} to create the handler for. */ createEdgeSegmentHandler(state: CellState) { - return new EdgeSegmentHandler(state); + // return new EdgeSegmentHandler(state); + return null; } /** @@ -448,7 +450,8 @@ class Graph extends EventSource { * @param state {@link CellState} to create the handler for. */ createElbowEdgeHandler(state: CellState) { - return new ElbowEdgeHandler(state); + // return new ElbowEdgeHandler(state); + return null; } /** @@ -1037,24 +1040,24 @@ class Graph extends EventSource { * @param edgeStyle the {@link EdgeStyleFunction} that let choose the actual edge handler. */ createEdgeHandler(state: CellState, edgeStyle: EdgeStyleFunction | null): EdgeHandler { - let result = null; + const result = null; if ( edgeStyle == EdgeStyle.Loop || edgeStyle == EdgeStyle.ElbowConnector || edgeStyle == EdgeStyle.SideToSide || edgeStyle == EdgeStyle.TopToBottom ) { - result = this.createElbowEdgeHandler(state); + // result = this.createElbowEdgeHandler(state); } else if ( edgeStyle == EdgeStyle.SegmentConnector || edgeStyle == EdgeStyle.OrthConnector ) { - result = this.createEdgeSegmentHandler(state); + // result = this.createEdgeSegmentHandler(state); } else { - result = this.createEdgeHandlerInstance(state); + // result = this.createEdgeHandlerInstance(state); } - return result; + return result!; } /***************************************************************************** From de8a868a3fded4d7c70c158793cd8797ad768b71 Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Sun, 9 Jun 2024 12:27:12 +0200 Subject: [PATCH 4/5] Revert "START simulate no EdgeHandler" This reverts commit 536416ab43915fb4affb6dfe7c65de08b6d7c585. --- packages/core/src/view/Graph.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/core/src/view/Graph.ts b/packages/core/src/view/Graph.ts index 1061426e95..77a16ba575 100644 --- a/packages/core/src/view/Graph.ts +++ b/packages/core/src/view/Graph.ts @@ -430,8 +430,7 @@ class Graph extends EventSource { createEdgeHandlerInstance(state: CellState): EdgeHandler { // Note this method not being called createEdgeHandler to keep compatibility // with older code which overrides/calls createEdgeHandler - // return new EdgeHandler(state); - return null!; + return new EdgeHandler(state); } /** @@ -440,8 +439,7 @@ class Graph extends EventSource { * @param state {@link CellState} to create the handler for. */ createEdgeSegmentHandler(state: CellState) { - // return new EdgeSegmentHandler(state); - return null; + return new EdgeSegmentHandler(state); } /** @@ -450,8 +448,7 @@ class Graph extends EventSource { * @param state {@link CellState} to create the handler for. */ createElbowEdgeHandler(state: CellState) { - // return new ElbowEdgeHandler(state); - return null; + return new ElbowEdgeHandler(state); } /** @@ -1040,24 +1037,24 @@ class Graph extends EventSource { * @param edgeStyle the {@link EdgeStyleFunction} that let choose the actual edge handler. */ createEdgeHandler(state: CellState, edgeStyle: EdgeStyleFunction | null): EdgeHandler { - const result = null; + let result = null; if ( edgeStyle == EdgeStyle.Loop || edgeStyle == EdgeStyle.ElbowConnector || edgeStyle == EdgeStyle.SideToSide || edgeStyle == EdgeStyle.TopToBottom ) { - // result = this.createElbowEdgeHandler(state); + result = this.createElbowEdgeHandler(state); } else if ( edgeStyle == EdgeStyle.SegmentConnector || edgeStyle == EdgeStyle.OrthConnector ) { - // result = this.createEdgeSegmentHandler(state); + result = this.createEdgeSegmentHandler(state); } else { - // result = this.createEdgeHandlerInstance(state); + result = this.createEdgeHandlerInstance(state); } - return result!; + return result; } /***************************************************************************** From 7a4b4a68eaf7c0aa3d9ab46219fe5b82585e301f Mon Sep 17 00:00:00 2001 From: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> Date: Sun, 9 Jun 2024 11:50:43 +0200 Subject: [PATCH 5/5] START simulate both no EdgeHandler and no VertexHandler [skip ci] --- packages/core/src/index.ts | 2 +- packages/core/src/view/Graph.ts | 22 +- .../core/src/view/handler/VertexHandler.ts | 4069 +++++++++-------- 3 files changed, 2057 insertions(+), 2036 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 617deba664..d3ab383827 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -82,7 +82,7 @@ export { default as PopupMenuHandler } from './view/handler/PopupMenuHandler'; export { default as RubberBandHandler } from './view/handler/RubberBandHandler'; export { default as SelectionCellsHandler } from './view/handler/SelectionCellsHandler'; export { default as TooltipHandler } from './view/handler/TooltipHandler'; -export { default as VertexHandler } from './view/handler/VertexHandler'; +export type { default as VertexHandler } from './view/handler/VertexHandler'; export { default as CircleLayout } from './view/layout/CircleLayout'; export { default as CompactTreeLayout } from './view/layout/CompactTreeLayout'; diff --git a/packages/core/src/view/Graph.ts b/packages/core/src/view/Graph.ts index 77a16ba575..3d2456353b 100644 --- a/packages/core/src/view/Graph.ts +++ b/packages/core/src/view/Graph.ts @@ -430,7 +430,8 @@ class Graph extends EventSource { createEdgeHandlerInstance(state: CellState): EdgeHandler { // Note this method not being called createEdgeHandler to keep compatibility // with older code which overrides/calls createEdgeHandler - return new EdgeHandler(state); + // return new EdgeHandler(state); + return null!; } /** @@ -439,7 +440,8 @@ class Graph extends EventSource { * @param state {@link CellState} to create the handler for. */ createEdgeSegmentHandler(state: CellState) { - return new EdgeSegmentHandler(state); + // return new EdgeSegmentHandler(state); + return null; } /** @@ -448,7 +450,8 @@ class Graph extends EventSource { * @param state {@link CellState} to create the handler for. */ createElbowEdgeHandler(state: CellState) { - return new ElbowEdgeHandler(state); + // return new ElbowEdgeHandler(state); + return null; } /** @@ -485,7 +488,8 @@ class Graph extends EventSource { * @param state {@link CellState} to create the handler for. */ createVertexHandler(state: CellState): VertexHandler { - return new VertexHandler(state); + // return new VertexHandler(state); + return null!; } // =================================================================================================================== @@ -1037,24 +1041,24 @@ class Graph extends EventSource { * @param edgeStyle the {@link EdgeStyleFunction} that let choose the actual edge handler. */ createEdgeHandler(state: CellState, edgeStyle: EdgeStyleFunction | null): EdgeHandler { - let result = null; + const result = null; if ( edgeStyle == EdgeStyle.Loop || edgeStyle == EdgeStyle.ElbowConnector || edgeStyle == EdgeStyle.SideToSide || edgeStyle == EdgeStyle.TopToBottom ) { - result = this.createElbowEdgeHandler(state); + // result = this.createElbowEdgeHandler(state); } else if ( edgeStyle == EdgeStyle.SegmentConnector || edgeStyle == EdgeStyle.OrthConnector ) { - result = this.createEdgeSegmentHandler(state); + // result = this.createEdgeSegmentHandler(state); } else { - result = this.createEdgeHandlerInstance(state); + // result = this.createEdgeHandlerInstance(state); } - return result; + return result!; } /***************************************************************************** diff --git a/packages/core/src/view/handler/VertexHandler.ts b/packages/core/src/view/handler/VertexHandler.ts index bafe886d16..8773f91b18 100644 --- a/packages/core/src/view/handler/VertexHandler.ts +++ b/packages/core/src/view/handler/VertexHandler.ts @@ -55,2032 +55,2049 @@ import SelectionCellsHandler from './SelectionCellsHandler'; * * This handler is automatically created in {@link Graph#createHandler}. */ -class VertexHandler { - escapeHandler: (sender: Listenable, evt: Event) => void; - selectionBounds: Rectangle; - bounds: Rectangle; - selectionBorder: RectangleShape; - - /** - * Reference to the enclosing {@link Graph}. - */ - graph: Graph; - - /** - * Reference to the {@link CellState} being modified. - */ +// class VertexHandler { +// escapeHandler: (sender: Listenable, evt: Event) => void; +// selectionBounds: Rectangle; +// bounds: Rectangle; +// selectionBorder: RectangleShape; +// +// /** +// * Reference to the enclosing {@link Graph}. +// */ +// graph: Graph; +// +// /** +// * Reference to the {@link CellState} being modified. +// */ +// state: CellState; +// +// sizers: Shape[] = []; +// +// /** +// * Specifies if only one sizer handle at the bottom, right corner should be used. +// * @default false +// */ +// singleSizer = false; +// +// /** +// * Holds the index of the current handle. +// */ +// index: number | null = null; +// +// /** +// * Specifies if the bounds of handles should be used for hit-detection in IE or if {@link tolerance} > 0. +// * @default true +// */ +// allowHandleBoundsCheck = true; +// +// /** +// * Optional {@link Image} to be used as handles. +// * @default null +// */ +// handleImage: Image | null = null; +// +// /** +// * If handles are currently visible. +// * @default true +// */ +// handlesVisible = true; +// +// /** +// * Optional tolerance for hit-detection in {@link getHandleForEvent}. +// * @default 0 +// */ +// tolerance = 0; +// +// /** +// * Specifies if a rotation handle should be visible. +// * @default false +// */ +// rotationEnabled = false; +// +// /** +// * Specifies if the parent should be highlighted if a child cell is selected. +// * @default false +// */ +// parentHighlightEnabled = false; +// +// /** +// * Specifies if rotation steps should be "rasterized" depending on the distance to the handle. +// * @default true +// */ +// rotationRaster = true; +// +// /** +// * Specifies the cursor for the rotation handle. +// * @default 'crosshair'. +// */ +// rotationCursor = 'crosshair'; +// +// /** +// * Specifies if resize should change the cell in-place. This is an experimental +// * feature for non-touch devices. +// * @default false +// */ +// livePreview = false; +// +// /** +// * Specifies if the live preview should be moved to the front. +// * @default false +// */ +// movePreviewToFront = false; +// +// /** +// * Specifies if sizers should be hidden and spaced if the vertex is small. +// * @default false +// */ +// manageSizers = false; +// +// /** +// * Specifies if the size of groups should be constrained by the children. +// * @default false +// */ +// constrainGroupByChildren = false; +// +// /** +// * Vertical spacing for rotation icon. +// * @default -16 +// */ +// rotationHandleVSpacing = -16; +// +// /** +// * The horizontal offset for the handles. This is updated in {@link redrawHandles} +// * if {@link manageSizers} is `true` and the sizers are offset horizontally. +// */ +// horizontalOffset = 0; +// +// /** +// * The horizontal offset for the handles. This is updated in +// * if {@link manageSizers} is true and the sizers are offset vertically. +// */ +// verticalOffset = 0; +// +// minBounds: Rectangle | null = null; +// +// x0 = 0; +// y0 = 0; +// +// customHandles: CellHandle[] = []; +// +// inTolerance = false; +// +// startX = 0; +// startY = 0; +// +// rotationShape: Shape | null = null; +// +// currentAlpha = 100; +// startAngle = 0; +// startDist = 0; +// +// ghostPreview: Shape | null = null; +// +// livePreviewActive = false; +// +// childOffsetX = 0; +// childOffsetY = 0; +// +// parentState: CellState | null = null; +// parentHighlight: RectangleShape | null = null; +// +// unscaledBounds: Rectangle | null = null; +// +// preview: Shape | null = null; +// +// labelShape: Shape | null = null; +// +// edgeHandlers: EdgeHandler[] = []; +// +// EMPTY_POINT = new Point(); +// +// /** +// * Constructs an event handler that allows to resize vertices and groups. +// * +// * @param state {@link CellState} of the cell to be resized. +// */ +// constructor(state: CellState) { +// this.state = state; +// this.graph = this.state.view.graph; +// this.selectionBounds = this.getSelectionBounds(this.state); +// this.bounds = new Rectangle( +// this.selectionBounds.x, +// this.selectionBounds.y, +// this.selectionBounds.width, +// this.selectionBounds.height +// ); +// this.selectionBorder = this.createSelectionShape(this.bounds); +// // VML dialect required here for event transparency in IE +// this.selectionBorder.dialect = DIALECT.SVG; +// this.selectionBorder.pointerEvents = false; +// this.selectionBorder.rotation = this.state.style.rotation ?? 0; +// this.selectionBorder.init(this.graph.getView().getOverlayPane()); +// InternalEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state); +// +// if (this.graph.isCellMovable(this.state.cell)) { +// this.selectionBorder.setCursor(CURSOR.MOVABLE_VERTEX); +// } +// +// const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; +// +// // Adds the sizer handles +// if ( +// selectionHandler && +// (selectionHandler.maxCells <= 0 || +// this.graph.getSelectionCount() < selectionHandler.maxCells) +// ) { +// const resizable = this.graph.isCellResizable(this.state.cell); +// this.sizers = []; +// +// if ( +// resizable || +// (this.graph.isLabelMovable(this.state.cell) && +// this.state.width >= 2 && +// this.state.height >= 2) +// ) { +// let i = 0; +// +// if (resizable) { +// if (!this.singleSizer) { +// this.sizers.push(this.createSizer('nw-resize', i++)); +// this.sizers.push(this.createSizer('n-resize', i++)); +// this.sizers.push(this.createSizer('ne-resize', i++)); +// this.sizers.push(this.createSizer('w-resize', i++)); +// this.sizers.push(this.createSizer('e-resize', i++)); +// this.sizers.push(this.createSizer('sw-resize', i++)); +// this.sizers.push(this.createSizer('s-resize', i++)); +// } +// +// this.sizers.push(this.createSizer('se-resize', i++)); +// } +// +// const geo = this.state.cell.getGeometry(); +// +// if ( +// geo != null && +// !geo.relative && +// //!this.graph.isSwimlane(this.state.cell) && disable for now +// this.graph.isLabelMovable(this.state.cell) +// ) { +// // Marks this as the label handle for getHandleForEvent +// this.labelShape = this.createSizer( +// CURSOR.LABEL_HANDLE, +// InternalEvent.LABEL_HANDLE, +// LABEL_HANDLE_SIZE, +// LABEL_HANDLE_FILLCOLOR +// ); +// this.sizers.push(this.labelShape); +// } +// } else if ( +// this.graph.isCellMovable(this.state.cell) && +// !this.graph.isCellResizable(this.state.cell) && +// this.state.width < 2 && +// this.state.height < 2 +// ) { +// this.labelShape = this.createSizer( +// CURSOR.MOVABLE_VERTEX, +// InternalEvent.LABEL_HANDLE, +// undefined, +// LABEL_HANDLE_FILLCOLOR +// ); +// this.sizers.push(this.labelShape); +// } +// } +// +// // Adds the rotation handler +// if (this.isRotationHandleVisible()) { +// this.rotationShape = this.createSizer( +// this.rotationCursor, +// InternalEvent.ROTATION_HANDLE, +// HANDLE_SIZE + 3, +// HANDLE_FILLCOLOR +// ); +// this.sizers.push(this.rotationShape); +// } +// +// this.customHandles = this.createCustomHandles(); +// this.redraw(); +// +// if (this.constrainGroupByChildren) { +// this.updateMinBounds(); +// } +// +// // Handles escape keystrokes +// this.escapeHandler = (sender: Listenable, evt: Event) => { +// if (this.livePreview && this.index != null) { +// // Redraws the live preview +// (this.state.view.graph).cellRenderer.redraw(this.state, true); +// +// // Redraws connected edges +// this.state.view.invalidate(this.state.cell); +// this.state.invalid = false; +// this.state.view.validate(); +// } +// +// this.reset(); +// }; +// +// (this.state.view.graph).addListener(InternalEvent.ESCAPE, this.escapeHandler); +// } +// +// /** +// * Returns `true` if the rotation handle should be showing. +// */ +// isRotationHandleVisible() { +// const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; +// const selectionHandlerCheck = selectionHandler +// ? selectionHandler.maxCells <= 0 || +// this.graph.getSelectionCount() < selectionHandler.maxCells +// : true; +// +// return ( +// this.graph.isEnabled() && +// this.rotationEnabled && +// this.graph.isCellRotatable(this.state.cell) && +// selectionHandlerCheck +// ); +// } +// +// /** +// * Returns `true` if the aspect ratio if the cell should be maintained. +// */ +// isConstrainedEvent(me: InternalMouseEvent) { +// return isShiftDown(me.getEvent()) || this.state.style.aspect === 'fixed'; +// } +// +// /** +// * Returns `true` if the center of the vertex should be maintained during the resize. +// */ +// isCenteredEvent(state: CellState, me: InternalMouseEvent) { +// return false; +// } +// +// /** +// * Returns an array of custom handles. This implementation returns null. +// */ +// createCustomHandles() { +// return []; +// } +// +// /** +// * Initializes the shapes required for this vertex handler. +// */ +// updateMinBounds() { +// const children = this.graph.getChildCells(this.state.cell); +// +// if (children.length > 0) { +// this.minBounds = this.graph.view.getBounds(children); +// +// if (this.minBounds) { +// const s = this.state.view.scale; +// const t = this.state.view.translate; +// +// this.minBounds.x -= this.state.x; +// this.minBounds.y -= this.state.y; +// this.minBounds.x /= s; +// this.minBounds.y /= s; +// this.minBounds.width /= s; +// this.minBounds.height /= s; +// this.x0 = this.state.x / s - t.x; +// this.y0 = this.state.y / s - t.y; +// } +// } +// } +// +// /** +// * Returns the mxRectangle that defines the bounds of the selection border. +// */ +// getSelectionBounds(state: CellState) { +// return new Rectangle( +// Math.round(state.x), +// Math.round(state.y), +// Math.round(state.width), +// Math.round(state.height) +// ); +// } +// +// /** +// * Creates the shape used to draw the selection border. +// */ +// createParentHighlightShape(bounds: Rectangle) { +// return this.createSelectionShape(bounds); +// } +// +// /** +// * Creates the shape used to draw the selection border. +// */ +// createSelectionShape(bounds: Rectangle) { +// const shape = new RectangleShape( +// Rectangle.fromRectangle(bounds), +// NONE, +// this.getSelectionColor() +// ); +// shape.strokeWidth = this.getSelectionStrokeWidth(); +// shape.isDashed = this.isSelectionDashed(); +// +// return shape; +// } +// +// /** +// * Returns {@link VERTEX_SELECTION_COLOR}. +// */ +// getSelectionColor() { +// return VERTEX_SELECTION_COLOR; +// } +// +// /** +// * Returns {@link VERTEX_SELECTION_STROKEWIDTH}. +// */ +// getSelectionStrokeWidth() { +// return VERTEX_SELECTION_STROKEWIDTH; +// } +// +// /** +// * Returns {@link VERTEX_SELECTION_DASHED}. +// */ +// isSelectionDashed() { +// return VERTEX_SELECTION_DASHED; +// } +// +// /** +// * Creates a sizer handle for the specified cursor and index and returns +// * the new {@link RectangleShape} that represents the handle. +// */ +// createSizer( +// cursor: string, +// index: number, +// size = HANDLE_SIZE, +// fillColor = HANDLE_FILLCOLOR +// ) { +// const bounds = new Rectangle(0, 0, size, size); +// const sizer = this.createSizerShape(bounds, index, fillColor); +// +// if ( +// sizer.bounds && +// sizer.isHtmlAllowed() && +// this.state.text && +// this.state.text.node.parentNode === this.graph.container +// ) { +// sizer.bounds.height -= 1; +// sizer.bounds.width -= 1; +// sizer.dialect = DIALECT.STRICTHTML; +// sizer.init(this.graph.container); +// } else { +// sizer.dialect = +// this.graph.dialect !== DIALECT.SVG ? DIALECT.MIXEDHTML : DIALECT.SVG; +// sizer.init(this.graph.getView().getOverlayPane()); +// } +// +// InternalEvent.redirectMouseEvents(sizer.node, this.graph, this.state); +// +// if (this.graph.isEnabled()) { +// sizer.setCursor(cursor); +// } +// +// if (!this.isSizerVisible(index)) { +// sizer.visible = false; +// } +// +// return sizer; +// } +// +// /** +// * Returns `true` if the sizer for the given index is visible. +// * +// * This implementation returns `true` for all given indices. +// */ +// isSizerVisible(index: number) { +// return true; +// } +// +// /** +// * Creates the shape used for the sizer handle for the specified bounds an +// * index. Only images and rectangles should be returned if support for HTML +// * labels with not foreign objects is required. +// */ +// createSizerShape(bounds: Rectangle, index: number, fillColor = HANDLE_FILLCOLOR) { +// if (this.handleImage) { +// bounds = new Rectangle( +// bounds.x, +// bounds.y, +// this.handleImage.width, +// this.handleImage.height +// ); +// const shape = new ImageShape(bounds, this.handleImage.src); +// +// // Allows HTML rendering of the images +// shape.preserveImageAspect = false; +// +// return shape; +// } +// if (index === InternalEvent.ROTATION_HANDLE) { +// return new EllipseShape(bounds, fillColor, HANDLE_STROKECOLOR); +// } +// return new RectangleShape(bounds, fillColor, HANDLE_STROKECOLOR); +// } +// +// /** +// * Helper method to create an {@link Rectangle} around the given center point +// * with a width and height of 2*s or 6, if no s is given. +// */ +// moveSizerTo(shape: Shape, x: number, y: number) { +// if (shape && shape.bounds) { +// shape.bounds.x = Math.floor(x - shape.bounds.width / 2); +// shape.bounds.y = Math.floor(y - shape.bounds.height / 2); +// +// // Fixes visible inactive handles in VML +// if (shape.node && shape.node.style.display !== 'none') { +// shape.redraw(); +// } +// } +// } +// +// /** +// * Returns the index of the handle for the given event. This returns the index +// * of the sizer from where the event originated or {@link Event.LABEL_HANDLE}. +// */ +// getHandleForEvent(me: InternalMouseEvent) { +// // Connection highlight may consume events before they reach sizer handle +// const tol = !isMouseEvent(me.getEvent()) ? this.tolerance : 1; +// const hit = +// this.allowHandleBoundsCheck && tol > 0 +// ? new Rectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) +// : null; +// +// const checkShape = (shape: Shape | null) => { +// const st = +// shape && shape.constructor !== ImageShape && this.allowHandleBoundsCheck +// ? shape.strokeWidth + shape.svgStrokeTolerance +// : null; +// const real = st +// ? new Rectangle( +// me.getGraphX() - Math.floor(st / 2), +// me.getGraphY() - Math.floor(st / 2), +// st, +// st +// ) +// : hit; +// +// return ( +// shape && +// shape.bounds && +// (me.isSource(shape) || +// (real && +// intersects(shape.bounds, real) && +// shape.node.style.display !== 'none' && +// shape.node.style.visibility !== 'hidden')) +// ); +// }; +// +// if (checkShape(this.rotationShape)) { +// return InternalEvent.ROTATION_HANDLE; +// } +// if (checkShape(this.labelShape)) { +// return InternalEvent.LABEL_HANDLE; +// } +// +// for (let i = 0; i < this.sizers.length; i += 1) { +// if (checkShape(this.sizers[i])) { +// return i; +// } +// } +// +// if (this.customHandles != null && this.isCustomHandleEvent(me)) { +// // Inverse loop order to match display order +// for (let i = this.customHandles.length - 1; i >= 0; i--) { +// if (checkShape(this.customHandles[i].shape)) { +// // LATER: Return reference to active shape +// return InternalEvent.CUSTOM_HANDLE - i; +// } +// } +// } +// +// return null; +// } +// +// /** +// * Returns `true` if the given event allows custom handles to be changed. +// * +// * This implementation returns `true`. +// */ +// isCustomHandleEvent(me: InternalMouseEvent) { +// return true; +// } +// +// /** +// * Handles the event if a handle has been clicked. By consuming the +// * event all subsequent events of the gesture are redirected to this +// * handler. +// */ +// mouseDown(sender: EventSource, me: InternalMouseEvent) { +// if (!me.isConsumed() && this.graph.isEnabled()) { +// const handle = this.getHandleForEvent(me); +// +// if (handle) { +// this.start(me.getGraphX(), me.getGraphY(), handle); +// me.consume(); +// } +// } +// } +// +// /** +// * Called if {@link livePreview} is enabled to check if a border should be painted. +// * +// * This implementation returns `true` if the shape is transparent. +// */ +// isLivePreviewBorder() { +// return ( +// this.state.shape && +// this.state.shape.fill === NONE && +// this.state.shape.stroke === NONE +// ); +// } +// +// /** +// * Starts the handling of the mouse gesture. +// */ +// start(x: number, y: number, index: number) { +// this.livePreviewActive = this.livePreview && this.state.cell.getChildCount() === 0; +// this.inTolerance = true; +// this.childOffsetX = 0; +// this.childOffsetY = 0; +// this.index = index; +// this.startX = x; +// this.startY = y; +// +// if (this.index <= InternalEvent.CUSTOM_HANDLE && this.isGhostPreview()) { +// this.ghostPreview = this.createGhostPreview(); +// } else { +// // Saves reference to parent state +// const { model } = this.state.view.graph; +// const parent = this.state.cell.getParent(); +// +// if ( +// this.state.view.currentRoot !== parent && +// parent && +// (parent.isVertex() || parent.isEdge()) +// ) { +// this.parentState = (this.state.view.graph).view.getState(parent); +// } +// +// // Creates a preview that can be on top of any HTML label +// this.selectionBorder.node.style.display = +// index === InternalEvent.ROTATION_HANDLE ? 'inline' : 'none'; +// +// // Creates the border that represents the new bounds +// if (!this.livePreviewActive || this.isLivePreviewBorder()) { +// this.preview = this.createSelectionShape(this.bounds); +// +// if ( +// !(Client.IS_SVG && Number(this.state.style.rotation || '0') !== 0) && +// this.state.text != null && +// this.state.text.node.parentNode === this.graph.container +// ) { +// this.preview.dialect = DIALECT.STRICTHTML; +// this.preview.init(this.graph.container); +// } else { +// this.preview.dialect = DIALECT.SVG; +// this.preview.init(this.graph.view.getOverlayPane()); +// } +// } +// +// if (index === InternalEvent.ROTATION_HANDLE) { +// // With the rotation handle in a corner, need the angle and distance +// const pos = this.getRotationHandlePosition(); +// +// const dx = pos.x - this.state.getCenterX(); +// const dy = pos.y - this.state.getCenterY(); +// +// this.startAngle = dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : 0; +// this.startDist = Math.sqrt(dx * dx + dy * dy); +// } +// +// // Prepares the handles for live preview +// if (this.livePreviewActive) { +// this.hideSizers(); +// +// if (index === InternalEvent.ROTATION_HANDLE && this.rotationShape) { +// this.rotationShape.node.style.display = ''; +// } else if (index === InternalEvent.LABEL_HANDLE && this.labelShape) { +// this.labelShape.node.style.display = ''; +// } else if (this.sizers[index]) { +// this.sizers[index].node.style.display = ''; +// } else if (index <= InternalEvent.CUSTOM_HANDLE) { +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].setVisible(true); +// } +// +// // Gets the array of connected edge handlers for redrawing +// const edges = this.state.cell.getEdges(); +// this.edgeHandlers = []; +// +// const selectionCellsHandler = this.graph.getPlugin( +// 'SelectionCellsHandler' +// ) as SelectionCellsHandler; +// +// for (let i = 0; i < edges.length; i += 1) { +// const handler = selectionCellsHandler?.getHandler(edges[i]); +// if (handler) { +// this.edgeHandlers.push(handler as EdgeHandler); +// } +// } +// } +// } +// } +// +// /** +// * Starts the handling of the mouse gesture. +// */ +// createGhostPreview() { +// const shape = this.graph.cellRenderer.createShape(this.state); +// shape.init(this.graph.view.getOverlayPane()); +// shape.scale = this.state.view.scale; +// shape.bounds = this.bounds; +// shape.outline = true; +// +// return shape; +// } +// +// /** +// * Shortcut to {@link hideSizers}. +// */ +// setHandlesVisible(visible: boolean) { +// this.handlesVisible = visible; +// +// for (let i = 0; i < this.sizers.length; i += 1) { +// this.sizers[i].node.style.display = visible ? '' : 'none'; +// } +// +// for (let i = 0; i < this.customHandles.length; i += 1) { +// this.customHandles[i].setVisible(visible); +// } +// } +// +// /** +// * Hides all sizers except. +// * +// * Starts the handling of the mouse gesture. +// */ +// hideSizers() { +// this.setHandlesVisible(false); +// } +// +// /** +// * Checks if the coordinates for the given event are within the +// * {@link Graph#tolerance}. If the event is a mouse event then the tolerance is +// * ignored. +// */ +// checkTolerance(me: InternalMouseEvent) { +// if (this.inTolerance && this.startX !== null && this.startY !== null) { +// if ( +// isMouseEvent(me.getEvent()) || +// Math.abs(me.getGraphX() - this.startX) > this.graph.getEventTolerance() || +// Math.abs(me.getGraphY() - this.startY) > this.graph.getEventTolerance() +// ) { +// this.inTolerance = false; +// } +// } +// } +// +// /** +// * Hook for subclasses do show details while the handler is active. +// */ +// updateHint(me: InternalMouseEvent) { +// return; +// } +// +// /** +// * Hooks for subclasses to hide details when the handler gets inactive. +// */ +// removeHint() { +// return; +// } +// +// /** +// * Hook for rounding the angle. This uses {@link Math.round}. +// */ +// roundAngle(angle: number) { +// return Math.round(angle * 10) / 10; +// } +// +// /** +// * Hook for rounding the unscaled width or height. This uses {@link Math.round}. +// */ +// roundLength(length: number) { +// return Math.round(length * 100) / 100; +// } +// +// /** +// * Handles the event by updating the preview. +// */ +// mouseMove(sender: EventSource, me: InternalMouseEvent) { +// if (!me.isConsumed() && this.index != null) { +// // Checks tolerance for ignoring single clicks +// this.checkTolerance(me); +// +// if (!this.inTolerance) { +// if (this.index <= InternalEvent.CUSTOM_HANDLE) { +// if (this.customHandles != null) { +// this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].processEvent(me); +// this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].active = true; +// +// if (this.ghostPreview != null) { +// this.ghostPreview.apply(this.state); +// this.ghostPreview.strokeWidth = +// this.getSelectionStrokeWidth() / +// this.ghostPreview.scale / +// this.ghostPreview.scale; +// this.ghostPreview.isDashed = this.isSelectionDashed(); +// this.ghostPreview.stroke = this.getSelectionColor(); +// this.ghostPreview.redraw(); +// +// if (this.selectionBounds != null) { +// this.selectionBorder.node.style.display = 'none'; +// } +// } else { +// if (this.movePreviewToFront) { +// this.moveToFront(); +// } +// +// this.customHandles[ +// InternalEvent.CUSTOM_HANDLE - this.index +// ].positionChanged(); +// } +// } +// } else if (this.index === InternalEvent.LABEL_HANDLE) { +// this.moveLabel(me); +// } else { +// if (this.index === InternalEvent.ROTATION_HANDLE) { +// this.rotateVertex(me); +// } else { +// this.resizeVertex(me); +// } +// +// this.updateHint(me); +// } +// } +// +// me.consume(); +// } +// // Workaround for disabling the connect highlight when over handle +// else if (!this.graph.isMouseDown && this.getHandleForEvent(me)) { +// me.consume(false); +// } +// } +// +// /** +// * Returns `true` if a ghost preview should be used for custom handles. +// */ +// isGhostPreview() { +// return this.state.cell.getChildCount() > 0; +// } +// +// /** +// * Moves the vertex. +// */ +// moveLabel(me: InternalMouseEvent) { +// const point = new Point(me.getGraphX(), me.getGraphY()); +// const tr = this.graph.view.translate; +// const { scale } = this.graph.view; +// +// if (this.graph.isGridEnabledEvent(me.getEvent())) { +// point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale; +// point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale; +// } +// +// const index = this.rotationShape ? this.sizers.length - 2 : this.sizers.length - 1; +// this.moveSizerTo(this.sizers[index], point.x, point.y); +// } +// +// /** +// * Rotates the vertex. +// */ +// rotateVertex(me: InternalMouseEvent) { +// const point = new Point(me.getGraphX(), me.getGraphY()); +// let dx = this.state.x + this.state.width / 2 - point.x; +// let dy = this.state.y + this.state.height / 2 - point.y; +// this.currentAlpha = +// dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : dy < 0 ? 180 : 0; +// +// if (dx > 0) { +// this.currentAlpha -= 180; +// } +// +// this.currentAlpha -= this.startAngle; +// +// // Rotation raster +// if (this.rotationRaster && this.graph.isGridEnabledEvent(me.getEvent())) { +// let raster; +// dx = point.x - this.state.getCenterX(); +// dy = point.y - this.state.getCenterY(); +// const dist = Math.sqrt(dx * dx + dy * dy); +// +// if (dist - this.startDist < 2) { +// raster = 15; +// } else if (dist - this.startDist < 25) { +// raster = 5; +// } else { +// raster = 1; +// } +// +// this.currentAlpha = Math.round(this.currentAlpha / raster) * raster; +// } else { +// this.currentAlpha = this.roundAngle(this.currentAlpha); +// } +// +// this.selectionBorder.rotation = this.currentAlpha; +// this.selectionBorder.redraw(); +// +// if (this.livePreviewActive) { +// this.redrawHandles(); +// } +// } +// +// /** +// * Resizes the vertex. +// */ +// resizeVertex(me: InternalMouseEvent) { +// const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); +// const alpha = toRadians(this.state.style.rotation ?? 0); +// const point = new Point(me.getGraphX(), me.getGraphY()); +// const tr = this.graph.view.translate; +// const { scale } = this.graph.view; +// let cos = Math.cos(-alpha); +// let sin = Math.sin(-alpha); +// +// let dx = point.x - this.startX; +// let dy = point.y - this.startY; +// +// // Rotates vector for mouse gesture +// const tx = cos * dx - sin * dy; +// const ty = sin * dx + cos * dy; +// +// dx = tx; +// dy = ty; +// +// const geo = this.state.cell.getGeometry(); +// if (geo && this.index !== null) { +// this.unscaledBounds = this.union( +// geo, +// dx / scale, +// dy / scale, +// this.index, +// this.graph.isGridEnabledEvent(me.getEvent()), +// 1, +// new Point(0, 0), +// this.isConstrainedEvent(me), +// this.isCenteredEvent(this.state, me) +// ); +// } +// +// // Keeps vertex within maximum graph or parent bounds +// if (geo && !geo.relative) { +// let max = this.graph.getMaximumGraphBounds(); +// +// // Handles child cells +// if (max != null && this.parentState != null) { +// max = Rectangle.fromRectangle(max); +// +// max.x -= (this.parentState.x - tr.x * scale) / scale; +// max.y -= (this.parentState.y - tr.y * scale) / scale; +// } +// +// if (this.graph.isConstrainChild(this.state.cell)) { +// let tmp = this.graph.getCellContainmentArea(this.state.cell); +// +// if (tmp != null) { +// const overlap = this.graph.getOverlap(this.state.cell); +// +// if (overlap > 0) { +// tmp = Rectangle.fromRectangle(tmp); +// +// tmp.x -= tmp.width * overlap; +// tmp.y -= tmp.height * overlap; +// tmp.width += 2 * tmp.width * overlap; +// tmp.height += 2 * tmp.height * overlap; +// } +// +// if (!max) { +// max = tmp; +// } else { +// max = Rectangle.fromRectangle(max); +// max.intersect(tmp); +// } +// } +// } +// +// if (max && this.unscaledBounds) { +// if (this.unscaledBounds.x < max.x) { +// this.unscaledBounds.width -= max.x - this.unscaledBounds.x; +// this.unscaledBounds.x = max.x; +// } +// +// if (this.unscaledBounds.y < max.y) { +// this.unscaledBounds.height -= max.y - this.unscaledBounds.y; +// this.unscaledBounds.y = max.y; +// } +// +// if (this.unscaledBounds.x + this.unscaledBounds.width > max.x + max.width) { +// this.unscaledBounds.width -= +// this.unscaledBounds.x + this.unscaledBounds.width - max.x - max.width; +// } +// +// if (this.unscaledBounds.y + this.unscaledBounds.height > max.y + max.height) { +// this.unscaledBounds.height -= +// this.unscaledBounds.y + this.unscaledBounds.height - max.y - max.height; +// } +// } +// } +// +// if (this.unscaledBounds) { +// const old = this.bounds; +// +// this.bounds = new Rectangle( +// (this.parentState ? this.parentState.x : tr.x * scale) + +// this.unscaledBounds.x * scale, +// (this.parentState ? this.parentState.y : tr.y * scale) + +// this.unscaledBounds.y * scale, +// this.unscaledBounds.width * scale, +// this.unscaledBounds.height * scale +// ); +// +// if (geo && geo.relative && this.parentState) { +// this.bounds.x += this.state.x - this.parentState.x; +// this.bounds.y += this.state.y - this.parentState.y; +// } +// +// cos = Math.cos(alpha); +// sin = Math.sin(alpha); +// +// const c2 = new Point(this.bounds.getCenterX(), this.bounds.getCenterY()); +// +// dx = c2.x - ct.x; +// dy = c2.y - ct.y; +// +// const dx2 = cos * dx - sin * dy; +// const dy2 = sin * dx + cos * dy; +// +// const dx3 = dx2 - dx; +// const dy3 = dy2 - dy; +// +// const dx4 = this.bounds.x - this.state.x; +// const dy4 = this.bounds.y - this.state.y; +// +// const dx5 = cos * dx4 - sin * dy4; +// const dy5 = sin * dx4 + cos * dy4; +// +// this.bounds.x += dx3; +// this.bounds.y += dy3; +// +// // Rounds unscaled bounds to int +// this.unscaledBounds.x = this.roundLength(this.unscaledBounds.x + dx3 / scale); +// this.unscaledBounds.y = this.roundLength(this.unscaledBounds.y + dy3 / scale); +// this.unscaledBounds.width = this.roundLength(this.unscaledBounds.width); +// this.unscaledBounds.height = this.roundLength(this.unscaledBounds.height); +// +// // Shifts the children according to parent offset +// if (!this.state.cell.isCollapsed() && (dx3 !== 0 || dy3 !== 0)) { +// this.childOffsetX = this.state.x - this.bounds.x + dx5; +// this.childOffsetY = this.state.y - this.bounds.y + dy5; +// } else { +// this.childOffsetX = 0; +// this.childOffsetY = 0; +// } +// +// if (!old.equals(this.bounds)) { +// if (this.livePreviewActive) { +// this.updateLivePreview(me); +// } +// +// if (this.preview != null) { +// this.drawPreview(); +// } else { +// this.updateParentHighlight(); +// } +// } +// } +// } +// +// /** +// * Repaints the live preview. +// */ +// updateLivePreview(me: InternalMouseEvent) { +// // TODO: Apply child offset to children in live preview +// const { scale } = this.graph.view; +// const tr = this.graph.view.translate; +// +// // Saves current state +// const tempState = this.state.clone(); +// +// // Temporarily changes size and origin +// this.state.x = this.bounds.x; +// this.state.y = this.bounds.y; +// this.state.origin = new Point( +// this.state.x / scale - tr.x, +// this.state.y / scale - tr.y +// ); +// this.state.width = this.bounds.width; +// this.state.height = this.bounds.height; +// +// // Redraws cell and handles +// let off = this.state.absoluteOffset; +// off = new Point(off.x, off.y); +// +// // Required to store and reset absolute offset for updating label position +// this.state.absoluteOffset.x = 0; +// this.state.absoluteOffset.y = 0; +// const geo = this.state.cell.getGeometry(); +// +// if (geo != null) { +// const offset = geo.offset || this.EMPTY_POINT; +// +// if (offset != null && !geo.relative) { +// this.state.absoluteOffset.x = this.state.view.scale * offset.x; +// this.state.absoluteOffset.y = this.state.view.scale * offset.y; +// } +// +// this.state.view.updateVertexLabelOffset(this.state); +// } +// +// // Draws the live preview +// (this.state.view.graph).cellRenderer.redraw(this.state, true); +// +// // Redraws connected edges TODO: Include child edges +// this.state.view.invalidate(this.state.cell); +// this.state.invalid = false; +// this.state.view.validate(); +// this.redrawHandles(); +// +// // Moves live preview to front +// if (this.movePreviewToFront) { +// this.moveToFront(); +// } +// +// // Hides folding icon +// if (this.state.control != null && this.state.control.node != null) { +// this.state.control.node.style.visibility = 'hidden'; +// } +// +// // Restores current state +// this.state.setState(tempState); +// } +// +// /** +// * Handles the event by applying the changes to the geometry. +// */ +// moveToFront() { +// if ( +// (this.state.text && this.state.text.node && this.state.text.node.nextSibling) || +// (this.state.shape && +// this.state.shape.node && +// this.state.shape.node.nextSibling && +// (!this.state.text || this.state.shape.node.nextSibling !== this.state.text.node)) +// ) { +// if (this.state.shape && this.state.shape.node && this.state.shape.node.parentNode) { +// this.state.shape.node.parentNode.appendChild(this.state.shape.node); +// } +// +// if (this.state.text && this.state.text.node && this.state.text.node.parentNode) { +// this.state.text.node.parentNode.appendChild(this.state.text.node); +// } +// } +// } +// +// /** +// * Handles the event by applying the changes to the geometry. +// */ +// mouseUp(sender: EventSource, me: InternalMouseEvent) { +// if (this.index != null && this.state != null) { +// const point = new Point(me.getGraphX(), me.getGraphY()); +// const { index } = this; +// this.index = null; +// +// if (this.ghostPreview == null) { +// // Required to restore order in case of no change +// this.state.view.invalidate(this.state.cell, false, false); +// this.state.view.validate(); +// } +// +// this.graph.batchUpdate(() => { +// if (index <= InternalEvent.CUSTOM_HANDLE) { +// if (this.customHandles != null) { +// // Creates style before changing cell state +// const style = (this.state.view.graph).getCellStyle(this.state.cell); +// +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].active = false; +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].execute(me); +// +// // Sets style and apply on shape to force repaint and +// // check if execute has removed custom handles +// if ( +// this.customHandles != null && +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index] != null +// ) { +// this.state.style = style; +// this.customHandles[InternalEvent.CUSTOM_HANDLE - index].positionChanged(); +// } +// } +// } else if (index === InternalEvent.ROTATION_HANDLE) { +// if (this.currentAlpha != null) { +// const delta = this.currentAlpha - (this.state.style.rotation ?? 0); +// +// if (delta !== 0) { +// this.rotateCell(this.state.cell, delta); +// } +// } else { +// this.rotateClick(); +// } +// } else { +// const gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); +// const alpha = toRadians(this.state.style.rotation ?? 0); +// const cos = Math.cos(-alpha); +// const sin = Math.sin(-alpha); +// +// let dx = point.x - this.startX; +// let dy = point.y - this.startY; +// +// // Rotates vector for mouse gesture +// const tx = cos * dx - sin * dy; +// const ty = sin * dx + cos * dy; +// +// dx = tx; +// dy = ty; +// +// const s = this.graph.view.scale; +// const recurse = this.isRecursiveResize(this.state, me); +// +// this.resizeCell( +// this.state.cell, +// this.roundLength(dx / s), +// this.roundLength(dy / s), +// index, +// gridEnabled, +// this.isConstrainedEvent(me), +// recurse +// ); +// } +// }); +// +// me.consume(); +// this.reset(); +// this.redrawHandles(); +// } +// } +// +// /** +// * Returns the `recursiveResize` status of the given state. +// * @param state the given {@link CellState}. This implementation takes the value of this state. +// * @param me the mouse event. +// */ +// isRecursiveResize(state: CellState, me: InternalMouseEvent) { +// return this.graph.isRecursiveResize(this.state); +// } +// +// /** +// * Hook for subclasses to implement a single click on the rotation handle. +// * This code is executed as part of the model transaction. This implementation +// * is empty. +// */ +// rotateClick() { +// return; +// } +// +// /** +// * Rotates the given cell and its children by the given angle in degrees. +// * +// * @param cell {@link Cell} to be rotated. +// * @param angle Angle in degrees. +// * @param parent if set, consider the parent in the rotation computation. +// */ +// rotateCell(cell: Cell, angle: number, parent?: Cell) { +// if (angle !== 0) { +// const model = this.graph.getDataModel(); +// +// if (cell.isVertex() || cell.isEdge()) { +// if (!cell.isEdge()) { +// const style = this.graph.getCurrentCellStyle(cell); +// const total = (style.rotation || 0) + angle; +// this.graph.setCellStyles('rotation', total, [cell]); +// } +// +// let geo = cell.getGeometry(); +// +// if (geo && parent) { +// const pgeo = parent.getGeometry(); +// +// if (pgeo != null && !parent.isEdge()) { +// geo = geo.clone(); +// geo.rotate(angle, new Point(pgeo.width / 2, pgeo.height / 2)); +// model.setGeometry(cell, geo); +// } +// +// if ((cell.isVertex() && !geo.relative) || cell.isEdge()) { +// // Recursive rotation +// const childCount = cell.getChildCount(); +// +// for (let i = 0; i < childCount; i += 1) { +// this.rotateCell(cell.getChildAt(i), angle, cell); +// } +// } +// } +// } +// } +// } +// +// /** +// * Resets the state of this handler. +// */ +// reset() { +// if (this.index !== null && this.sizers[this.index].node.style.display === 'none') { +// this.sizers[this.index].node.style.display = ''; +// } +// +// this.index = null; +// +// // TODO: Reset and redraw cell states for live preview +// if (this.preview) { +// this.preview.destroy(); +// this.preview = null; +// } +// +// if (this.ghostPreview) { +// this.ghostPreview.destroy(); +// this.ghostPreview = null; +// } +// +// if (this.livePreviewActive) { +// for (let i = 0; i < this.sizers.length; i += 1) { +// this.sizers[i].node.style.display = ''; +// } +// +// // Shows folding icon +// if (this.state.control && this.state.control.node) { +// this.state.control.node.style.visibility = ''; +// } +// } +// +// for (let i = 0; i < this.customHandles.length; i += 1) { +// if (this.customHandles[i].active) { +// this.customHandles[i].active = false; +// this.customHandles[i].reset(); +// } else { +// this.customHandles[i].setVisible(true); +// } +// } +// +// // Checks if handler has been destroyed +// this.selectionBorder.node.style.display = 'inline'; +// this.selectionBounds = this.getSelectionBounds(this.state); +// this.bounds = new Rectangle( +// this.selectionBounds.x, +// this.selectionBounds.y, +// this.selectionBounds.width, +// this.selectionBounds.height +// ); +// this.drawPreview(); +// +// this.removeHint(); +// this.redrawHandles(); +// this.edgeHandlers = []; +// this.handlesVisible = true; +// this.unscaledBounds = null; +// } +// +// /** +// * Uses the given vector to change the bounds of the given cell +// * in the graph using {@link Graph#resizeCell}. +// */ +// resizeCell( +// cell: Cell, +// dx: number, +// dy: number, +// index: number, +// gridEnabled: boolean, +// constrained: boolean, +// recurse: boolean +// ) { +// let geo = cell.getGeometry(); +// +// if (geo) { +// if ( +// index === InternalEvent.LABEL_HANDLE && +// this.labelShape && +// this.labelShape.bounds +// ) { +// const alpha = -toRadians(this.state.style.rotation ?? 0); +// const cos = Math.cos(alpha); +// const sin = Math.sin(alpha); +// const { scale } = this.graph.view; +// const pt = getRotatedPoint( +// new Point( +// Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale), +// Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale) +// ), +// cos, +// sin +// ); +// +// geo = geo.clone(); +// +// if (geo.offset == null) { +// geo.offset = pt; +// } else { +// geo.offset.x += pt.x; +// geo.offset.y += pt.y; +// } +// +// this.graph.model.setGeometry(cell, geo); +// } else if (this.unscaledBounds) { +// const { scale } = this.graph.view; +// +// if (this.childOffsetX !== 0 || this.childOffsetY !== 0) { +// this.moveChildren( +// cell, +// Math.round(this.childOffsetX / scale), +// Math.round(this.childOffsetY / scale) +// ); +// } +// +// this.graph.resizeCell(cell, this.unscaledBounds, recurse); +// } +// } +// } +// +// /** +// * Moves the children of the given cell by the given vector. +// */ +// moveChildren(cell: Cell, dx: number, dy: number) { +// const model = this.graph.getDataModel(); +// const childCount = cell.getChildCount(); +// +// for (let i = 0; i < childCount; i += 1) { +// const child = cell.getChildAt(i); +// let geo = child.getGeometry(); +// +// if (geo != null) { +// geo = geo.clone(); +// geo.translate(dx, dy); +// model.setGeometry(child, geo); +// } +// } +// } +// +// /** +// * Returns the union of the given bounds and location for the specified +// * handle index. +// * +// * To override this to limit the size of vertex via a minWidth/-Height style, +// * the following code can be used. +// * +// * ```javascript +// * let vertexHandlerUnion = union; +// * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> +// * { +// * let result = vertexHandlerUnion.apply(this, arguments); +// * +// * result.width = Math.max(result.width, mxUtils.getNumber(this.state.style, 'minWidth', 0)); +// * result.height = Math.max(result.height, mxUtils.getNumber(this.state.style, 'minHeight', 0)); +// * +// * return result; +// * }; +// * ``` +// * +// * The minWidth/-Height style can then be used as follows: +// * +// * ```javascript +// * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'minWidth=100;minHeight=100;'); +// * ``` +// * +// * To override this to update the height for a wrapped text if the width of a vertex is +// * changed, the following can be used. +// * +// * ```javascript +// * let mxVertexHandlerUnion = union; +// * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> +// * { +// * let result = mxVertexHandlerUnion.apply(this, arguments); +// * let s = this.state; +// * +// * if (this.graph.isHtmlLabel(s.cell) && (index == 3 || index == 4) && +// * s.text != null && s.style.whiteSpace == 'wrap') +// * { +// * let label = this.graph.getLabel(s.cell); +// * let fontSize = mxUtils.getNumber(s.style, 'fontSize', mxConstants.DEFAULT_FONTSIZE); +// * let ww = result.width / s.view.scale - s.text.spacingRight - s.text.spacingLeft +// * +// * result.height = mxUtils.getSizeForString(label, fontSize, s.style.fontFamily, ww).height; +// * } +// * +// * return result; +// * }; +// * ``` +// */ +// union( +// bounds: Rectangle, +// dx: number, +// dy: number, +// index: number, +// gridEnabled: boolean, +// scale: number, +// tr: Point, +// constrained: boolean, +// centered: boolean +// ) { +// gridEnabled = gridEnabled && this.graph.isGridEnabled(); +// +// if (this.singleSizer) { +// let x = bounds.x + bounds.width + dx; +// let y = bounds.y + bounds.height + dy; +// +// if (gridEnabled) { +// x = this.graph.snap(x / scale) * scale; +// y = this.graph.snap(y / scale) * scale; +// } +// +// const rect = new Rectangle(bounds.x, bounds.y, 0, 0); +// rect.add(new Rectangle(x, y, 0, 0)); +// +// return rect; +// } +// const w0 = bounds.width; +// const h0 = bounds.height; +// let left = bounds.x - tr.x * scale; +// let right = left + w0; +// let top = bounds.y - tr.y * scale; +// let bottom = top + h0; +// +// const cx = left + w0 / 2; +// const cy = top + h0 / 2; +// +// if (index > 4 /* Bottom Row */) { +// bottom += dy; +// +// if (gridEnabled) { +// bottom = this.graph.snap(bottom / scale) * scale; +// } else { +// bottom = Math.round(bottom / scale) * scale; +// } +// } else if (index < 3 /* Top Row */) { +// top += dy; +// +// if (gridEnabled) { +// top = this.graph.snap(top / scale) * scale; +// } else { +// top = Math.round(top / scale) * scale; +// } +// } +// +// if (index === 0 || index === 3 || index === 5 /* Left */) { +// left += dx; +// +// if (gridEnabled) { +// left = this.graph.snap(left / scale) * scale; +// } else { +// left = Math.round(left / scale) * scale; +// } +// } else if (index === 2 || index === 4 || index === 7 /* Right */) { +// right += dx; +// +// if (gridEnabled) { +// right = this.graph.snap(right / scale) * scale; +// } else { +// right = Math.round(right / scale) * scale; +// } +// } +// +// let width = right - left; +// let height = bottom - top; +// +// if (constrained) { +// const geo = this.state.cell.getGeometry(); +// +// if (geo != null) { +// const aspect = geo.width / geo.height; +// +// if (index === 1 || index === 2 || index === 7 || index === 6) { +// width = height * aspect; +// } else { +// height = width / aspect; +// } +// +// if (index === 0) { +// left = right - width; +// top = bottom - height; +// } +// } +// } +// +// if (centered) { +// width += width - w0; +// height += height - h0; +// +// const cdx = cx - (left + width / 2); +// const cdy = cy - (top + height / 2); +// +// left += cdx; +// top += cdy; +// right += cdx; +// bottom += cdy; +// } +// +// // Flips over left side +// if (width < 0) { +// left += width; +// width = Math.abs(width); +// } +// +// // Flips over top side +// if (height < 0) { +// top += height; +// height = Math.abs(height); +// } +// +// const result = new Rectangle(left + tr.x * scale, top + tr.y * scale, width, height); +// +// if (this.minBounds != null) { +// result.width = Math.max( +// result.width, +// this.minBounds.x * scale + +// this.minBounds.width * scale + +// Math.max(0, this.x0 * scale - result.x) +// ); +// result.height = Math.max( +// result.height, +// this.minBounds.y * scale + +// this.minBounds.height * scale + +// Math.max(0, this.y0 * scale - result.y) +// ); +// } +// +// return result; +// } +// +// /** +// * Redraws the handles and the preview. +// */ +// redraw(ignoreHandles?: boolean) { +// this.selectionBounds = this.getSelectionBounds(this.state); +// this.bounds = new Rectangle( +// this.selectionBounds.x, +// this.selectionBounds.y, +// this.selectionBounds.width, +// this.selectionBounds.height +// ); +// this.drawPreview(); +// +// if (!ignoreHandles) { +// this.redrawHandles(); +// } +// } +// +// /** +// * Returns the padding to be used for drawing handles for the current . +// */ +// getHandlePadding() { +// // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) +// const result = new Point(0, 0); +// let tol = this.tolerance; +// +// if ( +// this.sizers.length > 0 && +// this.sizers[0].bounds && +// (this.bounds.width < 2 * this.sizers[0].bounds.width + 2 * tol || +// this.bounds.height < 2 * this.sizers[0].bounds.height + 2 * tol) +// ) { +// tol /= 2; +// +// result.x = this.sizers[0].bounds.width + tol; +// result.y = this.sizers[0].bounds.height + tol; +// } +// +// return result; +// } +// +// /** +// * Returns the bounds used to paint the resize handles. +// */ +// getSizerBounds() { +// return this.bounds; +// } +// +// /** +// * Redraws the handles. To hide certain handles the following code can be used. +// * +// * ```javascript +// * redrawHandles() +// * { +// * mxVertexHandlerRedrawHandles.apply(this, arguments); +// * +// * if (this.sizers != null && this.sizers.length > 7) +// * { +// * this.sizers[1].node.style.display = 'none'; +// * this.sizers[6].node.style.display = 'none'; +// * } +// * }; +// * ``` +// */ +// redrawHandles() { +// let s = this.getSizerBounds(); +// const tol = this.tolerance; +// this.horizontalOffset = 0; +// this.verticalOffset = 0; +// +// for (let i = 0; i < this.customHandles.length; i += 1) { +// const shape = this.customHandles[i].shape; +// +// if (shape) { +// const temp = shape.node.style.display; +// this.customHandles[i].redraw(); +// shape.node.style.display = temp; +// +// // Hides custom handles during text editing +// shape.node.style.visibility = +// this.handlesVisible && this.isCustomHandleVisible(this.customHandles[i]) +// ? '' +// : 'hidden'; +// } +// } +// +// if (this.sizers.length > 0 && this.sizers[0]) { +// if (this.index === null && this.manageSizers && this.sizers.length >= 8) { +// // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) +// const padding = this.getHandlePadding(); +// this.horizontalOffset = padding.x; +// this.verticalOffset = padding.y; +// +// if (this.horizontalOffset !== 0 || this.verticalOffset !== 0) { +// s = new Rectangle(s.x, s.y, s.width, s.height); +// +// s.x -= this.horizontalOffset / 2; +// s.width += this.horizontalOffset; +// s.y -= this.verticalOffset / 2; +// s.height += this.verticalOffset; +// } +// +// if (this.sizers.length >= 8) { +// if ( +// this.sizers[0].bounds && +// (s.width < 2 * this.sizers[0].bounds.width + 2 * tol || +// s.height < 2 * this.sizers[0].bounds.height + 2 * tol) +// ) { +// this.sizers[0].node.style.display = 'none'; +// this.sizers[2].node.style.display = 'none'; +// this.sizers[5].node.style.display = 'none'; +// this.sizers[7].node.style.display = 'none'; +// } else if (this.handlesVisible) { +// this.sizers[0].node.style.display = ''; +// this.sizers[2].node.style.display = ''; +// this.sizers[5].node.style.display = ''; +// this.sizers[7].node.style.display = ''; +// } +// } +// } +// +// const r = s.x + s.width; +// const b = s.y + s.height; +// +// if (this.singleSizer) { +// this.moveSizerTo(this.sizers[0], r, b); +// } else { +// const cx = s.x + s.width / 2; +// const cy = s.y + s.height / 2; +// +// if (this.sizers.length >= 8) { +// const crs = [ +// 'nw-resize', +// 'n-resize', +// 'ne-resize', +// 'e-resize', +// 'se-resize', +// 's-resize', +// 'sw-resize', +// 'w-resize', +// ]; +// +// const alpha = toRadians(this.state.style.rotation ?? 0); +// const cos = Math.cos(alpha); +// const sin = Math.sin(alpha); +// const da = Math.round((alpha * 4) / Math.PI); +// +// const ct = new Point(s.getCenterX(), s.getCenterY()); +// let pt = getRotatedPoint(new Point(s.x, s.y), cos, sin, ct); +// this.moveSizerTo(this.sizers[0], pt.x, pt.y); +// this.sizers[0].setCursor(crs[mod(0 + da, crs.length)]); +// +// pt.x = cx; +// pt.y = s.y; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[1], pt.x, pt.y); +// this.sizers[1].setCursor(crs[mod(1 + da, crs.length)]); +// +// pt.x = r; +// pt.y = s.y; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[2], pt.x, pt.y); +// this.sizers[2].setCursor(crs[mod(2 + da, crs.length)]); +// +// pt.x = s.x; +// pt.y = cy; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[3], pt.x, pt.y); +// this.sizers[3].setCursor(crs[mod(7 + da, crs.length)]); +// +// pt.x = r; +// pt.y = cy; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[4], pt.x, pt.y); +// this.sizers[4].setCursor(crs[mod(3 + da, crs.length)]); +// +// pt.x = s.x; +// pt.y = b; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[5], pt.x, pt.y); +// this.sizers[5].setCursor(crs[mod(6 + da, crs.length)]); +// +// pt.x = cx; +// pt.y = b; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[6], pt.x, pt.y); +// this.sizers[6].setCursor(crs[mod(5 + da, crs.length)]); +// +// pt.x = r; +// pt.y = b; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[7], pt.x, pt.y); +// this.sizers[7].setCursor(crs[mod(4 + da, crs.length)]); +// +// pt.x = cx + this.state.absoluteOffset.x; +// pt.y = cy + this.state.absoluteOffset.y; +// pt = getRotatedPoint(pt, cos, sin, ct); +// this.moveSizerTo(this.sizers[8], pt.x, pt.y); +// } else if (this.state.width >= 2 && this.state.height >= 2) { +// this.moveSizerTo( +// this.sizers[0], +// cx + this.state.absoluteOffset.x, +// cy + this.state.absoluteOffset.y +// ); +// } else { +// this.moveSizerTo(this.sizers[0], this.state.x, this.state.y); +// } +// } +// } +// +// if (this.rotationShape) { +// const alpha = toRadians(this.currentAlpha); +// const cos = Math.cos(alpha); +// const sin = Math.sin(alpha); +// +// const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); +// const pt = getRotatedPoint(this.getRotationHandlePosition(), cos, sin, ct); +// +// if (this.rotationShape.node != null) { +// this.moveSizerTo(this.rotationShape, pt.x, pt.y); +// +// // Hides rotation handle during text editing +// this.rotationShape.node.style.visibility = +// (this.state.view.graph).isEditing() || !this.handlesVisible +// ? 'hidden' +// : ''; +// } +// } +// +// if (this.selectionBorder != null) { +// this.selectionBorder.rotation = this.state.style.rotation ?? 0; +// } +// +// if (this.edgeHandlers != null) { +// for (let i = 0; i < this.edgeHandlers.length; i += 1) { +// this.edgeHandlers[i].redraw(); +// } +// } +// } +// +// /** +// * Returns true if the given custom handle is visible. +// */ +// isCustomHandleVisible(handle: CellHandle) { +// return ( +// !this.graph.isEditing() && (this.state.view.graph).getSelectionCount() === 1 +// ); +// } +// +// /** +// * Returns an {@link Point} that defines the rotation handle position. +// */ +// getRotationHandlePosition() { +// return new Point( +// this.bounds.x + this.bounds.width / 2, +// this.bounds.y + this.rotationHandleVSpacing +// ); +// } +// +// /** +// * Returns `true` if the parent highlight should be visible. +// * +// * This implementation always returns `true`. +// */ +// isParentHighlightVisible() { +// const parent = this.state.cell.getParent(); +// return parent ? !this.graph.isCellSelected(parent) : false; +// } +// +// /** +// * Updates the highlight of the parent if {@link parentHighlightEnabled} is `true`. +// */ +// updateParentHighlight() { +// if (!this.isDestroyed()) { +// const visible = this.isParentHighlightVisible(); +// const parent = this.state.cell.getParent(); +// const pstate = parent ? this.graph.view.getState(parent) : null; +// +// if (this.parentHighlight) { +// if (parent && parent.isVertex() && visible) { +// const b = this.parentHighlight.bounds; +// +// if ( +// pstate && +// b && +// (b.x !== pstate.x || +// b.y !== pstate.y || +// b.width !== pstate.width || +// b.height !== pstate.height) +// ) { +// this.parentHighlight.bounds = Rectangle.fromRectangle(pstate); +// this.parentHighlight.redraw(); +// } +// } else { +// if (pstate != null && pstate.parentHighlight === this.parentHighlight) { +// pstate.parentHighlight = null; +// } +// +// this.parentHighlight.destroy(); +// this.parentHighlight = null; +// } +// } else if (this.parentHighlightEnabled && visible) { +// if ( +// parent && +// parent.isVertex() && +// pstate != null && +// pstate.parentHighlight == null +// ) { +// this.parentHighlight = this.createParentHighlightShape(pstate); +// // VML dialect required here for event transparency in IE +// this.parentHighlight.dialect = DIALECT.SVG; +// this.parentHighlight.pointerEvents = false; +// this.parentHighlight.rotation = pstate.style.rotation ?? 0; +// this.parentHighlight.init(this.graph.getView().getOverlayPane()); +// this.parentHighlight.redraw(); +// +// // Shows highlight once per parent +// pstate.parentHighlight = this.parentHighlight; +// } +// } +// } +// } +// +// /** +// * Redraws the preview. +// */ +// drawPreview() { +// if (this.preview != null) { +// this.preview.bounds = this.bounds; +// +// if (this.preview.node.parentNode === this.graph.container) { +// this.preview.bounds.width = Math.max(0, this.preview.bounds.width - 1); +// this.preview.bounds.height = Math.max(0, this.preview.bounds.height - 1); +// } +// +// this.preview.rotation = this.state.style.rotation ?? 0; +// this.preview.redraw(); +// } +// +// this.selectionBorder.bounds = this.getSelectionBorderBounds(); +// this.selectionBorder.redraw(); +// this.updateParentHighlight(); +// } +// +// /** +// * Returns the bounds for the selection border. +// */ +// getSelectionBorderBounds() { +// return this.bounds; +// } +// +// /** +// * Returns `true` if this handler was destroyed or not initialized. +// */ +// isDestroyed() { +// return this.selectionBorder == null; +// } +// +// /** +// * Destroys the handler and all its resources and DOM nodes. +// */ +// onDestroy() { +// (this.state.view.graph).removeListener(this.escapeHandler); +// this.escapeHandler = () => { +// return; +// }; +// +// if (this.preview) { +// this.preview.destroy(); +// this.preview = null; +// } +// +// if (this.parentHighlight) { +// const parent = this.state.cell.getParent(); +// const pstate = parent ? this.graph.view.getState(parent) : null; +// +// if (pstate && pstate.parentHighlight === this.parentHighlight) { +// pstate.parentHighlight = null; +// } +// +// this.parentHighlight.destroy(); +// this.parentHighlight = null; +// } +// +// if (this.ghostPreview) { +// this.ghostPreview.destroy(); +// this.ghostPreview = null; +// } +// +// if (this.selectionBorder) { +// this.selectionBorder.destroy(); +// } +// +// this.labelShape = null; +// this.removeHint(); +// +// for (let i = 0; i < this.sizers.length; i += 1) { +// this.sizers[i].destroy(); +// } +// +// this.sizers = []; +// +// for (let i = 0; i < this.customHandles.length; i += 1) { +// this.customHandles[i].destroy(); +// } +// +// this.customHandles = []; +// } +// } + +type VertexHandler = { + reset(): void; state: CellState; - - sizers: Shape[] = []; - - /** - * Specifies if only one sizer handle at the bottom, right corner should be used. - * @default false - */ - singleSizer = false; - - /** - * Holds the index of the current handle. - */ - index: number | null = null; - - /** - * Specifies if the bounds of handles should be used for hit-detection in IE or if {@link tolerance} > 0. - * @default true - */ - allowHandleBoundsCheck = true; - - /** - * Optional {@link Image} to be used as handles. - * @default null - */ - handleImage: Image | null = null; - - /** - * If handles are currently visible. - * @default true - */ - handlesVisible = true; - - /** - * Optional tolerance for hit-detection in {@link getHandleForEvent}. - * @default 0 - */ - tolerance = 0; - - /** - * Specifies if a rotation handle should be visible. - * @default false - */ - rotationEnabled = false; - - /** - * Specifies if the parent should be highlighted if a child cell is selected. - * @default false - */ - parentHighlightEnabled = false; - - /** - * Specifies if rotation steps should be "rasterized" depending on the distance to the handle. - * @default true - */ - rotationRaster = true; - - /** - * Specifies the cursor for the rotation handle. - * @default 'crosshair'. - */ - rotationCursor = 'crosshair'; - - /** - * Specifies if resize should change the cell in-place. This is an experimental - * feature for non-touch devices. - * @default false - */ - livePreview = false; - - /** - * Specifies if the live preview should be moved to the front. - * @default false - */ - movePreviewToFront = false; - - /** - * Specifies if sizers should be hidden and spaced if the vertex is small. - * @default false - */ - manageSizers = false; - - /** - * Specifies if the size of groups should be constrained by the children. - * @default false - */ - constrainGroupByChildren = false; - - /** - * Vertical spacing for rotation icon. - * @default -16 - */ - rotationHandleVSpacing = -16; - - /** - * The horizontal offset for the handles. This is updated in {@link redrawHandles} - * if {@link manageSizers} is `true` and the sizers are offset horizontally. - */ - horizontalOffset = 0; - - /** - * The horizontal offset for the handles. This is updated in - * if {@link manageSizers} is true and the sizers are offset vertically. - */ - verticalOffset = 0; - - minBounds: Rectangle | null = null; - - x0 = 0; - y0 = 0; - - customHandles: CellHandle[] = []; - - inTolerance = false; - - startX = 0; - startY = 0; - - rotationShape: Shape | null = null; - - currentAlpha = 100; - startAngle = 0; - startDist = 0; - - ghostPreview: Shape | null = null; - - livePreviewActive = false; - - childOffsetX = 0; - childOffsetY = 0; - - parentState: CellState | null = null; - parentHighlight: RectangleShape | null = null; - - unscaledBounds: Rectangle | null = null; - - preview: Shape | null = null; - - labelShape: Shape | null = null; - - edgeHandlers: EdgeHandler[] = []; - - EMPTY_POINT = new Point(); - - /** - * Constructs an event handler that allows to resize vertices and groups. - * - * @param state {@link CellState} of the cell to be resized. - */ - constructor(state: CellState) { - this.state = state; - this.graph = this.state.view.graph; - this.selectionBounds = this.getSelectionBounds(this.state); - this.bounds = new Rectangle( - this.selectionBounds.x, - this.selectionBounds.y, - this.selectionBounds.width, - this.selectionBounds.height - ); - this.selectionBorder = this.createSelectionShape(this.bounds); - // VML dialect required here for event transparency in IE - this.selectionBorder.dialect = DIALECT.SVG; - this.selectionBorder.pointerEvents = false; - this.selectionBorder.rotation = this.state.style.rotation ?? 0; - this.selectionBorder.init(this.graph.getView().getOverlayPane()); - InternalEvent.redirectMouseEvents(this.selectionBorder.node, this.graph, this.state); - - if (this.graph.isCellMovable(this.state.cell)) { - this.selectionBorder.setCursor(CURSOR.MOVABLE_VERTEX); - } - - const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; - - // Adds the sizer handles - if ( - selectionHandler && - (selectionHandler.maxCells <= 0 || - this.graph.getSelectionCount() < selectionHandler.maxCells) - ) { - const resizable = this.graph.isCellResizable(this.state.cell); - this.sizers = []; - - if ( - resizable || - (this.graph.isLabelMovable(this.state.cell) && - this.state.width >= 2 && - this.state.height >= 2) - ) { - let i = 0; - - if (resizable) { - if (!this.singleSizer) { - this.sizers.push(this.createSizer('nw-resize', i++)); - this.sizers.push(this.createSizer('n-resize', i++)); - this.sizers.push(this.createSizer('ne-resize', i++)); - this.sizers.push(this.createSizer('w-resize', i++)); - this.sizers.push(this.createSizer('e-resize', i++)); - this.sizers.push(this.createSizer('sw-resize', i++)); - this.sizers.push(this.createSizer('s-resize', i++)); - } - - this.sizers.push(this.createSizer('se-resize', i++)); - } - - const geo = this.state.cell.getGeometry(); - - if ( - geo != null && - !geo.relative && - //!this.graph.isSwimlane(this.state.cell) && disable for now - this.graph.isLabelMovable(this.state.cell) - ) { - // Marks this as the label handle for getHandleForEvent - this.labelShape = this.createSizer( - CURSOR.LABEL_HANDLE, - InternalEvent.LABEL_HANDLE, - LABEL_HANDLE_SIZE, - LABEL_HANDLE_FILLCOLOR - ); - this.sizers.push(this.labelShape); - } - } else if ( - this.graph.isCellMovable(this.state.cell) && - !this.graph.isCellResizable(this.state.cell) && - this.state.width < 2 && - this.state.height < 2 - ) { - this.labelShape = this.createSizer( - CURSOR.MOVABLE_VERTEX, - InternalEvent.LABEL_HANDLE, - undefined, - LABEL_HANDLE_FILLCOLOR - ); - this.sizers.push(this.labelShape); - } - } - - // Adds the rotation handler - if (this.isRotationHandleVisible()) { - this.rotationShape = this.createSizer( - this.rotationCursor, - InternalEvent.ROTATION_HANDLE, - HANDLE_SIZE + 3, - HANDLE_FILLCOLOR - ); - this.sizers.push(this.rotationShape); - } - - this.customHandles = this.createCustomHandles(); - this.redraw(); - - if (this.constrainGroupByChildren) { - this.updateMinBounds(); - } - - // Handles escape keystrokes - this.escapeHandler = (sender: Listenable, evt: Event) => { - if (this.livePreview && this.index != null) { - // Redraws the live preview - (this.state.view.graph).cellRenderer.redraw(this.state, true); - - // Redraws connected edges - this.state.view.invalidate(this.state.cell); - this.state.invalid = false; - this.state.view.validate(); - } - - this.reset(); - }; - - (this.state.view.graph).addListener(InternalEvent.ESCAPE, this.escapeHandler); - } - - /** - * Returns `true` if the rotation handle should be showing. - */ - isRotationHandleVisible() { - const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; - const selectionHandlerCheck = selectionHandler - ? selectionHandler.maxCells <= 0 || - this.graph.getSelectionCount() < selectionHandler.maxCells - : true; - - return ( - this.graph.isEnabled() && - this.rotationEnabled && - this.graph.isCellRotatable(this.state.cell) && - selectionHandlerCheck - ); - } - - /** - * Returns `true` if the aspect ratio if the cell should be maintained. - */ - isConstrainedEvent(me: InternalMouseEvent) { - return isShiftDown(me.getEvent()) || this.state.style.aspect === 'fixed'; - } - - /** - * Returns `true` if the center of the vertex should be maintained during the resize. - */ - isCenteredEvent(state: CellState, me: InternalMouseEvent) { - return false; - } - - /** - * Returns an array of custom handles. This implementation returns null. - */ - createCustomHandles() { - return []; - } - - /** - * Initializes the shapes required for this vertex handler. - */ - updateMinBounds() { - const children = this.graph.getChildCells(this.state.cell); - - if (children.length > 0) { - this.minBounds = this.graph.view.getBounds(children); - - if (this.minBounds) { - const s = this.state.view.scale; - const t = this.state.view.translate; - - this.minBounds.x -= this.state.x; - this.minBounds.y -= this.state.y; - this.minBounds.x /= s; - this.minBounds.y /= s; - this.minBounds.width /= s; - this.minBounds.height /= s; - this.x0 = this.state.x / s - t.x; - this.y0 = this.state.y / s - t.y; - } - } - } - - /** - * Returns the mxRectangle that defines the bounds of the selection border. - */ - getSelectionBounds(state: CellState) { - return new Rectangle( - Math.round(state.x), - Math.round(state.y), - Math.round(state.width), - Math.round(state.height) - ); - } - - /** - * Creates the shape used to draw the selection border. - */ - createParentHighlightShape(bounds: Rectangle) { - return this.createSelectionShape(bounds); - } - - /** - * Creates the shape used to draw the selection border. - */ - createSelectionShape(bounds: Rectangle) { - const shape = new RectangleShape( - Rectangle.fromRectangle(bounds), - NONE, - this.getSelectionColor() - ); - shape.strokeWidth = this.getSelectionStrokeWidth(); - shape.isDashed = this.isSelectionDashed(); - - return shape; - } - - /** - * Returns {@link VERTEX_SELECTION_COLOR}. - */ - getSelectionColor() { - return VERTEX_SELECTION_COLOR; - } - - /** - * Returns {@link VERTEX_SELECTION_STROKEWIDTH}. - */ - getSelectionStrokeWidth() { - return VERTEX_SELECTION_STROKEWIDTH; - } - - /** - * Returns {@link VERTEX_SELECTION_DASHED}. - */ - isSelectionDashed() { - return VERTEX_SELECTION_DASHED; - } - - /** - * Creates a sizer handle for the specified cursor and index and returns - * the new {@link RectangleShape} that represents the handle. - */ - createSizer( - cursor: string, - index: number, - size = HANDLE_SIZE, - fillColor = HANDLE_FILLCOLOR - ) { - const bounds = new Rectangle(0, 0, size, size); - const sizer = this.createSizerShape(bounds, index, fillColor); - - if ( - sizer.bounds && - sizer.isHtmlAllowed() && - this.state.text && - this.state.text.node.parentNode === this.graph.container - ) { - sizer.bounds.height -= 1; - sizer.bounds.width -= 1; - sizer.dialect = DIALECT.STRICTHTML; - sizer.init(this.graph.container); - } else { - sizer.dialect = - this.graph.dialect !== DIALECT.SVG ? DIALECT.MIXEDHTML : DIALECT.SVG; - sizer.init(this.graph.getView().getOverlayPane()); - } - - InternalEvent.redirectMouseEvents(sizer.node, this.graph, this.state); - - if (this.graph.isEnabled()) { - sizer.setCursor(cursor); - } - - if (!this.isSizerVisible(index)) { - sizer.visible = false; - } - - return sizer; - } - - /** - * Returns `true` if the sizer for the given index is visible. - * - * This implementation returns `true` for all given indices. - */ - isSizerVisible(index: number) { - return true; - } - - /** - * Creates the shape used for the sizer handle for the specified bounds an - * index. Only images and rectangles should be returned if support for HTML - * labels with not foreign objects is required. - */ - createSizerShape(bounds: Rectangle, index: number, fillColor = HANDLE_FILLCOLOR) { - if (this.handleImage) { - bounds = new Rectangle( - bounds.x, - bounds.y, - this.handleImage.width, - this.handleImage.height - ); - const shape = new ImageShape(bounds, this.handleImage.src); - - // Allows HTML rendering of the images - shape.preserveImageAspect = false; - - return shape; - } - if (index === InternalEvent.ROTATION_HANDLE) { - return new EllipseShape(bounds, fillColor, HANDLE_STROKECOLOR); - } - return new RectangleShape(bounds, fillColor, HANDLE_STROKECOLOR); - } - - /** - * Helper method to create an {@link Rectangle} around the given center point - * with a width and height of 2*s or 6, if no s is given. - */ - moveSizerTo(shape: Shape, x: number, y: number) { - if (shape && shape.bounds) { - shape.bounds.x = Math.floor(x - shape.bounds.width / 2); - shape.bounds.y = Math.floor(y - shape.bounds.height / 2); - - // Fixes visible inactive handles in VML - if (shape.node && shape.node.style.display !== 'none') { - shape.redraw(); - } - } - } - - /** - * Returns the index of the handle for the given event. This returns the index - * of the sizer from where the event originated or {@link Event.LABEL_HANDLE}. - */ - getHandleForEvent(me: InternalMouseEvent) { - // Connection highlight may consume events before they reach sizer handle - const tol = !isMouseEvent(me.getEvent()) ? this.tolerance : 1; - const hit = - this.allowHandleBoundsCheck && tol > 0 - ? new Rectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol) - : null; - - const checkShape = (shape: Shape | null) => { - const st = - shape && shape.constructor !== ImageShape && this.allowHandleBoundsCheck - ? shape.strokeWidth + shape.svgStrokeTolerance - : null; - const real = st - ? new Rectangle( - me.getGraphX() - Math.floor(st / 2), - me.getGraphY() - Math.floor(st / 2), - st, - st - ) - : hit; - - return ( - shape && - shape.bounds && - (me.isSource(shape) || - (real && - intersects(shape.bounds, real) && - shape.node.style.display !== 'none' && - shape.node.style.visibility !== 'hidden')) - ); - }; - - if (checkShape(this.rotationShape)) { - return InternalEvent.ROTATION_HANDLE; - } - if (checkShape(this.labelShape)) { - return InternalEvent.LABEL_HANDLE; - } - - for (let i = 0; i < this.sizers.length; i += 1) { - if (checkShape(this.sizers[i])) { - return i; - } - } - - if (this.customHandles != null && this.isCustomHandleEvent(me)) { - // Inverse loop order to match display order - for (let i = this.customHandles.length - 1; i >= 0; i--) { - if (checkShape(this.customHandles[i].shape)) { - // LATER: Return reference to active shape - return InternalEvent.CUSTOM_HANDLE - i; - } - } - } - - return null; - } - - /** - * Returns `true` if the given event allows custom handles to be changed. - * - * This implementation returns `true`. - */ - isCustomHandleEvent(me: InternalMouseEvent) { - return true; - } - - /** - * Handles the event if a handle has been clicked. By consuming the - * event all subsequent events of the gesture are redirected to this - * handler. - */ - mouseDown(sender: EventSource, me: InternalMouseEvent) { - if (!me.isConsumed() && this.graph.isEnabled()) { - const handle = this.getHandleForEvent(me); - - if (handle) { - this.start(me.getGraphX(), me.getGraphY(), handle); - me.consume(); - } - } - } - - /** - * Called if {@link livePreview} is enabled to check if a border should be painted. - * - * This implementation returns `true` if the shape is transparent. - */ - isLivePreviewBorder() { - return ( - this.state.shape && - this.state.shape.fill === NONE && - this.state.shape.stroke === NONE - ); - } - - /** - * Starts the handling of the mouse gesture. - */ - start(x: number, y: number, index: number) { - this.livePreviewActive = this.livePreview && this.state.cell.getChildCount() === 0; - this.inTolerance = true; - this.childOffsetX = 0; - this.childOffsetY = 0; - this.index = index; - this.startX = x; - this.startY = y; - - if (this.index <= InternalEvent.CUSTOM_HANDLE && this.isGhostPreview()) { - this.ghostPreview = this.createGhostPreview(); - } else { - // Saves reference to parent state - const { model } = this.state.view.graph; - const parent = this.state.cell.getParent(); - - if ( - this.state.view.currentRoot !== parent && - parent && - (parent.isVertex() || parent.isEdge()) - ) { - this.parentState = (this.state.view.graph).view.getState(parent); - } - - // Creates a preview that can be on top of any HTML label - this.selectionBorder.node.style.display = - index === InternalEvent.ROTATION_HANDLE ? 'inline' : 'none'; - - // Creates the border that represents the new bounds - if (!this.livePreviewActive || this.isLivePreviewBorder()) { - this.preview = this.createSelectionShape(this.bounds); - - if ( - !(Client.IS_SVG && Number(this.state.style.rotation || '0') !== 0) && - this.state.text != null && - this.state.text.node.parentNode === this.graph.container - ) { - this.preview.dialect = DIALECT.STRICTHTML; - this.preview.init(this.graph.container); - } else { - this.preview.dialect = DIALECT.SVG; - this.preview.init(this.graph.view.getOverlayPane()); - } - } - - if (index === InternalEvent.ROTATION_HANDLE) { - // With the rotation handle in a corner, need the angle and distance - const pos = this.getRotationHandlePosition(); - - const dx = pos.x - this.state.getCenterX(); - const dy = pos.y - this.state.getCenterY(); - - this.startAngle = dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : 0; - this.startDist = Math.sqrt(dx * dx + dy * dy); - } - - // Prepares the handles for live preview - if (this.livePreviewActive) { - this.hideSizers(); - - if (index === InternalEvent.ROTATION_HANDLE && this.rotationShape) { - this.rotationShape.node.style.display = ''; - } else if (index === InternalEvent.LABEL_HANDLE && this.labelShape) { - this.labelShape.node.style.display = ''; - } else if (this.sizers[index]) { - this.sizers[index].node.style.display = ''; - } else if (index <= InternalEvent.CUSTOM_HANDLE) { - this.customHandles[InternalEvent.CUSTOM_HANDLE - index].setVisible(true); - } - - // Gets the array of connected edge handlers for redrawing - const edges = this.state.cell.getEdges(); - this.edgeHandlers = []; - - const selectionCellsHandler = this.graph.getPlugin( - 'SelectionCellsHandler' - ) as SelectionCellsHandler; - - for (let i = 0; i < edges.length; i += 1) { - const handler = selectionCellsHandler?.getHandler(edges[i]); - if (handler) { - this.edgeHandlers.push(handler as EdgeHandler); - } - } - } - } - } - - /** - * Starts the handling of the mouse gesture. - */ - createGhostPreview() { - const shape = this.graph.cellRenderer.createShape(this.state); - shape.init(this.graph.view.getOverlayPane()); - shape.scale = this.state.view.scale; - shape.bounds = this.bounds; - shape.outline = true; - - return shape; - } - - /** - * Shortcut to {@link hideSizers}. - */ - setHandlesVisible(visible: boolean) { - this.handlesVisible = visible; - - for (let i = 0; i < this.sizers.length; i += 1) { - this.sizers[i].node.style.display = visible ? '' : 'none'; - } - - for (let i = 0; i < this.customHandles.length; i += 1) { - this.customHandles[i].setVisible(visible); - } - } - - /** - * Hides all sizers except. - * - * Starts the handling of the mouse gesture. - */ - hideSizers() { - this.setHandlesVisible(false); - } - - /** - * Checks if the coordinates for the given event are within the - * {@link Graph#tolerance}. If the event is a mouse event then the tolerance is - * ignored. - */ - checkTolerance(me: InternalMouseEvent) { - if (this.inTolerance && this.startX !== null && this.startY !== null) { - if ( - isMouseEvent(me.getEvent()) || - Math.abs(me.getGraphX() - this.startX) > this.graph.getEventTolerance() || - Math.abs(me.getGraphY() - this.startY) > this.graph.getEventTolerance() - ) { - this.inTolerance = false; - } - } - } - - /** - * Hook for subclasses do show details while the handler is active. - */ - updateHint(me: InternalMouseEvent) { - return; - } - - /** - * Hooks for subclasses to hide details when the handler gets inactive. - */ - removeHint() { - return; - } - - /** - * Hook for rounding the angle. This uses {@link Math.round}. - */ - roundAngle(angle: number) { - return Math.round(angle * 10) / 10; - } - - /** - * Hook for rounding the unscaled width or height. This uses {@link Math.round}. - */ - roundLength(length: number) { - return Math.round(length * 100) / 100; - } - - /** - * Handles the event by updating the preview. - */ - mouseMove(sender: EventSource, me: InternalMouseEvent) { - if (!me.isConsumed() && this.index != null) { - // Checks tolerance for ignoring single clicks - this.checkTolerance(me); - - if (!this.inTolerance) { - if (this.index <= InternalEvent.CUSTOM_HANDLE) { - if (this.customHandles != null) { - this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].processEvent(me); - this.customHandles[InternalEvent.CUSTOM_HANDLE - this.index].active = true; - - if (this.ghostPreview != null) { - this.ghostPreview.apply(this.state); - this.ghostPreview.strokeWidth = - this.getSelectionStrokeWidth() / - this.ghostPreview.scale / - this.ghostPreview.scale; - this.ghostPreview.isDashed = this.isSelectionDashed(); - this.ghostPreview.stroke = this.getSelectionColor(); - this.ghostPreview.redraw(); - - if (this.selectionBounds != null) { - this.selectionBorder.node.style.display = 'none'; - } - } else { - if (this.movePreviewToFront) { - this.moveToFront(); - } - - this.customHandles[ - InternalEvent.CUSTOM_HANDLE - this.index - ].positionChanged(); - } - } - } else if (this.index === InternalEvent.LABEL_HANDLE) { - this.moveLabel(me); - } else { - if (this.index === InternalEvent.ROTATION_HANDLE) { - this.rotateVertex(me); - } else { - this.resizeVertex(me); - } - - this.updateHint(me); - } - } - - me.consume(); - } - // Workaround for disabling the connect highlight when over handle - else if (!this.graph.isMouseDown && this.getHandleForEvent(me)) { - me.consume(false); - } - } - - /** - * Returns `true` if a ghost preview should be used for custom handles. - */ - isGhostPreview() { - return this.state.cell.getChildCount() > 0; - } - - /** - * Moves the vertex. - */ - moveLabel(me: InternalMouseEvent) { - const point = new Point(me.getGraphX(), me.getGraphY()); - const tr = this.graph.view.translate; - const { scale } = this.graph.view; - - if (this.graph.isGridEnabledEvent(me.getEvent())) { - point.x = (this.graph.snap(point.x / scale - tr.x) + tr.x) * scale; - point.y = (this.graph.snap(point.y / scale - tr.y) + tr.y) * scale; - } - - const index = this.rotationShape ? this.sizers.length - 2 : this.sizers.length - 1; - this.moveSizerTo(this.sizers[index], point.x, point.y); - } - - /** - * Rotates the vertex. - */ - rotateVertex(me: InternalMouseEvent) { - const point = new Point(me.getGraphX(), me.getGraphY()); - let dx = this.state.x + this.state.width / 2 - point.x; - let dy = this.state.y + this.state.height / 2 - point.y; - this.currentAlpha = - dx !== 0 ? (Math.atan(dy / dx) * 180) / Math.PI + 90 : dy < 0 ? 180 : 0; - - if (dx > 0) { - this.currentAlpha -= 180; - } - - this.currentAlpha -= this.startAngle; - - // Rotation raster - if (this.rotationRaster && this.graph.isGridEnabledEvent(me.getEvent())) { - let raster; - dx = point.x - this.state.getCenterX(); - dy = point.y - this.state.getCenterY(); - const dist = Math.sqrt(dx * dx + dy * dy); - - if (dist - this.startDist < 2) { - raster = 15; - } else if (dist - this.startDist < 25) { - raster = 5; - } else { - raster = 1; - } - - this.currentAlpha = Math.round(this.currentAlpha / raster) * raster; - } else { - this.currentAlpha = this.roundAngle(this.currentAlpha); - } - - this.selectionBorder.rotation = this.currentAlpha; - this.selectionBorder.redraw(); - - if (this.livePreviewActive) { - this.redrawHandles(); - } - } - - /** - * Resizes the vertex. - */ - resizeVertex(me: InternalMouseEvent) { - const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); - const alpha = toRadians(this.state.style.rotation ?? 0); - const point = new Point(me.getGraphX(), me.getGraphY()); - const tr = this.graph.view.translate; - const { scale } = this.graph.view; - let cos = Math.cos(-alpha); - let sin = Math.sin(-alpha); - - let dx = point.x - this.startX; - let dy = point.y - this.startY; - - // Rotates vector for mouse gesture - const tx = cos * dx - sin * dy; - const ty = sin * dx + cos * dy; - - dx = tx; - dy = ty; - - const geo = this.state.cell.getGeometry(); - if (geo && this.index !== null) { - this.unscaledBounds = this.union( - geo, - dx / scale, - dy / scale, - this.index, - this.graph.isGridEnabledEvent(me.getEvent()), - 1, - new Point(0, 0), - this.isConstrainedEvent(me), - this.isCenteredEvent(this.state, me) - ); - } - - // Keeps vertex within maximum graph or parent bounds - if (geo && !geo.relative) { - let max = this.graph.getMaximumGraphBounds(); - - // Handles child cells - if (max != null && this.parentState != null) { - max = Rectangle.fromRectangle(max); - - max.x -= (this.parentState.x - tr.x * scale) / scale; - max.y -= (this.parentState.y - tr.y * scale) / scale; - } - - if (this.graph.isConstrainChild(this.state.cell)) { - let tmp = this.graph.getCellContainmentArea(this.state.cell); - - if (tmp != null) { - const overlap = this.graph.getOverlap(this.state.cell); - - if (overlap > 0) { - tmp = Rectangle.fromRectangle(tmp); - - tmp.x -= tmp.width * overlap; - tmp.y -= tmp.height * overlap; - tmp.width += 2 * tmp.width * overlap; - tmp.height += 2 * tmp.height * overlap; - } - - if (!max) { - max = tmp; - } else { - max = Rectangle.fromRectangle(max); - max.intersect(tmp); - } - } - } - - if (max && this.unscaledBounds) { - if (this.unscaledBounds.x < max.x) { - this.unscaledBounds.width -= max.x - this.unscaledBounds.x; - this.unscaledBounds.x = max.x; - } - - if (this.unscaledBounds.y < max.y) { - this.unscaledBounds.height -= max.y - this.unscaledBounds.y; - this.unscaledBounds.y = max.y; - } - - if (this.unscaledBounds.x + this.unscaledBounds.width > max.x + max.width) { - this.unscaledBounds.width -= - this.unscaledBounds.x + this.unscaledBounds.width - max.x - max.width; - } - - if (this.unscaledBounds.y + this.unscaledBounds.height > max.y + max.height) { - this.unscaledBounds.height -= - this.unscaledBounds.y + this.unscaledBounds.height - max.y - max.height; - } - } - } - - if (this.unscaledBounds) { - const old = this.bounds; - - this.bounds = new Rectangle( - (this.parentState ? this.parentState.x : tr.x * scale) + - this.unscaledBounds.x * scale, - (this.parentState ? this.parentState.y : tr.y * scale) + - this.unscaledBounds.y * scale, - this.unscaledBounds.width * scale, - this.unscaledBounds.height * scale - ); - - if (geo && geo.relative && this.parentState) { - this.bounds.x += this.state.x - this.parentState.x; - this.bounds.y += this.state.y - this.parentState.y; - } - - cos = Math.cos(alpha); - sin = Math.sin(alpha); - - const c2 = new Point(this.bounds.getCenterX(), this.bounds.getCenterY()); - - dx = c2.x - ct.x; - dy = c2.y - ct.y; - - const dx2 = cos * dx - sin * dy; - const dy2 = sin * dx + cos * dy; - - const dx3 = dx2 - dx; - const dy3 = dy2 - dy; - - const dx4 = this.bounds.x - this.state.x; - const dy4 = this.bounds.y - this.state.y; - - const dx5 = cos * dx4 - sin * dy4; - const dy5 = sin * dx4 + cos * dy4; - - this.bounds.x += dx3; - this.bounds.y += dy3; - - // Rounds unscaled bounds to int - this.unscaledBounds.x = this.roundLength(this.unscaledBounds.x + dx3 / scale); - this.unscaledBounds.y = this.roundLength(this.unscaledBounds.y + dy3 / scale); - this.unscaledBounds.width = this.roundLength(this.unscaledBounds.width); - this.unscaledBounds.height = this.roundLength(this.unscaledBounds.height); - - // Shifts the children according to parent offset - if (!this.state.cell.isCollapsed() && (dx3 !== 0 || dy3 !== 0)) { - this.childOffsetX = this.state.x - this.bounds.x + dx5; - this.childOffsetY = this.state.y - this.bounds.y + dy5; - } else { - this.childOffsetX = 0; - this.childOffsetY = 0; - } - - if (!old.equals(this.bounds)) { - if (this.livePreviewActive) { - this.updateLivePreview(me); - } - - if (this.preview != null) { - this.drawPreview(); - } else { - this.updateParentHighlight(); - } - } - } - } - - /** - * Repaints the live preview. - */ - updateLivePreview(me: InternalMouseEvent) { - // TODO: Apply child offset to children in live preview - const { scale } = this.graph.view; - const tr = this.graph.view.translate; - - // Saves current state - const tempState = this.state.clone(); - - // Temporarily changes size and origin - this.state.x = this.bounds.x; - this.state.y = this.bounds.y; - this.state.origin = new Point( - this.state.x / scale - tr.x, - this.state.y / scale - tr.y - ); - this.state.width = this.bounds.width; - this.state.height = this.bounds.height; - - // Redraws cell and handles - let off = this.state.absoluteOffset; - off = new Point(off.x, off.y); - - // Required to store and reset absolute offset for updating label position - this.state.absoluteOffset.x = 0; - this.state.absoluteOffset.y = 0; - const geo = this.state.cell.getGeometry(); - - if (geo != null) { - const offset = geo.offset || this.EMPTY_POINT; - - if (offset != null && !geo.relative) { - this.state.absoluteOffset.x = this.state.view.scale * offset.x; - this.state.absoluteOffset.y = this.state.view.scale * offset.y; - } - - this.state.view.updateVertexLabelOffset(this.state); - } - - // Draws the live preview - (this.state.view.graph).cellRenderer.redraw(this.state, true); - - // Redraws connected edges TODO: Include child edges - this.state.view.invalidate(this.state.cell); - this.state.invalid = false; - this.state.view.validate(); - this.redrawHandles(); - - // Moves live preview to front - if (this.movePreviewToFront) { - this.moveToFront(); - } - - // Hides folding icon - if (this.state.control != null && this.state.control.node != null) { - this.state.control.node.style.visibility = 'hidden'; - } - - // Restores current state - this.state.setState(tempState); - } - - /** - * Handles the event by applying the changes to the geometry. - */ - moveToFront() { - if ( - (this.state.text && this.state.text.node && this.state.text.node.nextSibling) || - (this.state.shape && - this.state.shape.node && - this.state.shape.node.nextSibling && - (!this.state.text || this.state.shape.node.nextSibling !== this.state.text.node)) - ) { - if (this.state.shape && this.state.shape.node && this.state.shape.node.parentNode) { - this.state.shape.node.parentNode.appendChild(this.state.shape.node); - } - - if (this.state.text && this.state.text.node && this.state.text.node.parentNode) { - this.state.text.node.parentNode.appendChild(this.state.text.node); - } - } - } - - /** - * Handles the event by applying the changes to the geometry. - */ - mouseUp(sender: EventSource, me: InternalMouseEvent) { - if (this.index != null && this.state != null) { - const point = new Point(me.getGraphX(), me.getGraphY()); - const { index } = this; - this.index = null; - - if (this.ghostPreview == null) { - // Required to restore order in case of no change - this.state.view.invalidate(this.state.cell, false, false); - this.state.view.validate(); - } - - this.graph.batchUpdate(() => { - if (index <= InternalEvent.CUSTOM_HANDLE) { - if (this.customHandles != null) { - // Creates style before changing cell state - const style = (this.state.view.graph).getCellStyle(this.state.cell); - - this.customHandles[InternalEvent.CUSTOM_HANDLE - index].active = false; - this.customHandles[InternalEvent.CUSTOM_HANDLE - index].execute(me); - - // Sets style and apply on shape to force repaint and - // check if execute has removed custom handles - if ( - this.customHandles != null && - this.customHandles[InternalEvent.CUSTOM_HANDLE - index] != null - ) { - this.state.style = style; - this.customHandles[InternalEvent.CUSTOM_HANDLE - index].positionChanged(); - } - } - } else if (index === InternalEvent.ROTATION_HANDLE) { - if (this.currentAlpha != null) { - const delta = this.currentAlpha - (this.state.style.rotation ?? 0); - - if (delta !== 0) { - this.rotateCell(this.state.cell, delta); - } - } else { - this.rotateClick(); - } - } else { - const gridEnabled = this.graph.isGridEnabledEvent(me.getEvent()); - const alpha = toRadians(this.state.style.rotation ?? 0); - const cos = Math.cos(-alpha); - const sin = Math.sin(-alpha); - - let dx = point.x - this.startX; - let dy = point.y - this.startY; - - // Rotates vector for mouse gesture - const tx = cos * dx - sin * dy; - const ty = sin * dx + cos * dy; - - dx = tx; - dy = ty; - - const s = this.graph.view.scale; - const recurse = this.isRecursiveResize(this.state, me); - - this.resizeCell( - this.state.cell, - this.roundLength(dx / s), - this.roundLength(dy / s), - index, - gridEnabled, - this.isConstrainedEvent(me), - recurse - ); - } - }); - - me.consume(); - this.reset(); - this.redrawHandles(); - } - } - - /** - * Returns the `recursiveResize` status of the given state. - * @param state the given {@link CellState}. This implementation takes the value of this state. - * @param me the mouse event. - */ - isRecursiveResize(state: CellState, me: InternalMouseEvent) { - return this.graph.isRecursiveResize(this.state); - } - - /** - * Hook for subclasses to implement a single click on the rotation handle. - * This code is executed as part of the model transaction. This implementation - * is empty. - */ - rotateClick() { - return; - } - - /** - * Rotates the given cell and its children by the given angle in degrees. - * - * @param cell {@link Cell} to be rotated. - * @param angle Angle in degrees. - * @param parent if set, consider the parent in the rotation computation. - */ - rotateCell(cell: Cell, angle: number, parent?: Cell) { - if (angle !== 0) { - const model = this.graph.getDataModel(); - - if (cell.isVertex() || cell.isEdge()) { - if (!cell.isEdge()) { - const style = this.graph.getCurrentCellStyle(cell); - const total = (style.rotation || 0) + angle; - this.graph.setCellStyles('rotation', total, [cell]); - } - - let geo = cell.getGeometry(); - - if (geo && parent) { - const pgeo = parent.getGeometry(); - - if (pgeo != null && !parent.isEdge()) { - geo = geo.clone(); - geo.rotate(angle, new Point(pgeo.width / 2, pgeo.height / 2)); - model.setGeometry(cell, geo); - } - - if ((cell.isVertex() && !geo.relative) || cell.isEdge()) { - // Recursive rotation - const childCount = cell.getChildCount(); - - for (let i = 0; i < childCount; i += 1) { - this.rotateCell(cell.getChildAt(i), angle, cell); - } - } - } - } - } - } - - /** - * Resets the state of this handler. - */ - reset() { - if (this.index !== null && this.sizers[this.index].node.style.display === 'none') { - this.sizers[this.index].node.style.display = ''; - } - - this.index = null; - - // TODO: Reset and redraw cell states for live preview - if (this.preview) { - this.preview.destroy(); - this.preview = null; - } - - if (this.ghostPreview) { - this.ghostPreview.destroy(); - this.ghostPreview = null; - } - - if (this.livePreviewActive) { - for (let i = 0; i < this.sizers.length; i += 1) { - this.sizers[i].node.style.display = ''; - } - - // Shows folding icon - if (this.state.control && this.state.control.node) { - this.state.control.node.style.visibility = ''; - } - } - - for (let i = 0; i < this.customHandles.length; i += 1) { - if (this.customHandles[i].active) { - this.customHandles[i].active = false; - this.customHandles[i].reset(); - } else { - this.customHandles[i].setVisible(true); - } - } - - // Checks if handler has been destroyed - this.selectionBorder.node.style.display = 'inline'; - this.selectionBounds = this.getSelectionBounds(this.state); - this.bounds = new Rectangle( - this.selectionBounds.x, - this.selectionBounds.y, - this.selectionBounds.width, - this.selectionBounds.height - ); - this.drawPreview(); - - this.removeHint(); - this.redrawHandles(); - this.edgeHandlers = []; - this.handlesVisible = true; - this.unscaledBounds = null; - } - - /** - * Uses the given vector to change the bounds of the given cell - * in the graph using {@link Graph#resizeCell}. - */ - resizeCell( - cell: Cell, - dx: number, - dy: number, - index: number, - gridEnabled: boolean, - constrained: boolean, - recurse: boolean - ) { - let geo = cell.getGeometry(); - - if (geo) { - if ( - index === InternalEvent.LABEL_HANDLE && - this.labelShape && - this.labelShape.bounds - ) { - const alpha = -toRadians(this.state.style.rotation ?? 0); - const cos = Math.cos(alpha); - const sin = Math.sin(alpha); - const { scale } = this.graph.view; - const pt = getRotatedPoint( - new Point( - Math.round((this.labelShape.bounds.getCenterX() - this.startX) / scale), - Math.round((this.labelShape.bounds.getCenterY() - this.startY) / scale) - ), - cos, - sin - ); - - geo = geo.clone(); - - if (geo.offset == null) { - geo.offset = pt; - } else { - geo.offset.x += pt.x; - geo.offset.y += pt.y; - } - - this.graph.model.setGeometry(cell, geo); - } else if (this.unscaledBounds) { - const { scale } = this.graph.view; - - if (this.childOffsetX !== 0 || this.childOffsetY !== 0) { - this.moveChildren( - cell, - Math.round(this.childOffsetX / scale), - Math.round(this.childOffsetY / scale) - ); - } - - this.graph.resizeCell(cell, this.unscaledBounds, recurse); - } - } - } - - /** - * Moves the children of the given cell by the given vector. - */ - moveChildren(cell: Cell, dx: number, dy: number) { - const model = this.graph.getDataModel(); - const childCount = cell.getChildCount(); - - for (let i = 0; i < childCount; i += 1) { - const child = cell.getChildAt(i); - let geo = child.getGeometry(); - - if (geo != null) { - geo = geo.clone(); - geo.translate(dx, dy); - model.setGeometry(child, geo); - } - } - } - - /** - * Returns the union of the given bounds and location for the specified - * handle index. - * - * To override this to limit the size of vertex via a minWidth/-Height style, - * the following code can be used. - * - * ```javascript - * let vertexHandlerUnion = union; - * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> - * { - * let result = vertexHandlerUnion.apply(this, arguments); - * - * result.width = Math.max(result.width, mxUtils.getNumber(this.state.style, 'minWidth', 0)); - * result.height = Math.max(result.height, mxUtils.getNumber(this.state.style, 'minHeight', 0)); - * - * return result; - * }; - * ``` - * - * The minWidth/-Height style can then be used as follows: - * - * ```javascript - * graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30, 'minWidth=100;minHeight=100;'); - * ``` - * - * To override this to update the height for a wrapped text if the width of a vertex is - * changed, the following can be used. - * - * ```javascript - * let mxVertexHandlerUnion = union; - * union = (bounds, dx, dy, index, gridEnabled, scale, tr, constrained)=> - * { - * let result = mxVertexHandlerUnion.apply(this, arguments); - * let s = this.state; - * - * if (this.graph.isHtmlLabel(s.cell) && (index == 3 || index == 4) && - * s.text != null && s.style.whiteSpace == 'wrap') - * { - * let label = this.graph.getLabel(s.cell); - * let fontSize = mxUtils.getNumber(s.style, 'fontSize', mxConstants.DEFAULT_FONTSIZE); - * let ww = result.width / s.view.scale - s.text.spacingRight - s.text.spacingLeft - * - * result.height = mxUtils.getSizeForString(label, fontSize, s.style.fontFamily, ww).height; - * } - * - * return result; - * }; - * ``` - */ - union( - bounds: Rectangle, - dx: number, - dy: number, - index: number, - gridEnabled: boolean, - scale: number, - tr: Point, - constrained: boolean, - centered: boolean - ) { - gridEnabled = gridEnabled && this.graph.isGridEnabled(); - - if (this.singleSizer) { - let x = bounds.x + bounds.width + dx; - let y = bounds.y + bounds.height + dy; - - if (gridEnabled) { - x = this.graph.snap(x / scale) * scale; - y = this.graph.snap(y / scale) * scale; - } - - const rect = new Rectangle(bounds.x, bounds.y, 0, 0); - rect.add(new Rectangle(x, y, 0, 0)); - - return rect; - } - const w0 = bounds.width; - const h0 = bounds.height; - let left = bounds.x - tr.x * scale; - let right = left + w0; - let top = bounds.y - tr.y * scale; - let bottom = top + h0; - - const cx = left + w0 / 2; - const cy = top + h0 / 2; - - if (index > 4 /* Bottom Row */) { - bottom += dy; - - if (gridEnabled) { - bottom = this.graph.snap(bottom / scale) * scale; - } else { - bottom = Math.round(bottom / scale) * scale; - } - } else if (index < 3 /* Top Row */) { - top += dy; - - if (gridEnabled) { - top = this.graph.snap(top / scale) * scale; - } else { - top = Math.round(top / scale) * scale; - } - } - - if (index === 0 || index === 3 || index === 5 /* Left */) { - left += dx; - - if (gridEnabled) { - left = this.graph.snap(left / scale) * scale; - } else { - left = Math.round(left / scale) * scale; - } - } else if (index === 2 || index === 4 || index === 7 /* Right */) { - right += dx; - - if (gridEnabled) { - right = this.graph.snap(right / scale) * scale; - } else { - right = Math.round(right / scale) * scale; - } - } - - let width = right - left; - let height = bottom - top; - - if (constrained) { - const geo = this.state.cell.getGeometry(); - - if (geo != null) { - const aspect = geo.width / geo.height; - - if (index === 1 || index === 2 || index === 7 || index === 6) { - width = height * aspect; - } else { - height = width / aspect; - } - - if (index === 0) { - left = right - width; - top = bottom - height; - } - } - } - - if (centered) { - width += width - w0; - height += height - h0; - - const cdx = cx - (left + width / 2); - const cdy = cy - (top + height / 2); - - left += cdx; - top += cdy; - right += cdx; - bottom += cdy; - } - - // Flips over left side - if (width < 0) { - left += width; - width = Math.abs(width); - } - - // Flips over top side - if (height < 0) { - top += height; - height = Math.abs(height); - } - - const result = new Rectangle(left + tr.x * scale, top + tr.y * scale, width, height); - - if (this.minBounds != null) { - result.width = Math.max( - result.width, - this.minBounds.x * scale + - this.minBounds.width * scale + - Math.max(0, this.x0 * scale - result.x) - ); - result.height = Math.max( - result.height, - this.minBounds.y * scale + - this.minBounds.height * scale + - Math.max(0, this.y0 * scale - result.y) - ); - } - - return result; - } - - /** - * Redraws the handles and the preview. - */ - redraw(ignoreHandles?: boolean) { - this.selectionBounds = this.getSelectionBounds(this.state); - this.bounds = new Rectangle( - this.selectionBounds.x, - this.selectionBounds.y, - this.selectionBounds.width, - this.selectionBounds.height - ); - this.drawPreview(); - - if (!ignoreHandles) { - this.redrawHandles(); - } - } - - /** - * Returns the padding to be used for drawing handles for the current . - */ - getHandlePadding() { - // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) - const result = new Point(0, 0); - let tol = this.tolerance; - - if ( - this.sizers.length > 0 && - this.sizers[0].bounds && - (this.bounds.width < 2 * this.sizers[0].bounds.width + 2 * tol || - this.bounds.height < 2 * this.sizers[0].bounds.height + 2 * tol) - ) { - tol /= 2; - - result.x = this.sizers[0].bounds.width + tol; - result.y = this.sizers[0].bounds.height + tol; - } - - return result; - } - - /** - * Returns the bounds used to paint the resize handles. - */ - getSizerBounds() { - return this.bounds; - } - - /** - * Redraws the handles. To hide certain handles the following code can be used. - * - * ```javascript - * redrawHandles() - * { - * mxVertexHandlerRedrawHandles.apply(this, arguments); - * - * if (this.sizers != null && this.sizers.length > 7) - * { - * this.sizers[1].node.style.display = 'none'; - * this.sizers[6].node.style.display = 'none'; - * } - * }; - * ``` - */ - redrawHandles() { - let s = this.getSizerBounds(); - const tol = this.tolerance; - this.horizontalOffset = 0; - this.verticalOffset = 0; - - for (let i = 0; i < this.customHandles.length; i += 1) { - const shape = this.customHandles[i].shape; - - if (shape) { - const temp = shape.node.style.display; - this.customHandles[i].redraw(); - shape.node.style.display = temp; - - // Hides custom handles during text editing - shape.node.style.visibility = - this.handlesVisible && this.isCustomHandleVisible(this.customHandles[i]) - ? '' - : 'hidden'; - } - } - - if (this.sizers.length > 0 && this.sizers[0]) { - if (this.index === null && this.manageSizers && this.sizers.length >= 8) { - // KNOWN: Tolerance depends on event type (eg. 0 for mouse events) - const padding = this.getHandlePadding(); - this.horizontalOffset = padding.x; - this.verticalOffset = padding.y; - - if (this.horizontalOffset !== 0 || this.verticalOffset !== 0) { - s = new Rectangle(s.x, s.y, s.width, s.height); - - s.x -= this.horizontalOffset / 2; - s.width += this.horizontalOffset; - s.y -= this.verticalOffset / 2; - s.height += this.verticalOffset; - } - - if (this.sizers.length >= 8) { - if ( - this.sizers[0].bounds && - (s.width < 2 * this.sizers[0].bounds.width + 2 * tol || - s.height < 2 * this.sizers[0].bounds.height + 2 * tol) - ) { - this.sizers[0].node.style.display = 'none'; - this.sizers[2].node.style.display = 'none'; - this.sizers[5].node.style.display = 'none'; - this.sizers[7].node.style.display = 'none'; - } else if (this.handlesVisible) { - this.sizers[0].node.style.display = ''; - this.sizers[2].node.style.display = ''; - this.sizers[5].node.style.display = ''; - this.sizers[7].node.style.display = ''; - } - } - } - - const r = s.x + s.width; - const b = s.y + s.height; - - if (this.singleSizer) { - this.moveSizerTo(this.sizers[0], r, b); - } else { - const cx = s.x + s.width / 2; - const cy = s.y + s.height / 2; - - if (this.sizers.length >= 8) { - const crs = [ - 'nw-resize', - 'n-resize', - 'ne-resize', - 'e-resize', - 'se-resize', - 's-resize', - 'sw-resize', - 'w-resize', - ]; - - const alpha = toRadians(this.state.style.rotation ?? 0); - const cos = Math.cos(alpha); - const sin = Math.sin(alpha); - const da = Math.round((alpha * 4) / Math.PI); - - const ct = new Point(s.getCenterX(), s.getCenterY()); - let pt = getRotatedPoint(new Point(s.x, s.y), cos, sin, ct); - this.moveSizerTo(this.sizers[0], pt.x, pt.y); - this.sizers[0].setCursor(crs[mod(0 + da, crs.length)]); - - pt.x = cx; - pt.y = s.y; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[1], pt.x, pt.y); - this.sizers[1].setCursor(crs[mod(1 + da, crs.length)]); - - pt.x = r; - pt.y = s.y; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[2], pt.x, pt.y); - this.sizers[2].setCursor(crs[mod(2 + da, crs.length)]); - - pt.x = s.x; - pt.y = cy; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[3], pt.x, pt.y); - this.sizers[3].setCursor(crs[mod(7 + da, crs.length)]); - - pt.x = r; - pt.y = cy; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[4], pt.x, pt.y); - this.sizers[4].setCursor(crs[mod(3 + da, crs.length)]); - - pt.x = s.x; - pt.y = b; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[5], pt.x, pt.y); - this.sizers[5].setCursor(crs[mod(6 + da, crs.length)]); - - pt.x = cx; - pt.y = b; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[6], pt.x, pt.y); - this.sizers[6].setCursor(crs[mod(5 + da, crs.length)]); - - pt.x = r; - pt.y = b; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[7], pt.x, pt.y); - this.sizers[7].setCursor(crs[mod(4 + da, crs.length)]); - - pt.x = cx + this.state.absoluteOffset.x; - pt.y = cy + this.state.absoluteOffset.y; - pt = getRotatedPoint(pt, cos, sin, ct); - this.moveSizerTo(this.sizers[8], pt.x, pt.y); - } else if (this.state.width >= 2 && this.state.height >= 2) { - this.moveSizerTo( - this.sizers[0], - cx + this.state.absoluteOffset.x, - cy + this.state.absoluteOffset.y - ); - } else { - this.moveSizerTo(this.sizers[0], this.state.x, this.state.y); - } - } - } - - if (this.rotationShape) { - const alpha = toRadians(this.currentAlpha); - const cos = Math.cos(alpha); - const sin = Math.sin(alpha); - - const ct = new Point(this.state.getCenterX(), this.state.getCenterY()); - const pt = getRotatedPoint(this.getRotationHandlePosition(), cos, sin, ct); - - if (this.rotationShape.node != null) { - this.moveSizerTo(this.rotationShape, pt.x, pt.y); - - // Hides rotation handle during text editing - this.rotationShape.node.style.visibility = - (this.state.view.graph).isEditing() || !this.handlesVisible - ? 'hidden' - : ''; - } - } - - if (this.selectionBorder != null) { - this.selectionBorder.rotation = this.state.style.rotation ?? 0; - } - - if (this.edgeHandlers != null) { - for (let i = 0; i < this.edgeHandlers.length; i += 1) { - this.edgeHandlers[i].redraw(); - } - } - } - - /** - * Returns true if the given custom handle is visible. - */ - isCustomHandleVisible(handle: CellHandle) { - return ( - !this.graph.isEditing() && (this.state.view.graph).getSelectionCount() === 1 - ); - } - - /** - * Returns an {@link Point} that defines the rotation handle position. - */ - getRotationHandlePosition() { - return new Point( - this.bounds.x + this.bounds.width / 2, - this.bounds.y + this.rotationHandleVSpacing - ); - } - - /** - * Returns `true` if the parent highlight should be visible. - * - * This implementation always returns `true`. - */ - isParentHighlightVisible() { - const parent = this.state.cell.getParent(); - return parent ? !this.graph.isCellSelected(parent) : false; - } - - /** - * Updates the highlight of the parent if {@link parentHighlightEnabled} is `true`. - */ - updateParentHighlight() { - if (!this.isDestroyed()) { - const visible = this.isParentHighlightVisible(); - const parent = this.state.cell.getParent(); - const pstate = parent ? this.graph.view.getState(parent) : null; - - if (this.parentHighlight) { - if (parent && parent.isVertex() && visible) { - const b = this.parentHighlight.bounds; - - if ( - pstate && - b && - (b.x !== pstate.x || - b.y !== pstate.y || - b.width !== pstate.width || - b.height !== pstate.height) - ) { - this.parentHighlight.bounds = Rectangle.fromRectangle(pstate); - this.parentHighlight.redraw(); - } - } else { - if (pstate != null && pstate.parentHighlight === this.parentHighlight) { - pstate.parentHighlight = null; - } - - this.parentHighlight.destroy(); - this.parentHighlight = null; - } - } else if (this.parentHighlightEnabled && visible) { - if ( - parent && - parent.isVertex() && - pstate != null && - pstate.parentHighlight == null - ) { - this.parentHighlight = this.createParentHighlightShape(pstate); - // VML dialect required here for event transparency in IE - this.parentHighlight.dialect = DIALECT.SVG; - this.parentHighlight.pointerEvents = false; - this.parentHighlight.rotation = pstate.style.rotation ?? 0; - this.parentHighlight.init(this.graph.getView().getOverlayPane()); - this.parentHighlight.redraw(); - - // Shows highlight once per parent - pstate.parentHighlight = this.parentHighlight; - } - } - } - } - - /** - * Redraws the preview. - */ - drawPreview() { - if (this.preview != null) { - this.preview.bounds = this.bounds; - - if (this.preview.node.parentNode === this.graph.container) { - this.preview.bounds.width = Math.max(0, this.preview.bounds.width - 1); - this.preview.bounds.height = Math.max(0, this.preview.bounds.height - 1); - } - - this.preview.rotation = this.state.style.rotation ?? 0; - this.preview.redraw(); - } - - this.selectionBorder.bounds = this.getSelectionBorderBounds(); - this.selectionBorder.redraw(); - this.updateParentHighlight(); - } - - /** - * Returns the bounds for the selection border. - */ - getSelectionBorderBounds() { - return this.bounds; - } - - /** - * Returns `true` if this handler was destroyed or not initialized. - */ - isDestroyed() { - return this.selectionBorder == null; - } - - /** - * Destroys the handler and all its resources and DOM nodes. - */ - onDestroy() { - (this.state.view.graph).removeListener(this.escapeHandler); - this.escapeHandler = () => { - return; - }; - - if (this.preview) { - this.preview.destroy(); - this.preview = null; - } - - if (this.parentHighlight) { - const parent = this.state.cell.getParent(); - const pstate = parent ? this.graph.view.getState(parent) : null; - - if (pstate && pstate.parentHighlight === this.parentHighlight) { - pstate.parentHighlight = null; - } - - this.parentHighlight.destroy(); - this.parentHighlight = null; - } - - if (this.ghostPreview) { - this.ghostPreview.destroy(); - this.ghostPreview = null; - } - - if (this.selectionBorder) { - this.selectionBorder.destroy(); - } - - this.labelShape = null; - this.removeHint(); - - for (let i = 0; i < this.sizers.length; i += 1) { - this.sizers[i].destroy(); - } - - this.sizers = []; - - for (let i = 0; i < this.customHandles.length; i += 1) { - this.customHandles[i].destroy(); - } - - this.customHandles = []; - } -} + onDestroy(): void; + redraw(): void; + index: number | null; + startX: number; + startY: number; + setHandlesVisible(visible: boolean): void; + start(x: number, y: number, index: number): void; + // start(x: number | null, y: number | null, index: number | null): void; + updateParentHighlight(): void; + mouseDown(sender: EventSource, me: InternalMouseEvent): void; + mouseUp(sender: EventSource, me: InternalMouseEvent): void; + mouseMove(sender: EventSource, me: InternalMouseEvent): void; +}; export default VertexHandler;