UNPKG

@maxgraph/core

Version:

maxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.

1,218 lines (1,215 loc) 78.3 kB
"use strict"; /* Copyright 2021-present The maxGraph project Contributors Copyright (c) 2006-2015, JGraph Ltd Copyright (c) 2006-2015, Gaudenz Alder Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GraphView = void 0; const Point_js_1 = __importDefault(require("./geometry/Point.js")); const Rectangle_js_1 = __importDefault(require("./geometry/Rectangle.js")); const EventSource_js_1 = __importDefault(require("./event/EventSource.js")); const EventObject_js_1 = __importDefault(require("./event/EventObject.js")); const RectangleShape_js_1 = __importDefault(require("./shape/node/RectangleShape.js")); const Constants_js_1 = require("../util/Constants.js"); const Client_js_1 = __importDefault(require("../Client.js")); const InternalEvent_js_1 = __importDefault(require("./event/InternalEvent.js")); const styleUtils_js_1 = require("../util/styleUtils.js"); const mathUtils_js_1 = require("../util/mathUtils.js"); const config_js_1 = require("../util/config.js"); const CellState_js_1 = __importDefault(require("./cell/CellState.js")); const UndoableEdit_js_1 = __importDefault(require("./undoable_changes/UndoableEdit.js")); const ImageShape_js_1 = __importDefault(require("./shape/node/ImageShape.js")); const InternalMouseEvent_js_1 = __importDefault(require("./event/InternalMouseEvent.js")); const CurrentRootChange_js_1 = __importDefault(require("./undoable_changes/CurrentRootChange.js")); const EventUtils_js_1 = require("../util/EventUtils.js"); const cloneUtils_js_1 = require("../util/cloneUtils.js"); const EdgeStyleRegistry_js_1 = require("./style/edge/EdgeStyleRegistry.js"); const PerimeterRegistry_js_1 = require("./style/perimeter/PerimeterRegistry.js"); const utils_js_1 = require("../internal/utils.js"); const i18n_utils_js_1 = require("../internal/i18n-utils.js"); /** * @class GraphView * @extends {EventSource} * * Extends {@link EventSource} to implement a view for a graph. This class is in * charge of computing the absolute coordinates for the relative child * geometries, the points for perimeters and edge styles and keeping them * cached in {@link CellState}s for faster retrieval. The states are updated * whenever the model or the view state (translate, scale) changes. The scale * and translate are honoured in the bounds. * * #### Event: mxEvent.UNDO * * Fires after the root was changed in {@link setCurrentRoot}. The `edit` * property contains the {@link UndoableEdit} which contains the * {@link CurrentRootChange}. * * #### Event: mxEvent.SCALE_AND_TRANSLATE * * Fires after the scale and translate have been changed in {@link scaleAndTranslate}. * The `scale`, `previousScale`, `translate` * and `previousTranslate` properties contain the new and previous * scale and translate, respectively. * * #### Event: mxEvent.SCALE * * Fires after the scale was changed in {@link setScale}. The `scale` and * `previousScale` properties contain the new and previous scale. * * #### Event: mxEvent.TRANSLATE * * Fires after the translate was changed in {@link setTranslate}. The * `translate` and `previousTranslate` properties contain * the new and previous value for translate. * * #### Event: mxEvent.DOWN and mxEvent.UP * * Fire if the current root is changed by executing an {@link CurrentRootChange}. * The event name depends on the location of the root in the cell hierarchy * with respect to the current root. The `root` and * `previous` properties contain the new and previous root, * respectively. */ class GraphView extends EventSource_js_1.default { constructor(graph) { super(); // TODO: Document me! this.backgroundImage = null; this.backgroundPageShape = null; this.EMPTY_POINT = new Point_js_1.default(); /** * Specifies the resource key for the status message after a long operation. * If the resource for this key does not exist then the value is used as * the status message. * @default 'done' */ this.doneResource = (0, i18n_utils_js_1.isI18nEnabled)() ? 'done' : ''; /** * Specifies the resource key for the status message while the document is * being updated. If the resource for this key does not exist then the * value is used as the status message. * @default 'updatingSelection' */ this.updatingDocumentResource = (0, i18n_utils_js_1.isI18nEnabled)() ? 'updatingDocument' : ''; /** * Specifies if string values in cell styles should be evaluated using {@link eval}. * * This will only be used if the string values can't be mapped to objects using {@link EdgeStyleRegistry} or {@link PerimeterRegistry} when resolving {@link CellStateStyle.edgeStyle} and {@link CellStateStyle.perimeter} respectively. * * **WARNING**: Enabling this switch carries a possible security risk. * * @default false */ this.allowEval = false; /** * Specifies if a gesture should be captured when it goes outside of the * graph container. Default is true. */ this.captureDocumentGesture = true; /** * Specifies if shapes should be created, updated and destroyed using the * methods of {@link cellRenderer} in {@link graph}. Default is true. */ this.rendering = true; /** * {@link Cell} that acts as the root of the displayed cell hierarchy. */ this.currentRoot = null; this.graphBounds = new Rectangle_js_1.default(); this.scale = 1; /** * {@link Point} that specifies the current translation. Default is a new * empty {@link Point}. */ this.translate = new Point_js_1.default(); this.states = new Map(); /** * Specifies if the style should be updated in each validation step. If this * is false then the style is only updated if the state is created or if the * style of the cell was changed. Default is false. */ this.updateStyle = false; /** * During validation, this contains the last DOM node that was processed. */ this.lastNode = null; /** * During validation, this contains the last HTML DOM node that was processed. */ this.lastHtmlNode = null; /** * During validation, this contains the last edge's DOM node that was processed. */ this.lastForegroundNode = null; /** * During validation, this contains the last edge HTML DOM node that was processed. */ this.lastForegroundHtmlNode = null; this.endHandler = null; this.moveHandler = null; this.graph = graph; } /** * Returns {@link graphBounds}. */ getGraphBounds() { return this.graphBounds; } /** * Sets {@link graphBounds}. */ setGraphBounds(value) { this.graphBounds = value; } /** * Returns the {@link scale}. */ getScale() { return this.scale; } /** * Sets the scale and fires a {@link scale} event before calling {@link revalidate} followed * by {@link AbstractGraph.sizeDidChange}. * * @param value Decimal value that specifies the new scale (1 is 100%). */ setScale(value) { const previousScale = this.scale; if (previousScale !== value) { this.scale = value; if (this.isEventsEnabled()) { this.viewStateChanged(); } } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.SCALE, { scale: value, previousScale })); } /** * Returns the {@link translate}. */ getTranslate() { return this.translate; } isRendering() { return this.rendering; } setRendering(value) { this.rendering = value; } /** * Sets the translation and fires a {@link translate} event before calling * {@link revalidate} followed by {@link AbstractGraph.sizeDidChange}. The translation is the * negative of the origin. * * @param dx X-coordinate of the translation. * @param dy Y-coordinate of the translation. */ setTranslate(dx, dy) { const previousTranslate = new Point_js_1.default(this.translate.x, this.translate.y); if (this.translate.x !== dx || this.translate.y !== dy) { this.translate.x = dx; this.translate.y = dy; if (this.isEventsEnabled()) { this.viewStateChanged(); } } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.TRANSLATE, { translate: this.translate, previousTranslate: previousTranslate, })); } /** * Returns {@link allowEval}. */ isAllowEval() { return this.allowEval; } /** * Sets {@link allowEval}. */ setAllowEval(value) { this.allowEval = value; } /** * Returns {@link states}. */ getStates() { return this.states; } /** * Sets {@link states}. */ setStates(value) { this.states = value; } /** * Returns the DOM node that contains the background-, draw-, overlay- and decorator- panes. */ getCanvas() { return this.canvas; } /** * Returns the DOM node that represents the background layer. */ getBackgroundPane() { return this.backgroundPane; } /** * Returns the DOM node that represents the main drawing layer. */ getDrawPane() { return this.drawPane; } /** * Returns the DOM node that represents the layer above the drawing layer. */ getOverlayPane() { return this.overlayPane; } /** * Returns the DOM node that represents the topmost drawing layer. */ getDecoratorPane() { return this.decoratorPane; } /** * Returns the union of all {@link CellState}s for the given array of {@link Cell}. * * @param cells Array of {@link Cell} whose bounds should be returned. */ getBounds(cells) { let result = null; if (cells.length > 0) { for (let i = 0; i < cells.length; i += 1) { if (cells[i].isVertex() || cells[i].isEdge()) { const state = this.getState(cells[i]); if (state) { if (!result) { result = Rectangle_js_1.default.fromRectangle(state); } else { result.add(state); } } } } } return result; } /** * Sets and returns the current root and fires an {@link undo} event before * calling {@link AbstractGraph.sizeDidChange}. * * @param root {@link Cell} that specifies the root of the displayed cell hierarchy. */ setCurrentRoot(root) { if (this.currentRoot !== root) { const change = new CurrentRootChange_js_1.default(this, root); change.execute(); const edit = new UndoableEdit_js_1.default(this, true); edit.add(change); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.UNDO, { edit })); this.graph.sizeDidChange(); this.currentRoot = root; } return root; } /** * Sets the scale and translation and fires a {@link scale} and {@link translate} event * before calling {@link revalidate} followed by {@link AbstractGraph.sizeDidChange}. * * @param scale Decimal value that specifies the new scale (1 is 100%). * @param dx X-coordinate of the translation. * @param dy Y-coordinate of the translation. */ scaleAndTranslate(scale, dx, dy) { const previousScale = this.scale; const previousTranslate = new Point_js_1.default(this.translate.x, this.translate.y); if (this.scale !== scale || this.translate.x !== dx || this.translate.y !== dy) { this.scale = scale; this.translate.x = dx; this.translate.y = dy; if (this.isEventsEnabled()) { this.viewStateChanged(); } } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.SCALE_AND_TRANSLATE, { scale, previousScale, translate: this.translate, previousTranslate: previousTranslate, })); } /** * Invoked after {@link scale} and/or {@link translate} has changed. */ viewStateChanged() { this.revalidate(); this.graph.sizeDidChange(); } /** * Clears the view if {@link currentRoot} is not null and revalidates. */ refresh() { if (this.currentRoot) { this.clear(); } this.revalidate(); } /** * Revalidates the complete view with all cell states. */ revalidate() { this.invalidate(); this.validate(); } /** * Removes the state of the given cell and all descendants if the given cell is not the current root. * * @param cell Optional {@link Cell} for which the state should be removed. Default is the root of the model. * @param force Optional boolean indicating if the current root should be ignored for recursion. Default is `false`. * @param recurse Optional boolean indicating if the descendants should be cleared as well. Default is `true`. */ clear(cell, force = false, recurse = true) { if (!cell) { cell = this.graph.getDataModel().getRoot(); } if (cell) { this.removeState(cell); if (recurse && (force || cell !== this.currentRoot)) { const childCount = cell.getChildCount(); for (let i = 0; i < childCount; i += 1) { this.clear(cell.getChildAt(i), force); } } else { this.invalidate(cell); } } } /** * Invalidates the state of the given cell, all its descendants and * connected edges. * * @param cell Optional {@link Cell} to be invalidated. Default is the root of the * model. */ invalidate(cell = null, recurse = true, includeEdges = true) { const model = this.graph.getDataModel(); cell = cell ?? model.getRoot(); if (cell) { const state = this.getState(cell); if (state) { state.invalid = true; } // Avoids infinite loops for invalid graphs if (!cell.invalidating) { cell.invalidating = true; // Recursively invalidates all descendants if (recurse) { const childCount = cell.getChildCount(); for (let i = 0; i < childCount; i += 1) { const child = cell.getChildAt(i); this.invalidate(child, recurse, includeEdges); } } // Propagates invalidation to all connected edges if (includeEdges) { const edgeCount = cell.getEdgeCount(); for (let i = 0; i < edgeCount; i += 1) { this.invalidate(cell.getEdgeAt(i), recurse, includeEdges); } } cell.invalidating = false; } } } /** * Calls {@link validateCell} and {@link validateCellState} and updates the {@link graphBounds} * using {@link getBoundingBox}. Finally, the background is validated using * {@link validateBackground}. * * @param cell Optional {@link Cell} to be used as the root of the validation. * Default is {@link currentRoot} or the root of the model. */ validate(cell = null) { const t0 = config_js_1.GlobalConfig.logger.enter('GraphView.validate'); this.resetValidationState(); const c = cell || (this.currentRoot ?? this.graph.getDataModel().getRoot()); if (c) { const graphBounds = this.getBoundingBox(this.validateCellState(c ? this.validateCell(c) : null)); this.setGraphBounds(graphBounds ?? this.getEmptyBounds()); this.validateBackground(); this.resetValidationState(); } config_js_1.GlobalConfig.logger.leave('GraphView.validate', t0); } /** * Returns the bounds for an empty graph. This returns a rectangle at * {@link translate} with the size of 0 x 0. */ getEmptyBounds() { return new Rectangle_js_1.default(this.translate.x * this.scale, this.translate.y * this.scale); } /** * Returns the bounding box of the shape and the label for the given * {@link CellState} and its children if recurse is true. * * @param state {@link CellState} whose bounding box should be returned. * @param recurse Optional boolean indicating if the children should be included. * Default is true. */ getBoundingBox(state = null, recurse = true) { let bbox = null; if (state) { if (state.shape && state.shape.boundingBox) { bbox = state.shape.boundingBox.clone(); } // Adds label bounding box to graph bounds if (state.text && state.text.boundingBox) { if (bbox) { bbox.add(state.text.boundingBox); } else { bbox = state.text.boundingBox.clone(); } } if (recurse) { const childCount = state.cell.getChildCount(); for (let i = 0; i < childCount; i += 1) { const bounds = this.getBoundingBox(this.getState(state.cell.getChildAt(i))); if (bounds) { if (!bbox) { bbox = bounds; } else { bbox.add(bounds); } } } } } return bbox; } /** * Creates and returns the shape used as the background page. * * @param bounds {@link Rectangle} that represents the bounds of the shape. */ createBackgroundPageShape(bounds) { return new RectangleShape_js_1.default(bounds, 'white', 'black'); } /** * Calls {@link validateBackgroundImage} and {@link validateBackgroundPage}. */ validateBackground() { this.validateBackgroundImage(); this.validateBackgroundPage(); } /** * Validates the background image. */ validateBackgroundImage() { const bg = this.graph.getBackgroundImage(); if (bg) { if (!this.backgroundImage || this.backgroundImage.imageSrc !== bg.src) { if (this.backgroundImage) { this.backgroundImage.destroy(); } const bounds = new Rectangle_js_1.default(0, 0, 1, 1); this.backgroundImage = new ImageShape_js_1.default(bounds, bg.src); this.backgroundImage.dialect = this.graph.dialect; this.backgroundImage.init(this.backgroundPane); this.backgroundImage.redraw(); } this.redrawBackgroundImage(this.backgroundImage, bg); } else if (this.backgroundImage) { this.backgroundImage.destroy(); this.backgroundImage = null; } } /** * Validates the background page. */ validateBackgroundPage() { const graph = this.graph; if (graph.pageVisible) { const bounds = this.getBackgroundPageBounds(); if (this.backgroundPageShape == null) { this.backgroundPageShape = this.createBackgroundPageShape(bounds); this.backgroundPageShape.scale = this.scale; this.backgroundPageShape.isShadow = true; this.backgroundPageShape.dialect = this.graph.dialect; this.backgroundPageShape.init(this.backgroundPane); this.backgroundPageShape.redraw(); if (this.backgroundPageShape.node) { // Adds listener for double click handling on background if (graph.isNativeDblClickEnabled()) { InternalEvent_js_1.default.addListener(this.backgroundPageShape.node, 'dblclick', ((evt) => { graph.dblClick(evt); })); } // Adds basic listeners for graph event dispatching outside the // container and finishing the handling of a single gesture InternalEvent_js_1.default.addGestureListeners(this.backgroundPageShape.node, (evt) => { graph.fireMouseEvent(InternalEvent_js_1.default.MOUSE_DOWN, new InternalMouseEvent_js_1.default(evt)); }, (evt) => { const tooltipHandler = graph.getPlugin('TooltipHandler'); // Hides the tooltip if mouse is outside container if (tooltipHandler && tooltipHandler.isHideOnHover()) { tooltipHandler.hide(); } if (graph.isMouseDown && !(0, EventUtils_js_1.isConsumed)(evt)) { graph.fireMouseEvent(InternalEvent_js_1.default.MOUSE_MOVE, new InternalMouseEvent_js_1.default(evt)); } }, (evt) => { graph.fireMouseEvent(InternalEvent_js_1.default.MOUSE_UP, new InternalMouseEvent_js_1.default(evt)); }); } } else { this.backgroundPageShape.scale = this.scale; this.backgroundPageShape.bounds = bounds; this.backgroundPageShape.redraw(); } } else if (this.backgroundPageShape) { this.backgroundPageShape.destroy(); this.backgroundPageShape = null; } } /** * Returns the bounds for the background page. */ getBackgroundPageBounds() { const fmt = this.graph.pageFormat; const ps = this.scale * this.graph.pageScale; return new Rectangle_js_1.default(this.scale * this.translate.x, this.scale * this.translate.y, fmt.width * ps, fmt.height * ps); } /** * Updates the bounds and redraws the background image. * * Example: * * If the background image should not be scaled, this can be replaced with * the following. * * @example * ```javascript * redrawBackground(backgroundImage, bg) * { * backgroundImage.bounds.x = this.translate.x; * backgroundImage.bounds.y = this.translate.y; * backgroundImage.bounds.width = bg.width; * backgroundImage.bounds.height = bg.height; * * backgroundImage.redraw(); * }; * ``` * * @param backgroundImage {@link ImageShape} that represents the background image. * @param bg {@link Image} that specifies the image and its dimensions. */ redrawBackgroundImage(backgroundImage, bg) { backgroundImage.scale = this.scale; if (backgroundImage.bounds) { const bounds = backgroundImage.bounds; bounds.x = this.scale * this.translate.x; bounds.y = this.scale * this.translate.y; bounds.width = this.scale * bg.width; bounds.height = this.scale * bg.height; } backgroundImage.redraw(); } /** * Recursively creates the cell state for the given cell if visible is true and * the given cell is visible. If the cell is not visible but the state exists * then it is removed using {@link removeState}. * * @param cell {@link Cell} whose {@link CellState} should be created. * @param visible Optional boolean indicating if the cell should be visible. Default * is true. */ validateCell(cell, visible = true) { visible = visible && cell.isVisible(); const state = this.getState(cell, visible); if (state && !visible) { this.removeState(cell); } else { const childCount = cell.getChildCount(); for (let i = 0; i < childCount; i += 1) { this.validateCell(cell.getChildAt(i), visible && (!cell.isCollapsed() || cell === this.currentRoot)); } } return cell; } /** * Validates and repaints the {@link CellState} for the given {@link Cell}. * * @param cell {@link Cell} whose {@link CellState} should be validated. * @param recurse Optional boolean indicating if the children of the cell should be * validated. Default is true. */ validateCellState(cell, recurse = true) { let state = null; if (cell) { state = this.getState(cell); if (state) { if (state.invalid) { state.invalid = false; if (!state.style || state.invalidStyle) { state.style = this.graph.getCellStyle(state.cell); state.invalidStyle = false; } if (cell !== this.currentRoot) { this.validateCellState(cell.getParent(), false); } state.setVisibleTerminalState(this.validateCellState(this.getVisibleTerminal(cell, true), false), true); state.setVisibleTerminalState(this.validateCellState(this.getVisibleTerminal(cell, false), false), false); this.updateCellState(state); // Repaint happens immediately after the cell is validated if (cell !== this.currentRoot && !state.invalid) { this.graph.cellRenderer.redraw(state, false, this.isRendering()); // Handles changes to vertex paint bounds after update of rendering shape state.updateCachedBounds(); } } if (recurse && !state.invalid) { // Updates order in DOM if recursively traversing if (state.shape) { this.stateValidated(state); } const childCount = cell.getChildCount(); for (let i = 0; i < childCount; i += 1) { this.validateCellState(cell.getChildAt(i)); } } } } return state; } /** * Updates the given {@link CellState}. * * @param state {@link CellState} to be updated. */ updateCellState(state) { const absoluteOffset = state.absoluteOffset; const origin = state.origin; absoluteOffset.x = 0; absoluteOffset.y = 0; origin.x = 0; origin.y = 0; state.length = 0; if (state.cell !== this.currentRoot) { const parent = state.cell.getParent(); const pState = parent ? this.getState(parent) : null; if (pState && pState.cell !== this.currentRoot) { origin.x += pState.origin.x; origin.y += pState.origin.y; } let offset = this.graph.getChildOffsetForCell(state.cell); if (offset) { origin.x += offset.x; origin.y += offset.y; } const geo = state.cell.getGeometry(); if (geo) { if (!state.cell.isEdge()) { offset = geo.offset ? geo.offset : this.EMPTY_POINT; if (geo.relative && pState) { if (pState.cell.isEdge()) { const point = this.getPoint(pState, geo); if (point) { origin.x += point.x / this.scale - pState.origin.x - this.translate.x; origin.y += point.y / this.scale - pState.origin.y - this.translate.y; } } else { origin.x += geo.x * pState.unscaledWidth + offset.x; origin.y += geo.y * pState.unscaledHeight + offset.y; } } else { absoluteOffset.x = this.scale * offset.x; absoluteOffset.y = this.scale * offset.y; origin.x += geo.x; origin.y += geo.y; } } state.x = this.scale * (this.translate.x + origin.x); state.y = this.scale * (this.translate.y + origin.y); state.width = this.scale * geo.width; state.unscaledWidth = geo.width; state.height = this.scale * geo.height; state.unscaledHeight = geo.height; if (state.cell.isVertex()) { this.updateVertexState(state, geo); } if (state.cell.isEdge()) { this.updateEdgeState(state, geo); } } } state.updateCachedBounds(); } /** * Validates the given cell state. */ updateVertexState(state, geo) { const parent = state.cell.getParent(); const pState = parent ? this.getState(parent) : null; if (geo.relative && pState && !pState.cell.isEdge()) { const alpha = (0, mathUtils_js_1.toRadians)(pState.style.rotation ?? 0); if (alpha !== 0) { const cos = Math.cos(alpha); const sin = Math.sin(alpha); const ct = new Point_js_1.default(state.getCenterX(), state.getCenterY()); const cx = new Point_js_1.default(pState.getCenterX(), pState.getCenterY()); const pt = (0, mathUtils_js_1.getRotatedPoint)(ct, cos, sin, cx); state.x = pt.x - state.width / 2; state.y = pt.y - state.height / 2; } } this.updateVertexLabelOffset(state); } /** * Validates the given cell state. */ updateEdgeState(state, geo) { const source = state.getVisibleTerminalState(true); const target = state.getVisibleTerminalState(false); // This will remove edges with no terminals and no terminal points // as such edges are invalid and produce NPEs in the edge styles. // Also removes connected edges that have no visible terminals. if ((state.cell.getTerminal(true) && !source) || (!source && !geo.getTerminalPoint(true)) || (state.cell.getTerminal(false) && !target) || (!target && !geo.getTerminalPoint(false))) { this.clear(state.cell, true); } else { this.updateFixedTerminalPoints(state, source, target); this.updatePoints(state, geo.points, source, target); this.updateFloatingTerminalPoints(state, source, target); const pts = state.absolutePoints; if (state.cell !== this.currentRoot && (pts == null || pts.length < 2 || pts[0] == null || pts[pts.length - 1] == null)) { // This will remove edges with invalid points from the list of states in the view. // Happens if the one of the terminals and the corresponding terminal point is null. this.clear(state.cell, true); } else { this.updateEdgeBounds(state); this.updateEdgeLabelOffset(state); } } } /** * Updates the absoluteOffset of the given vertex cell state. This takes * into account the label position styles. * * @param state {@link CellState} whose absolute offset should be updated. */ updateVertexLabelOffset(state) { const h = state.style.labelPosition ?? 'center'; if (h === 'left') { let lw = state.style.labelWidth ?? null; if (lw != null) { lw *= this.scale; } else { lw = state.width; } // @ts-ignore state.absoluteOffset.x -= lw; } else if (h === 'right') { // @ts-ignore state.absoluteOffset.x += state.width; } else if (h === 'center') { const lw = state.style.labelWidth ?? null; if (lw != null) { // Aligns text block with given width inside the vertex width const align = state.style.align ?? 'center'; let dx = 0; if (align === 'center') { dx = 0.5; } else if (align === 'right') { dx = 1; } if (dx !== 0) { // @ts-ignore state.absoluteOffset.x -= (lw * this.scale - state.width) * dx; } } } const v = state.style.verticalLabelPosition ?? 'middle'; if (v === 'top') { // @ts-ignore state.absoluteOffset.y -= state.height; } else if (v === 'bottom') { // @ts-ignore state.absoluteOffset.y += state.height; } } /** * Resets the current validation state. */ resetValidationState() { this.lastNode = null; this.lastHtmlNode = null; this.lastForegroundNode = null; this.lastForegroundHtmlNode = null; } /** * Invoked when a state has been processed in {@link validatePoints}. This is used * to update the order of the DOM nodes of the shape. * * @param state {@link CellState} that represents the cell state. */ stateValidated(state) { const graph = this.graph; const fg = (state.cell.isEdge() && graph.keepEdgesInForeground) || (state.cell.isVertex() && graph.keepEdgesInBackground); const htmlNode = fg ? this.lastForegroundHtmlNode || this.lastHtmlNode : this.lastHtmlNode; const node = fg ? this.lastForegroundNode || this.lastNode : this.lastNode; const result = graph.cellRenderer.insertStateAfter(state, node, htmlNode); if (fg) { this.lastForegroundHtmlNode = result[1]; this.lastForegroundNode = result[0]; } else { this.lastHtmlNode = result[1]; this.lastNode = result[0]; } } /** * Sets the initial absolute terminal points in the given state before the edge * style is computed. * * @param edge {@link CellState} whose initial terminal points should be updated. * @param source {@link CellState} which represents the source terminal. * @param target {@link CellState} which represents the target terminal. */ updateFixedTerminalPoints(edge, source, target) { this.updateFixedTerminalPoint(edge, source, true, this.graph.getConnectionConstraint(edge, source, true)); this.updateFixedTerminalPoint(edge, target, false, this.graph.getConnectionConstraint(edge, target, false)); } /** * Sets the fixed source or target terminal point on the given edge. * * @param edge {@link CellState} whose terminal point should be updated. * @param terminal {@link CellState} which represents the actual terminal. * @param source Boolean that specifies if the terminal is the source. * @param constraint {@link ConnectionConstraint} that specifies the connection. */ updateFixedTerminalPoint(edge, terminal, source, constraint) { edge.setAbsoluteTerminalPoint(this.getFixedTerminalPoint(edge, terminal, source, constraint), source); } /** * Returns the fixed source or target terminal point for the given edge. * * @param edge {@link CellState} whose terminal point should be returned. * @param terminal {@link CellState} which represents the actual terminal. * @param source Boolean that specifies if the terminal is the source. * @param constraint {@link ConnectionConstraint} that specifies the connection. */ getFixedTerminalPoint(edge, terminal, source, constraint) { let pt = null; if (constraint && terminal) { pt = this.graph.getConnectionPoint(terminal, constraint, false); // FIXME Rounding introduced bugs when calculating label positions -> , this.graph.isOrthogonal(edge)); } if (!pt && !terminal) { const s = this.scale; const tr = this.translate; const orig = edge.origin; const geo = edge.cell.getGeometry(); pt = geo.getTerminalPoint(source); if (pt) { pt = new Point_js_1.default(s * (tr.x + pt.x + orig.x), s * (tr.y + pt.y + orig.y)); } } return pt; } /** * Updates the bounds of the given cell state to reflect the bounds of the stencil * if it has a fixed aspect and returns the previous bounds as an {@link Rectangle} if * the bounds have been modified or null otherwise. * * @param state {@link CellState} whose bounds should be updated. */ updateBoundsFromStencil(state) { let previous = null; if (state && state.shape && state.shape.stencil && state.shape.stencil.aspect === 'fixed') { previous = Rectangle_js_1.default.fromRectangle(state); const asp = state.shape.stencil.computeAspect(null, // this argument is not used state.x, state.y, state.width, state.height); state.setRect(asp.x, asp.y, state.shape.stencil.w0 * asp.width, state.shape.stencil.h0 * asp.height); } return previous; } /** * Updates the absolute points in the given state using the specified array * of {@link Point} as the relative points. * * @param edge {@link CellState} whose absolute points should be updated. * @param points Array of {@link Point} that constitute the relative points. * @param source {@link CellState} that represents the source terminal. * @param target {@link CellState} that represents the target terminal. */ updatePoints(edge, points, source, target) { const pts = []; pts.push(edge.absolutePoints[0]); const edgeStyle = this.getEdgeStyle(edge, points, source, target); if (edgeStyle && source) { // target can be null const src = this.getTerminalPort(edge, source, true); const trg = target ? this.getTerminalPort(edge, target, false) : null; // Uses the stencil bounds for routing and restores after routing const srcBounds = this.updateBoundsFromStencil(src); const trgBounds = this.updateBoundsFromStencil(trg); edgeStyle(edge, src, trg, points, pts); // Restores previous bounds if (src && srcBounds) { src.setRect(srcBounds.x, srcBounds.y, srcBounds.width, srcBounds.height); } if (trg && trgBounds) { trg.setRect(trgBounds.x, trgBounds.y, trgBounds.width, trgBounds.height); } } else if (points) { for (let i = 0; i < points.length; i += 1) { if (points[i]) { const pt = (0, cloneUtils_js_1.clone)(points[i]); pts.push(this.transformControlPoint(edge, pt)); } } } const tmp = edge.absolutePoints; pts.push(tmp[tmp.length - 1]); edge.absolutePoints = pts; } /** * Transforms the given control point to an absolute point. */ transformControlPoint(state, pt, ignoreScale = false) { if (state && pt) { const orig = state.origin; const scale = ignoreScale ? 1 : this.scale; return new Point_js_1.default(scale * (pt.x + this.translate.x + orig.x), scale * (pt.y + this.translate.y + orig.y)); } return null; } /** * Returns `true` if the given edge should be routed with {@link AbstractGraph.defaultLoopStyle} * or the {@link CellStateStyle.orthogonalLoop} defined for the given edge. * This implementation returns `true` if the given edge is a loop and does not */ isLoopStyleEnabled(edge, points = [], source = null, target = null) { const sc = this.graph.getConnectionConstraint(edge, source, true); const tc = this.graph.getConnectionConstraint(edge, target, false); if ((points == null || points.length < 2) && !((edge.style.orthogonalLoop ?? false) || ((sc == null || sc.point == null) && (tc == null || tc.point == null)))) { return source != null && source === target; } return false; } /** * Returns the edge style function to be used to render the given edge state. */ getEdgeStyle(edge, points = [], source = null, target = null) { let edgeStyle = this.isLoopStyleEnabled(edge, points, source, target) ? (edge.style.loopStyle ?? this.graph.defaultLoopStyle) : !(edge.style.noEdgeStyle ?? false) ? edge.style.edgeStyle : null; // Converts string values to objects if (typeof edgeStyle === 'string') { let tmp = EdgeStyleRegistry_js_1.EdgeStyleRegistry.get(edgeStyle); if (!tmp && this.isAllowEval()) { tmp = (0, utils_js_1.doEval)(edgeStyle); } edgeStyle = tmp; } if (typeof edgeStyle === 'function') { return edgeStyle; } return null; } /** * Updates the terminal points in the given state after the edge style was * computed for the edge. * * @param state {@link CellState} whose terminal points should be updated. * @param source {@link CellState} that represents the source terminal. * @param target {@link CellState} that represents the target terminal. */ updateFloatingTerminalPoints(state, source, target) { const pts = state.absolutePoints; const p0 = pts[0]; const pe = pts[pts.length - 1]; if (!pe && target) { this.updateFloatingTerminalPoint(state, target, source, false); } if (!p0 && source) { this.updateFloatingTerminalPoint(state, source, target, true); } } /** * Updates the absolute terminal point in the given state for the given * start and end state, where start is the source if source is true. * * @param edge {@link CellState} whose terminal point should be updated. * @param start {@link CellState} for the terminal on "this" side of the edge. * @param end {@link CellState} for the terminal on the other side of the edge. * @param source Boolean indicating if start is the source terminal state. */ updateFloatingTerminalPoint(edge, start, end, source) { edge.setAbsoluteTerminalPoint(this.getFloatingTerminalPoint(edge, start, end, source), source); } /** * Returns the floating terminal point for the given edge, start and end * state, where start is the source if source is true. * * @param edge {@link CellState} whose terminal point should be returned. * @param start {@link CellState} for the terminal on "this" side of the edge. * @param end {@link CellState} for the terminal on the other side of the edge. * @param source Boolean indicating if start is the source terminal state. */ getFloatingTerminalPoint(edge, start, end, source) { start = this.getTerminalPort(edge, start, source); let next = this.getNextPoint(edge, end, source); const orth = this.graph.isOrthogonal(edge); const alpha = (0, mathUtils_js_1.toRadians)(start.style.rotation ?? 0); const center = new Point_js_1.default(start.getCenterX(), start.getCenterY()); if (alpha !== 0) { const cos = Math.cos(-alpha); const sin = Math.sin(-alpha); next = (0, mathUtils_js_1.getRotatedPoint)(next, cos, sin, center); } let border = edge.style.perimeterSpacing ?? 0; border += edge.style[source ? 'sourcePerimeterSpacing' : 'targetPerimeterSpacing'] ?? 0; let pt = this.getPerimeterPoint(start, next, alpha === 0 && orth, border); if (pt && alpha !== 0) { const cos = Math.cos(alpha); const sin = Math.sin(alpha); pt = (0, mathUtils_js_1.getRotatedPoint)(pt, cos, sin, center); } return pt; } /** * Returns an {@link CellState} that represents the source or target terminal or * port for the given edge. * * @param state {@link CellState} that represents the state of the edge. * @param terminal {@link CellState} that represents the terminal. * @param source Boolean indicating if the given terminal is the source terminal. */ getTerminalPort(state, terminal, source = false) { const key = source ? 'sourcePort' : 'targetPort'; const id = state.style[key]; if (id) { const cell = this.graph.getDataModel().getCell(id); if (cell) { const tmp = this.getState(cell, false); // Only uses ports where a cell state exists if (tmp) { terminal = tmp; } } } return terminal; } /** * Returns an {@link Point} that defines the location of the intersection point between * the perimeter and the line between the center of the shape and the given point. * * @param terminal {@link CellState} for the source or target terminal. * @param next {@link Point} that lies outside the given terminal. * @param orthogonal Boolean that specifies if the orthogonal projection onto * the perimeter should be returned. If this is false then the intersection * of the perimeter and the line between the next and the center point is * returned. * @param border Optional border between the perimeter and the shape. */ getPerimeterPoint(terminal, next, orthogonal, border = 0) { let point = null; if (terminal != null) { const perimeter = this.getPerimeterFunction(terminal); if (perimeter != null && next != null) { const bounds = this.getPerimeterBounds(terminal, border); if (bounds.width > 0 || bounds.height > 0) { point = new Point_js_1.default(next.x, next.y); let flipH = false; let flipV = false; if (terminal.cell.isVertex()) { flipH = !!terminal.style.flipH; flipV = !!terminal.style.flipV; if (flipH) { point.x = 2 * bounds.getCenterX() - point.x; } if (flipV) { point.y = 2 * bounds.getCenterY() - point.y; } } point = perimeter(bounds, terminal, point, orthogonal); if (point != null) { if (flipH) { point.x = 2 * bounds.getCenterX() - point.x; } if (flipV) { point.y = 2 * bounds.getCenterY() - point.y; } } } } if (point == null) { point = this.getPoint(terminal); } } return point; } /** * Returns the x-coordinate of the center point for automatic routing. */ getRoutingCenterX(state) { const f = state.style ? (state.style.routingCenterX ?? 0) : 0; return state.getCenterX() + f * state.width; } /** * Returns the y-coordinate of the center point for automatic routing. */ getRoutingCenterY(state) { const f = state.style ? (state.style.routingCenterY ?? 0) : 0; return state.getCenterY() + f * state.height; } /** * Returns the perimeter bounds for the given terminal, edge pair as an * {@link Rectangle}. * * If you have a model where each terminal has a rela