use crate::{Path, PathBuilder, PathElement};
use skia_rs_core::{Point, Scalar};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum StrokeCap {
#[default]
Butt = 0,
Round,
Square,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum StrokeJoin {
#[default]
Miter = 0,
Round,
Bevel,
}
#[derive(Debug, Clone)]
pub struct StrokeParams {
pub width: Scalar,
pub cap: StrokeCap,
pub join: StrokeJoin,
pub miter_limit: Scalar,
}
impl Default for StrokeParams {
fn default() -> Self {
Self {
width: 1.0,
cap: StrokeCap::Butt,
join: StrokeJoin::Miter,
miter_limit: 4.0,
}
}
}
impl StrokeParams {
pub fn new(width: Scalar) -> Self {
Self {
width,
..Default::default()
}
}
pub fn with_cap(mut self, cap: StrokeCap) -> Self {
self.cap = cap;
self
}
pub fn with_join(mut self, join: StrokeJoin) -> Self {
self.join = join;
self
}
pub fn with_miter_limit(mut self, limit: Scalar) -> Self {
self.miter_limit = limit;
self
}
}
pub fn stroke_to_fill(path: &Path, params: &StrokeParams) -> Option<Path> {
if path.is_empty() || params.width <= 0.0 {
return None;
}
let half_width = params.width / 2.0;
let mut builder = PathBuilder::new();
let mut contours: Vec<(Vec<Point>, bool)> = Vec::new();
let mut current_contour: Vec<Point> = Vec::new();
let mut current_closed = false;
for element in path.iter() {
match element {
PathElement::Move(p) => {
if !current_contour.is_empty() {
contours.push((std::mem::take(&mut current_contour), current_closed));
}
current_contour.push(p);
current_closed = false;
}
PathElement::Line(p) => {
current_contour.push(p);
}
PathElement::Quad(ctrl, end) => {
if let Some(&start) = current_contour.last() {
flatten_quad(&mut current_contour, start, ctrl, end, 4);
}
}
PathElement::Cubic(ctrl1, ctrl2, end) => {
if let Some(&start) = current_contour.last() {
flatten_cubic(&mut current_contour, start, ctrl1, ctrl2, end, 8);
}
}
PathElement::Conic(ctrl, end, weight) => {
if let Some(&start) = current_contour.last() {
let mid_ctrl = Point::new(
start.x * (1.0 - weight) / 2.0
+ ctrl.x * weight
+ end.x * (1.0 - weight) / 2.0,
start.y * (1.0 - weight) / 2.0
+ ctrl.y * weight
+ end.y * (1.0 - weight) / 2.0,
);
flatten_quad(&mut current_contour, start, mid_ctrl, end, 4);
}
}
PathElement::Close => {
current_closed = true;
}
}
}
if !current_contour.is_empty() {
contours.push((current_contour, current_closed));
}
for (contour, is_closed) in &contours {
if contour.len() < 2 {
continue;
}
stroke_contour(&mut builder, contour, *is_closed, half_width, params);
}
Some(builder.build())
}
fn stroke_contour(
builder: &mut PathBuilder,
points: &[Point],
is_closed: bool,
half_width: Scalar,
params: &StrokeParams,
) {
if points.len() < 2 {
return;
}
let n = points.len();
let mut normals: Vec<Point> = Vec::with_capacity(n - 1);
for i in 0..n - 1 {
let dx = points[i + 1].x - points[i].x;
let dy = points[i + 1].y - points[i].y;
let len = (dx * dx + dy * dy).sqrt();
if len > 0.0 {
normals.push(Point::new(-dy / len, dx / len));
} else {
normals.push(Point::new(0.0, 1.0));
}
}
if normals.is_empty() {
return;
}
let mut left_side: Vec<Point> = Vec::with_capacity(n);
let mut right_side: Vec<Point> = Vec::with_capacity(n);
let first_normal = normals[0];
left_side.push(Point::new(
points[0].x + first_normal.x * half_width,
points[0].y + first_normal.y * half_width,
));
right_side.push(Point::new(
points[0].x - first_normal.x * half_width,
points[0].y - first_normal.y * half_width,
));
for i in 1..n - 1 {
let n1 = normals[i - 1];
let n2 = normals[i];
let avg = Point::new(n1.x + n2.x, n1.y + n2.y);
let avg_len = avg.length();
if avg_len > 0.001 {
let scale = half_width / avg_len;
let offset = Point::new(avg.x * scale, avg.y * scale);
match params.join {
StrokeJoin::Miter => {
let miter_len = 1.0 / (avg_len / 2.0);
if miter_len <= params.miter_limit {
left_side.push(Point::new(
points[i].x + offset.x * miter_len,
points[i].y + offset.y * miter_len,
));
right_side.push(Point::new(
points[i].x - offset.x * miter_len,
points[i].y - offset.y * miter_len,
));
} else {
left_side.push(Point::new(
points[i].x + n1.x * half_width,
points[i].y + n1.y * half_width,
));
left_side.push(Point::new(
points[i].x + n2.x * half_width,
points[i].y + n2.y * half_width,
));
right_side.push(Point::new(
points[i].x - n1.x * half_width,
points[i].y - n1.y * half_width,
));
right_side.push(Point::new(
points[i].x - n2.x * half_width,
points[i].y - n2.y * half_width,
));
}
}
StrokeJoin::Bevel => {
left_side.push(Point::new(
points[i].x + n1.x * half_width,
points[i].y + n1.y * half_width,
));
left_side.push(Point::new(
points[i].x + n2.x * half_width,
points[i].y + n2.y * half_width,
));
right_side.push(Point::new(
points[i].x - n1.x * half_width,
points[i].y - n1.y * half_width,
));
right_side.push(Point::new(
points[i].x - n2.x * half_width,
points[i].y - n2.y * half_width,
));
}
StrokeJoin::Round => {
let left_start_angle =
(n1.y * half_width).atan2(n1.x * half_width);
let left_end_angle =
(n2.y * half_width).atan2(n2.x * half_width);
let mut left_delta = left_end_angle - left_start_angle;
if left_delta > std::f32::consts::PI {
left_delta -= std::f32::consts::TAU;
} else if left_delta < -std::f32::consts::PI {
left_delta += std::f32::consts::TAU;
}
let n_segs = ((left_delta.abs()
/ std::f32::consts::FRAC_PI_4)
.ceil() as usize)
.max(4);
for k in 0..=n_segs {
let t = k as Scalar / n_segs as Scalar;
let a = left_start_angle + left_delta * t;
left_side.push(Point::new(
points[i].x + a.cos() * half_width,
points[i].y + a.sin() * half_width,
));
right_side.push(Point::new(
points[i].x - a.cos() * half_width,
points[i].y - a.sin() * half_width,
));
}
}
}
} else {
left_side.push(Point::new(
points[i].x + n1.x * half_width,
points[i].y + n1.y * half_width,
));
right_side.push(Point::new(
points[i].x - n1.x * half_width,
points[i].y - n1.y * half_width,
));
}
}
let last_normal = normals[normals.len() - 1];
left_side.push(Point::new(
points[n - 1].x + last_normal.x * half_width,
points[n - 1].y + last_normal.y * half_width,
));
right_side.push(Point::new(
points[n - 1].x - last_normal.x * half_width,
points[n - 1].y - last_normal.y * half_width,
));
if is_closed {
if !left_side.is_empty() {
builder.move_to(left_side[0].x, left_side[0].y);
for p in &left_side[1..] {
builder.line_to(p.x, p.y);
}
builder.close();
}
if !right_side.is_empty() {
builder.move_to(right_side[0].x, right_side[0].y);
for p in &right_side[1..] {
builder.line_to(p.x, p.y);
}
builder.close();
}
} else {
if !left_side.is_empty() {
builder.move_to(left_side[0].x, left_side[0].y);
add_cap(builder, points[0], normals[0], half_width, params.cap, true);
for p in &left_side {
builder.line_to(p.x, p.y);
}
add_cap(
builder,
points[n - 1],
normals[normals.len() - 1],
half_width,
params.cap,
false,
);
for p in right_side.iter().rev() {
builder.line_to(p.x, p.y);
}
builder.close();
}
}
}
fn add_cap(
builder: &mut PathBuilder,
center: Point,
normal: Point,
half_width: Scalar,
cap: StrokeCap,
is_start: bool,
) {
match cap {
StrokeCap::Butt => {
}
StrokeCap::Square => {
let dir = if is_start {
Point::new(-normal.y, normal.x)
} else {
Point::new(normal.y, -normal.x)
};
let ext = Point::new(dir.x * half_width, dir.y * half_width);
builder.line_to(
center.x + normal.x * half_width + ext.x,
center.y + normal.y * half_width + ext.y,
);
builder.line_to(
center.x - normal.x * half_width + ext.x,
center.y - normal.y * half_width + ext.y,
);
}
StrokeCap::Round => {
let steps = 8;
let start_angle = if is_start {
normal.y.atan2(normal.x)
} else {
(-normal.y).atan2(-normal.x)
};
for i in 0..=steps {
let t = i as Scalar / steps as Scalar;
let angle = start_angle + t * std::f32::consts::PI;
let x = center.x + angle.cos() * half_width;
let y = center.y + angle.sin() * half_width;
builder.line_to(x, y);
}
}
}
}
fn flatten_quad(points: &mut Vec<Point>, p0: Point, p1: Point, p2: Point, steps: usize) {
for i in 1..=steps {
let t = i as Scalar / steps as Scalar;
let mt = 1.0 - t;
let x = mt * mt * p0.x + 2.0 * mt * t * p1.x + t * t * p2.x;
let y = mt * mt * p0.y + 2.0 * mt * t * p1.y + t * t * p2.y;
points.push(Point::new(x, y));
}
}
fn flatten_cubic(
points: &mut Vec<Point>,
p0: Point,
p1: Point,
p2: Point,
p3: Point,
steps: usize,
) {
for i in 1..=steps {
let t = i as Scalar / steps as Scalar;
let mt = 1.0 - t;
let mt2 = mt * mt;
let t2 = t * t;
let x = mt2 * mt * p0.x + 3.0 * mt2 * t * p1.x + 3.0 * mt * t2 * p2.x + t2 * t * p3.x;
let y = mt2 * mt * p0.y + 3.0 * mt2 * t * p1.y + 3.0 * mt * t2 * p2.y + t2 * t * p3.y;
points.push(Point::new(x, y));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stroke_to_fill_line() {
let mut builder = PathBuilder::new();
builder.move_to(0.0, 0.0);
builder.line_to(100.0, 0.0);
let path = builder.build();
let params = StrokeParams::new(10.0);
let stroked = stroke_to_fill(&path, ¶ms).unwrap();
assert!(!stroked.is_empty());
}
#[test]
fn test_stroke_to_fill_triangle() {
let mut builder = PathBuilder::new();
builder.move_to(0.0, 0.0);
builder.line_to(100.0, 0.0);
builder.line_to(50.0, 100.0);
builder.close();
let path = builder.build();
let params = StrokeParams::new(5.0).with_join(StrokeJoin::Round);
let stroked = stroke_to_fill(&path, ¶ms).unwrap();
assert!(!stroked.is_empty());
}
#[test]
fn test_stroke_to_fill_multi_contour_mixed_closed_open() {
let mut builder = PathBuilder::new();
builder.move_to(0.0, 0.0);
builder.line_to(10.0, 0.0);
builder.line_to(5.0, 10.0);
builder.close();
builder.move_to(20.0, 0.0);
builder.line_to(30.0, 0.0);
let path = builder.build();
let params = StrokeParams::new(2.0);
let result = stroke_to_fill(&path, ¶ms);
assert!(result.is_some(), "stroke_to_fill should succeed for valid input");
let stroked = result.unwrap();
assert!(stroked.iter().count() > 0, "stroked path should not be empty");
}
#[test]
fn test_stroke_params() {
let params = StrokeParams::new(2.0)
.with_cap(StrokeCap::Round)
.with_join(StrokeJoin::Bevel)
.with_miter_limit(10.0);
assert_eq!(params.width, 2.0);
assert_eq!(params.cap, StrokeCap::Round);
assert_eq!(params.join, StrokeJoin::Bevel);
assert_eq!(params.miter_limit, 10.0);
}
#[test]
fn test_stroke_to_fill_round_join_generates_arc() {
let mut builder = PathBuilder::new();
builder.move_to(0.0, 0.0);
builder.line_to(50.0, 0.0);
builder.line_to(50.0, 50.0);
let path = builder.build();
let params = StrokeParams::new(10.0).with_join(StrokeJoin::Round);
let result = stroke_to_fill(&path, ¶ms);
assert!(result.is_some());
let stroked = result.unwrap();
let count = stroked.iter().count();
assert!(
count > 10,
"Round join should generate arc segments, got {} verbs",
count
);
}
}