From dd4a97b09f79ff2db152933c3eaec30adb00e1ef Mon Sep 17 00:00:00 2001 From: James Lindsay <78500760+0HyperCube@users.noreply.github.com> Date: Sun, 15 Sep 2024 23:26:11 +0100 Subject: [PATCH] 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 --- .vscode/settings.json | 1 - .../graph_operation_message_handler.rs | 5 +- .../document/node_graph/node_properties.rs | 53 +++++++++++++++++-- .../tool/tool_messages/ellipse_tool.rs | 7 ++- .../tool/tool_messages/freehand_tool.rs | 2 +- .../tool/tool_messages/gradient_tool.rs | 4 +- .../messages/tool/tool_messages/line_tool.rs | 5 +- .../tool/tool_messages/polygon_tool.rs | 7 ++- .../tool/tool_messages/rectangle_tool.rs | 7 ++- editor/src/node_graph_executor.rs | 2 +- .../reverse-radial-gradient-to-left.svg | 6 +++ .../reverse-radial-gradient-to-right.svg | 6 +++ frontend/assets/icon-16px-solid/reverse.svg | 3 ++ frontend/src/utility-functions/icons.ts | 6 +++ node-graph/gcore/src/graphic_element.rs | 10 +++- .../gcore/src/graphic_element/renderer.rs | 41 ++++++++------ node-graph/gcore/src/vector/style.rs | 29 ++++++---- node-graph/gcore/src/vector/vector_nodes.rs | 1 + .../graph-craft/benches/compile_demo_art.rs | 16 +++--- 19 files changed, 152 insertions(+), 59 deletions(-) create mode 100644 frontend/assets/icon-16px-solid/reverse-radial-gradient-to-left.svg create mode 100644 frontend/assets/icon-16px-solid/reverse-radial-gradient-to-right.svg create mode 100644 frontend/assets/icon-16px-solid/reverse.svg diff --git a/.vscode/settings.json b/.vscode/settings.json index 8f80b278..827bfeca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,7 +33,6 @@ }, // Rust Analyzer config "rust-analyzer.cargo.allTargets": false, - "rust-analyzer.check.command": "clippy", // ESLint config "eslint.format.enable": true, "eslint.workingDirectories": ["./frontend", "./website/other/bezier-rs-demos", "./website"], diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 9523c570..a6d8c1c4 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -266,7 +266,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); apply_usvg_fill(path.fill(), modify_inputs, transform * usvg_transform(node.abs_transform()), bounds_transform); - apply_usvg_stroke(path.stroke(), modify_inputs); + apply_usvg_stroke(path.stroke(), modify_inputs, transform * usvg_transform(node.abs_transform())); } usvg::Node::Image(_image) => { warn!("Skip image") @@ -279,7 +279,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, } } -fn apply_usvg_stroke(stroke: Option<&usvg::Stroke>, modify_inputs: &mut ModifyInputsContext) { +fn apply_usvg_stroke(stroke: Option<&usvg::Stroke>, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) { if let Some(stroke) = stroke { if let usvg::Paint::Color(color) = &stroke.paint() { modify_inputs.stroke_set(Stroke { @@ -299,6 +299,7 @@ fn apply_usvg_stroke(stroke: Option<&usvg::Stroke>, modify_inputs: &mut ModifyIn usvg::LineJoin::Bevel => LineJoin::Bevel, }, line_join_miter_limit: stroke.miterlimit().get() as f64, + transform, }) } else { warn!("Skip non-solid stroke") diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 5779ec18..32315a7c 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2597,7 +2597,28 @@ pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: let fill_type_switch = { let mut row = vec![TextLabel::new("").widget_holder()]; - add_blank_assist(&mut row); + match fill { + Fill::Solid(_) | Fill::None => add_blank_assist(&mut row), + Fill::Gradient(gradient) => { + let reverse_button = IconButton::new("Reverse", 24) + .tooltip("Reverse the gradient color stops") + .on_update(update_value( + { + let gradient = gradient.clone(); + move |_| { + let mut gradient = gradient.clone(); + gradient.stops = gradient.stops.reversed(); + TaggedValue::Fill(Fill::Gradient(gradient)) + } + }, + node_id, + fill_index, + )) + .widget_holder(); + row.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + row.push(reverse_button); + } + } let entries = vec![ RadioEntryData::new("solid") @@ -2619,9 +2640,35 @@ pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: }; widgets.push(fill_type_switch); - if let Fill::Gradient(gradient) = fill { + if let Fill::Gradient(gradient) = fill.clone() { let mut row = vec![TextLabel::new("").widget_holder()]; - add_blank_assist(&mut row); + match gradient.gradient_type { + GradientType::Linear => add_blank_assist(&mut row), + GradientType::Radial => { + let orientation = if (gradient.end.x - gradient.start.x).abs() > f64::EPSILON * 1e6 { + gradient.end.x > gradient.start.x + } else { + (gradient.start.x + gradient.start.y) < (gradient.end.x + gradient.end.y) + }; + let reverse_radial_gradient_button = IconButton::new(if orientation { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24) + .tooltip("Reverse which end the gradient radiates from") + .on_update(update_value( + { + let gradient = gradient.clone(); + move |_| { + let mut gradient = gradient.clone(); + std::mem::swap(&mut gradient.start, &mut gradient.end); + TaggedValue::Fill(Fill::Gradient(gradient)) + } + }, + node_id, + fill_index, + )) + .widget_holder(); + row.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + row.push(reverse_radial_gradient_button); + } + } let new_gradient1 = gradient.clone(); let new_gradient2 = gradient.clone(); diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs index 7331e3f3..690d13f9 100644 --- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs +++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs @@ -206,16 +206,15 @@ impl Fsm for EllipseToolFsmState { let nodes = vec![(NodeId(0), node)]; let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses); - tool_options.fill.apply_fill(layer, responses); - tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); - shape_data.layer = Some(layer); - responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), transform_in: TransformIn::Viewport, skip_rerender: false, }); + tool_options.fill.apply_fill(layer, responses); + tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); + shape_data.layer = Some(layer); EllipseToolFsmState::Drawing } diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index f9abd4d8..5fd2f313 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -233,7 +233,7 @@ impl Fsm for FreehandToolFsmState { tool_options.stroke.apply_stroke(tool_data.weight, layer, responses); tool_data.layer = Some(layer); - let transform = document.metadata().transform_to_viewport(layer); + let transform = document.metadata().transform_to_viewport(parent); let position = transform.inverse().transform_point2(input.mouse.position); extend_path_with_next_segment(tool_data, position, responses); diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index e90f6051..02d04b89 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -86,11 +86,11 @@ impl LayoutHolder for GradientTool { let gradient_type = RadioInput::new(vec![ RadioEntryData::new("linear") .label("Linear") - .tooltip("Linear Gradient") + .tooltip("Linear gradient") .on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Linear)).into()), RadioEntryData::new("radial") .label("Radial") - .tooltip("Radial Gradient") + .tooltip("Radial gradient") .on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Radial)).into()), ]) .selected_index(Some((self.selected_gradient().unwrap_or(self.options.gradient_type) == GradientType::Radial) as u32)) diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs index e1025422..2943193e 100644 --- a/editor/src/messages/tool/tool_messages/line_tool.rs +++ b/editor/src/messages/tool/tool_messages/line_tool.rs @@ -188,15 +188,14 @@ impl Fsm for LineToolFsmState { let nodes = vec![(NodeId(0), node)]; let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses); - tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); - tool_data.layer = Some(layer); - responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), transform_in: TransformIn::Viewport, skip_rerender: false, }); + tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); + tool_data.layer = Some(layer); tool_data.layer = Some(layer); tool_data.weight = tool_options.line_weight; diff --git a/editor/src/messages/tool/tool_messages/polygon_tool.rs b/editor/src/messages/tool/tool_messages/polygon_tool.rs index d3b7c155..96e076c7 100644 --- a/editor/src/messages/tool/tool_messages/polygon_tool.rs +++ b/editor/src/messages/tool/tool_messages/polygon_tool.rs @@ -265,16 +265,15 @@ impl Fsm for PolygonToolFsmState { let nodes = vec![(NodeId(0), node)]; let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(false), responses); - tool_options.fill.apply_fill(layer, responses); - tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); - polygon_data.layer = Some(layer); - responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), transform_in: TransformIn::Viewport, skip_rerender: false, }); + tool_options.fill.apply_fill(layer, responses); + tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); + polygon_data.layer = Some(layer); PolygonToolFsmState::Drawing } diff --git a/editor/src/messages/tool/tool_messages/rectangle_tool.rs b/editor/src/messages/tool/tool_messages/rectangle_tool.rs index 672d88be..7684c6b2 100644 --- a/editor/src/messages/tool/tool_messages/rectangle_tool.rs +++ b/editor/src/messages/tool/tool_messages/rectangle_tool.rs @@ -212,16 +212,15 @@ impl Fsm for RectangleToolFsmState { let nodes = vec![(NodeId(0), node)]; let layer = graph_modification_utils::new_custom(NodeId(generate_uuid()), nodes, document.new_layer_parent(true), responses); - tool_options.fill.apply_fill(layer, responses); - tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); - shape_data.layer = Some(layer); - responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), transform_in: TransformIn::Viewport, skip_rerender: false, }); + tool_options.fill.apply_fill(layer, responses); + tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); + shape_data.layer = Some(layer); RectangleToolFsmState::Drawing } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 028f5817..ed38fce7 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -284,7 +284,7 @@ impl NodeRuntime { for monitor_node_path in &self.monitor_nodes { // The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID - let Some(parent_network_node_id) = monitor_node_path.get(monitor_node_path.len() - 2).copied() else { + let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else { warn!("Monitor node has invalid node id"); continue; diff --git a/frontend/assets/icon-16px-solid/reverse-radial-gradient-to-left.svg b/frontend/assets/icon-16px-solid/reverse-radial-gradient-to-left.svg new file mode 100644 index 00000000..c3a16b09 --- /dev/null +++ b/frontend/assets/icon-16px-solid/reverse-radial-gradient-to-left.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/assets/icon-16px-solid/reverse-radial-gradient-to-right.svg b/frontend/assets/icon-16px-solid/reverse-radial-gradient-to-right.svg new file mode 100644 index 00000000..7fde22c2 --- /dev/null +++ b/frontend/assets/icon-16px-solid/reverse-radial-gradient-to-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/assets/icon-16px-solid/reverse.svg b/frontend/assets/icon-16px-solid/reverse.svg new file mode 100644 index 00000000..1fc2aec0 --- /dev/null +++ b/frontend/assets/icon-16px-solid/reverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index 0a309776..f6d4f156 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -148,6 +148,9 @@ import Regenerate from "@graphite-frontend/assets/icon-16px-solid/regenerate.svg import Reload from "@graphite-frontend/assets/icon-16px-solid/reload.svg"; import Rescale from "@graphite-frontend/assets/icon-16px-solid/rescale.svg"; import Reset from "@graphite-frontend/assets/icon-16px-solid/reset.svg"; +import ReverseRadialGradientToLeft from "@graphite-frontend/assets/icon-16px-solid/reverse-radial-gradient-to-left.svg"; +import ReverseRadialGradientToRight from "@graphite-frontend/assets/icon-16px-solid/reverse-radial-gradient-to-right.svg"; +import Reverse from "@graphite-frontend/assets/icon-16px-solid/reverse.svg"; import Settings from "@graphite-frontend/assets/icon-16px-solid/settings.svg"; import Stack from "@graphite-frontend/assets/icon-16px-solid/stack.svg"; import Trash from "@graphite-frontend/assets/icon-16px-solid/trash.svg"; @@ -223,6 +226,9 @@ const SOLID_16PX = { Reload: { svg: Reload, size: 16 }, Rescale: { svg: Rescale, size: 16 }, Reset: { svg: Reset, size: 16 }, + ReverseRadialGradientToLeft: { svg: ReverseRadialGradientToLeft, size: 16 }, + ReverseRadialGradientToRight: { svg: ReverseRadialGradientToRight, size: 16 }, + Reverse: { svg: Reverse, size: 16 }, Settings: { svg: Settings, size: 16 }, Stack: { svg: Stack, size: 16 }, Trash: { svg: Trash, size: 16 }, diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index 3dd9eb6e..1b32aeaf 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -244,7 +244,15 @@ async fn construct_layer + 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 } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 16f33d8a..d31dd2a8 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -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) { 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 { diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index a1e24b72..fa8c92a7 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -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}") } diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 556499bf..cd106165 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -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 } diff --git a/node-graph/graph-craft/benches/compile_demo_art.rs b/node-graph/graph-craft/benches/compile_demo_art.rs index c51e49d5..8ab08406 100644 --- a/node-graph/graph-craft/benches/compile_demo_art.rs +++ b/node-graph/graph-craft/benches/compile_demo_art.rs @@ -1,9 +1,11 @@ +use graph_craft::document::NodeNetwork; #[cfg(any(feature = "criterion", feature = "iai"))] -use graph_craft::{document::NodeNetwork, graphene_compiler::Compiler, proto::ProtoNetwork}; +use graph_craft::graphene_compiler::Compiler; +#[cfg(any(feature = "criterion", feature = "iai"))] +use graph_craft::proto::ProtoNetwork; #[cfg(feature = "criterion")] use criterion::{black_box, criterion_group, criterion_main, Criterion}; - #[cfg(all(not(feature = "criterion"), feature = "iai"))] use iai_callgrind::{black_box, library_benchmark, library_benchmark_group, main}; @@ -18,7 +20,6 @@ fn compile(network: NodeNetwork) -> ProtoNetwork { let compiler = Compiler {}; compiler.compile_single(network).unwrap() } - #[cfg(all(not(feature = "criterion"), feature = "iai"))] fn load_from_name(name: &str) -> NodeNetwork { let content = std::fs::read(&format!("../../demo-artwork/{name}.graphite")).expect("failed to read file"); @@ -27,7 +28,6 @@ fn load_from_name(name: &str) -> NodeNetwork { black_box(compile(black_box(network))); load_network(content) } - #[cfg(feature = "criterion")] fn compile_to_proto(c: &mut Criterion) { let artworks = glob::glob("../../demo-artwork/*.graphite").expect("failed to read glob pattern"); @@ -42,17 +42,15 @@ fn compile_to_proto(c: &mut Criterion) { #[cfg_attr(all(feature = "iai", not(feature = "criterion")), library_benchmark)] #[cfg_attr(all(feature = "iai", not(feature="criterion")), benches::with_setup(args = ["isometric-fountain", "painted-dreams", "procedural-string-lights", "red-dress", "valley-of-spires"], setup = load_from_name))] -#[cfg(all(not(feature = "criterion"), feature = "iai"))] -pub fn iai_compile_to_proto(input: NodeNetwork) { - black_box(compile(input)); +pub fn iai_compile_to_proto(_input: NodeNetwork) { + #[cfg(all(feature = "iai", not(feature = "criterion")))] + black_box(compile(_input)); } #[cfg(feature = "criterion")] criterion_group!(benches, compile_to_proto); - #[cfg(feature = "criterion")] criterion_main!(benches); - #[cfg(all(not(feature = "criterion"), feature = "iai"))] library_benchmark_group!(name = compile_group; benchmarks = iai_compile_to_proto);