UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

1,415 lines (1,201 loc) 37.1 kB
import { clamp, squaredLength } from '../util' import { Line } from '../line' import { Point } from '../point' import { Curve } from '../curve' import { Polyline } from '../polyline' import { Rectangle } from '../rectangle' import { Geometry } from '../geometry' import { Close } from './close' import { LineTo } from './lineto' import { MoveTo } from './moveto' import { CurveTo } from './curveto' import { Segment } from './segment' import { normalizePathData } from './normalize' import * as Util from './util' export class Path extends Geometry { protected readonly PRECISION: number = 3 public segments: Segment[] protected get [Symbol.toStringTag]() { return Path.toStringTag } constructor() constructor(line: Line) constructor(curve: Curve) constructor(polyline: Polyline) constructor(segment: Segment) constructor(segments: Segment[]) constructor(lines: Line[]) constructor(curves: Curve[]) constructor( args?: Line | Curve | Polyline | Segment | Segment[] | Line[] | Curve[], ) { super() this.segments = [] if (Array.isArray(args)) { if (Line.isLine(args[0]) || Curve.isCurve(args[0])) { let previousObj: Line | Curve | null = null const arr = args as Line[] | Curve[] arr.forEach((o: Line | Curve, i: number) => { if (i === 0) { this.appendSegment(Path.createSegment('M', o.start)) } if (previousObj != null && !previousObj.end.equals(o.start)) { this.appendSegment(Path.createSegment('M', o.start)) } if (Line.isLine(o)) { this.appendSegment(Path.createSegment('L', o.end)) } else if (Curve.isCurve(o)) { this.appendSegment( Path.createSegment('C', o.controlPoint1, o.controlPoint2, o.end), ) } previousObj = o }) } else { const arr = args as Segment[] arr.forEach((s) => { if (s.isSegment) { this.appendSegment(s) } }) } } else if (args != null) { if (Line.isLine(args)) { this.appendSegment(Path.createSegment('M', args.start)) this.appendSegment(Path.createSegment('L', args.end)) } else if (Curve.isCurve(args)) { this.appendSegment(Path.createSegment('M', args.start)) this.appendSegment( Path.createSegment( 'C', args.controlPoint1, args.controlPoint2, args.end, ), ) } else if (Polyline.isPolyline(args)) { if (args.points && args.points.length) { args.points.forEach((point, index) => { const segment = index === 0 ? Path.createSegment('M', point) : Path.createSegment('L', point) this.appendSegment(segment) }) } } else if (args.isSegment) { this.appendSegment(args) } } } get start() { const segments = this.segments const count = segments.length if (count === 0) { return null } for (let i = 0; i < count; i += 1) { const segment = segments[i] if (segment.isVisible) { return segment.start } } // if no visible segment, return last segment end point return segments[count - 1].end } get end() { const segments = this.segments const count = segments.length if (count === 0) { return null } for (let i = count - 1; i >= 0; i -= 1) { const segment = segments[i] if (segment.isVisible) { return segment.end } } // if no visible segment, return last segment end point return segments[count - 1].end } moveTo(x: number, y: number): this moveTo(point: Point.PointLike): this moveTo(line: Line): this moveTo(curve: Curve): this moveTo(point: Point.PointLike, ...points: Point.PointLike[]): this moveTo(x: number, y: number, ...coords: number[]): this moveTo(...args: any[]) { return this.appendSegment(MoveTo.create.call(null, ...args)) } lineTo(x: number, y: number): this lineTo(point: Point.PointLike): this lineTo(line: Line): this lineTo(x: number, y: number, ...coords: number[]): this lineTo(point: Point.PointLike, ...points: Point.PointLike[]): this lineTo(...args: any[]) { return this.appendSegment(LineTo.create.call(null, ...args)) } curveTo( x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, ): this curveTo( x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, ...coords: number[] ): this curveTo(p1: Point.PointLike, p2: Point.PointLike, p3: Point.PointLike): this curveTo( p1: Point.PointLike, p2: Point.PointLike, p3: Point.PointLike, ...points: Point.PointLike[] ): this curveTo(...args: any[]) { return this.appendSegment(CurveTo.create.call(null, ...args)) } arcTo( rx: number, ry: number, xAxisRotation: number, largeArcFlag: 0 | 1, sweepFlag: 0 | 1, endX: number, endY: number, ): this arcTo( rx: number, ry: number, xAxisRotation: number, largeArcFlag: 0 | 1, sweepFlag: 0 | 1, endPoint: Point.PointLike, ): this arcTo( rx: number, ry: number, xAxisRotation: number, largeArcFlag: 0 | 1, sweepFlag: 0 | 1, endX: number | Point.PointLike, endY?: number, ) { const start = this.end || new Point() const points = typeof endX === 'number' ? Util.arcToCurves( start.x, start.y, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, endX, endY as number, ) : Util.arcToCurves( start.x, start.y, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, endX.x, endX.y, ) if (points != null) { for (let i = 0, ii = points.length; i < ii; i += 6) { this.curveTo( points[i], points[i + 1], points[i + 2], points[i + 3], points[i + 4], points[i + 5], ) } } return this } quadTo(controlPoint: Point.PointLike, endPoint: Point.PointLike): this quadTo( controlPointX: number, controlPointY: number, endPointX: number, endPointY: number, ): this quadTo( x1: number | Point.PointLike, y1: number | Point.PointLike, x?: number, y?: number, ) { const start = this.end || new Point() const data = ['M', start.x, start.y] if (typeof x1 === 'number') { data.push('Q', x1, y1 as number, x as number, y as number) } else { const p = y1 as Point.PointLike data.push(`Q`, x1.x, x1.y, p.x, p.y) } const path = Path.parse(data.join(' ')) this.appendSegment(path.segments.slice(1)) return this } close() { return this.appendSegment(Close.create()) } drawPoints( points: (Point.PointLike | Point.PointData)[], options: Util.DrawPointsOptions = {}, ) { const raw = Util.drawPoints(points, options) const sub = Path.parse(raw) if (sub && sub.segments) { this.appendSegment(sub.segments) } } bbox() { const segments = this.segments const count = segments.length if (count === 0) { return null } let bbox for (let i = 0; i < count; i += 1) { const segment = segments[i] if (segment.isVisible) { const segmentBBox = segment.bbox() if (segmentBBox != null) { bbox = bbox ? bbox.union(segmentBBox) : segmentBBox } } } if (bbox != null) { return bbox } // if the path has only invisible elements, return end point of last segment const lastSegment = segments[count - 1] return new Rectangle(lastSegment.end.x, lastSegment.end.y, 0, 0) } appendSegment(seg: Segment | Segment[]) { const count = this.segments.length let previousSegment = count !== 0 ? this.segments[count - 1] : null let currentSegment const nextSegment = null if (Array.isArray(seg)) { for (let i = 0, ii = seg.length; i < ii; i += 1) { const segment = seg[i] currentSegment = this.prepareSegment( segment, previousSegment, nextSegment, ) this.segments.push(currentSegment) previousSegment = currentSegment } } else if (seg != null && seg.isSegment) { currentSegment = this.prepareSegment(seg, previousSegment, nextSegment) this.segments.push(currentSegment) } return this } insertSegment(index: number, seg: Segment | Segment[]) { const count = this.segments.length if (index < 0) { index = count + index + 1 // eslint-disable-line } if (index > count || index < 0) { throw new Error('Index out of range.') } let currentSegment let previousSegment = null let nextSegment = null if (count !== 0) { if (index >= 1) { previousSegment = this.segments[index - 1] nextSegment = previousSegment.nextSegment } else { previousSegment = null nextSegment = this.segments[0] } } if (!Array.isArray(seg)) { currentSegment = this.prepareSegment(seg, previousSegment, nextSegment) this.segments.splice(index, 0, currentSegment) } else { for (let i = 0, ii = seg.length; i < ii; i += 1) { const segment = seg[i] currentSegment = this.prepareSegment( segment, previousSegment, nextSegment, ) this.segments.splice(index + i, 0, currentSegment) previousSegment = currentSegment } } return this } removeSegment(index: number) { const idx = this.fixIndex(index) const removedSegment = this.segments.splice(idx, 1)[0] const previousSegment = removedSegment.previousSegment const nextSegment = removedSegment.nextSegment // link the previous and next segments together (if present) if (previousSegment) { previousSegment.nextSegment = nextSegment } if (nextSegment) { nextSegment.previousSegment = previousSegment } if (removedSegment.isSubpathStart && nextSegment) { this.updateSubpathStartSegment(nextSegment) } return removedSegment } replaceSegment(index: number, seg: Segment | Segment[]) { const idx = this.fixIndex(index) let currentSegment const replacedSegment = this.segments[idx] let previousSegment = replacedSegment.previousSegment const nextSegment = replacedSegment.nextSegment let updateSubpathStart = replacedSegment.isSubpathStart if (!Array.isArray(seg)) { currentSegment = this.prepareSegment(seg, previousSegment, nextSegment) this.segments.splice(idx, 1, currentSegment) if (updateSubpathStart && currentSegment.isSubpathStart) { // already updated by `prepareSegment` updateSubpathStart = false } } else { this.segments.splice(index, 1) for (let i = 0, ii = seg.length; i < ii; i += 1) { const segment = seg[i] currentSegment = this.prepareSegment( segment, previousSegment, nextSegment, ) this.segments.splice(index + i, 0, currentSegment) previousSegment = currentSegment if (updateSubpathStart && currentSegment.isSubpathStart) { updateSubpathStart = false } } } if (updateSubpathStart && nextSegment) { this.updateSubpathStartSegment(nextSegment) } } getSegment(index: number) { const idx = this.fixIndex(index) return this.segments[idx] } protected fixIndex(index: number) { const length = this.segments.length if (length === 0) { throw new Error('Path has no segments.') } let i = index while (i < 0) { i = length + i } if (i >= length || i < 0) { throw new Error('Index out of range.') } return i } segmentAt(ratio: number, options: Path.Options = {}) { const index = this.segmentIndexAt(ratio, options) if (!index) { return null } return this.getSegment(index) } segmentAtLength(length: number, options: Path.Options = {}) { const index = this.segmentIndexAtLength(length, options) if (!index) return null return this.getSegment(index) } segmentIndexAt(ratio: number, options: Path.Options = {}) { if (this.segments.length === 0) { return null } const rate = clamp(ratio, 0, 1) const opt = this.getOptions(options) const len = this.length(opt) const length = len * rate return this.segmentIndexAtLength(length, opt) } segmentIndexAtLength(length: number, options: Path.Options = {}) { const count = this.segments.length if (count === 0) { return null } let fromStart = true if (length < 0) { fromStart = false length = -length // eslint-disable-line } const precision = this.getPrecision(options) const segmentSubdivisions = this.getSubdivisions(options) let memo = 0 let lastVisibleIndex = null for (let i = 0; i < count; i += 1) { const index = fromStart ? i : count - 1 - i const segment = this.segments[index] const subdivisions = segmentSubdivisions[index] const len = segment.length({ precision, subdivisions }) if (segment.isVisible) { if (length <= memo + len) { return index } lastVisibleIndex = index } memo += len } // If length requested is higher than the length of the path, return // last visible segment index. If no visible segment, return null. return lastVisibleIndex } getSegmentSubdivisions(options: Path.Options = {}): Segment[][] { const precision = this.getPrecision(options) const segmentSubdivisions = [] for (let i = 0, ii = this.segments.length; i < ii; i += 1) { const segment = this.segments[i] const subdivisions = segment.getSubdivisions({ precision }) segmentSubdivisions.push(subdivisions) } return segmentSubdivisions } protected updateSubpathStartSegment(segment: Segment) { let previous = segment.previousSegment let current: Segment | null = segment while (current && !current.isSubpathStart) { // assign previous segment's subpath start segment to this segment if (previous != null) { current.subpathStartSegment = previous.subpathStartSegment } else { current.subpathStartSegment = null } previous = current current = current.nextSegment } } protected prepareSegment( segment: Segment, previousSegment: Segment | null, nextSegment: Segment | null, ) { segment.previousSegment = previousSegment segment.nextSegment = nextSegment if (previousSegment != null) { previousSegment.nextSegment = segment } if (nextSegment != null) { nextSegment.previousSegment = segment } let updateSubpathStart: Segment | null = segment if (segment.isSubpathStart) { // move to segment.subpathStartSegment = segment updateSubpathStart = nextSegment } // assign previous segment's subpath start (or self if it is a subpath start) to subsequent segments if (updateSubpathStart != null) { this.updateSubpathStartSegment(updateSubpathStart) } return segment } closestPoint(p: Point.PointLike, options: Path.Options = {}) { const t = this.closestPointT(p, options) if (!t) { return null } return this.pointAtT(t) } closestPointLength(p: Point.PointLike, options: Path.Options = {}) { const opts = this.getOptions(options) const t = this.closestPointT(p, opts) if (!t) { return 0 } return this.lengthAtT(t, opts) } closestPointNormalizedLength(p: Point.PointLike, options: Path.Options = {}) { const opts = this.getOptions(options) const cpLength = this.closestPointLength(p, opts) if (cpLength === 0) { return 0 } const length = this.length(opts) if (length === 0) { return 0 } return cpLength / length } closestPointT(p: Point.PointLike, options: Path.Options = {}) { if (this.segments.length === 0) { return null } const precision = this.getPrecision(options) const segmentSubdivisions = this.getSubdivisions(options) let closestPointT let minSquaredDistance = Infinity for (let i = 0, ii = this.segments.length; i < ii; i += 1) { const segment = this.segments[i] const subdivisions = segmentSubdivisions[i] if (segment.isVisible) { const segmentClosestPointT = segment.closestPointT(p, { precision, subdivisions, }) const segmentClosestPoint = segment.pointAtT(segmentClosestPointT) const squaredDistance = squaredLength(segmentClosestPoint, p) if (squaredDistance < minSquaredDistance) { closestPointT = { segmentIndex: i, value: segmentClosestPointT } minSquaredDistance = squaredDistance } } } if (closestPointT) { return closestPointT } return { segmentIndex: this.segments.length - 1, value: 1 } } closestPointTangent(p: Point.PointLike, options: Path.Options = {}) { if (this.segments.length === 0) { return null } const precision = this.getPrecision(options) const segmentSubdivisions = this.getSubdivisions(options) let closestPointTangent let minSquaredDistance = Infinity for (let i = 0, ii = this.segments.length; i < ii; i += 1) { const segment = this.segments[i] const subdivisions = segmentSubdivisions[i] if (segment.isDifferentiable()) { const segmentClosestPointT = segment.closestPointT(p, { precision, subdivisions, }) const segmentClosestPoint = segment.pointAtT(segmentClosestPointT) const squaredDistance = squaredLength(segmentClosestPoint, p) if (squaredDistance < minSquaredDistance) { closestPointTangent = segment.tangentAtT(segmentClosestPointT) minSquaredDistance = squaredDistance } } } if (closestPointTangent) { return closestPointTangent } return null } containsPoint(p: Point.PointLike, options: Path.Options = {}) { const polylines = this.toPolylines(options) if (!polylines) { return false } let numIntersections = 0 for (let i = 0, ii = polylines.length; i < ii; i += 1) { const polyline = polylines[i] if (polyline.containsPoint(p)) { numIntersections += 1 } } // returns `true` for odd numbers of intersections (even-odd algorithm) return numIntersections % 2 === 1 } pointAt(ratio: number, options: Path.Options = {}) { if (this.segments.length === 0) { return null } if (ratio <= 0) { return this.start!.clone() } if (ratio >= 1) { return this.end!.clone() } const opts = this.getOptions(options) const pathLength = this.length(opts) const length = pathLength * ratio return this.pointAtLength(length, opts) } pointAtLength(length: number, options: Path.Options = {}) { if (this.segments.length === 0) { return null } if (length === 0) { return this.start!.clone() } let fromStart = true if (length < 0) { fromStart = false length = -length // eslint-disable-line } const precision = this.getPrecision(options) const segmentSubdivisions = this.getSubdivisions(options) let lastVisibleSegment let memo = 0 for (let i = 0, ii = this.segments.length; i < ii; i += 1) { const index = fromStart ? i : ii - 1 - i const segment = this.segments[index] const subdivisions = segmentSubdivisions[index] const d = segment.length({ precision, subdivisions, }) if (segment.isVisible) { if (length <= memo + d) { return segment.pointAtLength((fromStart ? 1 : -1) * (length - memo), { precision, subdivisions, }) } lastVisibleSegment = segment } memo += d } // if length requested is higher than the length of the path, // return last visible segment endpoint if (lastVisibleSegment) { return fromStart ? lastVisibleSegment.end : lastVisibleSegment.start } // if no visible segment, return last segment end point const lastSegment = this.segments[this.segments.length - 1] return lastSegment.end.clone() } pointAtT(t: { segmentIndex: number; value: number }) { const segments = this.segments const numSegments = segments.length if (numSegments === 0) return null // if segments is an empty array const segmentIndex = t.segmentIndex if (segmentIndex < 0) return segments[0].pointAtT(0) if (segmentIndex >= numSegments) { return segments[numSegments - 1].pointAtT(1) } const tValue = clamp(t.value, 0, 1) return segments[segmentIndex].pointAtT(tValue) } divideAt(ratio: number, options: Path.Options = {}) { if (this.segments.length === 0) { return null } const rate = clamp(ratio, 0, 1) const opts = this.getOptions(options) const len = this.length(opts) const length = len * rate return this.divideAtLength(length, opts) } divideAtLength(length: number, options: Path.Options = {}) { if (this.segments.length === 0) { return null } let fromStart = true if (length < 0) { fromStart = false length = -length // eslint-disable-line } const precision = this.getPrecision(options) const segmentSubdivisions = this.getSubdivisions(options) let memo = 0 let divided let dividedSegmentIndex let lastValidSegment let lastValidSegmentIndex let t for (let i = 0, ii = this.segments.length; i < ii; i += 1) { const index = fromStart ? i : ii - 1 - i const segment = this.getSegment(index) const subdivisions = segmentSubdivisions[index] const opts = { precision, subdivisions } const len = segment.length(opts) if (segment.isDifferentiable()) { lastValidSegment = segment lastValidSegmentIndex = index if (length <= memo + len) { dividedSegmentIndex = index divided = segment.divideAtLength( (fromStart ? 1 : -1) * (length - memo), opts, ) break } } memo += len } if (!lastValidSegment) { return null } if (!divided) { dividedSegmentIndex = lastValidSegmentIndex t = fromStart ? 1 : 0 divided = lastValidSegment.divideAtT(t) } // create a copy of this path and replace the identified segment with its two divided parts: const pathCopy = this.clone() const index = dividedSegmentIndex as number pathCopy.replaceSegment(index, divided) const divisionStartIndex = index let divisionMidIndex = index + 1 let divisionEndIndex = index + 2 // do not insert the part if it looks like a point if (!divided[0].isDifferentiable()) { pathCopy.removeSegment(divisionStartIndex) divisionMidIndex -= 1 divisionEndIndex -= 1 } // insert a Moveto segment to ensure secondPath will be valid: const movetoEnd = pathCopy.getSegment(divisionMidIndex).start pathCopy.insertSegment(divisionMidIndex, Path.createSegment('M', movetoEnd)) divisionEndIndex += 1 // do not insert the part if it looks like a point if (!divided[1].isDifferentiable()) { pathCopy.removeSegment(divisionEndIndex - 1) divisionEndIndex -= 1 } // ensure that Closepath segments in secondPath will be assigned correct subpathStartSegment: const secondPathSegmentIndexConversion = divisionEndIndex - divisionStartIndex - 1 for ( let i = divisionEndIndex, ii = pathCopy.segments.length; i < ii; i += 1 ) { const originalSegment = this.getSegment( i - secondPathSegmentIndexConversion, ) const segment = pathCopy.getSegment(i) if ( segment.type === 'Z' && !originalSegment.subpathStartSegment!.end.equals( segment.subpathStartSegment!.end, ) ) { // pathCopy segment's subpathStartSegment is different from original segment's one // convert this Closepath segment to a Lineto and replace it in pathCopy const convertedSegment = Path.createSegment('L', originalSegment.end) pathCopy.replaceSegment(i, convertedSegment) } } // distribute pathCopy segments into two paths and return those: const firstPath = new Path(pathCopy.segments.slice(0, divisionMidIndex)) const secondPath = new Path(pathCopy.segments.slice(divisionMidIndex)) return [firstPath, secondPath] } intersectsWithLine(line: Line, options: Path.Options = {}) { const polylines = this.toPolylines(options) if (polylines == null) { return null } let intersections: Point[] | null = null for (let i = 0, ii = polylines.length; i < ii; i += 1) { const polyline = polylines[i] const intersection = line.intersect(polyline) if (intersection) { if (intersections == null) { intersections = [] } if (Array.isArray(intersection)) { intersections.push(...intersection) } else { intersections.push(intersection) } } } return intersections } isDifferentiable() { for (let i = 0, ii = this.segments.length; i < ii; i += 1) { const segment = this.segments[i] if (segment.isDifferentiable()) { return true } } return false } isValid() { const segments = this.segments const isValid = segments.length === 0 || segments[0].type === 'M' return isValid } length(options: Path.Options = {}) { if (this.segments.length === 0) { return 0 } const segmentSubdivisions = this.getSubdivisions(options) let length = 0 for (let i = 0, ii = this.segments.length; i < ii; i += 1) { const segment = this.segments[i] const subdivisions = segmentSubdivisions[i] length += segment.length({ subdivisions }) } return length } lengthAtT( t: { segmentIndex: number; value: number }, options: Path.Options = {}, ) { const count = this.segments.length if (count === 0) { return 0 } let segmentIndex = t.segmentIndex if (segmentIndex < 0) { return 0 } let tValue = clamp(t.value, 0, 1) if (segmentIndex >= count) { segmentIndex = count - 1 tValue = 1 } const precision = this.getPrecision(options) const segmentSubdivisions = this.getSubdivisions(options) let length = 0 for (let i = 0; i < segmentIndex; i += 1) { const segment = this.segments[i] const subdivisions = segmentSubdivisions[i] length += segment.length({ precision, subdivisions }) } const segment = this.segments[segmentIndex] const subdivisions = segmentSubdivisions[segmentIndex] length += segment.lengthAtT(tValue, { precision, subdivisions }) return length } tangentAt(ratio: number, options: Path.Options = {}) { if (this.segments.length === 0) { return null } const rate = clamp(ratio, 0, 1) const opts = this.getOptions(options) const len = this.length(opts) const length = len * rate return this.tangentAtLength(length, opts) } tangentAtLength(length: number, options: Path.Options = {}) { if (this.segments.length === 0) { return null } let fromStart = true if (length < 0) { fromStart = false length = -length // eslint-disable-line } const precision = this.getPrecision(options) const segmentSubdivisions = this.getSubdivisions(options) let lastValidSegment let memo = 0 for (let i = 0, ii = this.segments.length; i < ii; i += 1) { const index = fromStart ? i : ii - 1 - i const segment = this.segments[index] const subdivisions = segmentSubdivisions[index] const len = segment.length({ precision, subdivisions }) if (segment.isDifferentiable()) { if (length <= memo + len) { return segment.tangentAtLength( (fromStart ? 1 : -1) * (length - memo), { precision, subdivisions, }, ) } lastValidSegment = segment } memo += len } // if length requested is higher than the length of the path, return tangent of endpoint of last valid segment if (lastValidSegment) { const t = fromStart ? 1 : 0 return lastValidSegment.tangentAtT(t) } // if no valid segment, return null return null } tangentAtT(t: { segmentIndex: number; value: number }) { const count = this.segments.length if (count === 0) { return null } const segmentIndex = t.segmentIndex if (segmentIndex < 0) { return this.segments[0].tangentAtT(0) } if (segmentIndex >= count) { return this.segments[count - 1].tangentAtT(1) } const tValue = clamp(t.value, 0, 1) return this.segments[segmentIndex].tangentAtT(tValue) } protected getPrecision(options: Path.Options = {}) { return options.precision == null ? this.PRECISION : options.precision } protected getSubdivisions(options: Path.Options = {}) { if (options.segmentSubdivisions == null) { const precision = this.getPrecision(options) return this.getSegmentSubdivisions({ precision }) } return options.segmentSubdivisions } protected getOptions(options: Path.Options = {}) { const precision = this.getPrecision(options) const segmentSubdivisions = this.getSubdivisions(options) return { precision, segmentSubdivisions } } toPoints(options: Path.Options = {}) { const segments = this.segments const count = segments.length if (count === 0) { return null } const segmentSubdivisions = this.getSubdivisions(options) const points = [] let partialPoints = [] for (let i = 0; i < count; i += 1) { const segment = segments[i] if (segment.isVisible) { const divisions = segmentSubdivisions[i] if (divisions.length > 0) { // eslint-disable-next-line no-loop-func divisions.forEach((c) => partialPoints.push(c.start)) } else { partialPoints.push(segment.start) } } else if (partialPoints.length > 0) { partialPoints.push(segments[i - 1].end) points.push(partialPoints) partialPoints = [] } } if (partialPoints.length > 0) { partialPoints.push(this.end!) points.push(partialPoints) } return points } toPolylines(options: Path.Options = {}) { const points = this.toPoints(options) if (!points) { return null } return points.map((arr) => new Polyline(arr)) } scale(sx: number, sy: number, origin?: Point.PointLike) { this.segments.forEach((s) => s.scale(sx, sy, origin)) return this } rotate(angle: number, origin?: Point.PointLike | Point.PointData) { this.segments.forEach((segment) => segment.rotate(angle, origin)) return this } translate(tx: number, ty: number): this translate(p: Point.PointLike): this translate(tx: number | Point.PointLike, ty?: number) { if (typeof tx === 'number') { this.segments.forEach((s) => s.translate(tx, ty as number)) } else { this.segments.forEach((s) => s.translate(tx)) } return this } clone() { const path = new Path() this.segments.forEach((s) => path.appendSegment(s.clone())) return path } equals(p: Path) { if (p == null) { return false } const segments = this.segments const otherSegments = p.segments const count = segments.length if (otherSegments.length !== count) { return false } for (let i = 0; i < count; i += 1) { const a = segments[i] const b = otherSegments[i] if (a.type !== b.type || !a.equals(b)) { return false } } return true } toJSON() { return this.segments.map((s) => s.toJSON()) } serialize() { if (!this.isValid()) { throw new Error('Invalid path segments.') } return this.segments.map((s) => s.serialize()).join(' ') } toString() { return this.serialize() } } export namespace Path { export const toStringTag = `X6.Geometry.${Path.name}` export function isPath(instance: any): instance is Path { if (instance == null) { return false } if (instance instanceof Path) { return true } const tag = instance[Symbol.toStringTag] const path = instance as Path if ( (tag == null || tag === toStringTag) && Array.isArray(path.segments) && typeof path.moveTo === 'function' && typeof path.lineTo === 'function' && typeof path.curveTo === 'function' ) { return true } return false } } export namespace Path { export interface Options { precision?: number | null segmentSubdivisions?: Segment[][] | null } } export namespace Path { export function parse(pathData: string) { if (!pathData) { return new Path() } const path = new Path() const commandRe = /(?:[a-zA-Z] *)(?:(?:-?\d+(?:\.\d+)?(?:e[-+]?\d+)? *,? *)|(?:-?\.\d+ *,? *))+|(?:[a-zA-Z] *)(?! |\d|-|\.)/g const commands = normalize(pathData).match(commandRe) if (commands != null) { for (let i = 0, ii = commands.length; i < ii; i += 1) { const command = commands[i] const argRe = /(?:[a-zA-Z])|(?:(?:-?\d+(?:\.\d+)?(?:e[-+]?\d+)?))|(?:(?:-?\.\d+))/g // args = [type, coordinate1, coordinate2...] const args = command.match(argRe) if (args != null) { const type = args[0] const coords = args.slice(1).map((a) => +a) const segment = createSegment.call(null, type, ...coords) path.appendSegment(segment) } } } return path } export function createSegment(type: 'M', x: number, y: number): MoveTo export function createSegment(type: 'M', point: Point.PointLike): MoveTo export function createSegment(type: 'M', line: Line): MoveTo export function createSegment(type: 'M', curve: Curve): MoveTo export function createSegment( type: 'M', point: Point.PointLike, ...points: Point.PointLike[] ): Segment[] export function createSegment( type: 'M', x: number, y: number, ...coords: number[] ): Segment[] export function createSegment(type: 'L', x: number, y: number): LineTo export function createSegment(type: 'L', point: Point.PointLike): LineTo export function createSegment(type: 'L', line: Line): LineTo export function createSegment( type: 'L', point: Point.PointLike, ...points: Point.PointLike[] ): LineTo[] export function createSegment( type: 'L', x: number, y: number, ...coords: number[] ): LineTo[] export function createSegment( type: 'C', x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, ): CurveTo export function createSegment( type: 'C', x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, ...coords: number[] ): CurveTo[] export function createSegment( type: 'C', p1: Point.PointLike, p2: Point.PointLike, p3: Point.PointLike, ): CurveTo export function createSegment( type: 'C', p1: Point.PointLike, p2: Point.PointLike, p3: Point.PointLike, ...points: Point.PointLike[] ): CurveTo[] export function createSegment(type: 'Z' | 'z'): Close export function createSegment( type: 'M' | 'L' | 'C' | 'Z' | 'z', ...args: any[] ): | MoveTo | MoveTo[] | LineTo | LineTo[] | CurveTo | CurveTo[] | Close | Segment | Segment[] { if (type === 'M') { return MoveTo.create.call(null, ...args) } if (type === 'L') { return LineTo.create.call(null, ...args) } if (type === 'C') { return CurveTo.create.call(null, ...args) } if (type === 'z' || type === 'Z') { return Close.create() } throw new Error(`Invalid path segment type "${type}"`) } } export namespace Path { export const normalize = normalizePathData export const isValid = Util.isValid export const drawArc = Util.drawArc export const drawPoints = Util.drawPoints export const arcToCurves = Util.arcToCurves }