use crate::api::path::{Path, PathCmd, Point};
use crate::api::style::{StrokeCap, StrokeJoin};
pub struct StrokeOptions {
pub width: f64,
pub start_cap: StrokeCap,
pub end_cap: StrokeCap,
pub join: StrokeJoin,
pub miter_limit: f64,
pub dash_array: Vec<f64>,
pub dash_offset: f64,
}
impl Default for StrokeOptions {
fn default() -> Self {
Self {
width: 1.0,
start_cap: StrokeCap::default(),
end_cap: StrokeCap::default(),
join: StrokeJoin::default(),
miter_limit: 4.0,
dash_array: Vec::new(),
dash_offset: 0.0,
}
}
}
pub fn stroke_to_fill(input: &Path, options: &StrokeOptions, output: &mut Path) {
let mut workspace = StrokeWorkspace::new();
stroke_to_fill_with_workspace(input, options, output, &mut workspace);
}
pub fn stroke_to_fill_with_workspace(
input: &Path,
options: &StrokeOptions,
output: &mut Path,
workspace: &mut StrokeWorkspace,
) {
let half_width = options.width * 0.5;
if half_width <= 0.0 {
return;
}
flatten_into_workspace(input, workspace);
if !options.dash_array.is_empty() {
apply_dash(workspace, &options.dash_array, options.dash_offset);
}
for i in 0..workspace.subpath_ranges.len() {
let range = workspace.subpath_ranges[i];
let pts = &workspace.flat_points[range.start..range.end];
if pts.len() < 2 {
continue;
}
stroke_subpath_slice(
output,
pts,
range.closed,
half_width,
options.start_cap,
options.end_cap,
options.join,
options.miter_limit,
&mut workspace.seg_normals,
);
}
}
pub struct StrokeWorkspace {
flat_points: Vec<Point>,
subpath_ranges: Vec<SubpathRange>,
seg_normals: Vec<(usize, f64, f64)>,
dash_points: Vec<Point>,
dash_ranges: Vec<SubpathRange>,
}
#[derive(Clone, Copy)]
struct SubpathRange {
start: usize,
end: usize,
closed: bool,
}
impl StrokeWorkspace {
pub fn new() -> Self {
Self {
flat_points: Vec::new(),
subpath_ranges: Vec::new(),
seg_normals: Vec::new(),
dash_points: Vec::new(),
dash_ranges: Vec::new(),
}
}
}
impl Default for StrokeWorkspace {
fn default() -> Self {
Self::new()
}
}
fn apply_dash(workspace: &mut StrokeWorkspace, dash_array: &[f64], dash_offset: f64) {
let pattern: Vec<f64> = if dash_array.len() % 2 == 1 {
dash_array
.iter()
.chain(dash_array.iter())
.copied()
.collect()
} else {
dash_array.to_vec()
};
let pattern_len: f64 = pattern.iter().sum();
if pattern_len <= 0.0 {
return;
}
workspace.dash_points.clear();
workspace.dash_ranges.clear();
let original_ranges = workspace.subpath_ranges.clone();
let original_points = workspace.flat_points.clone();
for range in &original_ranges {
let pts = &original_points[range.start..range.end];
if pts.len() < 2 {
continue;
}
let mut offset = ((dash_offset % pattern_len) + pattern_len) % pattern_len;
let mut pat_idx = 0usize;
while offset > 0.0 && pat_idx < pattern.len() {
if offset < pattern[pat_idx] {
break;
}
offset -= pattern[pat_idx];
pat_idx = (pat_idx + 1) % pattern.len();
}
let mut remaining = pattern[pat_idx] - offset;
let mut drawing = pat_idx.is_multiple_of(2); let mut dash_start: Option<usize> = None;
if drawing {
dash_start = Some(workspace.dash_points.len());
workspace.dash_points.push(pts[0]);
}
for seg in 0..pts.len() - 1 {
let p0 = pts[seg];
let p1 = pts[seg + 1];
let dx = p1.x - p0.x;
let dy = p1.y - p0.y;
let seg_len = (dx * dx + dy * dy).sqrt();
if seg_len < 1e-10 {
continue;
}
let ux = dx / seg_len;
let uy = dy / seg_len;
let mut consumed = 0.0;
while consumed < seg_len {
let avail = seg_len - consumed;
if remaining <= avail {
consumed += remaining;
let split_x = p0.x + ux * consumed;
let split_y = p0.y + uy * consumed;
let split = Point::new(split_x, split_y);
if drawing {
workspace.dash_points.push(split);
if let Some(start) = dash_start.take() {
let end = workspace.dash_points.len();
if end - start >= 2 {
workspace.dash_ranges.push(SubpathRange {
start,
end,
closed: false,
});
}
}
} else {
dash_start = Some(workspace.dash_points.len());
workspace.dash_points.push(split);
}
drawing = !drawing;
pat_idx = (pat_idx + 1) % pattern.len();
remaining = pattern[pat_idx];
} else {
remaining -= avail;
if drawing {
workspace.dash_points.push(p1);
}
break;
}
}
}
if drawing && let Some(start) = dash_start.take() {
let end = workspace.dash_points.len();
if end - start >= 2 {
workspace.dash_ranges.push(SubpathRange {
start,
end,
closed: false,
});
}
}
}
std::mem::swap(&mut workspace.flat_points, &mut workspace.dash_points);
std::mem::swap(&mut workspace.subpath_ranges, &mut workspace.dash_ranges);
}
fn flatten_into_workspace(input: &Path, workspace: &mut StrokeWorkspace) {
workspace.flat_points.clear();
workspace.subpath_ranges.clear();
let cmds = input.cmds();
let points = input.points();
let conic_w = input.conic_weights();
let n_conic_cmds = cmds.iter().filter(|&&c| c == PathCmd::ConicTo).count();
debug_assert_eq!(
n_conic_cmds,
conic_w.len(),
"conic_weights length must match PathCmd::ConicTo count"
);
let mut conic_idx = 0usize;
let mut subpath_start_idx: Option<usize> = None;
let mut pt_idx = 0usize;
let mut cur = Point::new(0.0, 0.0);
let mut start = Point::new(0.0, 0.0);
for &cmd in cmds {
match cmd {
PathCmd::MoveTo => {
if let Some(sp_start) = subpath_start_idx.take() {
let len = workspace.flat_points.len() - sp_start;
if len >= 2 {
workspace.subpath_ranges.push(SubpathRange {
start: sp_start,
end: workspace.flat_points.len(),
closed: false,
});
}
}
let p = points[pt_idx];
pt_idx += 1;
start = p;
cur = p;
subpath_start_idx = Some(workspace.flat_points.len());
workspace.flat_points.push(p);
}
PathCmd::LineTo => {
let p = points[pt_idx];
pt_idx += 1;
if subpath_start_idx.is_some() {
workspace.flat_points.push(p);
}
cur = p;
}
PathCmd::CubicTo => {
let cp1 = points[pt_idx];
let cp2 = points[pt_idx + 1];
let end = points[pt_idx + 2];
pt_idx += 3;
if subpath_start_idx.is_some() {
flatten_cubic_into(&mut workspace.flat_points, cur, cp1, cp2, end, 0);
}
cur = end;
}
PathCmd::QuadTo => {
let cp = points[pt_idx];
let end = points[pt_idx + 1];
pt_idx += 2;
if subpath_start_idx.is_some() {
flatten_quad_into(&mut workspace.flat_points, cur, cp, end, 0);
}
cur = end;
}
PathCmd::ConicTo => {
let cp = points[pt_idx];
let end = points[pt_idx + 1];
pt_idx += 2;
let w = conic_w[conic_idx];
conic_idx += 1;
if subpath_start_idx.is_some() {
flatten_conic_into(&mut workspace.flat_points, cur, cp, end, w);
}
cur = end;
}
PathCmd::Close => {
if subpath_start_idx.is_some() && (cur.x != start.x || cur.y != start.y) {
workspace.flat_points.push(start);
}
cur = start;
if let Some(sp_start) = subpath_start_idx.take() {
let len = workspace.flat_points.len() - sp_start;
if len >= 2 {
workspace.subpath_ranges.push(SubpathRange {
start: sp_start,
end: workspace.flat_points.len(),
closed: true,
});
}
}
}
}
}
if let Some(sp_start) = subpath_start_idx.take() {
let len = workspace.flat_points.len() - sp_start;
if len >= 2 {
workspace.subpath_ranges.push(SubpathRange {
start: sp_start,
end: workspace.flat_points.len(),
closed: false,
});
}
}
}
const FLATNESS_TOLERANCE: f64 = 0.25;
const MAX_RECURSION_DEPTH: u32 = 16;
fn flatten_cubic_into(
points: &mut Vec<Point>,
p0: Point,
p1: Point,
p2: Point,
p3: Point,
depth: u32,
) {
if depth >= MAX_RECURSION_DEPTH || is_flat_cubic(p0, p1, p2, p3) {
points.push(p3);
return;
}
let m01 = mid(p0, p1);
let m12 = mid(p1, p2);
let m23 = mid(p2, p3);
let m012 = mid(m01, m12);
let m123 = mid(m12, m23);
let m0123 = mid(m012, m123);
flatten_cubic_into(points, p0, m01, m012, m0123, depth + 1);
flatten_cubic_into(points, m0123, m123, m23, p3, depth + 1);
}
fn flatten_quad_into(points: &mut Vec<Point>, p0: Point, p1: Point, p2: Point, depth: u32) {
if depth >= MAX_RECURSION_DEPTH || is_flat_quad(p0, p1, p2) {
points.push(p2);
return;
}
let m01 = mid(p0, p1);
let m12 = mid(p1, p2);
let m012 = mid(m01, m12);
flatten_quad_into(points, p0, m01, m012, depth + 1);
flatten_quad_into(points, m012, m12, p2, depth + 1);
}
fn flatten_conic_into(points: &mut Vec<Point>, p0: Point, p1: Point, p2: Point, w: f64) {
const N: usize = 24;
for i in 1..=N {
let t = i as f64 / N as f64;
points.push(eval_conic_stroke(p0, p1, p2, w, t));
}
}
fn eval_conic_stroke(p0: Point, p1: Point, p2: Point, w: f64, t: f64) -> Point {
let u = 1.0 - t;
let denom = u * u + 2.0 * w * u * t + t * t;
let x = (u * u * p0.x + 2.0 * w * u * t * p1.x + t * t * p2.x) / denom;
let y = (u * u * p0.y + 2.0 * w * u * t * p1.y + t * t * p2.y) / denom;
Point::new(x, y)
}
fn is_flat_cubic(p0: Point, p1: Point, p2: Point, p3: Point) -> bool {
let dx = p3.x - p0.x;
let dy = p3.y - p0.y;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-12 {
let d1_sq = (p1.x - p0.x).powi(2) + (p1.y - p0.y).powi(2);
let d2_sq = (p2.x - p0.x).powi(2) + (p2.y - p0.y).powi(2);
let tol_sq = FLATNESS_TOLERANCE * FLATNESS_TOLERANCE;
return d1_sq <= tol_sq && d2_sq <= tol_sq;
}
let cross1 = (p1.x - p0.x) * dy - (p1.y - p0.y) * dx;
let cross2 = (p2.x - p0.x) * dy - (p2.y - p0.y) * dx;
let tol_sq = FLATNESS_TOLERANCE * FLATNESS_TOLERANCE * len_sq;
cross1 * cross1 <= tol_sq && cross2 * cross2 <= tol_sq
}
fn is_flat_quad(p0: Point, p1: Point, p2: Point) -> bool {
let dx = p2.x - p0.x;
let dy = p2.y - p0.y;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-12 {
let d_sq = (p1.x - p0.x).powi(2) + (p1.y - p0.y).powi(2);
return d_sq <= FLATNESS_TOLERANCE * FLATNESS_TOLERANCE;
}
let cross = (p1.x - p0.x) * dy - (p1.y - p0.y) * dx;
let tol_sq = FLATNESS_TOLERANCE * FLATNESS_TOLERANCE * len_sq;
cross * cross <= tol_sq
}
fn mid(a: Point, b: Point) -> Point {
Point::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5)
}
fn unit_normal(p0: Point, p1: Point) -> (f64, f64) {
let dx = p1.x - p0.x;
let dy = p1.y - p0.y;
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-12 {
return (0.0, 0.0);
}
let inv_len = 1.0 / len;
(-dy * inv_len, dx * inv_len)
}
#[allow(clippy::too_many_arguments)]
fn stroke_subpath_slice(
output: &mut Path,
pts: &[Point],
closed: bool,
half_width: f64,
start_cap: StrokeCap,
end_cap: StrokeCap,
join: StrokeJoin,
miter_limit: f64,
seg_normals_buf: &mut Vec<(usize, f64, f64)>,
) {
let n = pts.len();
seg_normals_buf.clear();
for i in 0..n - 1 {
let (nx, ny) = unit_normal(pts[i], pts[i + 1]);
if nx == 0.0 && ny == 0.0 {
continue;
}
seg_normals_buf.push((i, nx, ny));
}
if seg_normals_buf.is_empty() {
return;
}
if closed {
stroke_closed_subpath(output, pts, seg_normals_buf, half_width, join, miter_limit);
} else {
stroke_open_subpath(
output,
pts,
seg_normals_buf,
half_width,
start_cap,
end_cap,
join,
miter_limit,
);
}
}
fn stroke_open_subpath(
output: &mut Path,
pts: &[Point],
seg_normals: &[(usize, f64, f64)],
half_width: f64,
start_cap: StrokeCap,
end_cap: StrokeCap,
join: StrokeJoin,
miter_limit: f64,
) {
let first_seg = seg_normals[0];
let last_seg = seg_normals[seg_normals.len() - 1];
let (_, n0x, n0y) = first_seg;
let p_start = pts[first_seg.0];
output.move_to(p_start.x + n0x * half_width, p_start.y + n0y * half_width);
let p_end = pts[first_seg.0 + 1];
output.line_to(p_end.x + n0x * half_width, p_end.y + n0y * half_width);
for i in 1..seg_normals.len() {
let (seg_idx, nx, ny) = seg_normals[i];
let (_, prev_nx, prev_ny) = seg_normals[i - 1];
let join_point = pts[seg_idx];
emit_join(
output,
join,
join_point,
prev_nx,
prev_ny,
nx,
ny,
half_width,
miter_limit,
true,
);
let seg_end = pts[seg_idx + 1];
output.line_to(seg_end.x + nx * half_width, seg_end.y + ny * half_width);
}
let (last_idx, last_nx, last_ny) = last_seg;
let end_point = pts[last_idx + 1];
emit_cap(
output, end_cap, end_point, last_nx, last_ny, half_width, false,
);
output.line_to(
end_point.x - last_nx * half_width,
end_point.y - last_ny * half_width,
);
let last_start = pts[last_idx];
output.line_to(
last_start.x - last_nx * half_width,
last_start.y - last_ny * half_width,
);
for i in (0..seg_normals.len() - 1).rev() {
let (seg_idx, nx, ny) = seg_normals[i];
let (_, next_nx, next_ny) = seg_normals[i + 1];
let join_point = pts[seg_idx + 1];
emit_join(
output,
join,
join_point,
-next_nx,
-next_ny,
-nx,
-ny,
half_width,
miter_limit,
true,
);
let seg_start = pts[seg_idx];
output.line_to(seg_start.x - nx * half_width, seg_start.y - ny * half_width);
}
emit_cap(output, start_cap, p_start, n0x, n0y, half_width, true);
output.close();
}
fn stroke_closed_subpath(
output: &mut Path,
pts: &[Point],
seg_normals: &[(usize, f64, f64)],
half_width: f64,
join: StrokeJoin,
miter_limit: f64,
) {
let num_segs = seg_normals.len();
{
let (first_idx, first_nx, first_ny) = seg_normals[0];
let (_, last_nx, last_ny) = seg_normals[num_segs - 1];
let first_point = pts[first_idx];
let start_x = first_point.x + first_nx * half_width;
let start_y = first_point.y + first_ny * half_width;
output.move_to(start_x, start_y);
let first_end = pts[first_idx + 1];
output.line_to(
first_end.x + first_nx * half_width,
first_end.y + first_ny * half_width,
);
for i in 1..num_segs {
let (seg_idx, nx, ny) = seg_normals[i];
let (_, prev_nx, prev_ny) = seg_normals[i - 1];
let join_point = pts[seg_idx];
emit_join(
output,
join,
join_point,
prev_nx,
prev_ny,
nx,
ny,
half_width,
miter_limit,
true,
);
let seg_end = pts[seg_idx + 1];
output.line_to(seg_end.x + nx * half_width, seg_end.y + ny * half_width);
}
emit_join(
output,
join,
first_point,
last_nx,
last_ny,
first_nx,
first_ny,
half_width,
miter_limit,
true,
);
output.close();
}
{
let (first_idx, first_nx, first_ny) = seg_normals[0];
let (_, last_nx, last_ny) = seg_normals[num_segs - 1];
let first_point = pts[first_idx];
let start_x = first_point.x - first_nx * half_width;
let start_y = first_point.y - first_ny * half_width;
output.move_to(start_x, start_y);
emit_join(
output,
join,
first_point,
-first_nx,
-first_ny,
-last_nx,
-last_ny,
half_width,
miter_limit,
true,
);
for i in (1..num_segs).rev() {
let (seg_idx, nx, ny) = seg_normals[i];
let seg_start = pts[seg_idx];
output.line_to(seg_start.x - nx * half_width, seg_start.y - ny * half_width);
let (_, prev_nx, prev_ny) = seg_normals[i - 1];
emit_join(
output,
join,
seg_start,
-nx,
-ny,
-prev_nx,
-prev_ny,
half_width,
miter_limit,
true,
);
}
output.line_to(start_x, start_y);
output.close();
}
}
fn emit_join(
output: &mut Path,
join: StrokeJoin,
p: Point,
prev_nx: f64,
prev_ny: f64,
next_nx: f64,
next_ny: f64,
half_width: f64,
miter_limit: f64,
left_side: bool,
) {
let _ = left_side;
let cross = prev_nx * next_ny - prev_ny * next_nx;
if cross.abs() < 1e-10 {
output.line_to(p.x + next_nx * half_width, p.y + next_ny * half_width);
return;
}
let dot = prev_nx * next_nx + prev_ny * next_ny;
let sin_half_sq = (1.0 - dot) * 0.5;
match join {
StrokeJoin::Bevel => {
output.line_to(p.x + next_nx * half_width, p.y + next_ny * half_width);
}
StrokeJoin::MiterClip | StrokeJoin::MiterBevel | StrokeJoin::MiterRound => {
if sin_half_sq < 1e-12 {
output.line_to(p.x + next_nx * half_width, p.y + next_ny * half_width);
return;
}
let miter_len = 1.0 / sin_half_sq.sqrt();
if miter_len <= miter_limit {
let mx = prev_nx + next_nx;
let my = prev_ny + next_ny;
let m_len = (mx * mx + my * my).sqrt();
if m_len > 1e-12 {
let scale = half_width * miter_len / m_len;
output.line_to(p.x + mx * scale, p.y + my * scale);
}
output.line_to(p.x + next_nx * half_width, p.y + next_ny * half_width);
} else {
match join {
StrokeJoin::MiterClip => {
emit_miter_clip(
output,
p,
prev_nx,
prev_ny,
next_nx,
next_ny,
half_width,
miter_limit,
);
output.line_to(p.x + next_nx * half_width, p.y + next_ny * half_width);
}
StrokeJoin::MiterBevel => {
output.line_to(p.x + next_nx * half_width, p.y + next_ny * half_width);
}
StrokeJoin::MiterRound => {
emit_arc(output, p, prev_nx, prev_ny, next_nx, next_ny, half_width);
output.line_to(p.x + next_nx * half_width, p.y + next_ny * half_width);
}
_ => unreachable!(),
}
}
}
StrokeJoin::Round => {
emit_arc(output, p, prev_nx, prev_ny, next_nx, next_ny, half_width);
output.line_to(p.x + next_nx * half_width, p.y + next_ny * half_width);
}
}
}
fn emit_miter_clip(
output: &mut Path,
p: Point,
prev_nx: f64,
prev_ny: f64,
next_nx: f64,
next_ny: f64,
half_width: f64,
miter_limit: f64,
) {
let mx = prev_nx + next_nx;
let my = prev_ny + next_ny;
let m_len = (mx * mx + my * my).sqrt();
if m_len < 1e-12 {
return;
}
let mdx = mx / m_len;
let mdy = my / m_len;
let clip_dist = miter_limit * half_width;
let clip_cx = p.x + mdx * clip_dist;
let clip_cy = p.y + mdy * clip_dist;
let perp_x = -mdy;
let perp_y = mdx;
let v1x = prev_nx * half_width - mdx * clip_dist;
let v1y = prev_ny * half_width - mdy * clip_dist;
let proj1 = v1x * perp_x + v1y * perp_y;
let v2x = next_nx * half_width - mdx * clip_dist;
let v2y = next_ny * half_width - mdy * clip_dist;
let proj2 = v2x * perp_x + v2y * perp_y;
output.line_to(clip_cx + perp_x * proj1, clip_cy + perp_y * proj1);
output.line_to(clip_cx + perp_x * proj2, clip_cy + perp_y * proj2);
}
fn emit_cap(
output: &mut Path,
cap: StrokeCap,
p: Point,
nx: f64,
ny: f64,
half_width: f64,
is_start: bool,
) {
let (tx, ty) = if is_start {
(-ny, nx)
} else {
(ny, -nx)
};
match cap {
StrokeCap::Butt => {
}
StrokeCap::Square => {
let left_x = p.x + nx * half_width;
let left_y = p.y + ny * half_width;
let right_x = p.x - nx * half_width;
let right_y = p.y - ny * half_width;
output.line_to(left_x + tx * half_width, left_y + ty * half_width);
output.line_to(right_x + tx * half_width, right_y + ty * half_width);
}
StrokeCap::Round => {
if is_start {
emit_semicircle(output, p, -nx, -ny, nx, ny, half_width);
} else {
emit_semicircle(output, p, nx, ny, -nx, -ny, half_width);
}
}
}
}
fn emit_semicircle(
output: &mut Path,
center: Point,
d0x: f64,
d0y: f64,
d1x: f64,
d1y: f64,
radius: f64,
) {
let mid_x = (d0y + d1y) * 0.5;
let mid_y = (-d0x - d1x) * 0.5;
let mid_len = (mid_x * mid_x + mid_y * mid_y).sqrt();
let (mid_x, mid_y) = if mid_len > 1e-12 {
(mid_x / mid_len, mid_y / mid_len)
} else {
(d0y, -d0x)
};
const ARC_KAPPA: f64 = 0.552_284_749_831;
let p0x = center.x + d0x * radius;
let p0y = center.y + d0y * radius;
let p3x = center.x + mid_x * radius;
let p3y = center.y + mid_y * radius;
let t0x = -d0y;
let t0y = d0x;
let sign0 = if t0x * (mid_x - d0x) + t0y * (mid_y - d0y) >= 0.0 {
1.0
} else {
-1.0
};
let t1x = -mid_y;
let t1y = mid_x;
let sign1 = if t1x * (d0x - mid_x) + t1y * (d0y - mid_y) >= 0.0 {
1.0
} else {
-1.0
};
output.cubic_to(
p0x + sign0 * t0x * radius * ARC_KAPPA,
p0y + sign0 * t0y * radius * ARC_KAPPA,
p3x + sign1 * t1x * radius * ARC_KAPPA,
p3y + sign1 * t1y * radius * ARC_KAPPA,
p3x,
p3y,
);
let q0x = p3x;
let q0y = p3y;
let q3x = center.x + d1x * radius;
let q3y = center.y + d1y * radius;
let t2x = -mid_y;
let t2y = mid_x;
let sign2 = if t2x * (d1x - mid_x) + t2y * (d1y - mid_y) >= 0.0 {
1.0
} else {
-1.0
};
let t3x = -d1y;
let t3y = d1x;
let sign3 = if t3x * (mid_x - d1x) + t3y * (mid_y - d1y) >= 0.0 {
1.0
} else {
-1.0
};
output.cubic_to(
q0x + sign2 * t2x * radius * ARC_KAPPA,
q0y + sign2 * t2y * radius * ARC_KAPPA,
q3x + sign3 * t3x * radius * ARC_KAPPA,
q3y + sign3 * t3y * radius * ARC_KAPPA,
q3x,
q3y,
);
}
fn emit_arc(output: &mut Path, center: Point, n0x: f64, n0y: f64, n1x: f64, n1y: f64, radius: f64) {
let dot = n0x * n1x + n0y * n1y;
let angle = dot.clamp(-1.0, 1.0).acos();
if angle < 1e-6 {
return;
}
if angle <= std::f64::consts::FRAC_PI_2 + 0.01 {
emit_arc_segment(output, center, n0x, n0y, n1x, n1y, radius);
} else {
let mx = n0x + n1x;
let my = n0y + n1y;
let m_len = (mx * mx + my * my).sqrt();
if m_len < 1e-12 {
return;
}
let mx = mx / m_len;
let my = my / m_len;
emit_arc_segment(output, center, n0x, n0y, mx, my, radius);
emit_arc_segment(output, center, mx, my, n1x, n1y, radius);
}
}
fn emit_arc_segment(
output: &mut Path,
center: Point,
n0x: f64,
n0y: f64,
n1x: f64,
n1y: f64,
radius: f64,
) {
let dot = (n0x * n1x + n0y * n1y).clamp(-1.0, 1.0);
let angle = dot.acos();
if angle < 1e-6 {
return;
}
let k = (4.0 / 3.0) * (angle / 4.0).tan();
let p0x = center.x + n0x * radius;
let p0y = center.y + n0y * radius;
let p3x = center.x + n1x * radius;
let p3y = center.y + n1y * radius;
let cross = n0x * n1y - n0y * n1x;
let sign = if cross >= 0.0 { 1.0 } else { -1.0 };
let t0x = -n0y * sign;
let t0y = n0x * sign;
let t1x = n1y * sign;
let t1y = -n1x * sign;
output.cubic_to(
p0x + t0x * radius * k,
p0y + t0y * radius * k,
p3x + t1x * radius * k,
p3y + t1y * radius * k,
p3x,
p3y,
);
}