import type { Vector2 } from "../math/Vector2.ts"; import { Curve } from "./Curve.ts"; import { LineCurve } from "./curves/LineCurve.ts"; /** Ordered sequence of connected curves forming a path. */ export class CurvePath extends Curve { override type = "CurvePath"; #curves: Curve[] = []; #autoClose = false; #cacheCurveLengths: number[] | undefined = undefined; #needsUpdate = true; get curves(): Curve[] { return this.#curves; } get autoClose(): boolean { return this.#autoClose; } set autoClose(value: boolean) { this.#autoClose = value; this.#invalidateCurveLengths(false); } /** Appends a curve to the path. */ add(curve: Curve): void { this.#curves.push(curve); this.#invalidateCurveLengths(false); } /** Closes the path by appending a LineCurve from the last point to the first. */ closePath(): void { const startPoint = this.#curves[0].getPoint(0) as Vector2; const endPoint = this.#curves[this.#curves.length - 1].getPoint( 1, ) as Vector2; if (!startPoint.equals(endPoint)) { this.#curves.push(new LineCurve(endPoint, startPoint)); this.#invalidateCurveLengths(false); } } #invalidateCurveLengths(invalidateChildren: boolean): void { this.#cacheCurveLengths = undefined; this.#needsUpdate = true; super.updateArcLengths(); if (invalidateChildren) { for (const curve of this.#curves) { curve.updateArcLengths(); } } } /** Returns the point on the path at parameter t, mapping across sub-curves. */ override getPoint( t: number, target?: { x: number; y: number; z?: number }, ): { x: number; y: number; z?: number } | undefined { const d = t * this.getLength(); const curveLengths = this.getCurveLengths(); let i = 0; while (i < curveLengths.length) { if (curveLengths[i] >= d) { const diff = curveLengths[i] - d; const curve = this.#curves[i]; const segmentLength = curve.getLength(); const u = segmentLength === 0 ? 0 : 1 - diff / segmentLength; return curve.getPointAt(u, target); } i++; } return undefined; } /** Returns the total arc length of the path. */ override getLength(): number { const lengths = this.getCurveLengths(); return lengths[lengths.length - 1]; } /** Returns an array of cumulative arc lengths for each sub-curve. */ getCurveLengths(): number[] { const curves = this.#curves; const curveCount = curves.length; if (curveCount === 0) return [0]; if ( this.#cacheCurveLengths !== undefined && this.#cacheCurveLengths.length === curveCount && !this.#needsUpdate ) { return this.#cacheCurveLengths; } const lengths = new Array(curveCount); let sum = 0; for (let i = 0; i < curveCount; i++) { sum += curves[i].getLength(); lengths[i] = sum; } this.#cacheCurveLengths = lengths; this.#needsUpdate = false; return lengths; } override updateArcLengths(): void { this.#invalidateCurveLengths(true); } /** Returns an array of (divisions + 1) points evenly spaced by parameter. */ override getPoints( divisions = 12, ): Array<{ x: number; y: number; z?: number } | undefined> { const points: Array<{ x: number; y: number; z?: number } | undefined> = []; for (let d = 0; d <= divisions; d++) { points.push(this.getPoint(d / divisions, undefined)); } return points; } /** Returns an array of (divisions + 1) points evenly spaced by arc length. */ override getSpacedPoints( divisions = 40, ): Array<{ x: number; y: number; z?: number } | undefined> { const points: Array<{ x: number; y: number; z?: number } | undefined> = []; for (let d = 0; d <= divisions; d++) { points.push(this.getPointAt(d / divisions)); } return points; } override clone(): CurvePath { const Ctor = this.constructor as new () => CurvePath; return new Ctor().copy(this); } override copy(source: CurvePath): this { super.copy(source); this.#curves = source.curves.map((c) => c.clone()); this.#autoClose = source.autoClose; this.#invalidateCurveLengths(false); return this; } }