Add Vello support for Outline view mode rendering; add non_scaling to strokes (SVG, not yet Vello) (#2455)

* fix noise pattern parameter issue

* removed the commented out line

* Fix outline mode stroke width not consistent

* add non scaling stroke option

* Fix backward compatibility

* Clean Debug Message

* clean code

* clean code 2

* Add vello outline support

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Ellen Gu 2025-04-06 20:02:11 -04:00 committed by GitHub
parent 3c425d9a71
commit 32aee1ebf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 185 additions and 121 deletions

View File

@ -386,6 +386,7 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
}, },
line_join_miter_limit: stroke.miterlimit().get() as f64, line_join_miter_limit: stroke.miterlimit().get() as f64,
transform, transform,
non_scaling: false,
}) })
} }
} }

View File

@ -2,7 +2,7 @@ use crate::raster::Color;
// RENDERING // RENDERING
pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK; pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 1.; pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5;
// Fonts // Fonts
pub const DEFAULT_FONT_FAMILY: &str = "Cabin"; pub const DEFAULT_FONT_FAMILY: &str = "Cabin";

View File

@ -1,6 +1,7 @@
mod quad; mod quad;
mod rect; mod rect;
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::raster::image::ImageFrameTable; use crate::raster::image::ImageFrameTable;
use crate::raster::{BlendMode, Image}; use crate::raster::{BlendMode, Image};
use crate::transform::{Footprint, Transform}; use crate::transform::{Footprint, Transform};
@ -274,8 +275,7 @@ pub trait GraphicElementRendered {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams); fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams);
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _render_context: &mut RenderContext) {} fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams);
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>; fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>;
// The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection // The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection
@ -325,18 +325,21 @@ impl GraphicElementRendered for GraphicGroupTable {
} }
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for instance in self.instances() { for instance in self.instances() {
let transform = transform * *instance.transform; let transform = transform * *instance.transform;
let alpha_blending = *instance.alpha_blending; let alpha_blending = *instance.alpha_blending;
let blending = vello::peniko::BlendMode::new(alpha_blending.blend_mode.into(), vello::peniko::Compose::SrcOver);
let mut layer = false; let mut layer = false;
if let Some(bounds) = self.instances().filter_map(|element| element.instance.bounding_box(transform)).reduce(Quad::combine_bounds) {
let blend_mode = match render_params.view_mode {
ViewMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.into(),
};
if alpha_blending.opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { if alpha_blending.opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
if let Some(bounds) = self.instances().filter_map(|element| element.instance.bounding_box(transform)).reduce(Quad::combine_bounds) {
scene.push_layer( scene.push_layer(
blending, peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
alpha_blending.opacity, alpha_blending.opacity,
kurbo::Affine::IDENTITY, kurbo::Affine::IDENTITY,
&vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y), &vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y),
@ -345,7 +348,7 @@ impl GraphicElementRendered for GraphicGroupTable {
} }
} }
instance.instance.render_to_vello(scene, transform, context); instance.instance.render_to_vello(scene, transform, context, render_params);
if layer { if layer {
scene.pop_layer(); scene.pop_layer();
@ -461,122 +464,149 @@ impl GraphicElementRendered for VectorDataTable {
} }
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) {
use crate::vector::style::GradientType; use crate::vector::style::{GradientType, LineCap, LineJoin};
use vello::kurbo::{Cap, Join};
use vello::peniko; use vello::peniko;
for instance in self.instances() { for instance in self.instances() {
let mut layer = false;
let multiplied_transform = parent_transform * *instance.transform; let multiplied_transform = parent_transform * *instance.transform;
let set_stroke_transform = instance let has_real_stroke = instance.instance.style.stroke().filter(|stroke| stroke.weight() > 0.);
.instance let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
.style
.stroke()
.map(|stroke| stroke.transform)
.filter(|transform| transform.matrix2.determinant() != 0.);
let applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform); let applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform);
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse()); let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY); let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
let layer_bounds = instance.instance.bounding_box().unwrap_or_default(); let layer_bounds = instance.instance.bounding_box().unwrap_or_default();
if instance.alpha_blending.opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() {
layer = true;
scene.push_layer(
peniko::BlendMode::new(instance.alpha_blending.blend_mode.into(), peniko::Compose::SrcOver),
instance.alpha_blending.opacity,
kurbo::Affine::new(multiplied_transform.to_cols_array()),
&kurbo::Rect::new(layer_bounds[0].x, layer_bounds[0].y, layer_bounds[1].x, layer_bounds[1].y),
);
}
let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y); let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y);
let mut path = kurbo::BezPath::new(); let mut path = kurbo::BezPath::new();
for subpath in instance.instance.stroke_bezier_paths() { for subpath in instance.instance.stroke_bezier_paths() {
subpath.to_vello_path(applied_stroke_transform, &mut path); subpath.to_vello_path(applied_stroke_transform, &mut path);
} }
match instance.instance.style.fill() { // If we're using opacity or a blend mode, we need to push a layer
Fill::Solid(color) => { let blend_mode = match render_params.view_mode {
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); ViewMode::Outline => peniko::Mix::Normal,
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path); _ => instance.alpha_blending.blend_mode.into(),
}
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 = instance.instance.nonzero_bounding_box();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let inverse_parent_transform = (parent_transform.matrix2.determinant() != 0.).then(|| parent_transform.inverse()).unwrap_or_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()
});
// 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 = (element_transform.matrix2.determinant() != 0.).then(|| element_transform.inverse()).unwrap_or_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 mut layer = false;
if instance.alpha_blending.opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() {
layer = true;
scene.push_layer(
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
instance.alpha_blending.opacity,
kurbo::Affine::new(multiplied_transform.to_cols_array()),
&kurbo::Rect::new(layer_bounds[0].x, layer_bounds[0].y, layer_bounds[1].x, layer_bounds[1].y),
);
}
if let Some(stroke) = instance.instance.style.stroke() { // Render the path
let color = match stroke.color { match render_params.view_mode {
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]), ViewMode::Outline => {
None => peniko::Color::TRANSPARENT, let outline_stroke = kurbo::Stroke {
}; width: LAYER_OUTLINE_STROKE_WEIGHT,
use crate::vector::style::{LineCap, LineJoin}; miter_limit: 4.,
use vello::kurbo::{Cap, Join}; join: kurbo::Join::Miter,
let cap = match stroke.line_cap { start_cap: kurbo::Cap::Butt,
LineCap::Butt => Cap::Butt, end_cap: kurbo::Cap::Butt,
LineCap::Round => Cap::Round, dash_pattern: Default::default(),
LineCap::Square => Cap::Square, dash_offset: 0.,
}; };
let join = match stroke.line_join { let outline_color = peniko::Color::new([
LineJoin::Miter => Join::Miter, LAYER_OUTLINE_STROKE_COLOR.r(),
LineJoin::Bevel => Join::Bevel, LAYER_OUTLINE_STROKE_COLOR.g(),
LineJoin::Round => Join::Round, LAYER_OUTLINE_STROKE_COLOR.b(),
}; LAYER_OUTLINE_STROKE_COLOR.a(),
let stroke = kurbo::Stroke { ]);
width: stroke.weight,
miter_limit: stroke.line_join_miter_limit, scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path);
join, }
start_cap: cap, _ => {
end_cap: cap, match instance.instance.style.fill() {
dash_pattern: stroke.dash_lengths.into(), Fill::Solid(color) => {
dash_offset: stroke.dash_offset, 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);
if stroke.width > 0. { }
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, 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 = instance.instance.nonzero_bounding_box();
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let inverse_parent_transform = (parent_transform.matrix2.determinant() != 0.).then(|| parent_transform.inverse()).unwrap_or_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()
});
// 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 = (element_transform.matrix2.determinant() != 0.).then(|| element_transform.inverse()).unwrap_or_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 => {}
};
if let Some(stroke) = instance.instance.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.line_cap {
LineCap::Butt => Cap::Butt,
LineCap::Round => Cap::Round,
LineCap::Square => Cap::Square,
};
let join = match stroke.line_join {
LineJoin::Miter => Join::Miter,
LineJoin::Bevel => Join::Bevel,
LineJoin::Round => Join::Round,
};
let stroke = kurbo::Stroke {
width: stroke.weight,
miter_limit: stroke.line_join_miter_limit,
join,
start_cap: cap,
end_cap: cap,
dash_pattern: stroke.dash_lengths.into(),
dash_offset: stroke.dash_offset,
};
// 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);
}
}
} }
} }
// If we pushed a layer for opacity or a blend mode, we need to pop it
if layer { if layer {
scene.pop_layer(); scene.pop_layer();
} }
@ -707,7 +737,7 @@ impl GraphicElementRendered for Artboard {
} }
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
use vello::peniko; use vello::peniko;
// Render background // Render background
@ -725,7 +755,7 @@ impl GraphicElementRendered for Artboard {
} }
// Since the graphic group's transform is right multiplied in when rendering the graphic group, we just need to right multiply by the offset here. // Since the graphic group's transform is right multiplied in when rendering the graphic group, we just need to right multiply by the offset here.
let child_transform = transform * DAffine2::from_translation(self.location.as_dvec2()); let child_transform = transform * DAffine2::from_translation(self.location.as_dvec2());
self.graphic_group.render_to_vello(scene, child_transform, context); self.graphic_group.render_to_vello(scene, child_transform, context, render_params);
if self.clip { if self.clip {
scene.pop_layer(); scene.pop_layer();
} }
@ -772,9 +802,9 @@ impl GraphicElementRendered for ArtboardGroupTable {
} }
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for instance in self.instances() { for instance in self.instances() {
instance.instance.render_to_vello(scene, transform, context) instance.instance.render_to_vello(scene, transform, context, render_params);
} }
} }
@ -837,7 +867,7 @@ impl GraphicElementRendered for ImageFrameTable<Color> {
} }
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, _render_params: &RenderParams) {
use vello::peniko; use vello::peniko;
for instance in self.instances() { for instance in self.instances() {
@ -887,7 +917,7 @@ impl GraphicElementRendered for RasterFrame {
} }
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams) {
use vello::peniko; use vello::peniko;
let mut render_stuff = |image: vello::peniko::Image, blend_mode: crate::AlphaBlending| { let mut render_stuff = |image: vello::peniko::Image, blend_mode: crate::AlphaBlending| {
@ -964,11 +994,11 @@ impl GraphicElementRendered for GraphicElement {
} }
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
match self { match self {
GraphicElement::VectorData(vector_data) => vector_data.render_to_vello(scene, transform, context), GraphicElement::VectorData(vector_data) => vector_data.render_to_vello(scene, transform, context, render_params),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.render_to_vello(scene, transform, context), GraphicElement::GraphicGroup(graphic_group) => graphic_group.render_to_vello(scene, transform, context, render_params),
GraphicElement::RasterFrame(raster) => raster.render_to_vello(scene, transform, context), GraphicElement::RasterFrame(raster) => raster.render_to_vello(scene, transform, context, render_params),
} }
} }
@ -1051,6 +1081,9 @@ impl<P: Primitive> GraphicElementRendered for P {
fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> {
None None
} }
#[cfg(feature = "vello")]
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {}
} }
impl GraphicElementRendered for Option<Color> { impl GraphicElementRendered for Option<Color> {
@ -1076,6 +1109,9 @@ impl GraphicElementRendered for Option<Color> {
fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> {
None None
} }
#[cfg(feature = "vello")]
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {}
} }
impl GraphicElementRendered for Vec<Color> { impl GraphicElementRendered for Vec<Color> {
@ -1097,6 +1133,9 @@ impl GraphicElementRendered for Vec<Color> {
fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> {
None None
} }
#[cfg(feature = "vello")]
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {}
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]

View File

@ -526,6 +526,8 @@ pub struct Stroke {
pub line_join_miter_limit: f64, pub line_join_miter_limit: f64,
#[serde(default = "daffine2_identity")] #[serde(default = "daffine2_identity")]
pub transform: DAffine2, pub transform: DAffine2,
#[serde(default)]
pub non_scaling: bool,
} }
impl core::hash::Hash for Stroke { impl core::hash::Hash for Stroke {
@ -538,6 +540,7 @@ impl core::hash::Hash for Stroke {
self.line_cap.hash(state); self.line_cap.hash(state);
self.line_join.hash(state); self.line_join.hash(state);
self.line_join_miter_limit.to_bits().hash(state); self.line_join_miter_limit.to_bits().hash(state);
self.non_scaling.hash(state);
} }
} }
@ -563,6 +566,7 @@ impl Stroke {
line_join: LineJoin::Miter, line_join: LineJoin::Miter,
line_join_miter_limit: 4., line_join_miter_limit: 4.,
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
non_scaling: false,
} }
} }
@ -579,6 +583,7 @@ impl Stroke {
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2, time * self.transform.matrix2 + (1. - time) * other.transform.matrix2,
self.transform.translation * time + other.transform.translation * (1. - time), self.transform.translation * time + other.transform.translation * (1. - time),
), ),
non_scaling: if time < 0.5 { self.non_scaling } else { other.non_scaling },
} }
} }
@ -655,7 +660,10 @@ impl Stroke {
if let Some(line_join_miter_limit) = line_join_miter_limit { if let Some(line_join_miter_limit) = line_join_miter_limit {
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit); let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit);
} }
// Add vector-effect attribute to make strokes non-scaling
if self.non_scaling {
let _ = write!(&mut attributes, r#" vector-effect="non-scaling-stroke""#);
}
attributes attributes
} }
@ -702,6 +710,11 @@ impl Stroke {
self.line_join_miter_limit = limit; self.line_join_miter_limit = limit;
self self
} }
pub fn with_non_scaling(mut self, non_scaling: bool) -> Self {
self.non_scaling = non_scaling;
self
}
} }
// Having an alpha of 1 to start with leads to a better experience with the properties panel // Having an alpha of 1 to start with leads to a better experience with the properties panel
@ -716,6 +729,7 @@ impl Default for Stroke {
line_join: LineJoin::Miter, line_join: LineJoin::Miter,
line_join_miter_limit: 4., line_join_miter_limit: 4.,
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
non_scaling: false,
} }
} }
} }
@ -878,7 +892,10 @@ impl PathStyle {
match view_mode { match view_mode {
ViewMode::Outline => { ViewMode::Outline => {
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds); let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
let stroke_attribute = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT).render(); let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT);
// Outline strokes should be non-scaling by default
outline_stroke.non_scaling = true;
let stroke_attribute = outline_stroke.render();
format!("{fill_attribute}{stroke_attribute}") format!("{fill_attribute}{stroke_attribute}")
} }
_ => { _ => {

View File

@ -183,6 +183,7 @@ where
line_join, line_join,
line_join_miter_limit: miter_limit, line_join_miter_limit: miter_limit,
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
non_scaling: false,
}; };
for vector in vector_data.vector_iter_mut() { for vector in vector_data.vector_iter_mut() {
let mut stroke = stroke.clone(); let mut stroke = stroke.clone();

View File

@ -116,7 +116,13 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRendered, editor: &WasmEditorApi, surface_handle: wgpu_executor::WgpuSurface) -> RenderOutputType { async fn render_canvas(
render_config: RenderConfig,
data: impl GraphicElementRendered,
editor: &WasmEditorApi,
surface_handle: wgpu_executor::WgpuSurface,
render_params: RenderParams,
) -> RenderOutputType {
use graphene_core::SurfaceFrame; use graphene_core::SurfaceFrame;
let footprint = render_config.viewport; let footprint = render_config.viewport;
@ -129,7 +135,7 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen
let mut child = Scene::new(); let mut child = Scene::new();
let mut context = wgpu_executor::RenderContext::default(); let mut context = wgpu_executor::RenderContext::default();
data.render_to_vello(&mut child, Default::default(), &mut context); data.render_to_vello(&mut child, Default::default(), &mut context, &render_params);
// TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(n) cost // TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(n) cost
scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array()))); scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array())));
@ -270,7 +276,7 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
if use_vello && editor_api.application_io.as_ref().unwrap().gpu_executor().is_some() { if use_vello && editor_api.application_io.as_ref().unwrap().gpu_executor().is_some() {
#[cfg(all(feature = "vello", target_arch = "wasm32"))] #[cfg(all(feature = "vello", target_arch = "wasm32"))]
return RenderOutput { return RenderOutput {
data: render_canvas(render_config, data, editor_api, surface_handle.unwrap()).await, data: render_canvas(render_config, data, editor_api, surface_handle.unwrap(), render_params).await,
metadata, metadata,
}; };
#[cfg(not(all(feature = "vello", target_arch = "wasm32")))] #[cfg(not(all(feature = "vello", target_arch = "wasm32")))]