#[cfg(test)]
use oxideav_core::vector::PathCommand;
use oxideav_core::vector::Point;
#[allow(clippy::too_many_arguments)]
pub fn svg_arc_to_cubics(
start: Point,
end: Point,
rx_in: f32,
ry_in: f32,
x_axis_rot: f32,
large_arc: bool,
sweep: bool,
) -> Vec<(Point, Point, Point)> {
if (start.x - end.x).abs() < 1e-6 && (start.y - end.y).abs() < 1e-6 {
return Vec::new();
}
let mut rx = rx_in.abs();
let mut ry = ry_in.abs();
if rx < 1e-6 || ry < 1e-6 {
let third = Point::new(
start.x + (end.x - start.x) / 3.0,
start.y + (end.y - start.y) / 3.0,
);
let two_thirds = Point::new(
start.x + 2.0 * (end.x - start.x) / 3.0,
start.y + 2.0 * (end.y - start.y) / 3.0,
);
return vec![(third, two_thirds, end)];
}
let phi = x_axis_rot;
let (sin_phi, cos_phi) = phi.sin_cos();
let dx = (start.x - end.x) / 2.0;
let dy = (start.y - end.y) / 2.0;
let x1p = cos_phi * dx + sin_phi * dy;
let y1p = -sin_phi * dx + cos_phi * dy;
let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
if lambda > 1.0 {
let s = lambda.sqrt();
rx *= s;
ry *= s;
}
let rx_sq = rx * rx;
let ry_sq = ry * ry;
let x1p_sq = x1p * x1p;
let y1p_sq = y1p * y1p;
let mut numer = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq;
if numer < 0.0 {
numer = 0.0;
}
let denom = rx_sq * y1p_sq + ry_sq * x1p_sq;
let factor = if denom == 0.0 {
0.0
} else {
(numer / denom).sqrt()
};
let sign = if large_arc == sweep { -1.0 } else { 1.0 };
let cxp = sign * factor * (rx * y1p / ry);
let cyp = sign * factor * -(ry * x1p / rx);
let cx = cos_phi * cxp - sin_phi * cyp + (start.x + end.x) / 2.0;
let cy = sin_phi * cxp + cos_phi * cyp + (start.y + end.y) / 2.0;
let ux = (x1p - cxp) / rx;
let uy = (y1p - cyp) / ry;
let vx = (-x1p - cxp) / rx;
let vy = (-y1p - cyp) / ry;
let theta_1 = angle((1.0, 0.0), (ux, uy));
let mut delta_theta = angle((ux, uy), (vx, vy));
if !sweep && delta_theta > 0.0 {
delta_theta -= std::f32::consts::TAU;
} else if sweep && delta_theta < 0.0 {
delta_theta += std::f32::consts::TAU;
}
let n_segments = (delta_theta.abs() / (std::f32::consts::PI / 2.0))
.ceil()
.max(1.0) as usize;
let dtheta = delta_theta / n_segments as f32;
let t = (4.0 / 3.0) * (dtheta / 4.0).tan();
let mut out = Vec::with_capacity(n_segments);
let mut theta = theta_1;
let mut prev = unit_to_world(theta, cx, cy, rx, ry, sin_phi, cos_phi);
for _ in 0..n_segments {
let next_theta = theta + dtheta;
let next = unit_to_world(next_theta, cx, cy, rx, ry, sin_phi, cos_phi);
let (sin_t, cos_t) = theta.sin_cos();
let (sin_n, cos_n) = next_theta.sin_cos();
let tan1 = unit_tangent_to_world(-sin_t, cos_t, rx, ry, sin_phi, cos_phi);
let tan2 = unit_tangent_to_world(-sin_n, cos_n, rx, ry, sin_phi, cos_phi);
let c1 = Point::new(prev.x + t * tan1.x, prev.y + t * tan1.y);
let c2 = Point::new(next.x - t * tan2.x, next.y - t * tan2.y);
let endp = if next_theta == theta_1 + delta_theta {
end
} else {
next
};
out.push((c1, c2, endp));
prev = next;
theta = next_theta;
}
if let Some(last) = out.last_mut() {
last.2 = end;
}
out
}
fn angle(u: (f32, f32), v: (f32, f32)) -> f32 {
let dot = u.0 * v.0 + u.1 * v.1;
let len = ((u.0 * u.0 + u.1 * u.1) * (v.0 * v.0 + v.1 * v.1)).sqrt();
let cos = (dot / len).clamp(-1.0, 1.0);
let sign = if u.0 * v.1 - u.1 * v.0 < 0.0 {
-1.0
} else {
1.0
};
sign * cos.acos()
}
fn unit_to_world(
theta: f32,
cx: f32,
cy: f32,
rx: f32,
ry: f32,
sin_phi: f32,
cos_phi: f32,
) -> Point {
let (sin_t, cos_t) = theta.sin_cos();
let x = rx * cos_t;
let y = ry * sin_t;
Point::new(
cos_phi * x - sin_phi * y + cx,
sin_phi * x + cos_phi * y + cy,
)
}
fn unit_tangent_to_world(dx: f32, dy: f32, rx: f32, ry: f32, sin_phi: f32, cos_phi: f32) -> Point {
let x = rx * dx;
let y = ry * dy;
Point::new(cos_phi * x - sin_phi * y, sin_phi * x + cos_phi * y)
}
#[cfg(test)]
pub(crate) fn arc_cubic_count(cmd: PathCommand, current: Point) -> usize {
match cmd {
PathCommand::ArcTo {
rx,
ry,
x_axis_rot,
large_arc,
sweep,
end,
} => svg_arc_to_cubics(current, end, rx, ry, x_axis_rot, large_arc, sweep).len(),
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coincident_endpoints_emit_nothing() {
let segs = svg_arc_to_cubics(
Point::new(10.0, 10.0),
Point::new(10.0, 10.0),
5.0,
5.0,
0.0,
false,
true,
);
assert!(segs.is_empty());
}
#[test]
fn quarter_circle_emits_one_cubic() {
let segs = svg_arc_to_cubics(
Point::new(1.0, 0.0),
Point::new(0.0, 1.0),
1.0,
1.0,
0.0,
false,
true,
);
assert_eq!(segs.len(), 1);
}
#[test]
fn full_circle_via_two_180_arcs() {
let half1 = svg_arc_to_cubics(
Point::new(1.0, 0.0),
Point::new(-1.0, 0.0),
1.0,
1.0,
0.0,
false,
true,
);
let half2 = svg_arc_to_cubics(
Point::new(-1.0, 0.0),
Point::new(1.0, 0.0),
1.0,
1.0,
0.0,
false,
true,
);
assert_eq!(half1.len(), 2);
assert_eq!(half2.len(), 2);
}
#[test]
fn endpoint_is_snapped_to_request() {
let target = Point::new(7.0, -3.0);
let segs = svg_arc_to_cubics(Point::new(0.0, 0.0), target, 10.0, 10.0, 0.5, false, true);
let last = segs.last().unwrap();
assert!((last.2.x - target.x).abs() < 1e-4);
assert!((last.2.y - target.y).abs() < 1e-4);
}
#[test]
fn arc_count_helper_matches_segment_count() {
let cmd = PathCommand::ArcTo {
rx: 1.0,
ry: 1.0,
x_axis_rot: 0.0,
large_arc: false,
sweep: true,
end: Point::new(0.0, 1.0),
};
assert_eq!(arc_cubic_count(cmd, Point::new(1.0, 0.0)), 1);
}
}