diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 4670272b88..b3fa97e253 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -642,6 +642,10 @@ impl RenderState { apply_to_current_surface: bool, offset: Option<(f32, f32)>, parent_shadows: Option>, +<<<<<<< Updated upstream +======= + outset: Option, +>>>>>>> Stashed changes ) { let surface_ids = fills_surface_id as u32 | strokes_surface_id as u32 @@ -710,6 +714,10 @@ impl RenderState { &visible_strokes, Some(SurfaceId::Current), antialias, +<<<<<<< Updated upstream +======= + outset, +>>>>>>> Stashed changes ); self.surfaces.apply_mut(SurfaceId::Current as u32, |s| { @@ -1055,10 +1063,31 @@ impl RenderState { { if let Some(fills_to_render) = self.nested_fills.last() { let fills_to_render = fills_to_render.clone(); +<<<<<<< Updated upstream fills::render(self, shape, &fills_to_render, antialias, fills_surface_id); } } else { fills::render(self, shape, &shape.fills, antialias, fills_surface_id); +======= + fills::render( + self, + shape, + &fills_to_render, + antialias, + fills_surface_id, + outset, + ); + } + } else { + fills::render( + self, + shape, + &shape.fills, + antialias, + fills_surface_id, + outset, + ); +>>>>>>> Stashed changes } // Skip stroke rendering for clipped frames - they are drawn in render_shape_exit @@ -1073,6 +1102,10 @@ impl RenderState { &visible_strokes, Some(strokes_surface_id), antialias, +<<<<<<< Updated upstream +======= + outset, +>>>>>>> Stashed changes ); if !fast_mode { for stroke in &visible_strokes { diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 0875fdd649..3a0cdd4b2f 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -2,7 +2,15 @@ use skia_safe::{self as skia, Paint, RRect}; use super::{filters, RenderState, SurfaceId}; use crate::render::get_source_rect; -use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type}; +use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, StrokeKind, Type}; + +/// True when the shape has at least one visible inner stroke. +fn has_inner_stroke(shape: &Shape) -> bool { + let is_open = shape.is_open(); + shape + .visible_strokes() + .any(|s| s.render_kind(is_open) == StrokeKind::Inner) +} fn draw_image_fill( render_state: &mut RenderState, @@ -97,6 +105,10 @@ pub fn render( fills: &[Fill], antialias: bool, surface_id: SurfaceId, +<<<<<<< Updated upstream +======= + outset: Option, +>>>>>>> Stashed changes ) { if fills.is_empty() { return; @@ -106,8 +118,18 @@ pub fn render( // and sampling options that get_fill_shader (used by merge_fills) lacks. let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_))); if has_image_fills { + let scale = render_state.get_scale().max(1e-6); + let inset = if has_inner_stroke(shape) { + Some(1.0 / scale) + } else { + None + }; for fill in fills.iter().rev() { +<<<<<<< Updated upstream render_single_fill(render_state, shape, fill, antialias, surface_id); +======= + render_single_fill(render_state, shape, fill, antialias, surface_id, outset, inset); +>>>>>>> Stashed changes } return; } @@ -115,6 +137,13 @@ pub fn render( let mut paint = merge_fills(fills, shape.selrect); paint.set_anti_alias(antialias); + let scale = render_state.get_scale().max(1e-6); + let inset = if has_inner_stroke(shape) { + Some(1.0 / scale) + } else { + None + }; + if let Some(image_filter) = shape.image_filter(1.) { let bounds = image_filter.compute_fast_bounds(shape.selrect); if filters::render_with_filter_surface( @@ -124,7 +153,11 @@ pub fn render( |state, temp_surface| { let mut filtered_paint = paint.clone(); filtered_paint.set_image_filter(image_filter.clone()); +<<<<<<< Updated upstream draw_fill_to_surface(state, shape, temp_surface, &filtered_paint); +======= + draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, outset, inset); +>>>>>>> Stashed changes }, ) { return; @@ -133,28 +166,53 @@ pub fn render( } } +<<<<<<< Updated upstream draw_fill_to_surface(render_state, shape, surface_id, &paint); +======= + draw_fill_to_surface(render_state, shape, surface_id, &paint, outset, inset); +>>>>>>> Stashed changes } /// Draws a single paint (with a merged shader) to the appropriate surface /// based on the shape type. +/// When `inset` is Some(eps), the fill is inset by eps (e.g. to avoid seam with inner strokes). fn draw_fill_to_surface( render_state: &mut RenderState, shape: &Shape, surface_id: SurfaceId, paint: &Paint, +<<<<<<< Updated upstream ) { match &shape.shape_type { Type::Rect(_) | Type::Frame(_) => { render_state.surfaces.draw_rect_to(surface_id, shape, paint); +======= + outset: Option, + inset: Option, +) { + match &shape.shape_type { + Type::Rect(_) | Type::Frame(_) => { + render_state + .surfaces + .draw_rect_to(surface_id, shape, paint, outset, inset); +>>>>>>> Stashed changes } Type::Circle => { render_state .surfaces +<<<<<<< Updated upstream .draw_circle_to(surface_id, shape, paint); } Type::Path(_) | Type::Bool(_) => { render_state.surfaces.draw_path_to(surface_id, shape, paint); +======= + .draw_circle_to(surface_id, shape, paint, outset, inset); + } + Type::Path(_) | Type::Bool(_) => { + render_state + .surfaces + .draw_path_to(surface_id, shape, paint, outset); +>>>>>>> Stashed changes } Type::Group(_) => {} _ => unreachable!("This shape should not have fills"), @@ -167,6 +225,11 @@ fn render_single_fill( fill: &Fill, antialias: bool, surface_id: SurfaceId, +<<<<<<< Updated upstream +======= + outset: Option, + inset: Option, +>>>>>>> Stashed changes ) { let mut paint = fill.to_paint(&shape.selrect, antialias); if let Some(image_filter) = shape.image_filter(1.) { @@ -185,6 +248,11 @@ fn render_single_fill( antialias, temp_surface, &filtered_paint, +<<<<<<< Updated upstream +======= + outset, + inset, +>>>>>>> Stashed changes ); }, ) { @@ -194,7 +262,20 @@ fn render_single_fill( } } +<<<<<<< Updated upstream draw_single_fill_to_surface(render_state, shape, fill, antialias, surface_id, &paint); +======= + draw_single_fill_to_surface( + render_state, + shape, + fill, + antialias, + surface_id, + &paint, + outset, + inset, + ); +>>>>>>> Stashed changes } fn draw_single_fill_to_surface( @@ -204,6 +285,11 @@ fn draw_single_fill_to_surface( antialias: bool, surface_id: SurfaceId, paint: &Paint, +<<<<<<< Updated upstream +======= + outset: Option, + inset: Option, +>>>>>>> Stashed changes ) { match (fill, &shape.shape_type) { (Fill::Image(image_fill), _) => { @@ -217,15 +303,30 @@ fn draw_single_fill_to_surface( ); } (_, Type::Rect(_) | Type::Frame(_)) => { +<<<<<<< Updated upstream render_state.surfaces.draw_rect_to(surface_id, shape, paint); +======= + render_state + .surfaces + .draw_rect_to(surface_id, shape, paint, outset, inset); +>>>>>>> Stashed changes } (_, Type::Circle) => { render_state .surfaces +<<<<<<< Updated upstream .draw_circle_to(surface_id, shape, paint); } (_, Type::Path(_)) | (_, Type::Bool(_)) => { render_state.surfaces.draw_path_to(surface_id, shape, paint); +======= + .draw_circle_to(surface_id, shape, paint, outset, inset); + } + (_, Type::Path(_)) | (_, Type::Bool(_)) => { + render_state + .surfaces + .draw_path_to(surface_id, shape, paint, outset); +>>>>>>> Stashed changes } (_, Type::Group(_)) => { // Groups can have fills but they propagate them to their children diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index 9a0862cbff..a97b70f889 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -106,12 +106,22 @@ fn render_shadow_paint( ) { match &shape.shape_type { Type::Rect(_) | Type::Frame(_) => { +<<<<<<< Updated upstream render_state.surfaces.draw_rect_to(surface_id, shape, paint); +======= + render_state + .surfaces + .draw_rect_to(surface_id, shape, paint, None, None); +>>>>>>> Stashed changes } Type::Circle => { render_state .surfaces +<<<<<<< Updated upstream .draw_circle_to(surface_id, shape, paint); +======= + .draw_circle_to(surface_id, shape, paint, None, None); +>>>>>>> Stashed changes } Type::Path(_) | Type::Bool(_) => { render_state.surfaces.draw_path_to(surface_id, shape, paint); diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index ff61502d7c..506e91b09b 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -526,6 +526,10 @@ pub fn render( strokes: &[&Stroke], surface_id: Option, antialias: bool, +<<<<<<< Updated upstream +======= + outset: Option, +>>>>>>> Stashed changes ) { if strokes.is_empty() { return; @@ -540,6 +544,13 @@ pub fn render( // edges semi-transparent and revealing strokes underneath. if let Some(image_filter) = shape.image_filter(1.) { let mut content_bounds = shape.selrect; +<<<<<<< Updated upstream +======= + // Expand for outset if provided + if let Some(s) = outset.filter(|&s| s > 0.0) { + content_bounds.outset((s, s)); + } +>>>>>>> Stashed changes let max_margin = strokes .iter() .map(|s| s.bounds_width(shape.is_open())) @@ -583,6 +594,10 @@ pub fn render( antialias, true, true, +<<<<<<< Updated upstream +======= + outset, +>>>>>>> Stashed changes ); } @@ -595,12 +610,36 @@ pub fn render( // No blur or filter surface unavailable — draw strokes individually. for stroke in strokes.iter().rev() { +<<<<<<< Updated upstream render_single(render_state, shape, stroke, surface_id, None, antialias); +======= + render_single( + render_state, + shape, + stroke, + surface_id, + None, + antialias, + outset, + ); +>>>>>>> Stashed changes } return; } +<<<<<<< Updated upstream render_merged(render_state, shape, strokes, surface_id, antialias, false); +======= + render_merged( + render_state, + shape, + strokes, + surface_id, + antialias, + false, + outset, + ); +>>>>>>> Stashed changes } fn strokes_share_geometry(strokes: &[&Stroke]) -> bool { @@ -620,6 +659,10 @@ fn render_merged( surface_id: Option, antialias: bool, bypass_filter: bool, +<<<<<<< Updated upstream +======= + outset: Option, +>>>>>>> Stashed changes ) { let representative = *strokes .last() @@ -635,6 +678,13 @@ fn render_merged( if !bypass_filter { if let Some(image_filter) = blur_filter.clone() { let mut content_bounds = shape.selrect; +<<<<<<< Updated upstream +======= + // Expand for outset if provided + if let Some(s) = outset.filter(|&s| s > 0.0) { + content_bounds.outset((s, s)); + } +>>>>>>> Stashed changes let stroke_margin = representative.bounds_width(shape.is_open()); if stroke_margin > 0.0 { content_bounds.inset((-stroke_margin, -stroke_margin)); @@ -660,7 +710,19 @@ fn render_merged( canvas.save_layer(&layer_rec); }); +<<<<<<< Updated upstream render_merged(state, shape, strokes, Some(temp_surface), antialias, true); +======= + render_merged( + state, + shape, + strokes, + Some(temp_surface), + antialias, + true, + outset, + ); +>>>>>>> Stashed changes state.surfaces.apply_mut(temp_surface as u32, |surface| { surface.canvas().restore(); @@ -676,7 +738,20 @@ fn render_merged( // via SrcOver), matching the non-merged path where strokes[0] is drawn last (on top). let fills: Vec = strokes.iter().map(|s| s.fill.clone()).collect(); +<<<<<<< Updated upstream let merged = merge_fills(&fills, shape.selrect); +======= + // Expand selrect if outset is provided + let selrect = if let Some(s) = outset.filter(|&s| s > 0.0) { + let mut r = shape.selrect; + r.outset((s, s)); + r + } else { + shape.selrect + }; + + let merged = merge_fills(&fills, selrect); +>>>>>>> Stashed changes let scale = render_state.get_scale(); let target_surface = surface_id.unwrap_or(SurfaceId::Strokes); let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); @@ -747,6 +822,10 @@ pub fn render_single( surface_id: Option, shadow: Option<&ImageFilter>, antialias: bool, +<<<<<<< Updated upstream +======= + outset: Option, +>>>>>>> Stashed changes ) { render_single_internal( render_state, @@ -757,6 +836,10 @@ pub fn render_single( antialias, false, false, +<<<<<<< Updated upstream +======= + outset, +>>>>>>> Stashed changes ); } @@ -770,10 +853,21 @@ fn render_single_internal( antialias: bool, bypass_filter: bool, skip_blur: bool, +<<<<<<< Updated upstream +======= + outset: Option, +>>>>>>> Stashed changes ) { if !bypass_filter { if let Some(image_filter) = shape.image_filter(1.) { let mut content_bounds = shape.selrect; +<<<<<<< Updated upstream +======= + // Expand for outset if provided + if let Some(s) = outset.filter(|&s| s > 0.0) { + content_bounds.outset((s, s)); + } +>>>>>>> Stashed changes let stroke_margin = stroke.bounds_width(shape.is_open()); if stroke_margin > 0.0 { content_bounds.inset((-stroke_margin, -stroke_margin)); @@ -799,6 +893,10 @@ fn render_single_internal( antialias, true, true, +<<<<<<< Updated upstream +======= + outset, +>>>>>>> Stashed changes ); }, ) { @@ -867,7 +965,25 @@ fn render_single_internal( shape_type @ (Type::Path(_) | Type::Bool(_)) => { if let Some(path) = shape_type.path() { let is_open = path.is_open(); +<<<<<<< Updated upstream let paint = stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias); +======= + let mut paint = + stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias); + // Apply outset by increasing stroke width + if let Some(s) = outset.filter(|&s| s > 0.0) { + let current_width = paint.stroke_width(); + // Path stroke kinds are built differently: + // - Center uses the stroke width directly. + // - Inner/Outer use a doubled width plus clipping/clearing logic. + // Compensate outset so visual growth is comparable across kinds. + let outset_growth = match stroke.render_kind(is_open) { + StrokeKind::Center => s * 2.0, + StrokeKind::Inner | StrokeKind::Outer => s * 4.0, + }; + paint.set_stroke_width(current_width + outset_growth); + } +>>>>>>> Stashed changes draw_stroke_on_path( canvas, stroke, diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 86a0f0422e..89c9b7ee29 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -355,9 +355,42 @@ impl Surfaces { )); } +<<<<<<< Updated upstream pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { if let Some(corners) = shape.shape_type.corners() { let rrect = RRect::new_rect_radii(shape.selrect, &corners); +======= + pub fn draw_rect_to( + &mut self, + id: SurfaceId, + shape: &Shape, + paint: &Paint, + outset: Option, + inset: Option, + ) { + let mut rect = if let Some(s) = outset.filter(|&s| s > 0.0) { + let mut r = shape.selrect; + r.outset((s, s)); + r + } else { + shape.selrect + }; + if let Some(eps) = inset.filter(|&e| e > 0.0) { + rect.inset((eps, eps)); + } + if let Some(corners) = shape.shape_type.corners() { + let corners = if let Some(eps) = inset.filter(|&e| e > 0.0) { + let mut c = corners; + for r in c.iter_mut() { + r.x = (r.x - eps).max(0.0); + r.y = (r.y - eps).max(0.0); + } + c + } else { + corners + }; + let rrect = RRect::new_rect_radii(rect, &corners); +>>>>>>> Stashed changes self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint); } else { self.canvas_and_mark_dirty(id) @@ -365,6 +398,7 @@ impl Surfaces { } } +<<<<<<< Updated upstream pub fn draw_circle_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { self.canvas_and_mark_dirty(id) .draw_oval(shape.selrect, paint); @@ -373,6 +407,46 @@ impl Surfaces { pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { if let Some(path) = shape.get_skia_path() { self.canvas_and_mark_dirty(id).draw_path(&path, paint); +======= + pub fn draw_circle_to( + &mut self, + id: SurfaceId, + shape: &Shape, + paint: &Paint, + outset: Option, + inset: Option, + ) { + let mut rect = if let Some(s) = outset.filter(|&s| s > 0.0) { + let mut r = shape.selrect; + r.outset((s, s)); + r + } else { + shape.selrect + }; + if let Some(eps) = inset.filter(|&e| e > 0.0) { + rect.inset((eps, eps)); + } + self.canvas_and_mark_dirty(id).draw_oval(rect, paint); + } + + pub fn draw_path_to( + &mut self, + id: SurfaceId, + shape: &Shape, + paint: &Paint, + outset: Option, + ) { + if let Some(path) = shape.get_skia_path() { + let canvas = self.canvas_and_mark_dirty(id); + if let Some(s) = outset.filter(|&s| s > 0.0) { + // Draw path as a thick stroke to get outset (expanded) silhouette + let mut stroke_paint = paint.clone(); + stroke_paint.set_stroke_width(s * 2.0); + canvas.draw_path(&path, &stroke_paint); + } else { + canvas.draw_path(&path, paint); + } +>>>>>>> Stashed changes } }