///|
/// A normalized projection on a spline, useful for path-following cameras.
pub(all) struct SplineProjection {
parameter : Double
distance : Double
value : Point2
error : Double
} derive(Debug)
///|
pub fn SplineProjection::parameter(self : SplineProjection) -> Double {
self.parameter
}
///|
pub fn SplineProjection::distance(self : SplineProjection) -> Double {
self.distance
}
///|
pub fn SplineProjection::value(self : SplineProjection) -> Point2 {
self.value
}
///|
pub fn SplineProjection::error(self : SplineProjection) -> Double {
self.error
}
///|
/// A Catmull-Rom spline over 2D control points.
///
/// `tension=0` is the standard centripetal-free Catmull-Rom form. The
/// `closed` option connects the last control point back to the first one.
pub struct Spline2D {
points : Array[Point2]
tension : Double
closed : Bool
} derive(Debug)
///|
pub fn Spline2D::new(
points : Array[Point2],
tension? : Double = 0.0,
closed? : Bool = false,
) -> Spline2D raise MotionError {
if points.length() < 2 {
raise MotionError::InvalidPointCount(points.length())
}
ensure_finite(tension)
if tension < 0.0 || tension > 1.0 {
raise MotionError::InvalidTension(tension)
}
{ points: points.copy(), tension, closed }
}
///|
pub fn Spline2D::control_points(self : Spline2D) -> Array[Point2] {
self.points.copy()
}
///|
pub fn Spline2D::point_count(self : Spline2D) -> Int {
self.points.length()
}
///|
pub fn Spline2D::tension(self : Spline2D) -> Double {
self.tension
}
///|
pub fn Spline2D::closed(self : Spline2D) -> Bool {
self.closed
}
///|
pub fn Spline2D::segment_count(self : Spline2D) -> Int {
if self.closed {
self.points.length()
} else {
self.points.length() - 1
}
}
///|
fn wrap_index(index : Int, length : Int) -> Int {
let remainder = index % length
if remainder < 0 {
remainder + length
} else {
remainder
}
}
///|
fn Spline2D::control_at(self : Spline2D, index : Int) -> Point2 {
if self.closed {
self.points[wrap_index(index, self.points.length())]
} else {
self.points[index.max(0).min(self.points.length() - 1)]
}
}
///|
fn catmull_component(
previous : Double,
start : Double,
end : Double,
next : Double,
t : Double,
tension : Double,
) -> Double {
let tangent_scale = (1.0 - tension) / 2.0
let tangent_start = (end - previous) * tangent_scale
let tangent_end = (next - start) * tangent_scale
let t2 = t * t
let t3 = t2 * t
let h00 = 2.0 * t3 - 3.0 * t2 + 1.0
let h10 = t3 - 2.0 * t2 + t
let h01 = -2.0 * t3 + 3.0 * t2
let h11 = t3 - t2
h00 * start + h10 * tangent_start + h01 * end + h11 * tangent_end
}
///|
pub fn catmull_rom_point2(
previous : Point2,
start : Point2,
end : Point2,
next : Point2,
t : Double,
tension? : Double = 0.0,
) -> Point2 {
{
x: catmull_component(previous.x(), start.x(), end.x(), next.x(), t, tension),
y: catmull_component(previous.y(), start.y(), end.y(), next.y(), t, tension),
}
}
///|
pub fn Spline2D::sample(self : Spline2D, parameter : Double) -> Point2 {
let normalized = clamp01(parameter)
let segment_count = self.segment_count()
if normalized >= 1.0 {
return self.control_at(
if self.closed {
segment_count
} else {
segment_count
},
)
}
let scaled = normalized * segment_count.to_double()
let index = scaled.to_int().min(segment_count - 1)
let segment_progress = scaled - index.to_double()
catmull_rom_point2(
self.control_at(index - 1),
self.control_at(index),
self.control_at(index + 1),
self.control_at(index + 2),
segment_progress,
tension=self.tension,
)
}
///|
pub fn Spline2D::tangent(self : Spline2D, parameter : Double) -> Point2 {
let normalized = clamp01(parameter)
let epsilon = 0.000001
let before = self.sample((normalized - epsilon).max(0.0))
let after = self.sample((normalized + epsilon).min(1.0))
{
x: (after.x() - before.x()) / (2.0 * epsilon),
y: (after.y() - before.y()) / (2.0 * epsilon),
}
}
///|
pub fn Spline2D::speed(self : Spline2D, parameter : Double) -> Double {
let tangent = self.tangent(parameter)
@math.hypot(tangent.x(), tangent.y())
}
///|
pub fn Spline2D::sample_many(
self : Spline2D,
count : Int,
) -> Array[Point2] raise MotionError {
if count < 2 {
raise MotionError::InvalidSampleCount(count)
}
let result : Array[Point2] = []
for index in 0.. Double {
if samples_per_segment <= 0 {
return 0.0
}
let total_samples = self.segment_count() * samples_per_segment
let mut length = 0.0
let mut previous = self.sample(0.0)
for index in 1..<=total_samples {
let current = self.sample(index.to_double() / total_samples.to_double())
length = length + previous.distance(current)
previous = current
}
length
}
///|
pub fn Spline2D::sample_by_distance(
self : Spline2D,
distance : Double,
samples_per_segment? : Int = 32,
) -> Point2 {
let segments = samples_per_segment.max(1)
let total_samples = self.segment_count() * segments
let total_length = self.length(segments)
if total_length <= 0.0 || distance <= 0.0 {
return self.sample(0.0)
}
if distance >= total_length {
return self.sample(1.0)
}
let target = distance
let mut travelled = 0.0
let mut previous = self.sample(0.0)
for index in 1..<=total_samples {
let parameter = index.to_double() / total_samples.to_double()
let current = self.sample(parameter)
let step = previous.distance(current)
if travelled + step >= target && step > 0.0 {
let ratio = (target - travelled) / step
return previous.lerp(current, ratio)
}
travelled = travelled + step
previous = current
}
self.sample(1.0)
}
///|
pub fn Spline2D::bounds(
self : Spline2D,
samples_per_segment? : Int = 32,
) -> Bounds2D {
let count = self.segment_count() * samples_per_segment.max(1)
let first = self.sample(0.0)
let mut min_x = first.x()
let mut max_x = first.x()
let mut min_y = first.y()
let mut max_y = first.y()
for index in 1..<=count {
let point = self.sample(index.to_double() / count.to_double())
min_x = min_x.min(point.x())
max_x = max_x.max(point.x())
min_y = min_y.min(point.y())
max_y = max_y.max(point.y())
}
{ minimum: point2(x=min_x, y=min_y), maximum: point2(x=max_x, y=max_y) }
}
///|
pub fn Spline2D::polyline(
self : Spline2D,
samples_per_segment : Int,
) -> Array[Point2] {
let count = self.segment_count() * samples_per_segment.max(1)
let points : Array[Point2] = []
for index in 0..<=count {
points.push(self.sample(index.to_double() / count.to_double()))
}
points
}
///|
/// Find the closest sampled point to a target, returning an approximate
/// normalized parameter and the actual point on the spline.
pub fn Spline2D::project(
self : Spline2D,
target : Point2,
samples_per_segment? : Int = 32,
) -> SplineProjection {
let count = self.segment_count() * samples_per_segment.max(1)
let mut best_parameter = 0.0
let mut best_value = self.sample(0.0)
let mut best_distance = best_value.distance(target)
for index in 1..<=count {
let parameter = index.to_double() / count.to_double()
let value = self.sample(parameter)
let distance = value.distance(target)
if distance < best_distance {
best_parameter = parameter
best_value = value
best_distance = distance
}
}
{
parameter: best_parameter,
distance: best_distance,
value: best_value,
error: best_distance,
}
}
///|
/// A Catmull-Rom spline over 3D control points for camera and scene motion.
pub struct Spline3D {
points : Array[Point3]
tension : Double
closed : Bool
} derive(Debug)
///|
pub fn Spline3D::new(
points : Array[Point3],
tension? : Double = 0.0,
closed? : Bool = false,
) -> Spline3D raise MotionError {
if points.length() < 2 {
raise MotionError::InvalidPointCount(points.length())
}
ensure_finite(tension)
if tension < 0.0 || tension > 1.0 {
raise MotionError::InvalidTension(tension)
}
{ points: points.copy(), tension, closed }
}
///|
pub fn Spline3D::control_points(self : Spline3D) -> Array[Point3] {
self.points.copy()
}
///|
pub fn Spline3D::point_count(self : Spline3D) -> Int {
self.points.length()
}
///|
pub fn Spline3D::tension(self : Spline3D) -> Double {
self.tension
}
///|
pub fn Spline3D::closed(self : Spline3D) -> Bool {
self.closed
}
///|
pub fn Spline3D::segment_count(self : Spline3D) -> Int {
if self.closed {
self.points.length()
} else {
self.points.length() - 1
}
}
///|
fn Spline3D::control_at(self : Spline3D, index : Int) -> Point3 {
if self.closed {
self.points[wrap_index(index, self.points.length())]
} else {
self.points[index.max(0).min(self.points.length() - 1)]
}
}
///|
pub fn catmull_rom_point3(
previous : Point3,
start : Point3,
end : Point3,
next : Point3,
t : Double,
tension? : Double = 0.0,
) -> Point3 {
{
x: catmull_component(previous.x(), start.x(), end.x(), next.x(), t, tension),
y: catmull_component(previous.y(), start.y(), end.y(), next.y(), t, tension),
z: catmull_component(previous.z(), start.z(), end.z(), next.z(), t, tension),
}
}
///|
pub fn Spline3D::sample(self : Spline3D, parameter : Double) -> Point3 {
let normalized = clamp01(parameter)
let segment_count = self.segment_count()
if normalized >= 1.0 {
return self.control_at(segment_count)
}
let scaled = normalized * segment_count.to_double()
let index = scaled.to_int().min(segment_count - 1)
let segment_progress = scaled - index.to_double()
catmull_rom_point3(
self.control_at(index - 1),
self.control_at(index),
self.control_at(index + 1),
self.control_at(index + 2),
segment_progress,
tension=self.tension,
)
}
///|
pub fn Spline3D::tangent(self : Spline3D, parameter : Double) -> Point3 {
let normalized = clamp01(parameter)
let epsilon = 0.000001
let before = self.sample((normalized - epsilon).max(0.0))
let after = self.sample((normalized + epsilon).min(1.0))
{
x: (after.x() - before.x()) / (2.0 * epsilon),
y: (after.y() - before.y()) / (2.0 * epsilon),
z: (after.z() - before.z()) / (2.0 * epsilon),
}
}
///|
pub fn Spline3D::speed(self : Spline3D, parameter : Double) -> Double {
let tangent = self.tangent(parameter)
@math.pow(
tangent.x() * tangent.x() +
tangent.y() * tangent.y() +
tangent.z() * tangent.z(),
0.5,
)
}
///|
pub fn Spline3D::sample_many(
self : Spline3D,
count : Int,
) -> Array[Point3] raise MotionError {
if count < 2 {
raise MotionError::InvalidSampleCount(count)
}
let result : Array[Point3] = []
for index in 0.. Double {
if samples_per_segment <= 0 {
return 0.0
}
let total_samples = self.segment_count() * samples_per_segment
let mut length = 0.0
let mut previous = self.sample(0.0)
for index in 1..<=total_samples {
let current = self.sample(index.to_double() / total_samples.to_double())
let dx = current.x() - previous.x()
let dy = current.y() - previous.y()
let dz = current.z() - previous.z()
length = length + @math.pow(dx * dx + dy * dy + dz * dz, 0.5)
previous = current
}
length
}
///|
pub fn Spline3D::sample_by_distance(
self : Spline3D,
distance : Double,
samples_per_segment? : Int = 32,
) -> Point3 {
let segments = samples_per_segment.max(1)
let total_samples = self.segment_count() * segments
let total_length = self.length(segments)
if total_length <= 0.0 || distance <= 0.0 {
return self.sample(0.0)
}
if distance >= total_length {
return self.sample(1.0)
}
let mut travelled = 0.0
let mut previous = self.sample(0.0)
for index in 1..<=total_samples {
let parameter = index.to_double() / total_samples.to_double()
let current = self.sample(parameter)
let dx = current.x() - previous.x()
let dy = current.y() - previous.y()
let dz = current.z() - previous.z()
let step = @math.pow(dx * dx + dy * dy + dz * dz, 0.5)
if travelled + step >= distance && step > 0.0 {
let ratio = (distance - travelled) / step
return {
x: previous.x() + (current.x() - previous.x()) * ratio,
y: previous.y() + (current.y() - previous.y()) * ratio,
z: previous.z() + (current.z() - previous.z()) * ratio,
}
}
travelled = travelled + step
previous = current
}
self.sample(1.0)
}
///|
pub fn Spline3D::polyline(
self : Spline3D,
samples_per_segment : Int,
) -> Array[Point3] {
let count = self.segment_count() * samples_per_segment.max(1)
let points : Array[Point3] = []
for index in 0..<=count {
points.push(self.sample(index.to_double() / count.to_double()))
}
points
}