mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-03 21:08:18 +00:00
Correctly apply transforms to vector data and strokes (#1977)
* Fix adding a layer to a transformed group * Fix assorted transform issues * Default stroke transform * Fix bench * Transform gradient * Gradient fix * Add gradient reversal buttons to Fill node in the Properties panel --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
2fa8773092
commit
dd4a97b09f
19 changed files with 152 additions and 59 deletions
|
@ -244,7 +244,15 @@ async fn construct_layer<Data: Into<GraphicElement> + Send>(
|
|||
) -> GraphicGroup {
|
||||
let graphic_element = self.graphic_element.eval(footprint).await;
|
||||
let mut stack = self.stack.eval(footprint).await;
|
||||
stack.push(graphic_element.into());
|
||||
let mut element: GraphicElement = graphic_element.into();
|
||||
if stack.transform.matrix2.determinant() != 0. {
|
||||
*element.transform_mut() = stack.transform.inverse() * element.transform();
|
||||
} else {
|
||||
stack.clear();
|
||||
stack.transform = DAffine2::IDENTITY;
|
||||
}
|
||||
|
||||
stack.push(element);
|
||||
stack
|
||||
}
|
||||
|
||||
|
|
|
@ -360,20 +360,27 @@ impl GraphicElementRendered for GraphicGroup {
|
|||
impl GraphicElementRendered for VectorData {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
let multiplied_transform = render.transform * self.transform;
|
||||
let set_stroke_transform = self.style.stroke().map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
|
||||
let applied_stroke_transform = set_stroke_transform.unwrap_or(self.transform);
|
||||
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
|
||||
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
|
||||
let layer_bounds = self.bounding_box().unwrap_or_default();
|
||||
let transformed_bounds = self.bounding_box_with_transform(multiplied_transform).unwrap_or_default();
|
||||
let transformed_bounds = self.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
|
||||
|
||||
let mut path = String::new();
|
||||
for subpath in self.stroke_bezier_paths() {
|
||||
let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
|
||||
let _ = subpath.subpath_to_svg(&mut path, applied_stroke_transform);
|
||||
}
|
||||
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("d", path);
|
||||
let matrix = format_transform_matrix(element_transform);
|
||||
attributes.push("transform", matrix);
|
||||
|
||||
let defs = &mut attributes.0.svg_defs;
|
||||
let fill_and_stroke = self
|
||||
.style
|
||||
.render(render_params.view_mode, &mut attributes.0.svg_defs, multiplied_transform, layer_bounds, transformed_bounds);
|
||||
.render(render_params.view_mode, defs, element_transform, applied_stroke_transform, layer_bounds, transformed_bounds);
|
||||
attributes.push_val(fill_and_stroke);
|
||||
|
||||
if self.alpha_blending.opacity < 1. {
|
||||
|
@ -411,31 +418,31 @@ impl GraphicElementRendered for VectorData {
|
|||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext) {
|
||||
use crate::vector::style::GradientType;
|
||||
use vello::peniko;
|
||||
|
||||
let transformed_bounds = GraphicElementRendered::bounding_box(self, transform).unwrap_or_default();
|
||||
let mut layer = false;
|
||||
let stroke_transform = self.style.stroke().map_or(DAffine2::IDENTITY, |stroke| stroke.transform);
|
||||
let path_transform = (transform * self.transform) * stroke_transform.inverse();
|
||||
let transformed_bounds = GraphicElementRendered::bounding_box(self, path_transform).unwrap_or_default();
|
||||
|
||||
if self.alpha_blending.opacity < 1. || self.alpha_blending.blend_mode != BlendMode::default() {
|
||||
layer = true;
|
||||
scene.push_layer(
|
||||
peniko::BlendMode::new(self.alpha_blending.blend_mode.into(), peniko::Compose::SrcOver),
|
||||
self.alpha_blending.opacity,
|
||||
kurbo::Affine::IDENTITY,
|
||||
kurbo::Affine::new((path_transform).to_cols_array()),
|
||||
&kurbo::Rect::new(transformed_bounds[0].x, transformed_bounds[0].y, transformed_bounds[1].x, transformed_bounds[1].y),
|
||||
);
|
||||
}
|
||||
|
||||
let kurbo_transform = kurbo::Affine::new(transform.to_cols_array());
|
||||
let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y);
|
||||
let mut path = kurbo::BezPath::new();
|
||||
for subpath in self.stroke_bezier_paths() {
|
||||
subpath.to_vello_path(self.transform, &mut path);
|
||||
subpath.to_vello_path(stroke_transform, &mut path);
|
||||
}
|
||||
|
||||
match self.style.fill() {
|
||||
Fill::Solid(color) => {
|
||||
let fill = peniko::Brush::Solid(peniko::Color::rgba(color.r() as f64, color.g() as f64, color.b() as f64, color.a() as f64));
|
||||
scene.fill(peniko::Fill::NonZero, kurbo_transform, &fill, None, &path);
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(path_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
|
@ -472,7 +479,7 @@ impl GraphicElementRendered for VectorData {
|
|||
stops,
|
||||
..Default::default()
|
||||
});
|
||||
scene.fill(peniko::Fill::NonZero, kurbo_transform, &fill, None, &path);
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(path_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::None => (),
|
||||
};
|
||||
|
@ -504,7 +511,7 @@ impl GraphicElementRendered for VectorData {
|
|||
dash_offset: stroke.dash_offset,
|
||||
};
|
||||
if stroke.width > 0. {
|
||||
scene.stroke(&stroke, kurbo_transform, color, None, &path);
|
||||
scene.stroke(&stroke, kurbo::Affine::new(path_transform.to_cols_array()), color, None, &path);
|
||||
}
|
||||
}
|
||||
if layer {
|
||||
|
@ -596,7 +603,8 @@ impl GraphicElementRendered for Artboard {
|
|||
|
||||
// Render background
|
||||
let color = peniko::Color::rgba(self.background.r() as f64, self.background.g() as f64, self.background.b() as f64, self.background.a() as f64);
|
||||
let rect = kurbo::Rect::new(self.location.x as f64, self.location.y as f64, self.dimensions.x as f64, self.dimensions.y as f64);
|
||||
let [a, b] = [self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()];
|
||||
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
|
||||
let blend_mode = peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcOver);
|
||||
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
|
||||
|
@ -606,7 +614,8 @@ impl GraphicElementRendered for Artboard {
|
|||
if self.clip {
|
||||
scene.push_layer(blend_mode, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
|
||||
}
|
||||
self.graphic_group.render_to_vello(scene, transform, context);
|
||||
let child_transform = transform * DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform;
|
||||
self.graphic_group.render_to_vello(scene, child_transform, context);
|
||||
if self.clip {
|
||||
scene.pop_layer();
|
||||
}
|
||||
|
@ -614,8 +623,10 @@ impl GraphicElementRendered for Artboard {
|
|||
|
||||
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
|
||||
subpath.apply_transform(self.graphic_group.transform.inverse());
|
||||
click_targets.push(ClickTarget::new(subpath, 0.));
|
||||
if self.graphic_group.transform.matrix2.determinant() != 0. {
|
||||
subpath.apply_transform(self.graphic_group.transform.inverse());
|
||||
click_targets.push(ClickTarget::new(subpath, 0.));
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_artboard(&self) -> bool {
|
||||
|
|
|
@ -145,10 +145,9 @@ impl Gradient {
|
|||
}
|
||||
|
||||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
|
||||
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
let updated_transform = multiplied_transform * bound_transform;
|
||||
let transformed_bound_transform = element_transform * DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
|
||||
let mut stop = String::new();
|
||||
for (position, color) in self.stops.0.iter() {
|
||||
|
@ -168,7 +167,7 @@ impl Gradient {
|
|||
} else {
|
||||
DAffine2::IDENTITY // Ignore if the transform cannot be inverted (the bounds are zero). See issue #1944.
|
||||
};
|
||||
let mod_points = updated_transform;
|
||||
let mod_points = element_transform * stroke_transform * bound_transform;
|
||||
|
||||
let start = mod_points.transform_point2(self.start);
|
||||
let end = mod_points.transform_point2(self.end);
|
||||
|
@ -301,7 +300,7 @@ impl Fill {
|
|||
}
|
||||
|
||||
/// Renders the fill, adding necessary defs through mutating the first argument.
|
||||
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
match self {
|
||||
Self::None => r#" fill="none""#.to_string(),
|
||||
Self::Solid(color) => {
|
||||
|
@ -312,7 +311,7 @@ impl Fill {
|
|||
result
|
||||
}
|
||||
Self::Gradient(gradient) => {
|
||||
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
|
||||
let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
format!(r##" fill="url('#{gradient_id}')""##)
|
||||
}
|
||||
}
|
||||
|
@ -450,6 +449,10 @@ impl Display for LineJoin {
|
|||
}
|
||||
}
|
||||
|
||||
fn daffine2_identity() -> DAffine2 {
|
||||
DAffine2::IDENTITY
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct Stroke {
|
||||
|
@ -462,6 +465,8 @@ pub struct Stroke {
|
|||
pub line_cap: LineCap,
|
||||
pub line_join: LineJoin,
|
||||
pub line_join_miter_limit: f64,
|
||||
#[serde(default = "daffine2_identity")]
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for Stroke {
|
||||
|
@ -487,6 +492,7 @@ impl Stroke {
|
|||
line_cap: LineCap::Butt,
|
||||
line_join: LineJoin::Miter,
|
||||
line_join_miter_limit: 4.,
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -499,6 +505,10 @@ impl Stroke {
|
|||
line_cap: if time < 0.5 { self.line_cap } else { other.line_cap },
|
||||
line_join: if time < 0.5 { self.line_join } else { other.line_join },
|
||||
line_join_miter_limit: self.line_join_miter_limit + (other.line_join_miter_limit - self.line_join_miter_limit) * time,
|
||||
transform: DAffine2::from_mat2_translation(
|
||||
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2,
|
||||
self.transform.translation * time + other.transform.translation * (1. - time),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -635,6 +645,7 @@ impl Default for Stroke {
|
|||
line_cap: LineCap::Butt,
|
||||
line_join: LineJoin::Miter,
|
||||
line_join_miter_limit: 4.,
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -787,15 +798,15 @@ impl PathStyle {
|
|||
}
|
||||
|
||||
/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
|
||||
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
match view_mode {
|
||||
ViewMode::Outline => {
|
||||
let fill_attribute = Fill::None.render(svg_defs, multiplied_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();
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
_ => {
|
||||
let fill_attribute = self.fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds);
|
||||
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
let stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render()).unwrap_or_default();
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
|
|
|
@ -156,6 +156,7 @@ fn set_vector_data_stroke(
|
|||
line_cap,
|
||||
line_join,
|
||||
line_join_miter_limit: miter_limit,
|
||||
transform: vector_data.transform,
|
||||
});
|
||||
vector_data
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue