diff --git a/render-wasm/src/shapes/paths.rs b/render-wasm/src/shapes/paths.rs index 33666bb0de..37bca34c98 100644 --- a/render-wasm/src/shapes/paths.rs +++ b/render-wasm/src/shapes/paths.rs @@ -156,6 +156,7 @@ impl Path { let mut current_point = 0; let mut current_conic = 0; let mut last_point = skia::Point::new(0.0, 0.0); + let mut subpath_start = skia::Point::new(0.0, 0.0); for verb in verbs { match verb { @@ -163,12 +164,15 @@ impl Path { let p = points[current_point]; segments.push(Segment::MoveTo((p.x, p.y))); last_point = p; + subpath_start = p; current_point += 1; } skia::PathVerb::Line => { let p = points[current_point]; - segments.push(Segment::LineTo((p.x, p.y))); - last_point = p; + if p != last_point { + segments.push(Segment::LineTo((p.x, p.y))); + last_point = p; + } current_point += 1; } skia::PathVerb::Quad => { @@ -239,11 +243,20 @@ impl Path { current_point += 3; } skia::PathVerb::Close => { + if let Some(Segment::LineTo(p)) = segments.last() { + if (p.0 - subpath_start.x).abs() < 1e-5 + && (p.1 - subpath_start.y).abs() < 1e-5 + { + segments.pop(); + } + } segments.push(Segment::Close); } } } + simplify_collinear_lines(&mut segments); + let mut result = Path::new(segments); result.skia_path.set_fill_type(fill_type); result @@ -341,3 +354,48 @@ impl Path { math::Bounds::from_rect(self.skia_path.bounds()) } } + +fn segment_endpoint(seg: &Segment) -> Option<(f32, f32)> { + match seg { + Segment::MoveTo(p) | Segment::LineTo(p) => Some(*p), + Segment::CurveTo((_, _, p)) => Some(*p), + Segment::Close => None, + } +} + +/// Removes intermediate LineTo segments that lie on a straight line +/// between their previous and next endpoints. Only merges when the segments +/// go in the same direction (dot >= 0), avoiding modification of backtracking paths. +fn simplify_collinear_lines(segments: &mut Vec) { + let mut i = 2; + while i < segments.len() { + let p0 = segment_endpoint(&segments[i - 2]); + let p1 = match &segments[i - 1] { + Segment::LineTo(p) => *p, + _ => { + i += 1; + continue; + } + }; + let p2 = match &segments[i] { + Segment::LineTo(p) => *p, + _ => { + i += 1; + continue; + } + }; + if let Some(p0) = p0 { + let dx1 = p1.0 - p0.0; + let dy1 = p1.1 - p0.1; + let dx2 = p2.0 - p1.0; + let dy2 = p2.1 - p1.1; + let cross = dx1 * dy2 - dy1 * dx2; + let dot = dx1 * dx2 + dy1 * dy2; + if cross.abs() < 1e-1 && dot >= 0.0 { + segments.remove(i - 1); + continue; + } + } + i += 1; + } +} diff --git a/render-wasm/src/shapes/stroke_paths.rs b/render-wasm/src/shapes/stroke_paths.rs index 358be86141..1618a962ea 100644 --- a/render-wasm/src/shapes/stroke_paths.rs +++ b/render-wasm/src/shapes/stroke_paths.rs @@ -58,27 +58,30 @@ pub fn stroke_to_path( // 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 as_winding() on the result fixes contour winding - // for Penpot's NonZero fill rule. + // 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.set_fill_type(skia::PathFillType::EvenOdd); - let inner = stroke_outline + stroke_outline.set_fill_type(skia::PathFillType::Winding); + let mut inner = stroke_outline .op(&transformed_shape_path, skia::PathOp::Intersect) .unwrap_or(stroke_outline); - inner.as_winding().unwrap_or(inner) + inner.set_fill_type(skia::PathFillType::Winding); + inner } StrokeKind::Outer => { - stroke_outline.set_fill_type(skia::PathFillType::EvenOdd); - let outer = stroke_outline + stroke_outline.set_fill_type(skia::PathFillType::Winding); + let mut outer = stroke_outline .op(&transformed_shape_path, skia::PathOp::Difference) .unwrap_or(stroke_outline); - outer.as_winding().unwrap_or(outer) - } - StrokeKind::Center => { - stroke_outline.set_fill_type(skia::PathFillType::EvenOdd); - stroke_outline.as_winding().unwrap_or(stroke_outline) + outer.set_fill_type(skia::PathFillType::Winding); + outer } + StrokeKind::Center => stroke_outline, }; // If there was a path_transform, invert it back to local coords