import { Curve3 } from './curve3.js';
import { Vector3 } from './vector3.js';
/**
* Minimum control point count for Catmull-Rom curve.
*
* @type {number}
*/
const MIN_CONTROL_POINT_COUNT = 2;
/**
* Default closed flag for Catmull-Rom curve.
*
* @type {boolean}
*/
const DEFAULT_CLOSED = false;
/**
* Clamp minimum for the curve parameter.
*
* @type {number}
*/
const CURVE_PARAMETER_MIN = 0;
/**
* Clamp maximum for the curve parameter.
*
* @type {number}
*/
const CURVE_PARAMETER_MAX = 1;
/**
* Zero value constant.
*
* @type {number}
*/
const ZERO_VALUE = 0;
/**
* Segment offset for the last segment in open curves.
*
* @type {number}
*/
const OPEN_CURVE_LAST_SEGMENT_OFFSET = 1;
/**
* One value used in math formulas.
*
* @type {number}
*/
const ONE_VALUE = 1;
/**
* Two value used in math formulas.
*
* @type {number}
*/
const TWO_VALUE = 2;
/**
* Three value used in math formulas.
*
* @type {number}
*/
const THREE_VALUE = 3;
/**
* Four value used in math formulas.
*
* @type {number}
*/
const FOUR_VALUE = 4;
/**
* Five value used in math formulas.
*
* @type {number}
*/
const FIVE_VALUE = 5;
/**
* Catmull-Rom tension multiplier.
*
* @type {number}
*/
const CATMULL_ROM_TENSION = 0.5;
/**
* Small epsilon used for tangent normalization.
*
* @type {number}
*/
const TANGENT_EPSILON = 1e-8;
/**
* Catmull-Rom curve in 3D.
*/
export class CatmullRomCurve3 extends Curve3 {
/**
* Control points used to build the curve.
*
* @type {Vector3[]}
* @private
*/
#points;
/**
* Whether the curve is closed (wraps around to the first point).
*
* @type {boolean}
* @private
*/
#closed;
/**
* @param {Vector3[]} points - Control points.
* @param {Object} [options] - Optional options.
* @param {boolean} [options.closed=false] - Whether the curve is closed.
* @throws {TypeError} When inputs are invalid.
* @throws {RangeError} When points are insufficient.
*/
constructor(points, options = {}) {
super();
if (!Array.isArray(points)) {
throw new TypeError('`CatmullRomCurve3` expects `points` as an array of `Vector3`.');
}
if (points.length < MIN_CONTROL_POINT_COUNT) {
throw new RangeError('`CatmullRomCurve3` expects at least 2 control points.');
}
for (const point of points) {
if (!(point instanceof Vector3)) {
throw new TypeError('`CatmullRomCurve3` expects all points to be `Vector3` instances.');
}
}
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError('`CatmullRomCurve3` expects options as a plain object.');
}
const { closed = DEFAULT_CLOSED } = options;
if (typeof closed !== 'boolean') {
throw new TypeError('`CatmullRomCurve3` option `closed` must be a boolean.');
}
this.#points = points.slice();
this.#closed = closed;
}
/**
* @returns {Vector3[]}
*/
get points() {
return this.#points;
}
/**
* @returns {boolean}
*/
get closed() {
return this.#closed;
}
/**
* @param {number} curveParameter - Parameter in [0..1].
* @param {Vector3} [out] - Optional output vector.
* @returns {Vector3}
* @throws {TypeError} When inputs are invalid.
*/
getPoint(curveParameter, out = new Vector3()) {
if (typeof curveParameter !== 'number' || !Number.isFinite(curveParameter)) {
throw new TypeError('`CatmullRomCurve3.getPoint` expects `curveParameter` as a finite number.');
}
if (!(out instanceof Vector3)) {
throw new TypeError('`CatmullRomCurve3.getPoint` expects `out` as a Vector3.');
}
// Clamp curve parameter and map it to the `segment index + local` segment parameter:
const clampedCurveParameter = CatmullRomCurve3.#clampCurveParameter(curveParameter);
const segmentCount = this.#closed ? this.#points.length : (this.#points.length - OPEN_CURVE_LAST_SEGMENT_OFFSET);
const scaledCurveParameter = clampedCurveParameter * segmentCount;
const segmentIndex = Math.min(Math.floor(scaledCurveParameter), segmentCount - OPEN_CURVE_LAST_SEGMENT_OFFSET);
const segmentParameter = scaledCurveParameter - segmentIndex;
// Resolve the four control points, affecting the current segment:
const previousControlPoint = this.#getPointForSegment(segmentIndex - ONE_VALUE);
const startControlPoint = this.#getPointForSegment(segmentIndex);
const endControlPoint = this.#getPointForSegment(segmentIndex + ONE_VALUE);
const nextControlPoint = this.#getPointForSegment(segmentIndex + TWO_VALUE);
// Evaluate the Catmull-Rom spline polynomial (per-axis) using the local segment parameter powers:
const segmentParameterSquared = segmentParameter * segmentParameter;
const segmentParameterCubed = segmentParameterSquared * segmentParameter;
const x = CATMULL_ROM_TENSION * (
(TWO_VALUE * startControlPoint.x)
+ (-previousControlPoint.x + endControlPoint.x) * segmentParameter
+ (TWO_VALUE * previousControlPoint.x - FIVE_VALUE * startControlPoint.x + FOUR_VALUE * endControlPoint.x - nextControlPoint.x) * segmentParameterSquared
+ (-previousControlPoint.x + THREE_VALUE * startControlPoint.x - THREE_VALUE * endControlPoint.x + nextControlPoint.x) * segmentParameterCubed
);
const y = CATMULL_ROM_TENSION * (
(TWO_VALUE * startControlPoint.y)
+ (-previousControlPoint.y + endControlPoint.y) * segmentParameter
+ (TWO_VALUE * previousControlPoint.y - FIVE_VALUE * startControlPoint.y + FOUR_VALUE * endControlPoint.y - nextControlPoint.y) * segmentParameterSquared
+ (-previousControlPoint.y + THREE_VALUE * startControlPoint.y - THREE_VALUE * endControlPoint.y + nextControlPoint.y) * segmentParameterCubed
);
const z = CATMULL_ROM_TENSION * (
(TWO_VALUE * startControlPoint.z)
+ (-previousControlPoint.z + endControlPoint.z) * segmentParameter
+ (TWO_VALUE * previousControlPoint.z - FIVE_VALUE * startControlPoint.z + FOUR_VALUE * endControlPoint.z - nextControlPoint.z) * segmentParameterSquared
+ (-previousControlPoint.z + THREE_VALUE * startControlPoint.z - THREE_VALUE * endControlPoint.z + nextControlPoint.z) * segmentParameterCubed
);
return out.set(x, y, z);
}
/**
* @param {number} curveParameter - Parameter in [0..1].
* @param {Vector3} [out] - Optional output vector.
* @returns {Vector3}
* @throws {TypeError} When inputs are invalid.
*/
getTangent(curveParameter, out = new Vector3()) {
if (typeof curveParameter !== 'number' || !Number.isFinite(curveParameter)) {
throw new TypeError('`CatmullRomCurve3.getTangent` expects `curveParameter` as a finite number.');
}
if (!(out instanceof Vector3)) {
throw new TypeError('`CatmullRomCurve3.getTangent` expects `out` as a Vector3.');
}
// Clamp curve parameter and map it to the `segment index + local` segment parameter:
const clampedCurveParameter = CatmullRomCurve3.#clampCurveParameter(curveParameter);
const segmentCount = this.#closed ? this.#points.length : (this.#points.length - OPEN_CURVE_LAST_SEGMENT_OFFSET);
const scaledCurveParameter = clampedCurveParameter * segmentCount;
const segmentIndex = Math.min(Math.floor(scaledCurveParameter), segmentCount - OPEN_CURVE_LAST_SEGMENT_OFFSET);
const segmentParameter = scaledCurveParameter - segmentIndex;
// Resolve the four control points, affecting the current segment:
const previousControlPoint = this.#getPointForSegment(segmentIndex - ONE_VALUE);
const startControlPoint = this.#getPointForSegment(segmentIndex);
const endControlPoint = this.#getPointForSegment(segmentIndex + ONE_VALUE);
const nextControlPoint = this.#getPointForSegment(segmentIndex + TWO_VALUE);
// Evaluate the Catmull-Rom tangent polynomial (per-axis), then normalize it to a unit direction vector:
const segmentParameterSquared = segmentParameter * segmentParameter;
const x = CATMULL_ROM_TENSION * (
(-previousControlPoint.x + endControlPoint.x)
+ (TWO_VALUE * (TWO_VALUE * previousControlPoint.x - FIVE_VALUE * startControlPoint.x + FOUR_VALUE * endControlPoint.x - nextControlPoint.x)) * segmentParameter
+ (THREE_VALUE * (-previousControlPoint.x + THREE_VALUE * startControlPoint.x - THREE_VALUE * endControlPoint.x + nextControlPoint.x)) * segmentParameterSquared
);
const y = CATMULL_ROM_TENSION * (
(-previousControlPoint.y + endControlPoint.y)
+ (TWO_VALUE * (TWO_VALUE * previousControlPoint.y - FIVE_VALUE * startControlPoint.y + FOUR_VALUE * endControlPoint.y - nextControlPoint.y)) * segmentParameter
+ (THREE_VALUE * (-previousControlPoint.y + THREE_VALUE * startControlPoint.y - THREE_VALUE * endControlPoint.y + nextControlPoint.y)) * segmentParameterSquared
);
const z = CATMULL_ROM_TENSION * (
(-previousControlPoint.z + endControlPoint.z)
+ (TWO_VALUE * (TWO_VALUE * previousControlPoint.z - FIVE_VALUE * startControlPoint.z + FOUR_VALUE * endControlPoint.z - nextControlPoint.z)) * segmentParameter
+ (THREE_VALUE * (-previousControlPoint.z + THREE_VALUE * startControlPoint.z - THREE_VALUE * endControlPoint.z + nextControlPoint.z)) * segmentParameterSquared
);
const length = Math.sqrt((x * x) + (y * y) + (z * z));
if (length <= TANGENT_EPSILON) {
return out.set(ZERO_VALUE, ZERO_VALUE, ZERO_VALUE);
}
return out.set(x / length, y / length, z / length);
}
/**
* @param {number} index - Segment index.
* @returns {Vector3}
* @private
*/
#getPointForSegment(index) {
const pointCount = this.#points.length;
if (this.#closed) {
const wrappedIndex = ((index % pointCount) + pointCount) % pointCount;
return this.#points[wrappedIndex];
}
if (index < ZERO_VALUE) {
return this.#points[ZERO_VALUE];
}
if (index >= pointCount) {
return this.#points[pointCount - ONE_VALUE];
}
return this.#points[index];
}
/**
* @param {number} curveParameter - Parameter value.
* @returns {number}
* @private
*/
static #clampCurveParameter(curveParameter) {
if (curveParameter <= CURVE_PARAMETER_MIN) {
return CURVE_PARAMETER_MIN;
}
if (curveParameter >= CURVE_PARAMETER_MAX) {
return CURVE_PARAMETER_MAX;
}
return curveParameter;
}
}