///|
/// Polygon orientation and simplification helpers for design geometry.
pub enum PolygonWinding {
Clockwise
CounterClockwise
Degenerate
} derive(Eq, Debug, ToJson)
///|
pub fn polygon_winding(points : Array[Vec2]) -> PolygonWinding {
let area = polygon_area(points)
if area > 0.000000001 {
CounterClockwise
} else if area < -0.000000001 {
Clockwise
} else {
Degenerate
}
}
///|
pub fn ensure_counter_clockwise(points : Array[Vec2]) -> Array[Vec2] {
match polygon_winding(points) {
Clockwise => {
let result = []
for offset in 1..<=points.length() {
result.push(points[points.length() - offset])
}
result
}
_ => points
}
}
///|
/// Removes consecutive vertices that are closer than `epsilon`.
pub fn deduplicate_points(
points : Array[Vec2],
epsilon : Double,
) -> Array[Vec2] {
let result : Array[Vec2] = []
for point in points {
if result.length() == 0 ||
result[result.length() - 1].distance(point) > epsilon {
result.push(point)
}
}
if result.length() > 1 &&
result[0].distance(result[result.length() - 1]) <= epsilon {
let _ = result.pop()
}
result
}
///|
/// Simplifies a polyline using a distance-to-segment tolerance.
pub fn simplify_polyline(
points : Array[Vec2],
tolerance : Double,
) -> Array[Vec2] {
if points.length() <= 2 {
points
} else {
let first = points[0]
let last = points[points.length() - 1]
let mut max_distance = 0.0
let mut split = 0
for i in 1..<(points.length() - 1) {
let distance = perpendicular_distance(points[i], first, last)
if distance > max_distance {
max_distance = distance
split = i
}
}
if max_distance > tolerance {
let left = []
let right = []
for i in 0..<=split {
left.push(points[i])
}
for i in split.. Double {
let direction = end.sub(start)
let length = direction.length()
if length == 0.0 {
point.distance(start)
} else {
direction.cross(point.sub(start)).abs() / length
}
}
///|
/// Clips a polygon against one axis-aligned rectangle edge.
pub enum ClipEdge {
Left
Right
Top
Bottom
} derive(Eq, Debug, ToJson)
///|
pub fn clip_polygon_edge(
points : Array[Vec2],
edge : ClipEdge,
limit : Double,
) -> Array[Vec2] {
let result = []
if points.length() == 0 {
result
} else {
let inside = fn(point : Vec2) -> Bool {
match edge {
Left => point.x >= limit
Right => point.x <= limit
Top => point.y >= limit
Bottom => point.y <= limit
}
}
let intersection = fn(a : Vec2, b : Vec2) -> Vec2 {
match edge {
Left | Right => {
let t = (limit - a.x) / (b.x - a.x)
a.lerp(b, t)
}
Top | Bottom => {
let t = (limit - a.y) / (b.y - a.y)
a.lerp(b, t)
}
}
}
let mut previous = points[points.length() - 1]
let mut previous_inside = inside(previous)
for current in points {
let current_inside = inside(current)
if current_inside != previous_inside {
result.push(intersection(previous, current))
}
if current_inside {
result.push(current)
}
previous = current
previous_inside = current_inside
}
result
}
}
///|
pub fn clip_polygon(points : Array[Vec2], bounds : Rect) -> Array[Vec2] {
let left = clip_polygon_edge(points, Left, bounds.x)
let right = clip_polygon_edge(left, Right, bounds.right())
let top = clip_polygon_edge(right, Top, bounds.y)
clip_polygon_edge(top, Bottom, bounds.bottom())
}