diff --git a/packages/core/src/view/style/EdgeStyle.ts b/packages/core/src/view/style/EdgeStyle.ts index 7dd50b2151..9ad87865a8 100644 --- a/packages/core/src/view/style/EdgeStyle.ts +++ b/packages/core/src/view/style/EdgeStyle.ts @@ -16,31 +16,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { - getBoundingBox, - getPortConstraints, - reversePortConstraints, -} from '../../util/mathUtils'; -import Point from '../geometry/Point'; -import type CellState from '../cell/CellState'; -import { - DEFAULT_MARKERSIZE, - DIRECTION, - DIRECTION_MASK, - NONE, -} from '../../util/Constants'; -import Rectangle from '../geometry/Rectangle'; -import Geometry from '../geometry/Geometry'; -import { scaleCellState, scalePointArray } from './edge/shared'; -import type { DirectionValue, EdgeStyleFunction } from '../../types'; - import { ElbowConnector as ElbowConnectorFunction } from './edge/Elbow'; import { EntityRelation as EntityRelationFunction } from './edge/EntityRelation'; import { Loop as LoopFunction } from './edge/Loop'; +import { ManhattanConnector as ManhattanConnectorFunction } from './edge/Manhattan'; +import { OrthogonalConnector as OrthogonalConnectorFunction } from './edge/Orthogonal'; import { SegmentConnector as SegmentConnectorFunction } from './edge/Segment'; import { SideToSide as SideToSideFunction } from './edge/SideToSide'; import { TopToBottom as TopToBottomFunction } from './edge/TopToBottom'; -import { ManhattanConnectorConfig, OrthConnectorConfig } from './config'; /** * Provides various edge styles to be used as the values for `edgeStyle` in a cell style. @@ -149,125 +132,6 @@ class EdgeStyle { */ static SegmentConnector = SegmentConnectorFunction; - static dirVectors = [ - [-1, 0], - [0, -1], - [1, 0], - [0, 1], - [-1, 0], - [0, -1], - [1, 0], - ]; - - static wayPoints1 = [ - [0, 0], - [0, 0], - [0, 0], - [0, 0], - [0, 0], - [0, 0], - [0, 0], - [0, 0], - [0, 0], - [0, 0], - [0, 0], - [0, 0], - ]; - - static routePatterns = [ - [ - [513, 2308, 2081, 2562], - [513, 1090, 514, 2184, 2114, 2561], - [513, 1090, 514, 2564, 2184, 2562], - [513, 2308, 2561, 1090, 514, 2568, 2308], - ], - [ - [514, 1057, 513, 2308, 2081, 2562], - [514, 2184, 2114, 2561], - [514, 2184, 2562, 1057, 513, 2564, 2184], - [514, 1057, 513, 2568, 2308, 2561], - ], - [ - [1090, 514, 1057, 513, 2308, 2081, 2562], - [2114, 2561], - [1090, 2562, 1057, 513, 2564, 2184], - [1090, 514, 1057, 513, 2308, 2561, 2568], - ], - [ - [2081, 2562], - [1057, 513, 1090, 514, 2184, 2114, 2561], - [1057, 513, 1090, 514, 2184, 2562, 2564], - [1057, 2561, 1090, 514, 2568, 2308], - ], - ]; - - static inlineRoutePatterns = [ - [null, [2114, 2568], null, null], - [null, [514, 2081, 2114, 2568], null, null], - [null, [2114, 2561], null, null], - [[2081, 2562], [1057, 2114, 2568], [2184, 2562], null], - ]; - - static vertexSeperations: any = []; - - static limits = [ - [0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0], - ]; - - static LEFT_MASK = 32; - - static TOP_MASK = 64; - - static RIGHT_MASK = 128; - - static BOTTOM_MASK = 256; - - static LEFT = 1; - - static TOP = 2; - - static RIGHT = 4; - - static BOTTOM = 8; - - // TODO remove magic numbers - static SIDE_MASK = 480; - // mxEdgeStyle.LEFT_MASK | mxEdgeStyle.TOP_MASK | mxEdgeStyle.RIGHT_MASK - // | mxEdgeStyle.BOTTOM_MASK, - - static CENTER_MASK = 512; - - static SOURCE_MASK = 1024; - - static TARGET_MASK = 2048; - - static VERTEX_MASK = 3072; - // mxEdgeStyle.SOURCE_MASK | mxEdgeStyle.TARGET_MASK, - - static getJettySize(state: CellState, isSource: boolean) { - const buffer = OrthConnectorConfig.buffer; - let value = - (isSource ? state.style.sourceJettySize : state.style.targetJettySize) ?? - state.style.jettySize ?? - buffer; - - if (value === 'auto') { - // Computes the automatic jetty size - const type = (isSource ? state.style.startArrow : state.style.endArrow) ?? NONE; - - if (type !== NONE) { - const size = - (isSource ? state.style.startSize : state.style.endSize) ?? DEFAULT_MARKERSIZE; - value = Math.max(2, Math.ceil((size + buffer) / buffer)) * buffer; - } else { - value = 2 * buffer; - } - } - - return value; - } - /** * Implements a local orthogonal router between the given cells. * @@ -278,1125 +142,14 @@ class EdgeStyle { * @param result Array of {@link Point}s that represent the actual points of the * edge. */ - static OrthConnector: EdgeStyleFunction = ( - state: CellState, - sourceScaled: CellState, - targetScaled: CellState | null, - controlHints: Point[], - result: Point[] - ) => { - // TODO: Figure out what to do when there are nulls in `pts`! - const pts = ( - (scalePointArray(state.absolutePoints, state.view.scale)) - ); - const source = scaleCellState(sourceScaled, state.view.scale); - const target = scaleCellState(targetScaled, state.view.scale); - - const sourceEdge = source == null ? false : source.cell.isEdge(); - const targetEdge = target == null ? false : target.cell.isEdge(); - - const p0 = pts[0]; - const pe = pts[pts.length - 1]; - - let sourceX = source != null ? source.x : p0.x; - let sourceY = source != null ? source.y : p0.y; - let sourceWidth = source != null ? source.width : 0; - let sourceHeight = source != null ? source.height : 0; - - let targetX = target != null ? target.x : pe.x; - let targetY = target != null ? target.y : pe.y; - let targetWidth = target != null ? target.width : 0; - let targetHeight = target != null ? target.height : 0; - - let sourceBuffer = EdgeStyle.getJettySize(state, true); - let targetBuffer = EdgeStyle.getJettySize(state, false); - - // Workaround for loop routing within buffer zone - if (source != null && target === source) { - targetBuffer = Math.max(sourceBuffer, targetBuffer); - sourceBuffer = targetBuffer; - } - - const totalBuffer = targetBuffer + sourceBuffer; - - let tooShort = false; - - // Checks minimum distance for fixed points and falls back to segment connector - if (p0 != null && pe != null) { - const dx = pe.x - p0.x; - const dy = pe.y - p0.y; - - tooShort = dx * dx + dy * dy < totalBuffer * totalBuffer; - } - - if ( - tooShort || - (OrthConnectorConfig.pointsFallback && - controlHints != null && - controlHints.length > 0) || - sourceEdge || - targetEdge - ) { - EdgeStyle.SegmentConnector(state, sourceScaled, targetScaled, controlHints, result); - - return; - } - - // Determine the side(s) of the source and target vertices - // that the edge may connect to - // portConstraint [source, target] - const portConstraint = [DIRECTION_MASK.ALL, DIRECTION_MASK.ALL]; - let rotation = 0; - - if (source != null) { - portConstraint[0] = getPortConstraints(source, state, true, DIRECTION_MASK.ALL); - rotation = source.style.rotation ?? 0; - - if (rotation !== 0) { - const newRect = ( - getBoundingBox( - new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight), - rotation - ) - ); - sourceX = newRect.x; - sourceY = newRect.y; - sourceWidth = newRect.width; - sourceHeight = newRect.height; - } - } - - if (target != null) { - portConstraint[1] = getPortConstraints(target, state, false, DIRECTION_MASK.ALL); - rotation = target.style.rotation ?? 0; - - if (rotation !== 0) { - const newRect = ( - getBoundingBox( - new Rectangle(targetX, targetY, targetWidth, targetHeight), - rotation - ) - ); - targetX = newRect.x; - targetY = newRect.y; - targetWidth = newRect.width; - targetHeight = newRect.height; - } - } - - const dir = [0, 0]; - - // Work out which faces of the vertices present against each other - // in a way that would allow a 3-segment connection if port constraints - // permitted. - // geo -> [source, target] [x, y, width, height] - const geo = [ - [sourceX, sourceY, sourceWidth, sourceHeight], - [targetX, targetY, targetWidth, targetHeight], - ]; - const buffer = [sourceBuffer, targetBuffer]; - - for (let i = 0; i < 2; i += 1) { - EdgeStyle.limits[i][1] = geo[i][0] - buffer[i]; - EdgeStyle.limits[i][2] = geo[i][1] - buffer[i]; - EdgeStyle.limits[i][4] = geo[i][0] + geo[i][2] + buffer[i]; - EdgeStyle.limits[i][8] = geo[i][1] + geo[i][3] + buffer[i]; - } - - // Work out which quad the target is in - const sourceCenX = geo[0][0] + geo[0][2] / 2.0; - const sourceCenY = geo[0][1] + geo[0][3] / 2.0; - const targetCenX = geo[1][0] + geo[1][2] / 2.0; - const targetCenY = geo[1][1] + geo[1][3] / 2.0; - - const dx = sourceCenX - targetCenX; - const dy = sourceCenY - targetCenY; - - let quad = 0; - - // 0 | 1 - // ----- - // 3 | 2 - - if (dx < 0) { - if (dy < 0) { - quad = 2; - } else { - quad = 1; - } - } else if (dy <= 0) { - quad = 3; - - // Special case on x = 0 and negative y - if (dx === 0) { - quad = 2; - } - } - - // Check for connection constraints - let currentTerm = null; - - if (source != null) { - currentTerm = p0; - } - - const constraint = [ - [0.5, 0.5], - [0.5, 0.5], - ]; - - for (let i = 0; i < 2; i += 1) { - if (currentTerm != null) { - constraint[i][0] = (currentTerm.x - geo[i][0]) / geo[i][2]; - - if (Math.abs(currentTerm.x - geo[i][0]) <= 1) { - dir[i] = DIRECTION_MASK.WEST; - } else if (Math.abs(currentTerm.x - geo[i][0] - geo[i][2]) <= 1) { - dir[i] = DIRECTION_MASK.EAST; - } - - constraint[i][1] = (currentTerm.y - geo[i][1]) / geo[i][3]; - - if (Math.abs(currentTerm.y - geo[i][1]) <= 1) { - dir[i] = DIRECTION_MASK.NORTH; - } else if (Math.abs(currentTerm.y - geo[i][1] - geo[i][3]) <= 1) { - dir[i] = DIRECTION_MASK.SOUTH; - } - } - - currentTerm = null; - - if (target != null) { - currentTerm = pe; - } - } - - const sourceTopDist = geo[0][1] - (geo[1][1] + geo[1][3]); - const sourceLeftDist = geo[0][0] - (geo[1][0] + geo[1][2]); - const sourceBottomDist = geo[1][1] - (geo[0][1] + geo[0][3]); - const sourceRightDist = geo[1][0] - (geo[0][0] + geo[0][2]); - - EdgeStyle.vertexSeperations[1] = Math.max(sourceLeftDist - totalBuffer, 0); - EdgeStyle.vertexSeperations[2] = Math.max(sourceTopDist - totalBuffer, 0); - EdgeStyle.vertexSeperations[4] = Math.max(sourceBottomDist - totalBuffer, 0); - EdgeStyle.vertexSeperations[3] = Math.max(sourceRightDist - totalBuffer, 0); - - //= ============================================================= - // Start of source and target direction determination - - // Work through the preferred orientations by relative positioning - // of the vertices and list them in preferred and available order - - const dirPref = []; - const horPref = []; - const vertPref = []; - - horPref[0] = - sourceLeftDist >= sourceRightDist ? DIRECTION_MASK.WEST : DIRECTION_MASK.EAST; - vertPref[0] = - sourceTopDist >= sourceBottomDist ? DIRECTION_MASK.NORTH : DIRECTION_MASK.SOUTH; - - horPref[1] = reversePortConstraints(horPref[0]); - vertPref[1] = reversePortConstraints(vertPref[0]); - - const preferredHorizDist = - sourceLeftDist >= sourceRightDist ? sourceLeftDist : sourceRightDist; - const preferredVertDist = - sourceTopDist >= sourceBottomDist ? sourceTopDist : sourceBottomDist; - - const prefOrdering = [ - [0, 0], - [0, 0], - ]; - let preferredOrderSet = false; - - // If the preferred port isn't available, switch it - for (let i = 0; i < 2; i += 1) { - if (dir[i] !== 0x0) { - continue; - } - - if ((horPref[i] & portConstraint[i]) === 0) { - horPref[i] = reversePortConstraints(horPref[i]); - } - - if ((vertPref[i] & portConstraint[i]) === 0) { - vertPref[i] = reversePortConstraints(vertPref[i]); - } - - prefOrdering[i][0] = vertPref[i]; - prefOrdering[i][1] = horPref[i]; - } - - if (preferredVertDist > 0 && preferredHorizDist > 0) { - // Possibility of two segment edge connection - if ((horPref[0] & portConstraint[0]) > 0 && (vertPref[1] & portConstraint[1]) > 0) { - prefOrdering[0][0] = horPref[0]; - prefOrdering[0][1] = vertPref[0]; - prefOrdering[1][0] = vertPref[1]; - prefOrdering[1][1] = horPref[1]; - preferredOrderSet = true; - } else if ( - (vertPref[0] & portConstraint[0]) > 0 && - (horPref[1] & portConstraint[1]) > 0 - ) { - prefOrdering[0][0] = vertPref[0]; - prefOrdering[0][1] = horPref[0]; - prefOrdering[1][0] = horPref[1]; - prefOrdering[1][1] = vertPref[1]; - preferredOrderSet = true; - } - } - - if (preferredVertDist > 0 && !preferredOrderSet) { - prefOrdering[0][0] = vertPref[0]; - prefOrdering[0][1] = horPref[0]; - prefOrdering[1][0] = vertPref[1]; - prefOrdering[1][1] = horPref[1]; - preferredOrderSet = true; - } - - if (preferredHorizDist > 0 && !preferredOrderSet) { - prefOrdering[0][0] = horPref[0]; - prefOrdering[0][1] = vertPref[0]; - prefOrdering[1][0] = horPref[1]; - prefOrdering[1][1] = vertPref[1]; - preferredOrderSet = true; - } - - // The source and target prefs are now an ordered list of - // the preferred port selections - // If the list contains gaps, compact it - - for (let i = 0; i < 2; i += 1) { - if (dir[i] !== 0x0) { - continue; - } - - if ((prefOrdering[i][0] & portConstraint[i]) === 0) { - prefOrdering[i][0] = prefOrdering[i][1]; - } - - dirPref[i] = prefOrdering[i][0] & portConstraint[i]; - dirPref[i] |= (prefOrdering[i][1] & portConstraint[i]) << 8; - dirPref[i] |= (prefOrdering[1 - i][i] & portConstraint[i]) << 16; - dirPref[i] |= (prefOrdering[1 - i][1 - i] & portConstraint[i]) << 24; - - if ((dirPref[i] & 0xf) === 0) { - dirPref[i] = dirPref[i] << 8; - } - - if ((dirPref[i] & 0xf00) === 0) { - dirPref[i] = (dirPref[i] & 0xf) | (dirPref[i] >> 8); - } - - if ((dirPref[i] & 0xf0000) === 0) { - dirPref[i] = (dirPref[i] & 0xffff) | ((dirPref[i] & 0xf000000) >> 8); - } - - dir[i] = dirPref[i] & 0xf; - - if ( - portConstraint[i] === DIRECTION_MASK.WEST || - portConstraint[i] === DIRECTION_MASK.NORTH || - portConstraint[i] === DIRECTION_MASK.EAST || - portConstraint[i] === DIRECTION_MASK.SOUTH - ) { - dir[i] = portConstraint[i]; - } - } - - //= ============================================================= - // End of source and target direction determination - - let sourceIndex = dir[0] === DIRECTION_MASK.EAST ? 3 : dir[0]; - let targetIndex = dir[1] === DIRECTION_MASK.EAST ? 3 : dir[1]; - - sourceIndex -= quad; - targetIndex -= quad; - - if (sourceIndex < 1) { - sourceIndex += 4; - } - - if (targetIndex < 1) { - targetIndex += 4; - } - - const routePattern = EdgeStyle.routePatterns[sourceIndex - 1][targetIndex - 1]; - - EdgeStyle.wayPoints1[0][0] = geo[0][0]; - EdgeStyle.wayPoints1[0][1] = geo[0][1]; - - switch (dir[0]) { - case DIRECTION_MASK.WEST: - EdgeStyle.wayPoints1[0][0] -= sourceBuffer; - EdgeStyle.wayPoints1[0][1] += constraint[0][1] * geo[0][3]; - break; - case DIRECTION_MASK.SOUTH: - EdgeStyle.wayPoints1[0][0] += constraint[0][0] * geo[0][2]; - EdgeStyle.wayPoints1[0][1] += geo[0][3] + sourceBuffer; - break; - case DIRECTION_MASK.EAST: - EdgeStyle.wayPoints1[0][0] += geo[0][2] + sourceBuffer; - EdgeStyle.wayPoints1[0][1] += constraint[0][1] * geo[0][3]; - break; - case DIRECTION_MASK.NORTH: - EdgeStyle.wayPoints1[0][0] += constraint[0][0] * geo[0][2]; - EdgeStyle.wayPoints1[0][1] -= sourceBuffer; - break; - } - - let currentIndex = 0; - - // Orientation, 0 horizontal, 1 vertical - let lastOrientation = - (dir[0] & (DIRECTION_MASK.EAST | DIRECTION_MASK.WEST)) > 0 ? 0 : 1; - const initialOrientation = lastOrientation; - let currentOrientation = 0; - - for (let i = 0; i < routePattern.length; i += 1) { - const nextDirection = routePattern[i] & 0xf; - - // Rotate the index of this direction by the quad - // to get the real direction - let directionIndex = nextDirection === DIRECTION_MASK.EAST ? 3 : nextDirection; - - directionIndex += quad; - - if (directionIndex > 4) { - directionIndex -= 4; - } - - const direction = EdgeStyle.dirVectors[directionIndex - 1]; - - currentOrientation = directionIndex % 2 > 0 ? 0 : 1; - // Only update the current index if the point moved - // in the direction of the current segment move, - // otherwise the same point is moved until there is - // a segment direction change - if (currentOrientation !== lastOrientation) { - currentIndex++; - // Copy the previous way point into the new one - // We can't base the new position on index - 1 - // because sometime elbows turn out not to exist, - // then we'd have to rewind. - EdgeStyle.wayPoints1[currentIndex][0] = EdgeStyle.wayPoints1[currentIndex - 1][0]; - EdgeStyle.wayPoints1[currentIndex][1] = EdgeStyle.wayPoints1[currentIndex - 1][1]; - } - - const tar = (routePattern[i] & EdgeStyle.TARGET_MASK) > 0; - const sou = (routePattern[i] & EdgeStyle.SOURCE_MASK) > 0; - let side = (routePattern[i] & EdgeStyle.SIDE_MASK) >> 5; - side <<= quad; - - if (side > 0xf) { - side >>= 4; - } - - const center = (routePattern[i] & EdgeStyle.CENTER_MASK) > 0; - - if ((sou || tar) && side < 9) { - let limit = 0; - const souTar = sou ? 0 : 1; - - if (center && currentOrientation === 0) { - limit = geo[souTar][0] + constraint[souTar][0] * geo[souTar][2]; - } else if (center) { - limit = geo[souTar][1] + constraint[souTar][1] * geo[souTar][3]; - } else { - limit = EdgeStyle.limits[souTar][side]; - } - - if (currentOrientation === 0) { - const lastX = EdgeStyle.wayPoints1[currentIndex][0]; - const deltaX = (limit - lastX) * direction[0]; - - if (deltaX > 0) { - EdgeStyle.wayPoints1[currentIndex][0] += direction[0] * deltaX; - } - } else { - const lastY = EdgeStyle.wayPoints1[currentIndex][1]; - const deltaY = (limit - lastY) * direction[1]; - - if (deltaY > 0) { - EdgeStyle.wayPoints1[currentIndex][1] += direction[1] * deltaY; - } - } - } else if (center) { - // Which center we're travelling to depend on the current direction - EdgeStyle.wayPoints1[currentIndex][0] += - direction[0] * Math.abs(EdgeStyle.vertexSeperations[directionIndex] / 2); - EdgeStyle.wayPoints1[currentIndex][1] += - direction[1] * Math.abs(EdgeStyle.vertexSeperations[directionIndex] / 2); - } - - if ( - currentIndex > 0 && - EdgeStyle.wayPoints1[currentIndex][currentOrientation] === - EdgeStyle.wayPoints1[currentIndex - 1][currentOrientation] - ) { - currentIndex--; - } else { - lastOrientation = currentOrientation; - } - } - - for (let i = 0; i <= currentIndex; i += 1) { - if (i === currentIndex) { - // Last point can cause last segment to be in - // same direction as jetty/approach. If so, - // check the number of points is consistent - // with the relative orientation of source and target - // jx. Same orientation requires an even - // number of turns (points), different requires - // odd. - const targetOrientation = - (dir[1] & (DIRECTION_MASK.EAST | DIRECTION_MASK.WEST)) > 0 ? 0 : 1; - const sameOrient = targetOrientation === initialOrientation ? 0 : 1; - - // (currentIndex + 1) % 2 is 0 for even number of points, - // 1 for odd - if (sameOrient !== (currentIndex + 1) % 2) { - // The last point isn't required - break; - } - } - - result.push( - new Point( - Math.round(EdgeStyle.wayPoints1[i][0] * state.view.scale * 10) / 10, - Math.round(EdgeStyle.wayPoints1[i][1] * state.view.scale * 10) / 10 - ) - ); - } - - // Removes duplicates - let index = 1; - - while (index < result.length) { - if ( - result[index - 1] == null || - result[index] == null || - result[index - 1].x !== result[index].x || - result[index - 1].y !== result[index].y - ) { - index++; - } else { - result.splice(index, 1); - } - } - }; + static OrthConnector = OrthogonalConnectorFunction; /** * ManhattanConnector code is based on code from https://github.com/mwangm/mxgraph-manhattan-connector * * Implements router to find the shortest route that avoids cells using manhattan distance as metric. */ - static ManhattanConnector: EdgeStyleFunction = ( - state: CellState, - source: CellState, - target: CellState | null, - points: Point[], - result: Point[] - ) => { - /** - * Adds all values from source geometry to target. - * Used to create padding box around cell geometry. - * @param target - * @param source - * @returns - */ - function moveAndExpand(target: Rectangle, source: Rectangle): Rectangle { - target.x += source.x || 0; - target.y += source.y || 0; - target.width += source.width || 0; - target.height += source.height || 0; - return target; - } - - function snapCoordinateToGrid(value: number, gridSize: number) { - return gridSize * Math.round(value / gridSize); - } - - function snapPointToGrid(p: Point, gx: number, gy?: number) { - p.x = snapCoordinateToGrid(p.x, gx); - p.y = snapCoordinateToGrid(p.y, gy || gx); - return p; - } - - function isPointInRectangle(rect: Rectangle, p: Point) { - return ( - p.x >= rect.x && - p.x <= rect.x + rect.width && - p.y >= rect.y && - p.y <= rect.y + rect.height - ); - } - - function getRectangleCenter(rect: Rectangle): Point { - return new Point(rect.x + rect.width / 2, rect.y + rect.height / 2); - } - - function getDifferencePoint(p1: Point, p2: Point): Point { - return new Point(p1.x - p2.x, p1.y - p2.y); - } - - function movePoint(p: Point, moveX?: number, moveY?: number): Point { - p.x += moveX || 0; - p.y += moveY || 0; - return p; - } - - function getPointTheta(p1: Point, p2: Point) { - const p = p2.clone(); - const y = -(p.y - p1.y); - const x = p.x - p1.x; - const PRECISION = 10; - const rad = - y.toFixed(PRECISION) == '0' && x.toFixed(PRECISION) == '0' ? 0 : Math.atan2(y, x); - return (180 * rad) / Math.PI; - } - - function normalizePoint(point: Point) { - return new Point( - point.x === 0 ? 0 : Math.abs(point.x) / point.x, - point.y === 0 ? 0 : Math.abs(point.y) / point.y - ); - } - - function getManhattanDistance(p1: Point, p2: Point) { - return Math.abs(p2.x - p1.x) + Math.abs(p2.y - p1.y); - } - - function toPointFromString(pointString: string) { - const xy = pointString.split(pointString.indexOf('@') === -1 ? ' ' : '@'); - return new Point(parseInt(xy[0], 10), parseInt(xy[1], 10)); - } - - function pointToString(point: Point) { - return `${point.x}@${point.y}`; - } - - function getCellAbsoluteBounds(cellState: CellState) { - const graph = cellState.view.graph; - const cellBounds = graph.getCellBounds(cellState.cell, false, false)?.clone(); - if (!cellBounds) return undefined; - const view = graph.view; - const { scale, translate } = view; - const { x, y } = translate; - const round = (v: number) => Math.round(v * 10) / 10; - const res = new Rectangle( - round(cellBounds.x / scale - x), - round(cellBounds.y / scale - y), - round(cellBounds.width / scale), - round(cellBounds.height / scale) - ); - - return res; - } - - const mStep = ManhattanConnectorConfig.step; - - const config = { - // Padding applied on the element bounding boxes - paddingBox: new Geometry(-mStep, -mStep, mStep * 2, mStep * 2), - - // An array of directions to find next points on the route - directions: [ - { - offsetX: mStep, - offsetY: 0, - cost: mStep, - angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(mStep, 0))), - }, - { - offsetX: 0, - offsetY: mStep, - cost: mStep, - angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(0, mStep))), - }, - { - offsetX: -mStep, - offsetY: 0, - cost: mStep, - angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(-mStep, 0))), - }, - { - offsetX: 0, - offsetY: -mStep, - cost: mStep, - angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(0, -mStep))), - }, - ], - - directionMap: { - east: { x: 1, y: 0 }, - south: { x: 0, y: 1 }, - west: { x: -1, y: 0 }, - north: { x: 0, y: -1 }, - }, - - // A penalty received for direction change - penaltiesGenerator: (angle: number) => { - if (angle == 45 || angle == 90 || angle == 180) return mStep / 2; - return 0; - }, - // If a function is provided, it's used to route the link while dragging an end - // i.e. function(from, to, opts) { return []; } - draggingRoute: null, - - previousDirAngle: 0, - }; - - /** - * Map of obstacles - * Helper structure to identify whether a point lies in an obstacle. - */ - class ObstacleMap { - map: Map; - options: typeof config; - // tells how to divide the paper when creating the elements map - mapGridSize: number; - constructor(opt: any) { - this.options = opt; - this.mapGridSize = 100; - this.map = new Map(); - } - - // Builds a map of all elements for quicker obstacle queries - // The svg is divided to cells, where each of them holds an information which - // elements belong to it. When we query whether a point is in an obstacle we don't need - // to go through all obstacles, we check only those in a particular cell. - build(source: CellState | null, target: CellState | null) { - const graph = source?.view.graph || target?.view.graph; - if (!graph) return; - return Array.from(graph.getView().getCellStates()) - .filter((s) => s.cell && s.cell.isVertex() && !s.cell.isEdge()) - .map((s) => getCellAbsoluteBounds(s)) - .map((bbox) => (bbox ? moveAndExpand(bbox, this.options.paddingBox) : null)) - .forEach((bbox) => { - if (!bbox) return; - - const origin = snapPointToGrid(new Point(bbox.x, bbox.y), this.mapGridSize); - const corner = snapPointToGrid( - new Point(bbox.x + bbox.width, bbox.y + bbox.height), - this.mapGridSize - ); - - for (let x = origin.x; x <= corner.x; x += this.mapGridSize) { - for (let y = origin.y; y <= corner.y; y += this.mapGridSize) { - const gridKey = x + '@' + y; - - const rectArr = this.map.get(gridKey) || []; - if (!this.map.has(gridKey)) this.map.set(gridKey, rectArr); - rectArr.push(bbox); - } - } - }); - } - - isPointAccessible(point: Point): boolean { - const mapKey = pointToString(snapPointToGrid(point.clone(), this.mapGridSize)); - const obstacles = this.map.get(mapKey); - if (obstacles) { - return obstacles.every((obstacle) => !isPointInRectangle(obstacle, point)); - } - return true; - } - } - - class SortedSet { - items: string[]; - - hash: Map; - - constructor() { - this.items = []; - this.hash = new Map(); - } - - add(key: string, value: number) { - const hashItem = this.hash.get(key); - if (hashItem) { - hashItem.value = value; - this.items.splice(this.items.indexOf(key), 1); - } else { - this.hash.set(key, { - value, - open: true, - }); - } - - this.items.push(key); - this.items.sort((i1, i2) => { - const hashItem1 = this.hash.get(i1); - const hashItem2 = this.hash.get(i2); - if (!hashItem1 || !hashItem2) return 0; - return hashItem1.value - hashItem2.value; - }); - } - - remove(key: string) { - const hashItem = this.hash.get(key); - if (hashItem) hashItem.open = false; - } - - isOpen(key: string) { - const hashItem = this.hash.get(key); - return hashItem && hashItem.open == true; - } - - isClose(key: string) { - const hashItem = this.hash.get(key); - return hashItem && hashItem.open == false; - } - - isEmpty() { - return this.items.length == 0; - } - - pop(): string | undefined { - const key = this.items.shift(); - if (key) this.remove(key); - return key; - } - } - - function reconstructRoute( - parents: { [key: string]: Point }, - endPoint: Point, - startCenter: Point, - endCenter: Point - ) { - const route: Point[] = []; - let previousDirection = normalizePoint(getDifferencePoint(endCenter, endPoint)); - let current = endPoint; - let parent; - - while (parents[pointToString(current)]) { - parent = parents[pointToString(current)]; - if (!parent) continue; - const direction = normalizePoint(getDifferencePoint(current, parent)); - - // Add point in when direction change - if (!direction.equals(previousDirection)) { - route.unshift(current); - previousDirection = direction; - } - current = parent; - } - - const startDirection = normalizePoint(getDifferencePoint(current, startCenter)); - if (!startDirection.equals(previousDirection)) { - route.unshift(current); - } - - return route; - } - - function getRectPoints( - bbox: Rectangle, - directions: DirectionValue[], - opt: typeof config - ): Point[] { - const step = ManhattanConnectorConfig.step; - const center = getRectangleCenter(bbox); - const res: Point[] = []; - for (const direction of directions) { - const directionPoint = opt.directionMap[direction]; - - const x = (directionPoint.x * bbox.width) / 2; - const y = (directionPoint.y * bbox.height) / 2; - - const point = movePoint(center.clone(), x, y); - - if (isPointInRectangle(bbox, point)) { - movePoint(point, directionPoint.x * step, directionPoint.y * step); - } - - res.push(snapPointToGrid(point, step)); - } - return res; - } - - function normalizeAngle(angle: number) { - return (angle % 360) + (angle < 0 ? 360 : 0); - } - - function getDirectionAngle(start: Point, end: Point, directionLength: number) { - const q = 360 / directionLength; - return Math.floor(normalizeAngle(getPointTheta(start, end) + q / 2) / q) * q; - } - - function getDirectionChange(angle1: number, angle2: number) { - const dirChange = Math.abs(angle1 - angle2); - return dirChange > 180 ? 360 - dirChange : dirChange; - } - - function estimateCost(from: Point, endPoints: Point[]) { - let min = Infinity; - - for (let i = 0, len = endPoints.length; i < len; i++) { - const cost = getManhattanDistance(from, endPoints[i]); - if (cost < min) min = cost; - } - - return min; - } - - function alignPointToCell( - point: Point, - edgeState: CellState, - cellState: CellState, - isSourceCell: boolean - ) { - const cellBounds = getCellAbsoluteBounds(cellState); - - const y = isSourceCell ? edgeState.style.exitY : edgeState.style.entryY; - const onlyHorizontalDirections = isSourceCell - ? ManhattanConnectorConfig.startDirections.every( - (d) => d != DIRECTION.NORTH && d != DIRECTION.SOUTH - ) - : ManhattanConnectorConfig.endDirections.every( - (d) => d != DIRECTION.NORTH && d != DIRECTION.SOUTH - ); - - if (y != undefined && onlyHorizontalDirections) { - const cellHeight = cellBounds?.height || 0; - point.y = - cellBounds?.y != undefined - ? cellBounds?.y + cellHeight * y - : point.y - cellHeight / 2 + cellHeight * y; - } - - const x = isSourceCell ? edgeState.style.exitX : edgeState.style.entryX; - const onlyVerticalDirections = isSourceCell - ? ManhattanConnectorConfig.startDirections.every( - (d) => d != DIRECTION.WEST && d != DIRECTION.EAST - ) - : ManhattanConnectorConfig.endDirections.every( - (d) => d != DIRECTION.WEST && d != DIRECTION.EAST - ); - if (x != undefined && onlyVerticalDirections) { - const cellWidth = cellBounds?.width || 0; - point.x = - cellBounds?.x != undefined - ? cellBounds?.x + cellWidth * x - : point.x - cellWidth / 2 + cellWidth * (x || 0); - } - } - - function findRoute( - start: Rectangle, - end: Rectangle, - obstacleMap: ObstacleMap, - opt: typeof config - ) { - // Calculate start points and end points - const step = ManhattanConnectorConfig.step; - const startPoints = getRectPoints( - start, - ManhattanConnectorConfig.startDirections, - opt - ).filter((p) => obstacleMap.isPointAccessible(p)); - - const startCenter = snapPointToGrid(getRectangleCenter(start), step); - const endPoints = getRectPoints( - end, - ManhattanConnectorConfig.endDirections, - opt - ).filter((p) => obstacleMap.isPointAccessible(p)); - const endCenter = snapPointToGrid(getRectangleCenter(end), step); - if (startPoints.length > 0 && endPoints.length > 0) { - // The set of possible points to be evaluated, initially containing the start points. - const openSet = new SortedSet(); - // Keeps predecessor of given element. - const parents: { [key: string]: Point } = {}; - // Cost from start to a point along best known path. - const costs: { [key: string]: number } = {}; - - startPoints.forEach((p) => { - const key = pointToString(p); - openSet.add(key, estimateCost(p, endPoints)); - costs[key] = 0; - }); - let loopsRemain = ManhattanConnectorConfig.maxLoops; - const endPointsKeys = endPoints.map((p) => pointToString(p)); - let currentDirectionAngle: number | undefined; - let previousDirectionAngle: number | undefined; - // Main route finding loop - while (!openSet.isEmpty() && loopsRemain > 0) { - const currentKey = openSet.pop(); - if (currentKey == undefined) { - continue; - } - - const currentPoint = toPointFromString(currentKey); - const currentCost = costs[currentKey]; - previousDirectionAngle = currentDirectionAngle; - currentDirectionAngle = parents[currentKey] - ? getDirectionAngle(parents[currentKey], currentPoint, opt.directions.length) - : opt.previousDirAngle != 0 - ? opt.previousDirAngle - : getDirectionAngle(startCenter, currentPoint, opt.directions.length); - - // if get the endpoint - if (endPointsKeys.indexOf(currentKey) >= 0) { - // stop route to enter the end point in opposite direction. - const directionChangedAngle = getDirectionChange( - currentDirectionAngle, - getDirectionAngle(currentPoint, endCenter, opt.directions.length) - ); - if (currentPoint.equals(endCenter) || directionChangedAngle < 180) { - opt.previousDirAngle = currentDirectionAngle; - return reconstructRoute(parents, currentPoint, startCenter, endCenter); - } - } - - // Go over all possible directions and find neighbors. - for (let i = 0; i < opt.directions.length; i++) { - const direction = opt.directions[i]; - const directionChangedAngle = getDirectionChange( - currentDirectionAngle, - direction.angle - ); - if ( - previousDirectionAngle && - directionChangedAngle > ManhattanConnectorConfig.maxAllowedDirectionChange - ) { - continue; - } - - const neighborPoint = movePoint( - currentPoint.clone(), - direction.offsetX, - direction.offsetY - ); - const neighborKey = pointToString(neighborPoint); - if ( - openSet.isClose(neighborKey) || - !obstacleMap.isPointAccessible(neighborPoint) - ) { - continue; - } - - const costFromStart = - currentCost + - direction.cost + - opt.penaltiesGenerator(directionChangedAngle); - - if (!openSet.isOpen(neighborKey) || costFromStart < costs[neighborKey]) { - // Neighbor point has not been processed yet or the cost of the path - // from start is lesser than previously calcluated. - parents[neighborKey] = currentPoint; - costs[neighborKey] = costFromStart; - openSet.add( - neighborKey, - costFromStart + estimateCost(neighborPoint, endPoints) - ); - } - } - - loopsRemain--; - } - return null; - } - return null; - } - - function router( - state: CellState, - source: CellState, - target: CellState | null, - points: Point[], - result: Point[], - opt: typeof config - ) { - // If edge is dragged after calculation, points will be filled, so fallback to SegmentConnector - if ((points != null && points.length > 0) || source == null || target == null) { - EdgeStyle.SegmentConnector(state, source, target, points, result); - return; - } - - let sourceBBox = getCellAbsoluteBounds(source); - sourceBBox = sourceBBox ? moveAndExpand(sourceBBox, opt.paddingBox) : undefined; - let targetBBox = getCellAbsoluteBounds(target); - targetBBox = targetBBox ? moveAndExpand(targetBBox, opt.paddingBox) : undefined; - const obstacleMap = new ObstacleMap(opt); - - obstacleMap.build(source, target); - if (!sourceBBox || !targetBBox) { - // Fallback to OrthConnector - return EdgeStyle.OrthConnector(state, source, target, points, result); - } - const routePoints = findRoute(sourceBBox, targetBBox, obstacleMap, opt); - - if (routePoints == null || routePoints.length == 0) { - // Fallback to OrthConnector - return EdgeStyle.OrthConnector(state, source, target, points, result); - } - if (state.style) { - if (state.visibleSourceState && routePoints.length > 0) { - // If there are at least one point, align it to source cell - alignPointToCell(routePoints[0], state, state.visibleSourceState, true); - } - - if (state.visibleTargetState && routePoints.length > 1) { - // If there are more than one point, align last point to target cell - alignPointToCell( - routePoints[routePoints.length - 1], - state, - state.visibleTargetState, - false - ); - } - } - - // Scaling and translating result points - const scale = state.view.scale; - routePoints.forEach((pt) => - result.push( - new Point( - Math.round((pt.x + state.view.translate.x) * scale * 10) / 10, - Math.round((pt.y + state.view.translate.y) * scale * 10) / 10 - ) - ) - ); - } - - router(state, source, target, points, result, config); - }; - - static getRoutePattern( - dir: number[], - quad: number, - dx: number, - dy: number - ): number[] | null { - let sourceIndex = dir[0] === DIRECTION_MASK.EAST ? 3 : dir[0]; - let targetIndex = dir[1] === DIRECTION_MASK.EAST ? 3 : dir[1]; - - sourceIndex -= quad; - targetIndex -= quad; - - if (sourceIndex < 1) { - sourceIndex += 4; - } - if (targetIndex < 1) { - targetIndex += 4; - } - - let result: number[] | null = - EdgeStyle.routePatterns[sourceIndex - 1][targetIndex - 1]; - - if (dx === 0 || dy === 0) { - if (EdgeStyle.inlineRoutePatterns[sourceIndex - 1][targetIndex - 1] != null) { - result = EdgeStyle.inlineRoutePatterns[sourceIndex - 1][targetIndex - 1]; - } - } - return result; - } + static ManhattanConnector = ManhattanConnectorFunction; } export default EdgeStyle; diff --git a/packages/core/src/view/style/config.ts b/packages/core/src/view/style/config.ts index cf15b25ff8..89c50649a8 100644 --- a/packages/core/src/view/style/config.ts +++ b/packages/core/src/view/style/config.ts @@ -19,7 +19,7 @@ import { shallowCopy } from '../../util/cloneUtils'; import type { DirectionValue } from '../../types'; /** - * Configure the `Entity Relation connector` defaults for maxGraph. + * Configure the {@link EdgeStyle.EntityRelation} connector. * * @experimental subject to change or removal. maxGraph's global configuration may be modified in the future without prior notice. * @since 0.15.0 @@ -47,7 +47,7 @@ export const resetEntityRelationConnectorConfig = (): void => { }; /** - * Configure the {@link OrthConnector}. + * Configure the {@link EdgeStyle.OrthConnector}. * * @experimental subject to change or removal. maxGraph's global configuration may be modified in the future without prior notice. * @since 0.16.0 @@ -114,7 +114,7 @@ export type ManhattanConnectorConfigType = { }; /** - * Configure the {@link ManhattanConnector}. + * Configure the {@link EdgeStyle.ManhattanConnector}. * * @experimental subject to change or removal. maxGraph's global configuration may be modified in the future without prior notice. * @since 0.16.0 diff --git a/packages/core/src/view/style/edge/Manhattan.ts b/packages/core/src/view/style/edge/Manhattan.ts new file mode 100644 index 0000000000..5a76b9f9ae --- /dev/null +++ b/packages/core/src/view/style/edge/Manhattan.ts @@ -0,0 +1,603 @@ +/* +Copyright 2023-present The maxGraph project Contributors + +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. +*/ + +import type { DirectionValue, EdgeStyleFunction } from '../../../types'; +import type CellState from '../../cell/CellState'; +import Point from '../../geometry/Point'; +import Rectangle from '../../geometry/Rectangle'; +import { ManhattanConnectorConfig } from '../config'; +import Geometry from '../../geometry/Geometry'; +import { DIRECTION } from '../../../util/Constants'; +import { OrthogonalConnector } from './Orthogonal'; +import { SegmentConnector } from './Segment'; + +/** + * ManhattanConnector code is based on code from https://github.com/mwangm/mxgraph-manhattan-connector + * + * Implements router to find the shortest route that avoids cells using manhattan distance as metric. + */ +export const ManhattanConnector: EdgeStyleFunction = ( + state: CellState, + source: CellState, + target: CellState | null, + points: Point[], + result: Point[] +) => { + /** + * Adds all values from source geometry to target. + * Used to create padding box around cell geometry. + * @param target + * @param source + * @returns + */ + function moveAndExpand(target: Rectangle, source: Rectangle): Rectangle { + target.x += source.x || 0; + target.y += source.y || 0; + target.width += source.width || 0; + target.height += source.height || 0; + return target; + } + + function snapCoordinateToGrid(value: number, gridSize: number) { + return gridSize * Math.round(value / gridSize); + } + + function snapPointToGrid(p: Point, gx: number, gy?: number) { + p.x = snapCoordinateToGrid(p.x, gx); + p.y = snapCoordinateToGrid(p.y, gy || gx); + return p; + } + + function isPointInRectangle(rect: Rectangle, p: Point) { + return ( + p.x >= rect.x && + p.x <= rect.x + rect.width && + p.y >= rect.y && + p.y <= rect.y + rect.height + ); + } + + function getRectangleCenter(rect: Rectangle): Point { + return new Point(rect.x + rect.width / 2, rect.y + rect.height / 2); + } + + function getDifferencePoint(p1: Point, p2: Point): Point { + return new Point(p1.x - p2.x, p1.y - p2.y); + } + + function movePoint(p: Point, moveX?: number, moveY?: number): Point { + p.x += moveX || 0; + p.y += moveY || 0; + return p; + } + + function getPointTheta(p1: Point, p2: Point) { + const p = p2.clone(); + const y = -(p.y - p1.y); + const x = p.x - p1.x; + const PRECISION = 10; + const rad = + y.toFixed(PRECISION) == '0' && x.toFixed(PRECISION) == '0' ? 0 : Math.atan2(y, x); + return (180 * rad) / Math.PI; + } + + function normalizePoint(point: Point) { + return new Point( + point.x === 0 ? 0 : Math.abs(point.x) / point.x, + point.y === 0 ? 0 : Math.abs(point.y) / point.y + ); + } + + function getManhattanDistance(p1: Point, p2: Point) { + return Math.abs(p2.x - p1.x) + Math.abs(p2.y - p1.y); + } + + function toPointFromString(pointString: string) { + const xy = pointString.split(pointString.indexOf('@') === -1 ? ' ' : '@'); + return new Point(parseInt(xy[0], 10), parseInt(xy[1], 10)); + } + + function pointToString(point: Point) { + return `${point.x}@${point.y}`; + } + + function getCellAbsoluteBounds(cellState: CellState) { + const graph = cellState.view.graph; + const cellBounds = graph.getCellBounds(cellState.cell, false, false)?.clone(); + if (!cellBounds) return undefined; + const view = graph.view; + const { scale, translate } = view; + const { x, y } = translate; + const round = (v: number) => Math.round(v * 10) / 10; + const res = new Rectangle( + round(cellBounds.x / scale - x), + round(cellBounds.y / scale - y), + round(cellBounds.width / scale), + round(cellBounds.height / scale) + ); + + return res; + } + + const mStep = ManhattanConnectorConfig.step; + + const config = { + // Padding applied on the element bounding boxes + paddingBox: new Geometry(-mStep, -mStep, mStep * 2, mStep * 2), + + // An array of directions to find next points on the route + directions: [ + { + offsetX: mStep, + offsetY: 0, + cost: mStep, + angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(mStep, 0))), + }, + { + offsetX: 0, + offsetY: mStep, + cost: mStep, + angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(0, mStep))), + }, + { + offsetX: -mStep, + offsetY: 0, + cost: mStep, + angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(-mStep, 0))), + }, + { + offsetX: 0, + offsetY: -mStep, + cost: mStep, + angle: normalizeAngle(getPointTheta(new Point(0, 0), new Point(0, -mStep))), + }, + ], + + directionMap: { + east: { x: 1, y: 0 }, + south: { x: 0, y: 1 }, + west: { x: -1, y: 0 }, + north: { x: 0, y: -1 }, + }, + + // A penalty received for direction change + penaltiesGenerator: (angle: number) => { + if (angle == 45 || angle == 90 || angle == 180) return mStep / 2; + return 0; + }, + // If a function is provided, it's used to route the link while dragging an end + // i.e. function(from, to, opts) { return []; } + draggingRoute: null, + + previousDirAngle: 0, + }; + + /** + * Map of obstacles + * Helper structure to identify whether a point lies in an obstacle. + */ + class ObstacleMap { + map: Map; + options: typeof config; + // tells how to divide the paper when creating the elements map + mapGridSize: number; + constructor(opt: any) { + this.options = opt; + this.mapGridSize = 100; + this.map = new Map(); + } + + // Builds a map of all elements for quicker obstacle queries + // The svg is divided to cells, where each of them holds an information which + // elements belong to it. When we query whether a point is in an obstacle we don't need + // to go through all obstacles, we check only those in a particular cell. + build(source: CellState | null, target: CellState | null) { + const graph = source?.view.graph || target?.view.graph; + if (!graph) return; + return Array.from(graph.getView().getCellStates()) + .filter((s) => s.cell && s.cell.isVertex() && !s.cell.isEdge()) + .map((s) => getCellAbsoluteBounds(s)) + .map((bbox) => (bbox ? moveAndExpand(bbox, this.options.paddingBox) : null)) + .forEach((bbox) => { + if (!bbox) return; + + const origin = snapPointToGrid(new Point(bbox.x, bbox.y), this.mapGridSize); + const corner = snapPointToGrid( + new Point(bbox.x + bbox.width, bbox.y + bbox.height), + this.mapGridSize + ); + + for (let x = origin.x; x <= corner.x; x += this.mapGridSize) { + for (let y = origin.y; y <= corner.y; y += this.mapGridSize) { + const gridKey = x + '@' + y; + + const rectArr = this.map.get(gridKey) || []; + if (!this.map.has(gridKey)) this.map.set(gridKey, rectArr); + rectArr.push(bbox); + } + } + }); + } + + isPointAccessible(point: Point): boolean { + const mapKey = pointToString(snapPointToGrid(point.clone(), this.mapGridSize)); + const obstacles = this.map.get(mapKey); + if (obstacles) { + return obstacles.every((obstacle) => !isPointInRectangle(obstacle, point)); + } + return true; + } + } + + class SortedSet { + items: string[]; + + hash: Map; + + constructor() { + this.items = []; + this.hash = new Map(); + } + + add(key: string, value: number) { + const hashItem = this.hash.get(key); + if (hashItem) { + hashItem.value = value; + this.items.splice(this.items.indexOf(key), 1); + } else { + this.hash.set(key, { + value, + open: true, + }); + } + + this.items.push(key); + this.items.sort((i1, i2) => { + const hashItem1 = this.hash.get(i1); + const hashItem2 = this.hash.get(i2); + if (!hashItem1 || !hashItem2) return 0; + return hashItem1.value - hashItem2.value; + }); + } + + remove(key: string) { + const hashItem = this.hash.get(key); + if (hashItem) hashItem.open = false; + } + + isOpen(key: string) { + const hashItem = this.hash.get(key); + return hashItem && hashItem.open == true; + } + + isClose(key: string) { + const hashItem = this.hash.get(key); + return hashItem && hashItem.open == false; + } + + isEmpty() { + return this.items.length == 0; + } + + pop(): string | undefined { + const key = this.items.shift(); + if (key) this.remove(key); + return key; + } + } + + function reconstructRoute( + parents: { [key: string]: Point }, + endPoint: Point, + startCenter: Point, + endCenter: Point + ) { + const route: Point[] = []; + let previousDirection = normalizePoint(getDifferencePoint(endCenter, endPoint)); + let current = endPoint; + let parent; + + while (parents[pointToString(current)]) { + parent = parents[pointToString(current)]; + if (!parent) continue; + const direction = normalizePoint(getDifferencePoint(current, parent)); + + // Add point in when direction change + if (!direction.equals(previousDirection)) { + route.unshift(current); + previousDirection = direction; + } + current = parent; + } + + const startDirection = normalizePoint(getDifferencePoint(current, startCenter)); + if (!startDirection.equals(previousDirection)) { + route.unshift(current); + } + + return route; + } + + function getRectPoints( + bbox: Rectangle, + directions: DirectionValue[], + opt: typeof config + ): Point[] { + const step = ManhattanConnectorConfig.step; + const center = getRectangleCenter(bbox); + const res: Point[] = []; + for (const direction of directions) { + const directionPoint = opt.directionMap[direction]; + + const x = (directionPoint.x * bbox.width) / 2; + const y = (directionPoint.y * bbox.height) / 2; + + const point = movePoint(center.clone(), x, y); + + if (isPointInRectangle(bbox, point)) { + movePoint(point, directionPoint.x * step, directionPoint.y * step); + } + + res.push(snapPointToGrid(point, step)); + } + return res; + } + + function normalizeAngle(angle: number) { + return (angle % 360) + (angle < 0 ? 360 : 0); + } + + function getDirectionAngle(start: Point, end: Point, directionLength: number) { + const q = 360 / directionLength; + return Math.floor(normalizeAngle(getPointTheta(start, end) + q / 2) / q) * q; + } + + function getDirectionChange(angle1: number, angle2: number) { + const dirChange = Math.abs(angle1 - angle2); + return dirChange > 180 ? 360 - dirChange : dirChange; + } + + function estimateCost(from: Point, endPoints: Point[]) { + let min = Infinity; + + for (let i = 0, len = endPoints.length; i < len; i++) { + const cost = getManhattanDistance(from, endPoints[i]); + if (cost < min) min = cost; + } + + return min; + } + + function alignPointToCell( + point: Point, + edgeState: CellState, + cellState: CellState, + isSourceCell: boolean + ) { + const cellBounds = getCellAbsoluteBounds(cellState); + + const y = isSourceCell ? edgeState.style.exitY : edgeState.style.entryY; + const onlyHorizontalDirections = isSourceCell + ? ManhattanConnectorConfig.startDirections.every( + (d) => d != DIRECTION.NORTH && d != DIRECTION.SOUTH + ) + : ManhattanConnectorConfig.endDirections.every( + (d) => d != DIRECTION.NORTH && d != DIRECTION.SOUTH + ); + + if (y != undefined && onlyHorizontalDirections) { + const cellHeight = cellBounds?.height || 0; + point.y = + cellBounds?.y != undefined + ? cellBounds?.y + cellHeight * y + : point.y - cellHeight / 2 + cellHeight * y; + } + + const x = isSourceCell ? edgeState.style.exitX : edgeState.style.entryX; + const onlyVerticalDirections = isSourceCell + ? ManhattanConnectorConfig.startDirections.every( + (d) => d != DIRECTION.WEST && d != DIRECTION.EAST + ) + : ManhattanConnectorConfig.endDirections.every( + (d) => d != DIRECTION.WEST && d != DIRECTION.EAST + ); + if (x != undefined && onlyVerticalDirections) { + const cellWidth = cellBounds?.width || 0; + point.x = + cellBounds?.x != undefined + ? cellBounds?.x + cellWidth * x + : point.x - cellWidth / 2 + cellWidth * (x || 0); + } + } + + function findRoute( + start: Rectangle, + end: Rectangle, + obstacleMap: ObstacleMap, + opt: typeof config + ) { + // Calculate start points and end points + const step = ManhattanConnectorConfig.step; + const startPoints = getRectPoints( + start, + ManhattanConnectorConfig.startDirections, + opt + ).filter((p) => obstacleMap.isPointAccessible(p)); + + const startCenter = snapPointToGrid(getRectangleCenter(start), step); + const endPoints = getRectPoints( + end, + ManhattanConnectorConfig.endDirections, + opt + ).filter((p) => obstacleMap.isPointAccessible(p)); + const endCenter = snapPointToGrid(getRectangleCenter(end), step); + if (startPoints.length > 0 && endPoints.length > 0) { + // The set of possible points to be evaluated, initially containing the start points. + const openSet = new SortedSet(); + // Keeps predecessor of given element. + const parents: { [key: string]: Point } = {}; + // Cost from start to a point along best known path. + const costs: { [key: string]: number } = {}; + + startPoints.forEach((p) => { + const key = pointToString(p); + openSet.add(key, estimateCost(p, endPoints)); + costs[key] = 0; + }); + let loopsRemain = ManhattanConnectorConfig.maxLoops; + const endPointsKeys = endPoints.map((p) => pointToString(p)); + let currentDirectionAngle: number | undefined; + let previousDirectionAngle: number | undefined; + // Main route finding loop + while (!openSet.isEmpty() && loopsRemain > 0) { + const currentKey = openSet.pop(); + if (currentKey == undefined) { + continue; + } + + const currentPoint = toPointFromString(currentKey); + const currentCost = costs[currentKey]; + previousDirectionAngle = currentDirectionAngle; + currentDirectionAngle = parents[currentKey] + ? getDirectionAngle(parents[currentKey], currentPoint, opt.directions.length) + : opt.previousDirAngle != 0 + ? opt.previousDirAngle + : getDirectionAngle(startCenter, currentPoint, opt.directions.length); + + // if get the endpoint + if (endPointsKeys.indexOf(currentKey) >= 0) { + // stop route to enter the end point in opposite direction. + const directionChangedAngle = getDirectionChange( + currentDirectionAngle, + getDirectionAngle(currentPoint, endCenter, opt.directions.length) + ); + if (currentPoint.equals(endCenter) || directionChangedAngle < 180) { + opt.previousDirAngle = currentDirectionAngle; + return reconstructRoute(parents, currentPoint, startCenter, endCenter); + } + } + + // Go over all possible directions and find neighbors. + for (let i = 0; i < opt.directions.length; i++) { + const direction = opt.directions[i]; + const directionChangedAngle = getDirectionChange( + currentDirectionAngle, + direction.angle + ); + if ( + previousDirectionAngle && + directionChangedAngle > ManhattanConnectorConfig.maxAllowedDirectionChange + ) { + continue; + } + + const neighborPoint = movePoint( + currentPoint.clone(), + direction.offsetX, + direction.offsetY + ); + const neighborKey = pointToString(neighborPoint); + if ( + openSet.isClose(neighborKey) || + !obstacleMap.isPointAccessible(neighborPoint) + ) { + continue; + } + + const costFromStart = + currentCost + direction.cost + opt.penaltiesGenerator(directionChangedAngle); + + if (!openSet.isOpen(neighborKey) || costFromStart < costs[neighborKey]) { + // Neighbor point has not been processed yet or the cost of the path + // from start is lesser than previously calcluated. + parents[neighborKey] = currentPoint; + costs[neighborKey] = costFromStart; + openSet.add( + neighborKey, + costFromStart + estimateCost(neighborPoint, endPoints) + ); + } + } + + loopsRemain--; + } + return null; + } + return null; + } + + function router( + state: CellState, + source: CellState, + target: CellState | null, + points: Point[], + result: Point[], + opt: typeof config + ) { + // If edge is dragged after calculation, points will be filled, so fallback to SegmentConnector + if ((points != null && points.length > 0) || source == null || target == null) { + SegmentConnector(state, source, target, points, result); + return; + } + + let sourceBBox = getCellAbsoluteBounds(source); + sourceBBox = sourceBBox ? moveAndExpand(sourceBBox, opt.paddingBox) : undefined; + let targetBBox = getCellAbsoluteBounds(target); + targetBBox = targetBBox ? moveAndExpand(targetBBox, opt.paddingBox) : undefined; + const obstacleMap = new ObstacleMap(opt); + + obstacleMap.build(source, target); + if (!sourceBBox || !targetBBox) { + // Fallback to OrthogonalConnector + return OrthogonalConnector(state, source, target, points, result); + } + const routePoints = findRoute(sourceBBox, targetBBox, obstacleMap, opt); + + if (routePoints == null || routePoints.length == 0) { + // Fallback to OrthogonalConnector + return OrthogonalConnector(state, source, target, points, result); + } + if (state.style) { + if (state.visibleSourceState && routePoints.length > 0) { + // If there are at least one point, align it to source cell + alignPointToCell(routePoints[0], state, state.visibleSourceState, true); + } + + if (state.visibleTargetState && routePoints.length > 1) { + // If there are more than one point, align last point to target cell + alignPointToCell( + routePoints[routePoints.length - 1], + state, + state.visibleTargetState, + false + ); + } + } + + // Scaling and translating result points + const scale = state.view.scale; + routePoints.forEach((pt) => + result.push( + new Point( + Math.round((pt.x + state.view.translate.x) * scale * 10) / 10, + Math.round((pt.y + state.view.translate.y) * scale * 10) / 10 + ) + ) + ); + } + + router(state, source, target, points, result, config); +}; diff --git a/packages/core/src/view/style/edge/Orthogonal.ts b/packages/core/src/view/style/edge/Orthogonal.ts new file mode 100644 index 0000000000..a3e0ef10a8 --- /dev/null +++ b/packages/core/src/view/style/edge/Orthogonal.ts @@ -0,0 +1,661 @@ +/* +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. +*/ + +import { scaleCellState, scalePointArray } from './shared'; +import { DEFAULT_MARKERSIZE, DIRECTION_MASK, NONE } from '../../../util/Constants'; +import { + getBoundingBox, + getPortConstraints, + reversePortConstraints, +} from '../../../util/mathUtils'; +import type CellState from '../../cell/CellState'; +import { OrthConnectorConfig } from '../config'; +import type { EdgeStyleFunction } from '../../../types'; +import Point from '../../geometry/Point'; +import Rectangle from '../../geometry/Rectangle'; +import { SegmentConnector } from './Segment'; + +const dirVectors = [ + [-1, 0], + [0, -1], + [1, 0], + [0, 1], + [-1, 0], + [0, -1], + [1, 0], +]; + +const wayPoints1 = [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], +]; + +const routePatterns = [ + [ + [513, 2308, 2081, 2562], + [513, 1090, 514, 2184, 2114, 2561], + [513, 1090, 514, 2564, 2184, 2562], + [513, 2308, 2561, 1090, 514, 2568, 2308], + ], + [ + [514, 1057, 513, 2308, 2081, 2562], + [514, 2184, 2114, 2561], + [514, 2184, 2562, 1057, 513, 2564, 2184], + [514, 1057, 513, 2568, 2308, 2561], + ], + [ + [1090, 514, 1057, 513, 2308, 2081, 2562], + [2114, 2561], + [1090, 2562, 1057, 513, 2564, 2184], + [1090, 514, 1057, 513, 2308, 2561, 2568], + ], + [ + [2081, 2562], + [1057, 513, 1090, 514, 2184, 2114, 2561], + [1057, 513, 1090, 514, 2184, 2562, 2564], + [1057, 2561, 1090, 514, 2568, 2308], + ], +]; + +const vertexSeparations: any = []; + +const limits = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], +]; + +// const LEFT_MASK = 32; +// +// const TOP_MASK = 64; +// +// const RIGHT_MASK = 128; +// +// const BOTTOM_MASK = 256; +// +// const LEFT = 1; +// +// const TOP = 2; +// +// const RIGHT = 4; +// +// const BOTTOM = 8; + +// TODO remove magic numbers +const SIDE_MASK = 480; +// LEFT_MASK | TOP_MASK | RIGHT_MASK | BOTTOM_MASK, + +const CENTER_MASK = 512; + +const SOURCE_MASK = 1024; + +const TARGET_MASK = 2048; + +const VERTEX_MASK = 3072; +// SOURCE_MASK | TARGET_MASK, + +function getJettySize(state: CellState, isSource: boolean): number { + const buffer = OrthConnectorConfig.buffer; + let value = + (isSource ? state.style.sourceJettySize : state.style.targetJettySize) ?? + state.style.jettySize ?? + buffer; + + if (value === 'auto') { + // Computes the automatic jetty size + const type = (isSource ? state.style.startArrow : state.style.endArrow) ?? NONE; + + if (type !== NONE) { + const size = + (isSource ? state.style.startSize : state.style.endSize) ?? DEFAULT_MARKERSIZE; + value = Math.max(2, Math.ceil((size + buffer) / buffer)) * buffer; + } else { + value = 2 * buffer; + } + } + + return value; +} + +/** + * Implements a local orthogonal router between the given cells. + * + * @param state {@link CellState} that represents the edge to be updated. + * @param sourceScaled {@link CellState} that represents the source terminal. + * @param targetScaled {@link CellState} that represents the target terminal. + * @param controlHints List of relative control {@link Point}s. + * @param result Array of {@link Point}s that represent the actual points of the + * edge. + */ +export const OrthogonalConnector: EdgeStyleFunction = ( + state: CellState, + sourceScaled: CellState, + targetScaled: CellState | null, + controlHints: Point[], + result: Point[] +) => { + // TODO: Figure out what to do when there are nulls in `pts`! + const pts = ( + (scalePointArray(state.absolutePoints, state.view.scale)) + ); + const source = scaleCellState(sourceScaled, state.view.scale); + const target = scaleCellState(targetScaled, state.view.scale); + + const sourceEdge = source == null ? false : source.cell.isEdge(); + const targetEdge = target == null ? false : target.cell.isEdge(); + + const p0 = pts[0]; + const pe = pts[pts.length - 1]; + + let sourceX = source != null ? source.x : p0.x; + let sourceY = source != null ? source.y : p0.y; + let sourceWidth = source != null ? source.width : 0; + let sourceHeight = source != null ? source.height : 0; + + let targetX = target != null ? target.x : pe.x; + let targetY = target != null ? target.y : pe.y; + let targetWidth = target != null ? target.width : 0; + let targetHeight = target != null ? target.height : 0; + + let sourceBuffer = getJettySize(state, true); + let targetBuffer = getJettySize(state, false); + + // Workaround for loop routing within buffer zone + if (source != null && target === source) { + targetBuffer = Math.max(sourceBuffer, targetBuffer); + sourceBuffer = targetBuffer; + } + + const totalBuffer = targetBuffer + sourceBuffer; + + let tooShort = false; + + // Checks minimum distance for fixed points and falls back to segment connector + if (p0 != null && pe != null) { + const dx = pe.x - p0.x; + const dy = pe.y - p0.y; + + tooShort = dx * dx + dy * dy < totalBuffer * totalBuffer; + } + + if ( + tooShort || + (OrthConnectorConfig.pointsFallback && + controlHints != null && + controlHints.length > 0) || + sourceEdge || + targetEdge + ) { + SegmentConnector(state, sourceScaled, targetScaled, controlHints, result); + return; + } + + // Determine the side(s) of the source and target vertices + // that the edge may connect to + // portConstraint [source, target] + const portConstraint = [DIRECTION_MASK.ALL, DIRECTION_MASK.ALL]; + let rotation = 0; + + if (source != null) { + portConstraint[0] = getPortConstraints(source, state, true, DIRECTION_MASK.ALL); + rotation = source.style.rotation ?? 0; + + if (rotation !== 0) { + const newRect = ( + getBoundingBox( + new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight), + rotation + ) + ); + sourceX = newRect.x; + sourceY = newRect.y; + sourceWidth = newRect.width; + sourceHeight = newRect.height; + } + } + + if (target != null) { + portConstraint[1] = getPortConstraints(target, state, false, DIRECTION_MASK.ALL); + rotation = target.style.rotation ?? 0; + + if (rotation !== 0) { + const newRect = ( + getBoundingBox( + new Rectangle(targetX, targetY, targetWidth, targetHeight), + rotation + ) + ); + targetX = newRect.x; + targetY = newRect.y; + targetWidth = newRect.width; + targetHeight = newRect.height; + } + } + + const dir = [0, 0]; + + // Work out which faces of the vertices present against each other + // in a way that would allow a 3-segment connection if port constraints + // permitted. + // geo -> [source, target] [x, y, width, height] + const geo = [ + [sourceX, sourceY, sourceWidth, sourceHeight], + [targetX, targetY, targetWidth, targetHeight], + ]; + const buffer = [sourceBuffer, targetBuffer]; + + for (let i = 0; i < 2; i += 1) { + limits[i][1] = geo[i][0] - buffer[i]; + limits[i][2] = geo[i][1] - buffer[i]; + limits[i][4] = geo[i][0] + geo[i][2] + buffer[i]; + limits[i][8] = geo[i][1] + geo[i][3] + buffer[i]; + } + + // Work out which quad the target is in + const sourceCenX = geo[0][0] + geo[0][2] / 2.0; + const sourceCenY = geo[0][1] + geo[0][3] / 2.0; + const targetCenX = geo[1][0] + geo[1][2] / 2.0; + const targetCenY = geo[1][1] + geo[1][3] / 2.0; + + const dx = sourceCenX - targetCenX; + const dy = sourceCenY - targetCenY; + + let quad = 0; + + // 0 | 1 + // ----- + // 3 | 2 + + if (dx < 0) { + if (dy < 0) { + quad = 2; + } else { + quad = 1; + } + } else if (dy <= 0) { + quad = 3; + + // Special case on x = 0 and negative y + if (dx === 0) { + quad = 2; + } + } + + // Check for connection constraints + let currentTerm = null; + + if (source != null) { + currentTerm = p0; + } + + const constraint = [ + [0.5, 0.5], + [0.5, 0.5], + ]; + + for (let i = 0; i < 2; i += 1) { + if (currentTerm != null) { + constraint[i][0] = (currentTerm.x - geo[i][0]) / geo[i][2]; + + if (Math.abs(currentTerm.x - geo[i][0]) <= 1) { + dir[i] = DIRECTION_MASK.WEST; + } else if (Math.abs(currentTerm.x - geo[i][0] - geo[i][2]) <= 1) { + dir[i] = DIRECTION_MASK.EAST; + } + + constraint[i][1] = (currentTerm.y - geo[i][1]) / geo[i][3]; + + if (Math.abs(currentTerm.y - geo[i][1]) <= 1) { + dir[i] = DIRECTION_MASK.NORTH; + } else if (Math.abs(currentTerm.y - geo[i][1] - geo[i][3]) <= 1) { + dir[i] = DIRECTION_MASK.SOUTH; + } + } + + currentTerm = null; + + if (target != null) { + currentTerm = pe; + } + } + + const sourceTopDist = geo[0][1] - (geo[1][1] + geo[1][3]); + const sourceLeftDist = geo[0][0] - (geo[1][0] + geo[1][2]); + const sourceBottomDist = geo[1][1] - (geo[0][1] + geo[0][3]); + const sourceRightDist = geo[1][0] - (geo[0][0] + geo[0][2]); + + vertexSeparations[1] = Math.max(sourceLeftDist - totalBuffer, 0); + vertexSeparations[2] = Math.max(sourceTopDist - totalBuffer, 0); + vertexSeparations[4] = Math.max(sourceBottomDist - totalBuffer, 0); + vertexSeparations[3] = Math.max(sourceRightDist - totalBuffer, 0); + + //= ============================================================= + // Start of source and target direction determination + + // Work through the preferred orientations by relative positioning + // of the vertices and list them in preferred and available order + + const dirPref = []; + const horPref = []; + const vertPref = []; + + horPref[0] = + sourceLeftDist >= sourceRightDist ? DIRECTION_MASK.WEST : DIRECTION_MASK.EAST; + vertPref[0] = + sourceTopDist >= sourceBottomDist ? DIRECTION_MASK.NORTH : DIRECTION_MASK.SOUTH; + + horPref[1] = reversePortConstraints(horPref[0]); + vertPref[1] = reversePortConstraints(vertPref[0]); + + const preferredHorizDist = + sourceLeftDist >= sourceRightDist ? sourceLeftDist : sourceRightDist; + const preferredVertDist = + sourceTopDist >= sourceBottomDist ? sourceTopDist : sourceBottomDist; + + const prefOrdering = [ + [0, 0], + [0, 0], + ]; + let preferredOrderSet = false; + + // If the preferred port isn't available, switch it + for (let i = 0; i < 2; i += 1) { + if (dir[i] !== 0x0) { + continue; + } + + if ((horPref[i] & portConstraint[i]) === 0) { + horPref[i] = reversePortConstraints(horPref[i]); + } + + if ((vertPref[i] & portConstraint[i]) === 0) { + vertPref[i] = reversePortConstraints(vertPref[i]); + } + + prefOrdering[i][0] = vertPref[i]; + prefOrdering[i][1] = horPref[i]; + } + + if (preferredVertDist > 0 && preferredHorizDist > 0) { + // Possibility of two segment edge connection + if ((horPref[0] & portConstraint[0]) > 0 && (vertPref[1] & portConstraint[1]) > 0) { + prefOrdering[0][0] = horPref[0]; + prefOrdering[0][1] = vertPref[0]; + prefOrdering[1][0] = vertPref[1]; + prefOrdering[1][1] = horPref[1]; + preferredOrderSet = true; + } else if ( + (vertPref[0] & portConstraint[0]) > 0 && + (horPref[1] & portConstraint[1]) > 0 + ) { + prefOrdering[0][0] = vertPref[0]; + prefOrdering[0][1] = horPref[0]; + prefOrdering[1][0] = horPref[1]; + prefOrdering[1][1] = vertPref[1]; + preferredOrderSet = true; + } + } + + if (preferredVertDist > 0 && !preferredOrderSet) { + prefOrdering[0][0] = vertPref[0]; + prefOrdering[0][1] = horPref[0]; + prefOrdering[1][0] = vertPref[1]; + prefOrdering[1][1] = horPref[1]; + preferredOrderSet = true; + } + + if (preferredHorizDist > 0 && !preferredOrderSet) { + prefOrdering[0][0] = horPref[0]; + prefOrdering[0][1] = vertPref[0]; + prefOrdering[1][0] = horPref[1]; + prefOrdering[1][1] = vertPref[1]; + preferredOrderSet = true; + } + + // The source and target prefs are now an ordered list of + // the preferred port selections + // If the list contains gaps, compact it + + for (let i = 0; i < 2; i += 1) { + if (dir[i] !== 0x0) { + continue; + } + + if ((prefOrdering[i][0] & portConstraint[i]) === 0) { + prefOrdering[i][0] = prefOrdering[i][1]; + } + + dirPref[i] = prefOrdering[i][0] & portConstraint[i]; + dirPref[i] |= (prefOrdering[i][1] & portConstraint[i]) << 8; + dirPref[i] |= (prefOrdering[1 - i][i] & portConstraint[i]) << 16; + dirPref[i] |= (prefOrdering[1 - i][1 - i] & portConstraint[i]) << 24; + + if ((dirPref[i] & 0xf) === 0) { + dirPref[i] = dirPref[i] << 8; + } + + if ((dirPref[i] & 0xf00) === 0) { + dirPref[i] = (dirPref[i] & 0xf) | (dirPref[i] >> 8); + } + + if ((dirPref[i] & 0xf0000) === 0) { + dirPref[i] = (dirPref[i] & 0xffff) | ((dirPref[i] & 0xf000000) >> 8); + } + + dir[i] = dirPref[i] & 0xf; + + if ( + portConstraint[i] === DIRECTION_MASK.WEST || + portConstraint[i] === DIRECTION_MASK.NORTH || + portConstraint[i] === DIRECTION_MASK.EAST || + portConstraint[i] === DIRECTION_MASK.SOUTH + ) { + dir[i] = portConstraint[i]; + } + } + + //= ============================================================= + // End of source and target direction determination + + let sourceIndex = dir[0] === DIRECTION_MASK.EAST ? 3 : dir[0]; + let targetIndex = dir[1] === DIRECTION_MASK.EAST ? 3 : dir[1]; + + sourceIndex -= quad; + targetIndex -= quad; + + if (sourceIndex < 1) { + sourceIndex += 4; + } + + if (targetIndex < 1) { + targetIndex += 4; + } + + const routePattern = routePatterns[sourceIndex - 1][targetIndex - 1]; + + wayPoints1[0][0] = geo[0][0]; + wayPoints1[0][1] = geo[0][1]; + + switch (dir[0]) { + case DIRECTION_MASK.WEST: + wayPoints1[0][0] -= sourceBuffer; + wayPoints1[0][1] += constraint[0][1] * geo[0][3]; + break; + case DIRECTION_MASK.SOUTH: + wayPoints1[0][0] += constraint[0][0] * geo[0][2]; + wayPoints1[0][1] += geo[0][3] + sourceBuffer; + break; + case DIRECTION_MASK.EAST: + wayPoints1[0][0] += geo[0][2] + sourceBuffer; + wayPoints1[0][1] += constraint[0][1] * geo[0][3]; + break; + case DIRECTION_MASK.NORTH: + wayPoints1[0][0] += constraint[0][0] * geo[0][2]; + wayPoints1[0][1] -= sourceBuffer; + break; + } + + let currentIndex = 0; + + // Orientation, 0 horizontal, 1 vertical + let lastOrientation = + (dir[0] & (DIRECTION_MASK.EAST | DIRECTION_MASK.WEST)) > 0 ? 0 : 1; + const initialOrientation = lastOrientation; + let currentOrientation = 0; + + for (let i = 0; i < routePattern.length; i += 1) { + const nextDirection = routePattern[i] & 0xf; + + // Rotate the index of this direction by the quad + // to get the real direction + let directionIndex = nextDirection === DIRECTION_MASK.EAST ? 3 : nextDirection; + + directionIndex += quad; + + if (directionIndex > 4) { + directionIndex -= 4; + } + + const direction = dirVectors[directionIndex - 1]; + + currentOrientation = directionIndex % 2 > 0 ? 0 : 1; + // Only update the current index if the point moved + // in the direction of the current segment move, + // otherwise the same point is moved until there is + // a segment direction change + if (currentOrientation !== lastOrientation) { + currentIndex++; + // Copy the previous way point into the new one + // We can't base the new position on index - 1 + // because sometime elbows turn out not to exist, + // then we'd have to rewind. + wayPoints1[currentIndex][0] = wayPoints1[currentIndex - 1][0]; + wayPoints1[currentIndex][1] = wayPoints1[currentIndex - 1][1]; + } + + const tar = (routePattern[i] & TARGET_MASK) > 0; + const sou = (routePattern[i] & SOURCE_MASK) > 0; + let side = (routePattern[i] & SIDE_MASK) >> 5; + side <<= quad; + + if (side > 0xf) { + side >>= 4; + } + + const center = (routePattern[i] & CENTER_MASK) > 0; + + if ((sou || tar) && side < 9) { + let limit = 0; + const souTar = sou ? 0 : 1; + + if (center && currentOrientation === 0) { + limit = geo[souTar][0] + constraint[souTar][0] * geo[souTar][2]; + } else if (center) { + limit = geo[souTar][1] + constraint[souTar][1] * geo[souTar][3]; + } else { + limit = limits[souTar][side]; + } + + if (currentOrientation === 0) { + const lastX = wayPoints1[currentIndex][0]; + const deltaX = (limit - lastX) * direction[0]; + + if (deltaX > 0) { + wayPoints1[currentIndex][0] += direction[0] * deltaX; + } + } else { + const lastY = wayPoints1[currentIndex][1]; + const deltaY = (limit - lastY) * direction[1]; + + if (deltaY > 0) { + wayPoints1[currentIndex][1] += direction[1] * deltaY; + } + } + } else if (center) { + // Which center we're travelling to depend on the current direction + wayPoints1[currentIndex][0] += + direction[0] * Math.abs(vertexSeparations[directionIndex] / 2); + wayPoints1[currentIndex][1] += + direction[1] * Math.abs(vertexSeparations[directionIndex] / 2); + } + + if ( + currentIndex > 0 && + wayPoints1[currentIndex][currentOrientation] === + wayPoints1[currentIndex - 1][currentOrientation] + ) { + currentIndex--; + } else { + lastOrientation = currentOrientation; + } + } + + for (let i = 0; i <= currentIndex; i += 1) { + if (i === currentIndex) { + // Last point can cause last segment to be in + // same direction as jetty/approach. If so, + // check the number of points is consistent + // with the relative orientation of source and target + // jx. Same orientation requires an even + // number of turns (points), different requires + // odd. + const targetOrientation = + (dir[1] & (DIRECTION_MASK.EAST | DIRECTION_MASK.WEST)) > 0 ? 0 : 1; + const sameOrient = targetOrientation === initialOrientation ? 0 : 1; + + // (currentIndex + 1) % 2 is 0 for even number of points, + // 1 for odd + if (sameOrient !== (currentIndex + 1) % 2) { + // The last point isn't required + break; + } + } + + result.push( + new Point( + Math.round(wayPoints1[i][0] * state.view.scale * 10) / 10, + Math.round(wayPoints1[i][1] * state.view.scale * 10) / 10 + ) + ); + } + + // Removes duplicates + let index = 1; + + while (index < result.length) { + if ( + result[index - 1] == null || + result[index] == null || + result[index - 1].x !== result[index].x || + result[index - 1].y !== result[index].y + ) { + index++; + } else { + result.splice(index, 1); + } + } +}; diff --git a/packages/ts-example-without-defaults/vite.config.js b/packages/ts-example-without-defaults/vite.config.js index d79241833b..1078cea700 100644 --- a/packages/ts-example-without-defaults/vite.config.js +++ b/packages/ts-example-without-defaults/vite.config.js @@ -27,7 +27,7 @@ export default defineConfig(({ mode }) => { }, }, }, - chunkSizeWarningLimit: 441, // @maxgraph/core + chunkSizeWarningLimit: 440, // @maxgraph/core }, }; }); diff --git a/packages/ts-example/vite.config.js b/packages/ts-example/vite.config.js index 462aeae469..1c75c65ea6 100644 --- a/packages/ts-example/vite.config.js +++ b/packages/ts-example/vite.config.js @@ -27,7 +27,7 @@ export default defineConfig(({ mode }) => { }, }, }, - chunkSizeWarningLimit: 445, // @maxgraph/core + chunkSizeWarningLimit: 444, // @maxgraph/core }, }; }); diff --git a/packages/website/docs/usage/migrate-from-mxgraph.md b/packages/website/docs/usage/migrate-from-mxgraph.md index 2e93d46c89..ab6cbc2ffd 100644 --- a/packages/website/docs/usage/migrate-from-mxgraph.md +++ b/packages/website/docs/usage/migrate-from-mxgraph.md @@ -365,11 +365,15 @@ managing development mode in that way anymore. - `mxResourceExtension`: it was only used in `Translations`, so only keep the property in `Translations` Removed methods +- `isBrowserSupported` (0.16.0): no longer necessary - `setForceIncludes`: the `mxForceIncludes` property has been removed - `setLoadResources`: the `mxLoadResources` property has been removed - `setLoadStylesheets`: the `mxLoadStylesheets` property has been removed - `setResourceExtension`: the `mxResourceExtension` property has been removed +Moved properties +- `VERSION` moved to `constants.VERSION` (0.16.0) + Moved methods - `link` method moved and renamed `domUtils.addLinkToHead` @@ -394,6 +398,14 @@ But it is possible to configure their values globally. See the [Global Configura ::: +### `mxEdgeStyle` + +The `mxEdgeStyle` value object has been replaced by the `EdgeStyle` class in `maxGraph`. + +As of version `0.16.0`, `EdgeStyle` only includes the properties referencing connectors. The other properties and methods are no longer available. \ +The connectors can be configured using dedicated global configuration objects. For more details, see the [Global Configuration](./global-configuration.md) documentation. + + ### Cell manipulation Some methods previously available in `mxGraph` and `mxGraphModel` have been removed. These methods allowed for customizing the behavior of `mxGraphModel` and `mxCell`. However, now only the methods specific to `Cell` remain.