diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 3a142b05e6..6f31573288 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -302,6 +302,12 @@ fn handle_stroke_caps( return; } + // When both ends share the same simple line cap, Skia already drew it + // natively via `PaintCap` on the stroke paint, so skip the manual overlay. + if stroke.to_skia_linecap().is_some() { + return; + } + // Curves can have duplicated points, so let's remove consecutive duplicated points let mut points = path.points().to_vec(); points.dedup(); diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index 4ca3b5f906..6b3f9443ed 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -293,6 +293,10 @@ impl Stroke { } } + if let Some(cap) = self.to_skia_linecap() { + paint.set_stroke_cap(cap); + } + paint } @@ -330,6 +334,19 @@ impl Stroke { cap_margin_for_cap(self.cap_start, self.width) .max(cap_margin_for_cap(self.cap_end, self.width)) } + + /// Returns a Skia `PaintCap` to apply natively on the stroke paint when + /// both ends share the same simple line cap (`Round/Round` or + /// `Square/Square`). Skia only emits cap geometry at sub-path endpoints, + /// so this is a no-op on closed paths and avoids the extra fill draw the + /// manual caps would otherwise require on open paths. + pub fn to_skia_linecap(&self) -> Option { + match (self.cap_start, self.cap_end) { + (Some(StrokeCap::Round), Some(StrokeCap::Round)) => Some(skia::paint::Cap::Round), + (Some(StrokeCap::Square), Some(StrokeCap::Square)) => Some(skia::paint::Cap::Square), + _ => None, + } + } } fn align_rect_to_half_pixel(rect: &Rect, stroke_width: f32, scale: f32) -> Rect {