///|
/// A measured point along a path or polyline.
pub(all) struct PathSample {
distance : Double
point : Vec2
tangent : Vec2
} derive(Eq, Debug, ToJson)
///|
/// Sampling controls used by curve flattening and animation paths.
pub(all) struct PathSampleOptions {
count : Int
subdivisions : Int
closed : Bool
} derive(Eq, Debug, ToJson)
///|
/// Creates safe defaults for evenly distributed samples.
pub fn PathSampleOptions::new(count : Int) -> PathSampleOptions {
{ count: if count < 0 { 0 } else { count }, subdivisions: 12, closed: false }
}
///|
/// Sets the number of curve subdivisions used during flattening.
pub fn PathSampleOptions::with_subdivisions(
self : PathSampleOptions,
subdivisions : Int,
) -> PathSampleOptions {
{ ..self, subdivisions: if subdivisions < 1 { 1 } else { subdivisions } }
}
///|
/// Requests a closed sampling loop.
pub fn PathSampleOptions::as_closed(
self : PathSampleOptions,
) -> PathSampleOptions {
{ ..self, closed: true }
}
///|
/// A compact summary of a sampled path.
pub(all) struct PolylineMetrics {
point_count : Int
segment_count : Int
length : Double
bounds : Rect
closed : Bool
} derive(Eq, Debug, ToJson)
///|
fn safe_steps(steps : Int) -> Int {
if steps < 1 {
1
} else {
steps
}
}
///|
fn distance_or_zero(left : Vec2, right : Vec2) -> Double {
left.distance(right)
}
///|
/// Returns the length of a polyline, optionally including its closing edge.
pub fn polyline_length(points : Array[Vec2], closed? : Bool = false) -> Double {
if points.length() < 2 {
0.0
} else {
let mut total = 0.0
for i in 1.. Array[Double] {
let distances = []
if points.length() > 0 {
distances.push(0.0)
let mut total = 0.0
for i in 1.. Vec2 {
right.sub(left).normalized()
}
///|
/// Finds a point at a distance along an open or closed polyline.
pub fn point_on_polyline(
points : Array[Vec2],
distance : Double,
closed? : Bool = false,
) -> Vec2 {
if points.length() == 0 {
Vec2::zero()
} else if points.length() == 1 {
points[0]
} else {
let total = polyline_length(points, closed~)
if total <= 0.0 {
points[0]
} else {
let target = if closed {
let wrapped = distance % total
if wrapped < 0.0 {
wrapped + total
} else {
wrapped
}
} else {
distance.max(0.0).min(total)
}
let limit = if closed { points.length() } else { points.length() - 1 }
let mut travelled = 0.0
let mut result = points[0]
for i in 0..= points.length() { 0 } else { i + 1 }
let left = points[i]
let right = points[next_index]
let segment = distance_or_zero(left, right)
if segment > 0.0 && target <= travelled + segment {
let amount = (target - travelled) / segment
result = left.lerp(right, amount)
break
}
travelled += segment
result = right
}
result
}
}
}
///|
/// Finds the unit tangent at a distance along a polyline.
pub fn tangent_on_polyline(
points : Array[Vec2],
distance : Double,
closed? : Bool = false,
) -> Vec2 {
if points.length() < 2 {
Vec2::zero()
} else {
let total = polyline_length(points, closed~)
if total <= 0.0 {
Vec2::zero()
} else {
let target = if closed {
let wrapped = distance % total
if wrapped < 0.0 {
wrapped + total
} else {
wrapped
}
} else {
distance.max(0.0).min(total)
}
let limit = if closed { points.length() } else { points.length() - 1 }
let mut travelled = 0.0
let mut result = Vec2::zero()
for i in 0..= points.length() { 0 } else { i + 1 }
let left = points[i]
let right = points[next_index]
let segment = distance_or_zero(left, right)
if segment > 0.0 && target <= travelled + segment {
result = segment_tangent(left, right)
break
}
travelled += segment
result = segment_tangent(left, right)
}
result
}
}
}
///|
/// Evenly distributes samples over a polyline by arc length.
pub fn sample_polyline(
points : Array[Vec2],
count : Int,
closed? : Bool = false,
) -> Array[PathSample] {
let result = []
if points.length() == 0 || count <= 0 {
result
} else if count == 1 {
result.push({
distance: 0.0,
point: points[0],
tangent: tangent_on_polyline(points, 0.0, closed~),
})
result
} else {
let total = polyline_length(points, closed~)
for i in 0.. PolylineMetrics {
{
point_count: points.length(),
segment_count: if points.length() < 2 {
0
} else if closed {
points.length()
} else {
points.length() - 1
},
length: polyline_length(points, closed~),
bounds: bounds_of_points(points),
closed,
}
}
///|
/// Removes consecutive points closer than the given tolerance.
pub fn deduplicate_polyline(
points : Array[Vec2],
tolerance : Double,
) -> Array[Vec2] {
let result = []
if points.length() > 0 {
result.push(points[0])
let threshold = tolerance.max(0.0)
for point in points[1:] {
if point.distance(result[result.length() - 1]) > threshold {
result.push(point)
}
}
}
result
}
///|
/// Resamples a polyline while preserving its total arc length.
pub fn resample_polyline(
points : Array[Vec2],
count : Int,
closed? : Bool = false,
) -> Array[Vec2] {
let result = []
for sample in sample_polyline(points, count, closed~) {
result.push(sample.point)
}
result
}
///|
/// Simplifies a polyline using a radial-distance pass.
pub fn simplify_sampled_polyline(
points : Array[Vec2],
tolerance : Double,
) -> Array[Vec2] {
let cleaned = deduplicate_polyline(points, tolerance)
if cleaned.length() < 3 {
cleaned
} else {
let result = [cleaned[0]]
let threshold = tolerance.max(0.0)
for i in 1..<(cleaned.length() - 1) {
let previous = result[result.length() - 1]
let next = cleaned[i + 1]
let direct = previous.distance(next)
let through = previous.distance(cleaned[i]) + cleaned[i].distance(next)
if through - direct > threshold {
result.push(cleaned[i])
}
}
result.push(cleaned[cleaned.length() - 1])
result
}
}
///|
/// Applies a moving-average filter to a polyline.
pub fn smooth_polyline(
points : Array[Vec2],
radius : Int,
closed? : Bool = false,
) -> Array[Vec2] {
if points.length() == 0 || radius <= 0 {
points.copy()
} else {
let result = []
let span = radius * 2 + 1
for i in 0.. Array[Vec2] {
let result = []
let steps = safe_steps(subdivisions)
let mut current = Vec2::zero()
let mut start = Vec2::zero()
for segment in self.segments {
match segment {
MoveTo(x, y) => {
current = Vec2::new(x, y)
start = current
result.push(current)
}
LineTo(x, y) => {
current = Vec2::new(x, y)
result.push(current)
}
QuadTo(x1~, y1~, x~, y~) => {
let p0 = current
let p1 = Vec2::new(x1, y1)
let p2 = Vec2::new(x, y)
for i in 1..<=steps {
let t = i.to_double() / steps.to_double()
let u = 1.0 - t
let point = p0
.scale(u * u)
.add(p1.scale(2.0 * u * t))
.add(p2.scale(t * t))
result.push(point)
}
current = p2
}
CubicTo(x1~, y1~, x2~, y2~, x~, y~) => {
let p0 = current
let p1 = Vec2::new(x1, y1)
let p2 = Vec2::new(x2, y2)
let p3 = Vec2::new(x, y)
for i in 1..<=steps {
let t = i.to_double() / steps.to_double()
let u = 1.0 - t
let point = p0
.scale(u * u * u)
.add(p1.scale(3.0 * u * u * t))
.add(p2.scale(3.0 * u * t * t))
.add(p3.scale(t * t * t))
result.push(point)
}
current = p3
}
ClosePath => {
if current.distance(start) > 0.0 {
result.push(start)
}
current = start
}
}
}
result
}
///|
/// Samples a path by flattening its curves and measuring arc length.
pub fn Path::sample(
self : Path,
options : PathSampleOptions,
) -> Array[PathSample] {
sample_polyline(
self.flatten(subdivisions=options.subdivisions),
options.count,
closed=options.closed,
)
}
///|
/// Returns a path point and tangent at normalized progress in [0, 1].
pub fn Path::point_at(self : Path, progress : Double) -> PathSample {
let points = self.flatten(subdivisions=16)
let total = polyline_length(points)
let distance = total * progress.max(0.0).min(1.0)
{
distance,
point: point_on_polyline(points, distance),
tangent: tangent_on_polyline(points, distance),
}
}
///|
/// Creates a path from measured samples, preserving the sampled tangent as a hint.
pub fn samples_to_path(
samples : Array[PathSample],
closed? : Bool = false,
) -> Path {
let path = Path::new()
if samples.length() > 0 {
let _ = path.move_to(samples[0].point.x, samples[0].point.y)
for sample in samples[1:] {
let _ = path.line_to(sample.point.x, sample.point.y)
}
if closed {
let _ = path.close()
}
}
path
}
///|
/// Generates a dashed polyline by alternating visible and hidden distances.
pub fn dash_polyline(
points : Array[Vec2],
dash : Array[Double],
closed? : Bool = false,
) -> Array[Array[Vec2]] {
let result = []
if points.length() < 2 || dash.length() == 0 {
result
} else {
let total = polyline_length(points, closed~)
let mut position = 0.0
let mut pattern_index = 0
let mut visible = true
while position < total {
let segment_length = dash[pattern_index % dash.length()].max(0.000001)
let end = (position + segment_length).min(total)
if visible {
result.push([
point_on_polyline(points, position, closed~),
point_on_polyline(points, end, closed~),
])
}
position = end
pattern_index += 1
visible = !visible
}
result
}
}
///|
/// Offsets a sampled polyline along its local normals.
pub fn offset_samples(
samples : Array[PathSample],
distance : Double,
) -> Array[Vec2] {
let result = []
for sample in samples {
let normal = Vec2::new(-sample.tangent.y, sample.tangent.x)
result.push(sample.point.add(normal.scale(distance)))
}
result
}