use std::f64::consts::PI;
use crate::path::{Path, PathBuilder};
use crate::types::{BEZIER_CIRCLE, LineCap, LineJoin, splash_floor};
const BEZIER_CIRCLE2: f64 = 0.5 * BEZIER_CIRCLE;
const MITER_NEARLY_STRAIGHT: f64 = 0.999_9;
#[must_use]
pub(super) fn flatten_path(path: &Path, _matrix: &[f64; 6], flatness: f64) -> Path {
use crate::path::flatten::{CurveData, flatten_curve};
let flatness_sq = flatness * flatness;
let mut builder = PathBuilder::new();
let mut curve_data: Option<Box<CurveData>> = None;
let mut i = 0usize;
while i < path.pts.len() {
let flag = path.flags[i];
if flag.is_first() {
let _ = builder.move_to(path.pts[i].x, path.pts[i].y);
i += 1;
} else if flag.is_curve() {
let p0 = path.pts[i - 1];
let p1 = path.pts[i];
let p2 = path.pts[i + 1];
let p3 = path.pts[i + 2];
let mut flat_pts: Vec<crate::path::PathPoint> = Vec::new();
flatten_curve(p0, p1, p2, p3, flatness_sq, &mut flat_pts, &mut curve_data);
for pt in &flat_pts {
let _ = builder.line_to(pt.x, pt.y);
}
i += 3;
if path.flags[i - 1].is_closed() {
let _ = builder.close(false);
}
} else {
let _ = builder.line_to(path.pts[i].x, path.pts[i].y);
i += 1;
if path.flags[i - 1].is_closed() {
let _ = builder.close(false);
}
}
}
builder.build()
}
#[must_use]
#[expect(
clippy::suboptimal_flops,
reason = "a + b*c expressions match the C++ source; mul_add would obscure the 1:1 correspondence"
)]
#[expect(
clippy::while_float,
reason = "direct port of Splash::makeDashedPath; seg_len is decremented toward 0 each iteration"
)]
pub(super) fn make_dashed_path(path: &Path, line_dash: &[f64], line_dash_phase: f64) -> Path {
let line_dash_total: f64 = line_dash.iter().sum();
if line_dash_total < f64::EPSILON {
return Path::new();
}
let mut phase = line_dash_phase;
{
let periods = splash_floor(phase / line_dash_total);
phase -= f64::from(periods) * line_dash_total;
}
let mut dash_start_on = true;
let mut dash_start_idx: usize = 0;
if phase > 0.0 {
while dash_start_idx < line_dash.len() && phase >= line_dash[dash_start_idx] {
dash_start_on = !dash_start_on;
phase -= line_dash[dash_start_idx];
dash_start_idx += 1;
}
if dash_start_idx == line_dash.len() {
return Path::new();
}
}
let mut builder = PathBuilder::new();
let mut i = 0usize;
while i < path.pts.len() {
let mut j = i;
while j < path.pts.len().saturating_sub(1) && !path.flags[j].is_last() {
j += 1;
}
let mut dash_on = dash_start_on;
let mut dash_idx = dash_start_idx;
let mut dash_dist = line_dash[dash_idx] - phase;
let mut new_path = true;
for k in i..j {
let x0 = path.pts[k].x;
let y0 = path.pts[k].y;
let x1 = path.pts[k + 1].x;
let y1 = path.pts[k + 1].y;
let mut seg_len = splash_dist(x0, y0, x1, y1);
let mut xa = x0;
let mut ya = y0;
while seg_len > 0.0 {
if dash_dist >= seg_len {
if dash_on {
if new_path {
let _ = builder.move_to(xa, ya);
new_path = false;
}
let _ = builder.line_to(x1, y1);
}
dash_dist -= seg_len;
seg_len = 0.0;
} else {
let xb = xa + (dash_dist / seg_len) * (x1 - xa);
let yb = ya + (dash_dist / seg_len) * (y1 - ya);
if dash_on {
if new_path {
let _ = builder.move_to(xa, ya);
new_path = false;
}
let _ = builder.line_to(xb, yb);
}
xa = xb;
ya = yb;
seg_len -= dash_dist;
dash_dist = 0.0;
}
if dash_dist <= 0.0 {
dash_on = !dash_on;
dash_idx += 1;
if dash_idx == line_dash.len() {
dash_idx = 0;
}
dash_dist = line_dash[dash_idx];
new_path = true;
}
}
}
i = j + 1;
}
let result = builder.build();
if result.pts.is_empty() && !path.pts.is_empty() {
let all_same = path.pts.windows(2).all(|w| w[0] == w[1]);
if all_same {
let mut b2 = PathBuilder::new();
let _ = b2.move_to(path.pts[0].x, path.pts[0].y);
let _ = b2.line_to(path.pts[0].x, path.pts[0].y);
return b2.build();
}
}
result
}
#[must_use]
#[expect(
clippy::too_many_lines,
reason = "direct port of Splash::makeStrokePath; splitting would obscure the 1:1 correspondence"
)]
#[expect(
clippy::similar_names,
reason = "geometry variables dx/dy/wdx/wdy share a common prefix by convention; renaming harms readability"
)]
#[expect(
clippy::suboptimal_flops,
reason = "a + b*c expressions match the C++ source exactly; mul_add would obscure the 1:1 correspondence"
)]
pub(super) fn make_stroke_path(path: &Path, w: f64, params: &super::StrokeParams<'_>) -> Path {
if path.pts.is_empty() {
return Path::new();
}
let mut out = PathBuilder::new();
let mut subpath_start0: usize = 0;
let mut subpath_start1: usize = 0;
let mut seg: usize = 0;
let mut closed = false;
let mut left0: usize = 0;
let mut left1: usize = 0;
let mut right0: usize = 0;
let mut right1: usize = 0;
let mut join0: usize = 0;
let mut join1: usize = 0;
let mut left_first: usize = 0;
let mut right_first: usize = 0;
let mut first_pt: usize = 0;
let mut i0: usize = 0;
let mut i1 = advance_past_coincident(path, i0);
while i1 < path.pts.len() {
let first = path.flags[i0].is_first();
if first {
subpath_start0 = i0;
subpath_start1 = i1;
seg = 0;
closed = path.flags[i0].is_closed();
}
let j0 = i1 + 1;
let j1 = if j0 < path.pts.len() {
advance_past_coincident(path, j0)
} else {
j0
};
if path.flags[i1].is_last() {
if first && params.line_cap == LineCap::Round {
let cx = path.pts[i0].x;
let cy = path.pts[i0].y;
let r = 0.5 * w;
let bc2w = BEZIER_CIRCLE2 * w;
let _ = out.move_to(cx + r, cy);
let _ = out.curve_to(cx + r, cy + bc2w, cx + bc2w, cy + r, cx, cy + r);
let _ = out.curve_to(cx - bc2w, cy + r, cx - r, cy + bc2w, cx - r, cy);
let _ = out.curve_to(cx - r, cy - bc2w, cx - bc2w, cy - r, cx, cy - r);
let _ = out.curve_to(cx + bc2w, cy - r, cx + r, cy - bc2w, cx + r, cy);
let _ = out.close(false);
}
i0 = j0;
i1 = j1;
continue;
}
let last = path.flags[j1].is_last();
let k0 = if last { subpath_start1 + 1 } else { j1 + 1 };
let seg_dist = splash_dist(
path.pts[i1].x,
path.pts[i1].y,
path.pts[j0].x,
path.pts[j0].y,
);
if seg_dist == 0.0 {
i0 = j0;
i1 = j1;
seg += 1;
continue;
}
let d = 1.0 / seg_dist;
let dx = d * (path.pts[j0].x - path.pts[i1].x);
let dy = d * (path.pts[j0].y - path.pts[i1].y);
let wdx = 0.5 * w * dx;
let wdy = 0.5 * w * dy;
if out
.move_to(path.pts[i0].x - wdy, path.pts[i0].y + wdx)
.is_err()
{
break;
}
if i0 == subpath_start0 {
first_pt = out.pts_len() - 1;
}
if first && !closed {
match params.line_cap {
LineCap::Butt => {
let _ = out.line_to(path.pts[i0].x + wdy, path.pts[i0].y - wdx);
}
LineCap::Round => {
let _ = out.curve_to(
path.pts[i0].x - wdy - BEZIER_CIRCLE * wdx,
path.pts[i0].y + wdx - BEZIER_CIRCLE * wdy,
path.pts[i0].x - wdx - BEZIER_CIRCLE * wdy,
path.pts[i0].y - wdy + BEZIER_CIRCLE * wdx,
path.pts[i0].x - wdx,
path.pts[i0].y - wdy,
);
let _ = out.curve_to(
path.pts[i0].x - wdx + BEZIER_CIRCLE * wdy,
path.pts[i0].y - wdy - BEZIER_CIRCLE * wdx,
path.pts[i0].x + wdy - BEZIER_CIRCLE * wdx,
path.pts[i0].y - wdx - BEZIER_CIRCLE * wdy,
path.pts[i0].x + wdy,
path.pts[i0].y - wdx,
);
}
LineCap::Projecting => {
let _ = out.line_to(path.pts[i0].x - wdx - wdy, path.pts[i0].y + wdx - wdy);
let _ = out.line_to(path.pts[i0].x - wdx + wdy, path.pts[i0].y - wdx - wdy);
let _ = out.line_to(path.pts[i0].x + wdy, path.pts[i0].y - wdx);
}
}
} else {
let _ = out.line_to(path.pts[i0].x + wdy, path.pts[i0].y - wdx);
}
let left2 = out.pts_len() - 1;
let _ = out.line_to(path.pts[j0].x + wdy, path.pts[j0].y - wdx);
if last && !closed {
match params.line_cap {
LineCap::Butt => {
let _ = out.line_to(path.pts[j0].x - wdy, path.pts[j0].y + wdx);
}
LineCap::Round => {
let _ = out.curve_to(
path.pts[j0].x + wdy + BEZIER_CIRCLE * wdx,
path.pts[j0].y - wdx + BEZIER_CIRCLE * wdy,
path.pts[j0].x + wdx + BEZIER_CIRCLE * wdy,
path.pts[j0].y + wdy - BEZIER_CIRCLE * wdx,
path.pts[j0].x + wdx,
path.pts[j0].y + wdy,
);
let _ = out.curve_to(
path.pts[j0].x + wdx - BEZIER_CIRCLE * wdy,
path.pts[j0].y + wdy + BEZIER_CIRCLE * wdx,
path.pts[j0].x - wdy + BEZIER_CIRCLE * wdx,
path.pts[j0].y + wdx + BEZIER_CIRCLE * wdy,
path.pts[j0].x - wdy,
path.pts[j0].y + wdx,
);
}
LineCap::Projecting => {
let _ = out.line_to(path.pts[j0].x + wdy + wdx, path.pts[j0].y - wdx + wdy);
let _ = out.line_to(path.pts[j0].x - wdy + wdx, path.pts[j0].y + wdx + wdy);
let _ = out.line_to(path.pts[j0].x - wdy, path.pts[j0].y + wdx);
}
}
} else {
let _ = out.line_to(path.pts[j0].x - wdy, path.pts[j0].y + wdx);
}
let right2 = out.pts_len() - 1;
let _ = out.close(params.stroke_adjust);
let join2 = out.pts_len();
if !last || closed {
let dn = 1.0
/ splash_dist(
path.pts[j1].x,
path.pts[j1].y,
path.pts[k0].x,
path.pts[k0].y,
);
let dx_next = dn * (path.pts[k0].x - path.pts[j1].x);
let dy_next = dn * (path.pts[k0].y - path.pts[j1].y);
let wdx_next = 0.5 * w * dx_next;
let wdy_next = 0.5 * w * dy_next;
let crossprod = dx * dy_next - dy * dx_next;
let dotprod = -(dx * dx_next + dy * dy_next);
let has_angle = crossprod != 0.0 || dx * dx_next < 0.0 || dy * dy_next < 0.0;
let (miter, m) = if dotprod > MITER_NEARLY_STRAIGHT {
((params.miter_limit + 1.0) * (params.miter_limit + 1.0), 0.0)
} else {
let mi = (2.0 / (1.0 - dotprod)).max(1.0);
let mv = (mi - 1.0).sqrt();
(mi, mv)
};
if has_angle && params.line_join == LineJoin::Round {
if crossprod < 0.0 {
let angle = f64::atan2(dx, -dy);
let angle_next = f64::atan2(dx_next, -dy_next);
let angle = if angle < angle_next {
angle + 2.0 * PI
} else {
angle
};
let d_angle = (angle - angle_next) / PI;
if d_angle < 0.501 {
let kappa = d_angle * BEZIER_CIRCLE * w;
let cx1 = path.pts[j0].x - wdy + kappa * dx;
let cy1 = path.pts[j0].y + wdx + kappa * dy;
let cx2 = path.pts[j0].x - wdy_next - kappa * dx_next;
let cy2 = path.pts[j0].y + wdx_next - kappa * dy_next;
let _ = out.move_to(path.pts[j0].x, path.pts[j0].y);
let _ = out.line_to(path.pts[j0].x - wdy_next, path.pts[j0].y + wdx_next);
let _ = out.curve_to(
cx2,
cy2,
cx1,
cy1,
path.pts[j0].x - wdy,
path.pts[j0].y + wdx,
);
} else {
let d_join = splash_dist(-wdy, wdx, -wdy_next, wdx_next);
if d_join > 0.0 {
let dx_join = (-wdy_next + wdy) / d_join;
let dy_join = (wdx_next - wdx) / d_join;
let xc =
path.pts[j0].x + 0.5 * w * f64::cos(0.5 * (angle + angle_next));
let yc =
path.pts[j0].y + 0.5 * w * f64::sin(0.5 * (angle + angle_next));
let kappa = d_angle * BEZIER_CIRCLE2 * w;
let cx1 = path.pts[j0].x - wdy + kappa * dx;
let cy1 = path.pts[j0].y + wdx + kappa * dy;
let cx2 = xc - kappa * dx_join;
let cy2 = yc - kappa * dy_join;
let cx3 = xc + kappa * dx_join;
let cy3 = yc + kappa * dy_join;
let cx4 = path.pts[j0].x - wdy_next - kappa * dx_next;
let cy4 = path.pts[j0].y + wdx_next - kappa * dy_next;
let _ = out.move_to(path.pts[j0].x, path.pts[j0].y);
let _ =
out.line_to(path.pts[j0].x - wdy_next, path.pts[j0].y + wdx_next);
let _ = out.curve_to(cx4, cy4, cx3, cy3, xc, yc);
let _ = out.curve_to(
cx2,
cy2,
cx1,
cy1,
path.pts[j0].x - wdy,
path.pts[j0].y + wdx,
);
}
}
} else {
let angle = f64::atan2(-dx, dy);
let angle_next = f64::atan2(-dx_next, dy_next);
let angle_next = if angle_next < angle {
angle_next + 2.0 * PI
} else {
angle_next
};
let d_angle = (angle_next - angle) / PI;
if d_angle < 0.501 {
let kappa = d_angle * BEZIER_CIRCLE * w;
let cx1 = path.pts[j0].x + wdy + kappa * dx;
let cy1 = path.pts[j0].y - wdx + kappa * dy;
let cx2 = path.pts[j0].x + wdy_next - kappa * dx_next;
let cy2 = path.pts[j0].y - wdx_next - kappa * dy_next;
let _ = out.move_to(path.pts[j0].x, path.pts[j0].y);
let _ = out.line_to(path.pts[j0].x + wdy, path.pts[j0].y - wdx);
let _ = out.curve_to(
cx1,
cy1,
cx2,
cy2,
path.pts[j0].x + wdy_next,
path.pts[j0].y - wdx_next,
);
} else {
let d_join = splash_dist(wdy, -wdx, wdy_next, -wdx_next);
if d_join > 0.0 {
let dx_join = (wdy_next - wdy) / d_join;
let dy_join = (-wdx_next + wdx) / d_join;
let xc =
path.pts[j0].x + 0.5 * w * f64::cos(0.5 * (angle + angle_next));
let yc =
path.pts[j0].y + 0.5 * w * f64::sin(0.5 * (angle + angle_next));
let kappa = d_angle * BEZIER_CIRCLE2 * w;
let cx1 = path.pts[j0].x + wdy + kappa * dx;
let cy1 = path.pts[j0].y - wdx + kappa * dy;
let cx2 = xc - kappa * dx_join;
let cy2 = yc - kappa * dy_join;
let cx3 = xc + kappa * dx_join;
let cy3 = yc + kappa * dy_join;
let cx4 = path.pts[j0].x + wdy_next - kappa * dx_next;
let cy4 = path.pts[j0].y - wdx_next - kappa * dy_next;
let _ = out.move_to(path.pts[j0].x, path.pts[j0].y);
let _ = out.line_to(path.pts[j0].x + wdy, path.pts[j0].y - wdx);
let _ = out.curve_to(cx1, cy1, cx2, cy2, xc, yc);
let _ = out.curve_to(
cx3,
cy3,
cx4,
cy4,
path.pts[j0].x + wdy_next,
path.pts[j0].y - wdx_next,
);
}
}
}
} else if has_angle {
let _ = out.move_to(path.pts[j0].x, path.pts[j0].y);
if crossprod < 0.0 {
let _ = out.line_to(path.pts[j0].x - wdy_next, path.pts[j0].y + wdx_next);
if params.line_join == LineJoin::Miter && miter.sqrt() <= params.miter_limit {
let _ = out.line_to(
path.pts[j0].x - wdy + wdx * m,
path.pts[j0].y + wdx + wdy * m,
);
}
let _ = out.line_to(path.pts[j0].x - wdy, path.pts[j0].y + wdx);
} else {
let _ = out.line_to(path.pts[j0].x + wdy, path.pts[j0].y - wdx);
if params.line_join == LineJoin::Miter && miter.sqrt() <= params.miter_limit {
let _ = out.line_to(
path.pts[j0].x + wdy + wdx * m,
path.pts[j0].y - wdx + wdy * m,
);
}
let _ = out.line_to(path.pts[j0].x + wdy_next, path.pts[j0].y - wdx_next);
}
}
let _ = out.close(false);
}
if params.stroke_adjust {
if seg == 0 && !closed {
match params.line_cap {
LineCap::Butt => {
out.add_stroke_adjust_hint(first_pt, left2 + 1, first_pt, first_pt + 1);
if last {
out.add_stroke_adjust_hint(first_pt, left2 + 1, left2 + 1, left2 + 2);
}
}
LineCap::Projecting => {
if last {
out.add_stroke_adjust_hint(
first_pt + 1,
left2 + 2,
first_pt + 1,
first_pt + 2,
);
out.add_stroke_adjust_hint(
first_pt + 1,
left2 + 2,
left2 + 2,
left2 + 3,
);
} else {
out.add_stroke_adjust_hint(
first_pt + 1,
left2 + 1,
first_pt + 1,
first_pt + 2,
);
}
}
LineCap::Round => {}
}
}
if seg >= 1 {
if seg >= 2 {
out.add_stroke_adjust_hint(left1, right1, left0 + 1, right0);
out.add_stroke_adjust_hint(left1, right1, join0, left2);
} else {
out.add_stroke_adjust_hint(left1, right1, first_pt, left2);
}
out.add_stroke_adjust_hint(left1, right1, right2 + 1, right2 + 1);
}
left0 = left1;
left1 = left2;
right0 = right1;
right1 = right2;
join0 = join1;
join1 = join2;
if seg == 0 {
left_first = left2;
right_first = right2;
}
if last {
if seg >= 2 {
out.add_stroke_adjust_hint(left1, right1, left0 + 1, right0);
out.add_stroke_adjust_hint(left1, right1, join0, out.pts_len() - 1);
} else {
out.add_stroke_adjust_hint(left1, right1, first_pt, out.pts_len() - 1);
}
if closed {
out.add_stroke_adjust_hint(left1, right1, first_pt, left_first);
out.add_stroke_adjust_hint(left1, right1, right_first + 1, right_first + 1);
out.add_stroke_adjust_hint(left_first, right_first, left1 + 1, right1);
out.add_stroke_adjust_hint(left_first, right_first, join1, out.pts_len() - 1);
}
if !closed && seg > 0 {
match params.line_cap {
LineCap::Butt => {
out.add_stroke_adjust_hint(left1 - 1, left1 + 1, left1 + 1, left1 + 2);
}
LineCap::Projecting => {
out.add_stroke_adjust_hint(left1 - 1, left1 + 2, left1 + 2, left1 + 3);
}
LineCap::Round => {}
}
}
}
}
i0 = j0;
i1 = j1;
seg += 1;
}
out.build()
}
#[expect(
clippy::float_cmp,
reason = "exact equality is correct here: the C++ source also uses == to detect coincident \
points (degenerate zero-length segments), and floating-point inequality is \
intentional — even a tiny numerical difference should not be collapsed"
)]
fn advance_past_coincident(path: &Path, start: usize) -> usize {
let mut i = start;
while !path.flags[i].is_last()
&& i + 1 < path.pts.len()
&& path.pts[i + 1].x == path.pts[i].x
&& path.pts[i + 1].y == path.pts[i].y
{
i += 1;
}
i
}
#[inline]
fn splash_dist(x0: f64, y0: f64, x1: f64, y1: f64) -> f64 {
let dx = x1 - x0;
let dy = y1 - y0;
dx.hypot(dy)
}