penpot/render-wasm/src/shapes/stroke_paths.rs
Aitor Moreno 2d4a24bf97
🐛 Fix stroke to path extra points (#10190)
* 🐛 Fix stroke to path extra points

* 🐛 Set evenodd when needed on stroke to path (#10446)

---------

Co-authored-by: Elena Torró <elenatorro@gmail.com>
2026-06-29 10:34:44 +02:00

94 lines
3.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use skia_safe::{self as skia};
use super::paths::Path;
use super::strokes::{Stroke, StrokeKind};
use super::svg_attrs::SvgAttrs;
use crate::math::Rect;
/// Converts a stroke into a filled path outline.
///
/// Uses Skia's `fill_path_with_paint` to expand the stroke into a filled region,
/// then clips it via boolean ops for inner/outer alignment. The optional
/// `path_transform` maps from local shape coords to the drawing space (and back).
///
/// When `solid_outline` is true, any dash/dot PathEffect is stripped so the result
/// is a continuous stroke region — useful for clipping (e.g. drag crop cache) where
/// dash gaps should not punch holes in the clip mask.
pub fn stroke_to_path(
stroke: &Stroke,
shape_path: &Path,
path_transform: Option<&skia::Matrix>,
selrect: &Rect,
svg_attrs: Option<&SvgAttrs>,
solid_outline: bool,
) -> Option<Path> {
let skia_shape_path = shape_path.to_skia_path(svg_attrs);
let transformed_shape_path = if let Some(pt) = path_transform {
skia_shape_path.make_transform(pt)
} else {
skia_shape_path.clone()
};
let is_open = shape_path.is_open();
let mut paint = stroke.to_paint(selrect, svg_attrs, true);
if solid_outline {
paint.set_path_effect(None);
}
let render_kind = stroke.render_kind(is_open);
if render_kind != StrokeKind::Center {
paint.set_stroke_width(stroke.width * 2.0);
}
let mut stroke_outline = skia::Path::default();
let success = skia::path_utils::fill_path_with_paint(
&transformed_shape_path,
&paint,
&mut stroke_outline,
None,
None,
);
if !success {
return None;
}
// For inner/outer strokes, use boolean ops to clip
// the 2×-width stroke outline to the correct region.
// Set EvenOdd to preserve the annular ring's inner hole,
// then switch to Winding for Penpot's NonZero fill rule.
// Use set_fill_type instead of as_winding() because as_winding()
// decomposes self-intersecting geometry, which removes points
// at intersections of straight lines in closed paths.
// Center strokes skip the conversion: fill_path_with_paint
// already produces correctly-wound contours.
let final_path = match render_kind {
StrokeKind::Inner => stroke_outline
.simplify()
.unwrap()
.op(&transformed_shape_path, skia::PathOp::Intersect)
.unwrap_or(stroke_outline),
StrokeKind::Outer => stroke_outline
.simplify()
.unwrap()
.op(&transformed_shape_path, skia::PathOp::Difference)
.unwrap_or(stroke_outline),
StrokeKind::Center => stroke_outline.simplify().unwrap_or(stroke_outline),
};
// If there was a path_transform, invert it back to local coords
let final_path = if let Some(pt) = path_transform {
if let Some(inv) = pt.invert() {
final_path.make_transform(&inv)
} else {
final_path
}
} else {
final_path
};
Some(Path::from_skia_path_accurate(final_path))
}