diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 3ee4040d3..7af2457f6 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -19,7 +19,7 @@ use graphene_core::transform::{Footprint, Transform}; use graphene_core::uuid::{NodeId, generate_uuid}; use graphene_core::vector::Vector; use graphene_core::vector::click_target::{ClickTarget, FreePoint}; -use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode}; +use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode, PaintOrder}; use graphene_core::{Artboard, Graphic}; use kurbo::Affine; use num_traits::Zero; @@ -697,13 +697,15 @@ impl Render for Table { } else { MaskType::Mask }; + let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let can_draw_aligned_stroke = path_is_closed && vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()); let can_use_paint_order = !(row.element.style.fill().is_none() || !row.element.style.fill().is_opaque() || mask_type == MaskType::Clip); let needs_separate_fill = can_draw_aligned_stroke && !can_use_paint_order; + let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); - if needs_separate_fill { + if needs_separate_fill && !wants_stroke_below { render.leaf_tag("path", |attributes| { attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); @@ -742,7 +744,7 @@ impl Render for Table { }); render.leaf_tag("path", |attributes| { - attributes.push("d", path); + attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); if !matrix.is_empty() { attributes.push("transform", matrix); @@ -753,8 +755,8 @@ impl Render for Table { let mut svg = SvgRender::new(); vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform)); let stroke = row.element.style.stroke().unwrap(); - let stroke_px = stroke.weight * max_scale(applied_stroke_transform); - let quad = Quad::from_box(transformed_bounds).inflate(stroke_px); + let weight = stroke.weight * max_scale(applied_stroke_transform); + let quad = Quad::from_box(transformed_bounds).inflate(weight); let (x, y) = quad.top_left().into(); let (width, height) = (quad.bottom_right() - quad.top_left()).into(); @@ -765,7 +767,7 @@ impl Render for Table { MaskType::Clip => write!(defs, r##"{}"##, svg.svg.to_svg_string()).unwrap(), MaskType::Mask => write!( defs, - r##"{}{}"##, + r##"{}{}"##, rect, svg.svg.to_svg_string() ) @@ -799,6 +801,28 @@ impl Render for Table { attributes.push("style", row.alpha_blending.blend_mode.render()); } }); + + // When splitting passes and stroke is below, draw the fill after the stroke. + if needs_separate_fill && wants_stroke_below { + render.leaf_tag("path", |attributes| { + attributes.push("d", path); + let matrix = format_transform_matrix(element_transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + let mut style = row.element.style.clone(); + style.clear_stroke(); + let fill_and_stroke = style.render( + &mut attributes.0.svg_defs, + element_transform, + applied_stroke_transform, + bounds_matrix, + transformed_bounds_matrix, + &render_params, + ); + attributes.push_val(fill_and_stroke); + }); + } } } @@ -839,7 +863,7 @@ impl Render for Table { if opacity < 1. || row.alpha_blending.blend_mode != BlendMode::default() { layer = true; let weight = row.element.style.stroke().unwrap().weight; - let quad = Quad::from_box(layer_bounds).inflate(weight * element_transform.matrix2.determinant()); + let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform)); let layer_bounds = quad.bounding_box(); scene.push_layer( peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver), @@ -852,36 +876,100 @@ impl Render for Table { let can_draw_aligned_stroke = row.element.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && row.element.stroke_bezier_paths().all(|path| path.closed()); - let reorder_part = row.element.style.stroke().is_some_and(|stroke| stroke.align == StrokeAlign::Outside) && !row.element.style.fill().is_none(); - let reorder_for_outside = reorder_part && row.element.style.fill().is_opaque(); - let use_layer = can_draw_aligned_stroke && !reorder_part; - if use_layer { - let mut element = row.element.clone(); - element.style.clear_stroke(); - element.style.set_fill(Fill::solid(Color::BLACK)); + let use_layer = can_draw_aligned_stroke; + let wants_stroke_below = row.element.style.stroke().is_some_and(|s| s.paint_order == graphene_core::vector::style::PaintOrder::StrokeBelow); - let vector_table = Table::new_from_row(TableRow { - element, - alpha_blending: *row.alpha_blending, - transform: *row.transform, - source_node_id: None, - }); + // Closures to avoid duplicated fill/stroke drawing logic + let do_fill = |scene: &mut Scene| { + match row.element.style.fill() { + Fill::Solid(color) => { + let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path); + } + Fill::Gradient(gradient) => { + let mut stops = peniko::ColorStops::new(); + for &(offset, color) in &gradient.stops { + stops.push(peniko::ColorStop { + offset: offset as f32, + color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])), + }); + } - let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); - let weight = row.element.style.stroke().unwrap().weight; - let quad = Quad::from_box(bounds).inflate(weight * element_transform.matrix2.determinant()); - let bounds = quad.bounding_box(); - let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); - let compose = if row.element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) { - peniko::Compose::SrcOut - } else { - peniko::Compose::SrcIn - }; + let bounds = row.element.nonzero_bounding_box(); + let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); - scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect); - vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform)); - scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect); - } + let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. { + parent_transform.inverse() + } else { + Default::default() + }; + let mod_points = inverse_parent_transform * multiplied_transform * bound_transform; + + let start = mod_points.transform_point2(gradient.start); + let end = mod_points.transform_point2(gradient.end); + + let fill = peniko::Brush::Gradient(peniko::Gradient { + kind: match gradient.gradient_type { + GradientType::Linear => peniko::GradientKind::Linear { + start: to_point(start), + end: to_point(end), + }, + GradientType::Radial => { + let radius = start.distance(end); + peniko::GradientKind::Radial { + start_center: to_point(start), + start_radius: 0., + end_center: to_point(start), + end_radius: radius as f32, + } + } + }, + stops, + ..Default::default() + }); + let inverse_element_transform = if element_transform.matrix2.determinant() != 0. { + element_transform.inverse() + } else { + Default::default() + }; + let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array()); + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path); + } + Fill::None => {} + } + }; + + let do_stroke = |scene: &mut Scene, width_scale: f64| { + if let Some(stroke) = row.element.style.stroke() { + let color = match stroke.color { + Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]), + None => peniko::Color::TRANSPARENT, + }; + let cap = match stroke.cap { + StrokeCap::Butt => Cap::Butt, + StrokeCap::Round => Cap::Round, + StrokeCap::Square => Cap::Square, + }; + let join = match stroke.join { + StrokeJoin::Miter => Join::Miter, + StrokeJoin::Bevel => Join::Bevel, + StrokeJoin::Round => Join::Round, + }; + let stroke = kurbo::Stroke { + width: stroke.weight * width_scale, + miter_limit: stroke.join_miter_limit, + join, + start_cap: cap, + end_cap: cap, + dash_pattern: stroke.dash_lengths.into(), + dash_offset: stroke.dash_offset, + }; + + if stroke.width > 0. { + scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path); + } + } + }; // Render the path match render_params.view_mode { @@ -905,120 +993,76 @@ impl Render for Table { scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path); } _ => { - enum Op { - Fill, - Stroke, - } + if use_layer { + let mut element = row.element.clone(); + element.style.clear_stroke(); + element.style.set_fill(Fill::solid(Color::BLACK)); - let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) || reorder_for_outside { - true => [Op::Stroke, Op::Fill], - false => [Op::Fill, Op::Stroke], // Default - }; + let vector_table = Table::new_from_row(TableRow { + element, + alpha_blending: *row.alpha_blending, + transform: *row.transform, + source_node_id: None, + }); - for operation in order { - match operation { - Op::Fill => { - match row.element.style.fill() { - Fill::Solid(color) => { - let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); - scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path); - } - Fill::Gradient(gradient) => { - let mut stops = peniko::ColorStops::new(); - for &(offset, color) in &gradient.stops { - stops.push(peniko::ColorStop { - offset: offset as f32, - color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])), - }); - } - // Compute bounding box of the shape to determine the gradient start and end points - let bounds = row.element.nonzero_bounding_box(); - let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); + let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); + let weight = row.element.style.stroke().unwrap().weight; + let quad = Quad::from_box(bounds).inflate(weight * max_scale(applied_stroke_transform)); + let bounds = quad.bounding_box(); + let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); - let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. { - parent_transform.inverse() - } else { - Default::default() - }; - let mod_points = inverse_parent_transform * multiplied_transform * bound_transform; + let compose = if row.element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) { + peniko::Compose::SrcOut + } else { + peniko::Compose::SrcIn + }; - let start = mod_points.transform_point2(gradient.start); - let end = mod_points.transform_point2(gradient.end); + if wants_stroke_below { + scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect); + vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform)); + scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect); - let fill = peniko::Brush::Gradient(peniko::Gradient { - kind: match gradient.gradient_type { - GradientType::Linear => peniko::GradientKind::Linear { - start: to_point(start), - end: to_point(end), - }, - GradientType::Radial => { - let radius = start.distance(end); - peniko::GradientKind::Radial { - start_center: to_point(start), - start_radius: 0., - end_center: to_point(start), - end_radius: radius as f32, - } - } - }, - stops, - ..Default::default() - }); - // Vello does `element_transform * brush_transform` internally. We don't want element_transform to have any impact so we need to left multiply by the inverse. - // This makes the final internal brush transform equal to `parent_transform`, allowing you to stretch a gradient by transforming the parent folder. - let inverse_element_transform = if element_transform.matrix2.determinant() != 0. { - element_transform.inverse() - } else { - Default::default() - }; - let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array()); - scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path); - } - Fill::None => {} - }; - } - Op::Stroke => { - if let Some(stroke) = row.element.style.stroke() { - let color = match stroke.color { - Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]), - None => peniko::Color::TRANSPARENT, - }; - let cap = match stroke.cap { - StrokeCap::Butt => Cap::Butt, - StrokeCap::Round => Cap::Round, - StrokeCap::Square => Cap::Square, - }; - let join = match stroke.join { - StrokeJoin::Miter => Join::Miter, - StrokeJoin::Bevel => Join::Bevel, - StrokeJoin::Round => Join::Round, - }; - let stroke = kurbo::Stroke { - width: stroke.weight * if can_draw_aligned_stroke { 2. } else { 1. }, - miter_limit: stroke.join_miter_limit, - join, - start_cap: cap, - end_cap: cap, - dash_pattern: stroke.dash_lengths.into(), - dash_offset: stroke.dash_offset, - }; + do_stroke(scene, 2.); - // Draw the stroke if it's visible - if stroke.width > 0. { - scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path); - } - } + scene.pop_layer(); + scene.pop_layer(); + + do_fill(scene); + } else { + // Fill first (unclipped), then stroke (clipped) above + do_fill(scene); + + scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect); + vector_table.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform)); + scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect); + + do_stroke(scene, 2.); + + scene.pop_layer(); + scene.pop_layer(); + } + } else { + // Non-aligned strokes or open paths: default order behavior + enum Op { + Fill, + Stroke, + } + + let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) { + true => [Op::Stroke, Op::Fill], + false => [Op::Fill, Op::Stroke], // Default + }; + + for operation in order { + match operation { + Op::Fill => do_fill(scene), + Op::Stroke => do_stroke(scene, 1.), } } } } } - if use_layer { - scene.pop_layer(); - scene.pop_layer(); - } - // If we pushed a layer for opacity or a blend mode, we need to pop it if layer { scene.pop_layer();