UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

852 lines (731 loc) 20.8 kB
import { Size } from '../types' import * as util from './util' import { Angle } from './angle' import { Line } from './line' import { Point } from './point' import { Ellipse } from './ellipse' import { Geometry } from './geometry' export class Rectangle extends Geometry implements Rectangle.RectangleLike { x: number y: number width: number height: number protected get [Symbol.toStringTag]() { return Rectangle.toStringTag } get left() { return this.x } get top() { return this.y } get right() { return this.x + this.width } get bottom() { return this.y + this.height } get origin() { return new Point(this.x, this.y) } get topLeft() { return new Point(this.x, this.y) } get topCenter() { return new Point(this.x + this.width / 2, this.y) } get topRight() { return new Point(this.x + this.width, this.y) } get center() { return new Point(this.x + this.width / 2, this.y + this.height / 2) } get bottomLeft() { return new Point(this.x, this.y + this.height) } get bottomCenter() { return new Point(this.x + this.width / 2, this.y + this.height) } get bottomRight() { return new Point(this.x + this.width, this.y + this.height) } get corner() { return new Point(this.x + this.width, this.y + this.height) } get rightMiddle() { return new Point(this.x + this.width, this.y + this.height / 2) } get leftMiddle() { return new Point(this.x, this.y + this.height / 2) } get topLine() { return new Line(this.topLeft, this.topRight) } get rightLine() { return new Line(this.topRight, this.bottomRight) } get bottomLine() { return new Line(this.bottomLeft, this.bottomRight) } get leftLine() { return new Line(this.topLeft, this.bottomLeft) } constructor(x?: number, y?: number, width?: number, height?: number) { super() this.x = x == null ? 0 : x this.y = y == null ? 0 : y this.width = width == null ? 0 : width this.height = height == null ? 0 : height } getOrigin() { return this.origin } getTopLeft() { return this.topLeft } getTopCenter() { return this.topCenter } getTopRight() { return this.topRight } getCenter() { return this.center } getCenterX() { return this.x + this.width / 2 } getCenterY() { return this.y + this.height / 2 } getBottomLeft() { return this.bottomLeft } getBottomCenter() { return this.bottomCenter } getBottomRight() { return this.bottomRight } getCorner() { return this.corner } getRightMiddle() { return this.rightMiddle } getLeftMiddle() { return this.leftMiddle } getTopLine() { return this.topLine } getRightLine() { return this.rightLine } getBottomLine() { return this.bottomLine } getLeftLine() { return this.leftLine } /** * Returns a rectangle that is the bounding box of the rectangle. * * If `angle` is specified, the bounding box calculation will take into * account the rotation of the rectangle by angle degrees around its center. */ bbox(angle?: number) { if (!angle) { return this.clone() } const rad = Angle.toRad(angle) const st = Math.abs(Math.sin(rad)) const ct = Math.abs(Math.cos(rad)) const w = this.width * ct + this.height * st const h = this.width * st + this.height * ct return new Rectangle( this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h, ) } round(precision = 0) { this.x = util.round(this.x, precision) this.y = util.round(this.y, precision) this.width = util.round(this.width, precision) this.height = util.round(this.height, precision) return this } add(x: number, y: number, width: number, height: number): this add(rect: Rectangle.RectangleLike | Rectangle.RectangleData): this add( x: number | Rectangle.RectangleLike | Rectangle.RectangleData, y?: number, width?: number, height?: number, ): this { const rect = Rectangle.create(x, y, width, height) const minX = Math.min(this.x, rect.x) const minY = Math.min(this.y, rect.y) const maxX = Math.max(this.x + this.width, rect.x + rect.width) const maxY = Math.max(this.y + this.height, rect.y + rect.height) this.x = minX this.y = minY this.width = maxX - minX this.height = maxY - minY return this } update(x: number, y: number, width: number, height: number): this update(rect: Rectangle.RectangleLike | Rectangle.RectangleData): this update( x: number | Rectangle.RectangleLike | Rectangle.RectangleData, y?: number, width?: number, height?: number, ): this { const rect = Rectangle.create(x, y, width, height) this.x = rect.x this.y = rect.y this.width = rect.width this.height = rect.height return this } inflate(amount: number): this /** * Returns a rectangle inflated in axis-x by `2*dx` and in axis-y by `2*dy`. */ inflate(dx: number, dy: number): this inflate(dx: number, dy?: number): this { const w = dx const h = dy != null ? dy : dx this.x -= w this.y -= h this.width += 2 * w this.height += 2 * h return this } /** * Adjust the position and dimensions of the rectangle such that its edges * are on the nearest increment of `gx` on the x-axis and `gy` on the y-axis. */ snapToGrid(gridSize: number): this snapToGrid(gx: number, gy: number): this snapToGrid(gx: number, gy?: number): this snapToGrid(gx: number, gy?: number): this { const origin = this.origin.snapToGrid(gx, gy) const corner = this.corner.snapToGrid(gx, gy) this.x = origin.x this.y = origin.y this.width = corner.x - origin.x this.height = corner.y - origin.y return this } translate(tx: number, ty: number): this translate(p: Point.PointLike | Point.PointData): this translate(tx: number | Point.PointLike | Point.PointData, ty?: number): this { const p = Point.create(tx, ty) this.x += p.x this.y += p.y return this } scale( sx: number, sy: number, origin: Point.PointLike | Point.PointData = new Point(), ) { const pos = this.origin.scale(sx, sy, origin) this.x = pos.x this.y = pos.y this.width *= sx this.height *= sy return this } rotate( degree: number, center: Point.PointLike | Point.PointData = this.getCenter(), ) { if (degree !== 0) { const rad = Angle.toRad(degree) const cos = Math.cos(rad) const sin = Math.sin(rad) let p1 = this.getOrigin() let p2 = this.getTopRight() let p3 = this.getBottomRight() let p4 = this.getBottomLeft() p1 = Point.rotateEx(p1, cos, sin, center) p2 = Point.rotateEx(p2, cos, sin, center) p3 = Point.rotateEx(p3, cos, sin, center) p4 = Point.rotateEx(p4, cos, sin, center) const rect = new Rectangle(p1.x, p1.y, 0, 0) rect.add(p2.x, p2.y, 0, 0) rect.add(p3.x, p3.y, 0, 0) rect.add(p4.x, p4.y, 0, 0) this.update(rect) } return this } rotate90() { const t = (this.width - this.height) / 2 this.x += t this.y -= t const tmp = this.width this.width = this.height this.height = tmp return this } /** * Translates the rectangle by `rect.x` and `rect.y` and expand it by * `rect.width` and `rect.height`. */ moveAndExpand(rect: Rectangle.RectangleLike | Rectangle.RectangleData) { const ref = Rectangle.clone(rect) this.x += ref.x || 0 this.y += ref.y || 0 this.width += ref.width || 0 this.height += ref.height || 0 return this } /** * Returns an object where `sx` and `sy` give the maximum scaling that can be * applied to the rectangle so that it would still fit into `limit`. If * `origin` is specified, the rectangle is scaled around it; otherwise, it is * scaled around its center. */ getMaxScaleToFit( limit: Rectangle.RectangleLike | Rectangle.RectangleData, origin: Point = this.center, ) { const rect = Rectangle.clone(limit) const ox = origin.x const oy = origin.y // Find the maximal possible scale for all corners, so when the scale // is applied the point is still inside the rectangle. let sx1 = Infinity let sx2 = Infinity let sx3 = Infinity let sx4 = Infinity let sy1 = Infinity let sy2 = Infinity let sy3 = Infinity let sy4 = Infinity // Top Left const p1 = rect.topLeft if (p1.x < ox) { sx1 = (this.x - ox) / (p1.x - ox) } if (p1.y < oy) { sy1 = (this.y - oy) / (p1.y - oy) } // Bottom Right const p2 = rect.bottomRight if (p2.x > ox) { sx2 = (this.x + this.width - ox) / (p2.x - ox) } if (p2.y > oy) { sy2 = (this.y + this.height - oy) / (p2.y - oy) } // Top Right const p3 = rect.topRight if (p3.x > ox) { sx3 = (this.x + this.width - ox) / (p3.x - ox) } if (p3.y < oy) { sy3 = (this.y - oy) / (p3.y - oy) } // Bottom Left const p4 = rect.bottomLeft if (p4.x < ox) { sx4 = (this.x - ox) / (p4.x - ox) } if (p4.y > oy) { sy4 = (this.y + this.height - oy) / (p4.y - oy) } return { sx: Math.min(sx1, sx2, sx3, sx4), sy: Math.min(sy1, sy2, sy3, sy4), } } /** * Returns a number that specifies the maximum scaling that can be applied to * the rectangle along both axes so that it would still fit into `limit`. If * `origin` is specified, the rectangle is scaled around it; otherwise, it is * scaled around its center. */ getMaxUniformScaleToFit( limit: Rectangle.RectangleLike | Rectangle.RectangleData, origin: Point = this.center, ) { const scale = this.getMaxScaleToFit(limit, origin) return Math.min(scale.sx, scale.sy) } /** * Returns `true` if the point is inside the rectangle (inclusive). * Returns `false` otherwise. */ containsPoint(x: number, y: number): boolean containsPoint(point: Point.PointLike | Point.PointData): boolean containsPoint( x: number | Point.PointLike | Point.PointData, y?: number, ): boolean { return util.containsPoint(this, Point.create(x, y)) } /** * Returns `true` if the rectangle is (completely) inside the * rectangle (inclusive). Returns `false` otherwise. */ containsRect(x: number, y: number, w: number, h: number): boolean containsRect(rect: Rectangle.RectangleLike | Rectangle.RectangleData): boolean containsRect( x: number | Rectangle.RectangleLike | Rectangle.RectangleData, y?: number, width?: number, height?: number, ) { const b = Rectangle.create(x, y, width, height) const x1 = this.x const y1 = this.y const w1 = this.width const h1 = this.height const x2 = b.x const y2 = b.y const w2 = b.width const h2 = b.height // one of the dimensions is 0 if (w1 === 0 || h1 === 0 || w2 === 0 || h2 === 0) { return false } return x2 >= x1 && y2 >= y1 && x2 + w2 <= x1 + w1 && y2 + h2 <= y1 + h1 } /** * Returns an array of the intersection points of the rectangle and the line. * Return `null` if no intersection exists. */ intersectsWithLine(line: Line) { const rectLines = [ this.topLine, this.rightLine, this.bottomLine, this.leftLine, ] const points: Point[] = [] const dedupeArr: string[] = [] rectLines.forEach((l) => { const p = line.intersectsWithLine(l) if (p !== null && dedupeArr.indexOf(p.toString()) < 0) { points.push(p) dedupeArr.push(p.toString()) } }) return points.length > 0 ? points : null } /** * Returns the point on the boundary of the rectangle that is the intersection * of the rectangle with a line starting in the center the rectangle ending in * the point `p`. * * If `angle` is specified, the intersection will take into account the * rotation of the rectangle by `angle` degrees around its center. */ intersectsWithLineFromCenterToPoint( p: Point.PointLike | Point.PointData, angle?: number, ) { const ref = Point.clone(p) const center = this.center let result: Point | null = null if (angle != null && angle !== 0) { ref.rotate(angle, center) } const sides = [this.topLine, this.rightLine, this.bottomLine, this.leftLine] const connector = new Line(center, ref) for (let i = sides.length - 1; i >= 0; i -= 1) { const intersection = sides[i].intersectsWithLine(connector) if (intersection !== null) { result = intersection break } } if (result && angle != null && angle !== 0) { result.rotate(-angle, center) } return result } /** * Returns a rectangle that is a subtraction of the two rectangles if such an * object exists (the two rectangles intersect). Returns `null` otherwise. */ intersectsWithRect( x: number, y: number, w: number, h: number, ): Rectangle | null intersectsWithRect( rect: Rectangle.RectangleLike | Rectangle.RectangleData, ): Rectangle | null intersectsWithRect( x: number | Rectangle.RectangleLike | Rectangle.RectangleData, y?: number, width?: number, height?: number, ) { const ref = Rectangle.create(x, y, width, height) // no intersection if (!this.isIntersectWithRect(ref)) { return null } const myOrigin = this.origin const myCorner = this.corner const rOrigin = ref.origin const rCorner = ref.corner const xx = Math.max(myOrigin.x, rOrigin.x) const yy = Math.max(myOrigin.y, rOrigin.y) return new Rectangle( xx, yy, Math.min(myCorner.x, rCorner.x) - xx, Math.min(myCorner.y, rCorner.y) - yy, ) } isIntersectWithRect(x: number, y: number, w: number, h: number): boolean isIntersectWithRect( rect: Rectangle.RectangleLike | Rectangle.RectangleData, ): boolean isIntersectWithRect( x: number | Rectangle.RectangleLike | Rectangle.RectangleData, y?: number, width?: number, height?: number, ) { const ref = Rectangle.create(x, y, width, height) const myOrigin = this.origin const myCorner = this.corner const rOrigin = ref.origin const rCorner = ref.corner if ( rCorner.x <= myOrigin.x || rCorner.y <= myOrigin.y || rOrigin.x >= myCorner.x || rOrigin.y >= myCorner.y ) { return false } return true } /** * Normalize the rectangle, i.e. make it so that it has non-negative * width and height. If width is less than `0`, the function swaps left and * right corners and if height is less than `0`, the top and bottom corners * are swapped. */ normalize() { let newx = this.x let newy = this.y let newwidth = this.width let newheight = this.height if (this.width < 0) { newx = this.x + this.width newwidth = -this.width } if (this.height < 0) { newy = this.y + this.height newheight = -this.height } this.x = newx this.y = newy this.width = newwidth this.height = newheight return this } /** * Returns a rectangle that is a union of this rectangle and rectangle `rect`. */ union(rect: Rectangle.RectangleLike | Rectangle.RectangleData) { const ref = Rectangle.clone(rect) const myOrigin = this.origin const myCorner = this.corner const rOrigin = ref.origin const rCorner = ref.corner const originX = Math.min(myOrigin.x, rOrigin.x) const originY = Math.min(myOrigin.y, rOrigin.y) const cornerX = Math.max(myCorner.x, rCorner.x) const cornerY = Math.max(myCorner.y, rCorner.y) return new Rectangle(originX, originY, cornerX - originX, cornerY - originY) } /** * Returns a string ("top", "left", "right" or "bottom") denoting the side of * the rectangle which is nearest to the point `p`. */ getNearestSideToPoint(p: Point.PointLike | Point.PointData): Rectangle.Side { const ref = Point.clone(p) const distLeft = ref.x - this.x const distRight = this.x + this.width - ref.x const distTop = ref.y - this.y const distBottom = this.y + this.height - ref.y let closest = distLeft let side: Rectangle.Side = 'left' if (distRight < closest) { closest = distRight side = 'right' } if (distTop < closest) { closest = distTop side = 'top' } if (distBottom < closest) { side = 'bottom' } return side } /** * Returns a point on the boundary of the rectangle nearest to the point `p`. */ getNearestPointToPoint(p: Point.PointLike | Point.PointData) { const ref = Point.clone(p) if (this.containsPoint(ref)) { const side = this.getNearestSideToPoint(ref) switch (side) { case 'right': return new Point(this.x + this.width, ref.y) case 'left': return new Point(this.x, ref.y) case 'bottom': return new Point(ref.x, this.y + this.height) case 'top': return new Point(ref.x, this.y) default: break } } return ref.adhereToRect(this) } equals(rect: Rectangle.RectangleLike) { return ( rect != null && rect.x === this.x && rect.y === this.y && rect.width === this.width && rect.height === this.height ) } clone() { return new Rectangle(this.x, this.y, this.width, this.height) } toJSON() { return { x: this.x, y: this.y, width: this.width, height: this.height } } serialize() { return `${this.x} ${this.y} ${this.width} ${this.height}` } } export namespace Rectangle { export const toStringTag = `X6.Geometry.${Rectangle.name}` export function isRectangle(instance: any): instance is Rectangle { if (instance == null) { return false } if (instance instanceof Rectangle) { return true } const tag = instance[Symbol.toStringTag] const rect = instance as Rectangle if ( (tag == null || tag === toStringTag) && typeof rect.x === 'number' && typeof rect.y === 'number' && typeof rect.width === 'number' && typeof rect.height === 'number' && typeof rect.inflate === 'function' && typeof rect.moveAndExpand === 'function' ) { return true } return false } } export namespace Rectangle { export type RectangleData = [number, number, number, number] export interface RectangleLike extends Point.PointLike { x: number y: number width: number height: number } export function isRectangleLike(o: any): o is RectangleLike { return ( o != null && typeof o === 'object' && typeof o.x === 'number' && typeof o.y === 'number' && typeof o.width === 'number' && typeof o.height === 'number' ) } export type Side = 'left' | 'right' | 'top' | 'bottom' export type KeyPoint = | 'center' | 'origin' | 'corner' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'rightMiddle' | 'leftMiddle' } export namespace Rectangle { export function create(rect: RectangleLike | RectangleData): Rectangle export function create( x?: number, y?: number, width?: number, height?: number, ): Rectangle export function create( x?: number | RectangleLike | RectangleData, y?: number, width?: number, height?: number, ): Rectangle export function create( x?: number | RectangleLike | RectangleData, y?: number, width?: number, height?: number, ): Rectangle { if (x == null || typeof x === 'number') { return new Rectangle(x, y, width, height) } return clone(x) } export function clone(rect: RectangleLike | RectangleData) { if (Rectangle.isRectangle(rect)) { return rect.clone() } if (Array.isArray(rect)) { return new Rectangle(rect[0], rect[1], rect[2], rect[3]) } return new Rectangle(rect.x, rect.y, rect.width, rect.height) } export function fromSize(size: Size) { return new Rectangle(0, 0, size.width, size.height) } export function fromPositionAndSize(pos: Point.PointLike, size: Size) { return new Rectangle(pos.x, pos.y, size.width, size.height) } /** * Returns a new rectangle from the given ellipse. */ export function fromEllipse(ellipse: Ellipse) { return new Rectangle( ellipse.x - ellipse.a, ellipse.y - ellipse.b, 2 * ellipse.a, 2 * ellipse.b, ) } }