///|
/// Geometric segment types that can be composed into a renderable path.
pub(all) enum PathSegment2D {
Line(Point2, Point2)
Cubic(CubicPath2D)
} derive(Debug)
///|
pub fn path_line(start : Point2, end : Point2) -> PathSegment2D {
Line(start, end)
}
///|
pub fn path_cubic(
start : Point2,
control_start : Point2,
control_end : Point2,
end : Point2,
) -> PathSegment2D {
Cubic(cubic_path2d(start, control_start, control_end, end))
}
///|
fn PathSegment2D::start(self : PathSegment2D) -> Point2 {
match self {
Line(start, _) => start
Cubic(path) => path.sample(0.0)
}
}
///|
fn PathSegment2D::end(self : PathSegment2D) -> Point2 {
match self {
Line(_, end) => end
Cubic(path) => path.sample(1.0)
}
}
///|
fn PathSegment2D::sample(self : PathSegment2D, ratio : Double) -> Point2 {
match self {
Line(start, end) => start.lerp(end, clamp01(ratio))
Cubic(path) => path.sample(ratio)
}
}
///|
fn PathSegment2D::tangent(self : PathSegment2D, ratio : Double) -> Point2 {
match self {
Line(start, end) => { x: end.x() - start.x(), y: end.y() - start.y() }
Cubic(path) => path.tangent(ratio)
}
}
///|
fn PathSegment2D::length(self : PathSegment2D, samples : Int) -> Double {
match self {
Line(start, end) => start.distance(end)
Cubic(path) => path.length(samples)
}
}
///|
pub fn PathSegment2D::bounds(
self : PathSegment2D,
samples? : Int = 32,
) -> Bounds2D {
let count = samples.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 PathSegment2D::polyline(
self : PathSegment2D,
samples : Int,
) -> Array[Point2] {
let count = samples.max(1)
let result : Array[Point2] = []
for index in 0..<=count {
result.push(self.sample(index.to_double() / count.to_double()))
}
result
}
///|
/// A length-indexed composition of line and cubic path segments.
pub struct Path2D {
segments : Array[PathSegment2D]
lengths : Array[Double]
total_length : Double
} derive(Debug)
///|
pub fn Path2D::new(
segments : Array[PathSegment2D],
samples_per_curve? : Int = 32,
) -> Path2D raise MotionError {
if segments.length() == 0 {
raise MotionError::EmptyTrack
}
let lengths : Array[Double] = []
let mut total = 0.0
for segment in segments {
let length = segment.length(samples_per_curve.max(1))
lengths.push(length)
total = total + length
}
{ segments: segments.copy(), lengths, total_length: total }
}
///|
pub fn Path2D::segments(self : Path2D) -> Array[PathSegment2D] {
self.segments.copy()
}
///|
pub fn Path2D::length(self : Path2D) -> Int {
self.segments.length()
}
///|
pub fn Path2D::total_length(self : Path2D) -> Double {
self.total_length
}
///|
pub fn Path2D::start(self : Path2D) -> Point2 {
self.segments[0].start()
}
///|
pub fn Path2D::end(self : Path2D) -> Point2 {
self.segments[self.segments.length() - 1].end()
}
///|
fn Path2D::locate_distance(self : Path2D, distance : Double) -> (Int, Double) {
if self.total_length <= 0.0 {
return (0, 0.0)
}
let target = clamp(distance, 0.0, self.total_length)
let mut previous = 0.0
for index in 0.. Point2 {
let (index, ratio) = self.locate_distance(
clamp01(parameter) * self.total_length,
)
self.segments[index].sample(ratio)
}
///|
pub fn Path2D::sample_by_distance(self : Path2D, distance : Double) -> Point2 {
let (index, ratio) = self.locate_distance(distance)
self.segments[index].sample(ratio)
}
///|
pub fn Path2D::tangent(self : Path2D, parameter : Double) -> Point2 {
let (index, ratio) = self.locate_distance(
clamp01(parameter) * self.total_length,
)
self.segments[index].tangent(ratio)
}
///|
pub fn Path2D::bounds(self : Path2D, samples_per_curve? : Int = 32) -> Bounds2D {
let first = self.segments[0].bounds(samples=samples_per_curve)
let mut min_x = first.minimum().x()
let mut max_x = first.maximum().x()
let mut min_y = first.minimum().y()
let mut max_y = first.maximum().y()
for index in 1.. Array[Point2] {
let result : Array[Point2] = []
for segment_index in 0.. 0 {
result.push(points[point_index])
}
}
}
result
}
///|
/// Approximate the nearest point by scanning each segment's polyline.
pub fn Path2D::project(
self : Path2D,
target : Point2,
samples_per_segment? : Int = 32,
) -> SplineProjection {
let steps = samples_per_segment.max(1)
let mut best_parameter = 0.0
let mut best_value = self.start()
let mut best_distance = best_value.distance(target)
let mut offset = 0.0
for segment_index in 0.. Path2D raise MotionError {
let segments = self.segments.copy()
segments.push(segment)
Path2D::new(segments, samples_per_curve~)
}
///|
/// Builder for paths constructed from a start point and successive endpoints.
pub struct PathBuilder2D {
mut current : Point2
segments : Array[PathSegment2D]
} derive(Debug)
///|
pub fn path_builder2d(start : Point2) -> PathBuilder2D {
{ current: start, segments: [] }
}
///|
pub fn PathBuilder2D::current(self : PathBuilder2D) -> Point2 {
self.current
}
///|
pub fn PathBuilder2D::line_to(self : PathBuilder2D, end : Point2) -> Unit {
self.segments.push(Line(self.current, end))
self.current = end
}
///|
pub fn PathBuilder2D::cubic_to(
self : PathBuilder2D,
control_start : Point2,
control_end : Point2,
end : Point2,
) -> Unit {
self.segments.push(
Cubic(cubic_path2d(self.current, control_start, control_end, end)),
)
self.current = end
}
///|
pub fn PathBuilder2D::close(self : PathBuilder2D) -> Unit {
if self.segments.length() > 0 {
self.line_to(self.segments[0].start())
}
}
///|
pub fn PathBuilder2D::length(self : PathBuilder2D) -> Int {
self.segments.length()
}
///|
pub fn PathBuilder2D::build(
self : PathBuilder2D,
samples_per_curve? : Int = 32,
) -> Path2D raise MotionError {
Path2D::new(self.segments, samples_per_curve~)
}