UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering

512 lines (442 loc) 13.3 kB
import { Point, PointOptions } from './point' import { Geometry } from './geometry' import { Rectangle } from './rectangle' import { Ellipse } from './ellipse' import { Path, type PathOptions } from './path' import { Polyline } from './polyline' export class Line extends Geometry { static isLine(instance: any): instance is Line { return instance != null && instance instanceof Line } public start: Point public end: Point public get center() { return new Point( (this.start.x + this.end.x) / 2, (this.start.y + this.end.y) / 2, ) } constructor() constructor(x1: number, y1: number, x2: number, y2: number) constructor(p1: PointOptions, p2: PointOptions) constructor( x1?: number | PointOptions, y1?: number | PointOptions, x2?: number, y2?: number, ) { super() if (typeof x1 === 'number' && typeof y1 === 'number') { this.start = new Point(x1, y1) this.end = new Point(x2, y2) } else { this.start = Point.create(x1) this.end = Point.create(y1) } } getCenter() { return this.center } /** * Rounds the line to the given `precision`. */ round(precision = 0) { this.start.round(precision) this.end.round(precision) return this } translate(tx: number, ty: number): this translate(p: PointOptions): this translate(tx: number | PointOptions, ty?: number) { if (typeof tx === 'number') { this.start.translate(tx, ty as number) this.end.translate(tx, ty as number) } else { this.start.translate(tx) this.end.translate(tx) } return this } /** * Rotate the line by `angle` around `origin`. */ rotate(angle: number, origin?: PointOptions) { this.start.rotate(angle, origin) this.end.rotate(angle, origin) return this } /** * Scale the line by `sx` and `sy` about the given `origin`. If origin is not * specified, the line is scaled around `0,0`. */ scale(sx: number, sy: number, origin?: PointOptions) { this.start.scale(sx, sy, origin) this.end.scale(sx, sy, origin) return this } /** * Returns the length of the line. */ length() { return Math.sqrt(this.squaredLength()) } /** * Useful for distance comparisons in which real length is not necessary * (saves one `Math.sqrt()` operation). */ squaredLength() { const dx = this.start.x - this.end.x const dy = this.start.y - this.end.y return dx * dx + dy * dy } /** * Scale the line so that it has the requested length. The start point of * the line is preserved. */ setLength(length: number) { const total = this.length() if (!total) { return this } const scale = length / total return this.scale(scale, scale, this.start) } parallel(distance: number) { const line = this.clone() if (!line.isDifferentiable()) { return line } const { start, end } = line const eRef = start.clone().rotate(270, end) const sRef = end.clone().rotate(90, start) start.move(sRef, distance) end.move(eRef, distance) return line } /** * Returns the vector of the line with length equal to length of the line. */ vector() { return new Point(this.end.x - this.start.x, this.end.y - this.start.y) } /** * Returns the angle of incline of the line. * * The function returns `NaN` if the start and end endpoints of the line * both lie at the same coordinates(it is impossible to determine the angle * of incline of a line that appears to be a point). The * `line.isDifferentiable()` function may be used in advance to determine * whether the angle of incline can be computed for a given line. */ angle() { const ref = new Point(this.start.x + 1, this.start.y) return this.start.angleBetween(this.end, ref) } /** * Returns a rectangle that is the bounding box of the line. */ bbox() { const left = Math.min(this.start.x, this.end.x) const top = Math.min(this.start.y, this.end.y) const right = Math.max(this.start.x, this.end.x) const bottom = Math.max(this.start.y, this.end.y) return new Rectangle(left, top, right - left, bottom - top) } /** * Returns the bearing (cardinal direction) of the line. * * The return value is one of the following strings: * 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW' and 'N'. * * The function returns 'N' if the two endpoints of the line are coincident. */ bearing() { return this.start.bearing(this.end) } /** * Returns the point on the line that lies closest to point `p`. */ closestPoint(p: PointOptions) { return this.pointAt(this.closestPointNormalizedLength(p)) } /** * Returns the length of the line up to the point that lies closest to point `p`. */ closestPointLength(p: PointOptions) { return this.closestPointNormalizedLength(p) * this.length() } /** * Returns a line that is tangent to the line at the point that lies closest * to point `p`. */ closestPointTangent(p: PointOptions) { return this.tangentAt(this.closestPointNormalizedLength(p)) } /** * Returns the normalized length (distance from the start of the line / total * line length) of the line up to the point that lies closest to point. */ closestPointNormalizedLength(p: PointOptions) { const product = this.vector().dot(new Line(this.start, p).vector()) const normalized = Math.min(1, Math.max(0, product / this.squaredLength())) // normalized returns `NaN` if this line has zero length if (Number.isNaN(normalized)) { return 0 } return normalized } /** * Returns a point on the line that lies `rate` (normalized length) away from * the beginning of the line. */ pointAt(ratio: number) { const start = this.start const end = this.end if (ratio <= 0) { return start.clone() } if (ratio >= 1) { return end.clone() } return start.lerp(end, ratio) } /** * Returns a point on the line that lies length away from the beginning of * the line. */ pointAtLength(length: number) { const start = this.start const end = this.end let fromStart = true if (length < 0) { fromStart = false // start calculation from end point length = -length // eslint-disable-line } const total = this.length() if (length >= total) { return fromStart ? end.clone() : start.clone() } const rate = (fromStart ? length : total - length) / total return this.pointAt(rate) } /** * Divides the line into two lines at the point that lies `rate` (normalized * length) away from the beginning of the line. */ divideAt(ratio: number) { const dividerPoint = this.pointAt(ratio) return [ new Line(this.start, dividerPoint), new Line(dividerPoint, this.end), ] } /** * Divides the line into two lines at the point that lies length away from * the beginning of the line. */ divideAtLength(length: number) { const dividerPoint = this.pointAtLength(length) return [ new Line(this.start, dividerPoint), new Line(dividerPoint, this.end), ] } /** * Returns `true` if the point `p` lies on the line. Return `false` otherwise. */ containsPoint(p: PointOptions) { const start = this.start const end = this.end // cross product of 0 indicates that this line and // the vector to `p` are collinear. if (start.cross(p, end) !== 0) { return false } const length = this.length() if (new Line(start, p).length() > length) { return false } if (new Line(p, end).length() > length) { return false } return true } /** * Returns an array of the intersection points of the line with another * geometry shape. */ intersect(shape: Line | Rectangle | Polyline | Ellipse): Point[] | null intersect(shape: Path, options?: PathOptions): Point[] | null intersect( shape: Line | Rectangle | Polyline | Ellipse | Path, options?: PathOptions, ): Point[] | null { const ret = shape.intersectsWithLine(this, options) if (ret) { return Array.isArray(ret) ? ret : [ret] } return null } /** * Returns the intersection point of the line with another line. Returns * `null` if no intersection exists. */ intersectsWithLine(line: Line) { const pt1Dir = new Point( this.end.x - this.start.x, this.end.y - this.start.y, ) const pt2Dir = new Point( line.end.x - line.start.x, line.end.y - line.start.y, ) const det = pt1Dir.x * pt2Dir.y - pt1Dir.y * pt2Dir.x const deltaPt = new Point( line.start.x - this.start.x, line.start.y - this.start.y, ) const alpha = deltaPt.x * pt2Dir.y - deltaPt.y * pt2Dir.x const beta = deltaPt.x * pt1Dir.y - deltaPt.y * pt1Dir.x if (det === 0 || alpha * det < 0 || beta * det < 0) { return null } if (det > 0) { if (alpha > det || beta > det) { return null } } else if (alpha < det || beta < det) { return null } return new Point( this.start.x + (alpha * pt1Dir.x) / det, this.start.y + (alpha * pt1Dir.y) / det, ) } /** * Returns `true` if a tangent line can be found for the line. * * Tangents cannot be found if both of the line endpoints are coincident * (the line appears to be a point). */ isDifferentiable() { return !this.start.equals(this.end) } /** * Returns the perpendicular distance between the line and point. The * distance is positive if the point lies to the right of the line, negative * if the point lies to the left of the line, and `0` if the point lies on * the line. */ pointOffset(p: PointOptions) { const ref = Point.clone(p) const start = this.start const end = this.end const determinant = (end.x - start.x) * (ref.y - start.y) - (end.y - start.y) * (ref.x - start.x) return determinant / this.length() } /** * Returns the squared distance between the line and the point. */ pointSquaredDistance(x: number, y: number): number pointSquaredDistance(p: PointOptions): number pointSquaredDistance(x: number | PointOptions, y?: number) { const p = Point.create(x, y) return this.closestPoint(p).squaredDistance(p) } /** * Returns the distance between the line and the point. */ pointDistance(x: number, y: number): number pointDistance(p: PointOptions): number pointDistance(x: number | PointOptions, y?: number) { const p = Point.create(x, y) return this.closestPoint(p).distance(p) } /** * Returns a line tangent to the line at point that lies `rate` (normalized * length) away from the beginning of the line. */ tangentAt(ratio: number) { if (!this.isDifferentiable()) { return null } const start = this.start const end = this.end const tangentStart = this.pointAt(ratio) const tangentLine = new Line(start, end) tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y) return tangentLine } /** * Returns a line tangent to the line at point that lies `length` away from * the beginning of the line. */ tangentAtLength(length: number) { if (!this.isDifferentiable()) { return null } const start = this.start const end = this.end const tangentStart = this.pointAtLength(length) const tangentLine = new Line(start, end) tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y) return tangentLine } /** * Returns which direction the line would have to rotate in order to direct * itself at a point. * * Returns 1 if the given point on the right side of the segment, 0 if its * on the segment, and -1 if the point is on the left side of the segment. * * @see https://softwareengineering.stackexchange.com/questions/165776/what-do-ptlinedist-and-relativeccw-do */ relativeCcw(x: number, y: number): -1 | 0 | 1 relativeCcw(p: PointOptions): -1 | 0 | 1 relativeCcw(x: number | PointOptions, y?: number) { const ref = Point.create(x, y) let dx1 = ref.x - this.start.x let dy1 = ref.y - this.start.y const dx2 = this.end.x - this.start.x const dy2 = this.end.y - this.start.y let ccw = dx1 * dy2 - dy1 * dx2 if (ccw === 0) { ccw = dx1 * dx2 + dy1 * dy2 if (ccw > 0.0) { dx1 -= dx2 dy1 -= dy2 ccw = dx1 * dx2 + dy1 * dy2 if (ccw < 0.0) { ccw = 0.0 } } } return ccw < 0.0 ? -1 : ccw > 0.0 ? 1 : 0 } /** * Return `true` if the line equals the other line. */ equals(l: Line) { return ( l != null && this.start.x === l.start.x && this.start.y === l.start.y && this.end.x === l.end.x && this.end.y === l.end.y ) } /** * Returns another line which is a clone of the line. */ clone() { return new Line(this.start, this.end) } toJSON() { return { start: this.start.toJSON(), end: this.end.toJSON() } } serialize() { return [this.start.serialize(), this.end.serialize()].join(' ') } }