// Path segment flattening for fill, stroke, export and hit testing.
///|
priv struct FlattenedSubpath {
points : Array[@math.Vec2]
closed : Bool
}
///|
fn append_without_first(
destination : Array[@math.Vec2],
source : Array[@math.Vec2],
) -> Unit {
for i = 1; i < source.length(); i = i + 1 {
destination.push(source[i])
}
}
///|
fn append_arc_points(
start : @math.Vec2,
radius_x : Double,
radius_y : Double,
rotation_degrees : Double,
large_arc : Bool,
sweep : Bool,
end : @math.Vec2,
output : Array[@math.Vec2],
) -> Unit {
let rx_input = radius_x.abs()
let ry_input = radius_y.abs()
if rx_input <= 0.000000000001 || ry_input <= 0.000000000001 {
output.push(end)
} else if start.distance_sq(end) <= 0.000000000001 {
output.push(end)
} else {
let pi = 3.141592653589793
let phi = rotation_degrees * pi / 180.0
let cos_phi = @cmath.cos(phi)
let sin_phi = @cmath.sin(phi)
let dx = (start.x - end.x) * 0.5
let dy = (start.y - end.y) * 0.5
let x1_prime = cos_phi * dx + sin_phi * dy
let y1_prime = -sin_phi * dx + cos_phi * dy
let mut rx = rx_input
let mut ry = ry_input
let lambda = x1_prime * x1_prime / (rx * rx) +
y1_prime * y1_prime / (ry * ry)
if lambda > 1.0 {
let scale = lambda.sqrt()
rx = rx * scale
ry = ry * scale
}
let rx_sq = rx * rx
let ry_sq = ry * ry
let x1_sq = x1_prime * x1_prime
let y1_sq = y1_prime * y1_prime
let numerator = rx_sq * ry_sq - rx_sq * y1_sq - ry_sq * x1_sq
let denominator = rx_sq * y1_sq + ry_sq * x1_sq
let ratio = if denominator <= 0.000000000001 {
0.0
} else {
let raw_ratio = numerator / denominator
if raw_ratio < 0.0 {
0.0
} else {
raw_ratio
}
}
let sign = if large_arc == sweep { -1.0 } else { 1.0 }
let coefficient = sign * ratio.sqrt()
let cx_prime = coefficient * (rx * y1_prime / ry)
let cy_prime = coefficient * (-ry * x1_prime / rx)
let center_x = cos_phi * cx_prime -
sin_phi * cy_prime +
(start.x + end.x) * 0.5
let center_y = sin_phi * cx_prime +
cos_phi * cy_prime +
(start.y + end.y) * 0.5
let unit_start = @math.Vec2::new(
(x1_prime - cx_prime) / rx,
(y1_prime - cy_prime) / ry,
)
let unit_end = @math.Vec2::new(
(-x1_prime - cx_prime) / rx,
(-y1_prime - cy_prime) / ry,
)
let start_angle = @cmath.atan2(unit_start.y, unit_start.x)
let mut delta_angle = @cmath.atan2(
unit_start.cross(unit_end),
unit_start.dot(unit_end),
)
if !sweep && delta_angle > 0.0 {
delta_angle = delta_angle - 2.0 * pi
} else if sweep && delta_angle < 0.0 {
delta_angle = delta_angle + 2.0 * pi
}
let magnitude = delta_angle.abs()
let segments = if magnitude <= pi * 0.5 {
1
} else if magnitude <= pi {
2
} else if magnitude <= pi * 1.5 {
3
} else {
4
}
for i = 1; i <= segments; i = i + 1 {
let angle = start_angle +
delta_angle * i.to_double() / segments.to_double()
let cos_angle = @cmath.cos(angle)
let sin_angle = @cmath.sin(angle)
output.push(
@math.Vec2::new(
center_x + cos_phi * rx * cos_angle - sin_phi * ry * sin_angle,
center_y + sin_phi * rx * cos_angle + cos_phi * ry * sin_angle,
),
)
}
}
}
///|
fn flatten_path_internal(
path : Path,
tolerance : Double,
max_segments : Int,
) -> Array[FlattenedSubpath] {
let result : Array[FlattenedSubpath] = []
let mut points : Array[@math.Vec2] = []
let mut current = @math.Vec2::zero()
let mut subpath_start = @math.Vec2::zero()
let mut has_current = false
let mut closed = false
let depth = if max_segments < 1 { 1 } else { max_segments }
let flush = fn() {
if points.length() > 0 {
result.push({ points, closed })
}
points = []
closed = false
}
for i = 0; i < path.segments.length(); i = i + 1 {
match path.segments[i] {
MoveTo(point) => {
flush()
points.push(point)
current = point
subpath_start = point
has_current = true
}
LineTo(point) => {
if !has_current {
points.push(point)
subpath_start = point
has_current = true
} else {
points.push(point)
}
current = point
}
QuadTo(control, end) => {
let start = if has_current { current } else { @math.Vec2::zero() }
if !has_current {
points.push(start)
subpath_start = start
has_current = true
}
append_without_first(
points,
@math.flatten_quadratic(
@math.QuadraticBezier::new(start, control, end),
tolerance,
depth,
),
)
current = end
}
CubicTo(control1, control2, end) => {
let start = if has_current { current } else { @math.Vec2::zero() }
if !has_current {
points.push(start)
subpath_start = start
has_current = true
}
append_without_first(
points,
@math.flatten_cubic(
@math.CubicBezier::new(start, control1, control2, end),
tolerance,
depth,
),
)
current = end
}
ArcTo(rx, ry, rotation, large_arc, sweep, end) => {
let start = if has_current { current } else { @math.Vec2::zero() }
if !has_current {
points.push(start)
subpath_start = start
has_current = true
}
append_arc_points(
start, rx, ry, rotation, large_arc, sweep, end, points,
)
current = end
}
Close =>
if has_current {
if current.distance_sq(subpath_start) > 0.000000000001 {
points.push(subpath_start)
}
current = subpath_start
closed = true
}
}
}
flush()
result
}
///|
pub fn flatten_path(
path : Path,
tolerance : Double,
max_segments : Int,
) -> Array[Array[@math.Vec2]] {
let internal = flatten_path_internal(path, tolerance, max_segments)
let result : Array[Array[@math.Vec2]] = []
for i = 0; i < internal.length(); i = i + 1 {
result.push(internal[i].points)
}
result
}