UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering

309 lines (263 loc) 7.49 kB
import { Line } from './line' import { Point, PointOptions, PointLike } from './point' import { Rectangle } from './rectangle' import { Geometry } from './geometry' interface EllipseLike extends PointLike { x: number y: number a: number b: number } type EllipseData = [number, number, number, number] export class Ellipse extends Geometry implements EllipseLike { static isEllipse(instance: any): instance is Ellipse { return instance != null && instance instanceof Ellipse } static create( x?: number | Ellipse | EllipseLike | EllipseData, y?: number, a?: number, b?: number, ): Ellipse { if (x == null || typeof x === 'number') { // @ts-ignore return new Ellipse(x, y, a, b) } return Ellipse.parse(x) } static parse(e: Ellipse | EllipseLike | EllipseData) { if (Ellipse.isEllipse(e)) { return e.clone() } if (Array.isArray(e)) { return new Ellipse(e[0], e[1], e[2], e[3]) } return new Ellipse(e.x, e.y, e.a, e.b) } static fromRect(rect: Rectangle) { const center = rect.center return new Ellipse(center.x, center.y, rect.width / 2, rect.height / 2) } public x: number public y: number public a: number public b: number public get center() { return new Point(this.x, this.y) } constructor(x?: number, y?: number, a?: number, b?: number) { super() this.x = x == null ? 0 : x this.y = y == null ? 0 : y this.a = a == null ? 0 : a this.b = b == null ? 0 : b } /** * Returns a rectangle that is the bounding box of the ellipse. */ bbox() { return Rectangle.fromEllipse(this) } /** * Returns a point that is the center of the ellipse. */ getCenter() { return this.center } /** * Returns ellipse inflated in axis-x by `2 * amount` and in axis-y by * `2 * amount`. */ inflate(amount: number): this /** * Returns ellipse 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.a += 2 * w this.b += 2 * h return this } /** * Returns a normalized distance from the ellipse center to point `p`. * Returns `n < 1` for points inside the ellipse, `n = 1` for points * lying on the ellipse boundary and `n > 1` for points outside the ellipse. */ normalizedDistance(x: number, y: number): number normalizedDistance(p: PointOptions): number normalizedDistance(x: number | PointOptions, y?: number) { const ref = Point.create(x, y) const dx = ref.x - this.x const dy = ref.y - this.y const a = this.a const b = this.b return (dx * dx) / (a * a) + (dy * dy) / (b * b) } /** * Returns `true` if the point `p` is inside the ellipse (inclusive). * Returns `false` otherwise. */ containsPoint(x: number, y: number): boolean containsPoint(p: PointOptions): boolean containsPoint(x: number | PointOptions, y?: number) { return this.normalizedDistance(x as number, y as number) <= 1 } /** * Returns an array of the intersection points of the ellipse and the line. * Returns `null` if no intersection exists. */ intersectsWithLine(line: Line) { const intersections = [] const rx = this.a const ry = this.b const a1 = line.start const a2 = line.end const dir = line.vector() const diff = a1.diff(new Point(this.x, this.y)) const mDir = new Point(dir.x / (rx * rx), dir.y / (ry * ry)) const mDiff = new Point(diff.x / (rx * rx), diff.y / (ry * ry)) const a = dir.dot(mDir) const b = dir.dot(mDiff) const c = diff.dot(mDiff) - 1.0 const d = b * b - a * c if (d < 0) { return null } if (d > 0) { const root = Math.sqrt(d) const ta = (-b - root) / a const tb = (-b + root) / a if ((ta < 0 || ta > 1) && (tb < 0 || tb > 1)) { // outside return null } if (ta >= 0 && ta <= 1) { intersections.push(a1.lerp(a2, ta)) } if (tb >= 0 && tb <= 1) { intersections.push(a1.lerp(a2, tb)) } } else { const t = -b / a if (t >= 0 && t <= 1) { intersections.push(a1.lerp(a2, t)) } else { // outside return null } } return intersections } /** * Returns the point on the boundary of the ellipse that is the * intersection of the ellipse with a line starting in the center * of the ellipse ending in the point `p`. * * If angle is specified, the intersection will take into account * the rotation of the ellipse by angle degrees around its center. */ intersectsWithLineFromCenterToPoint(p: PointOptions, angle = 0) { const ref = Point.clone(p) if (angle) { ref.rotate(angle, this.getCenter()) } const dx = ref.x - this.x const dy = ref.y - this.y let result if (dx === 0) { result = this.bbox().getNearestPointToPoint(ref) if (angle) { return result.rotate(-angle, this.getCenter()) } return result } const m = dy / dx const mSquared = m * m const aSquared = this.a * this.a const bSquared = this.b * this.b let x = Math.sqrt(1 / (1 / aSquared + mSquared / bSquared)) x = dx < 0 ? -x : x const y = m * x result = new Point(this.x + x, this.y + y) if (angle) { return result.rotate(-angle, this.getCenter()) } return result } /** * Returns the angle between the x-axis and the tangent from a point. It is * valid for points lying on the ellipse boundary only. */ tangentTheta(p: PointOptions) { const ref = Point.clone(p) const x0 = ref.x const y0 = ref.y const a = this.a const b = this.b const center = this.bbox().center const cx = center.x const cy = center.y const refPointDelta = 30 const q1 = x0 > center.x + a / 2 const q3 = x0 < center.x - a / 2 let x let y if (q1 || q3) { y = x0 > center.x ? y0 - refPointDelta : y0 + refPointDelta x = (a * a) / (x0 - cx) - (a * a * (y0 - cy) * (y - cy)) / (b * b * (x0 - cx)) + cx } else { x = y0 > center.y ? x0 + refPointDelta : x0 - refPointDelta y = (b * b) / (y0 - cy) - (b * b * (x0 - cx) * (x - cx)) / (a * a * (y0 - cy)) + cy } return new Point(x, y).theta(ref) } scale(sx: number, sy: number) { this.a *= sx this.b *= sy return this } rotate(angle: number, origin?: PointOptions) { const rect = Rectangle.fromEllipse(this) rect.rotate(angle, origin) const ellipse = Ellipse.fromRect(rect) this.a = ellipse.a this.b = ellipse.b this.x = ellipse.x this.y = ellipse.y return this } translate(dx: number, dy: number): this translate(p: PointOptions): this translate(dx: number | PointOptions, dy?: number): this { const p = Point.create(dx, dy) this.x += p.x this.y += p.y return this } equals(ellipse: Ellipse) { return ( ellipse != null && ellipse.x === this.x && ellipse.y === this.y && ellipse.a === this.a && ellipse.b === this.b ) } clone() { return new Ellipse(this.x, this.y, this.a, this.b) } toJSON() { return { x: this.x, y: this.y, a: this.a, b: this.b } } serialize() { return `${this.x} ${this.y} ${this.a} ${this.b}` } }