diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 7d986bce1b..8f3b421e06 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -10,7 +10,7 @@ use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke}; +use graphene_std::vector::style::{GradientSpreadMethod, GradientType, GradientUnits, Stroke}; use graphene_std::vector::{GradientStops, PointId, VectorModificationType}; #[impl_message(Message, DocumentMessage, GraphOperation)] @@ -25,6 +25,7 @@ pub enum GraphOperationMessage { gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, + gradient_units: GradientUnits, transform: DAffine2, }, BlendingFillSet { 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 4e64599e79..33fcf5df1e 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 @@ -13,7 +13,7 @@ use graph_craft::document::{NodeId, NodeInput}; use graphene_std::list::List; use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, GradientUnits, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::{Artboard, Color}; #[derive(ExtractField)] @@ -44,10 +44,11 @@ impl MessageHandler> for gradient, gradient_type, spread_method, + gradient_units, transform, } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { - modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform); + modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, gradient_units, transform); } } GraphOperationMessage::BlendingFillSet { layer, fill } => { @@ -462,18 +463,11 @@ impl MessageHandler> for }; placement_transform.translation = placement_transform.translation.round(); - let graphite_gradient_stops = extract_graphite_gradient_stops(&svg); + let imported_gradient_info = extract_svg_gradient_info(&svg); // Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`. // The placement offset is then applied once to the root group layer below. - import_usvg_node( - &mut modify_inputs, - &usvg::Node::Group(Box::new(tree.root().clone())), - id, - parent, - insert_index, - &graphite_gradient_stops, - ); + import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), id, parent, insert_index, &imported_gradient_info); // After import, `layer_node` is set to the root group. Apply the placement transform to it // (skipped automatically when identity, so file-open with content at origin creates no Transform node). @@ -506,33 +500,48 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 { const GRAPHITE_NAMESPACE: &str = "https://graphite.art"; /// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes. -/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops -/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes. -/// Returns a map from gradient element `id` to `GradientStops` containing only the real stops. -fn extract_graphite_gradient_stops(svg: &str) -> HashMap { - let mut result = HashMap::new(); - - // Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing - if !svg.contains("graphite:midpoint") { - return result; - } +struct ImportedGradientInfo { + stops: HashMap, + units: HashMap, +} + +fn extract_svg_gradient_info(svg: &str) -> ImportedGradientInfo { + let mut stops = HashMap::new(); + let mut units = HashMap::new(); let doc = match usvg::roxmltree::Document::parse(svg) { Ok(doc) => doc, - Err(_) => return result, + Err(_) => return ImportedGradientInfo { stops, units }, }; + let mut gradient_nodes = HashMap::new(); for node in doc.descendants() { - match node.tag_name().name() { - "linearGradient" | "radialGradient" => {} - _ => continue, + if matches!(node.tag_name().name(), "linearGradient" | "radialGradient") + && let Some(gradient_id) = node.attribute("id") + { + gradient_nodes.insert(gradient_id, node); + } + } + + fn resolve_units<'a>(node: usvg::roxmltree::Node<'a, 'a>, gradient_nodes: &HashMap<&'a str, usvg::roxmltree::Node<'a, 'a>>, seen: &mut Vec<&'a str>) -> Option { + match node.attribute("gradientUnits") { + Some("userSpaceOnUse") => return Some(GradientUnits::UserSpaceOnUse), + Some("objectBoundingBox") => return Some(GradientUnits::ObjectBoundingBox), + Some(_) => return None, + None => {} } - let gradient_id = match node.attribute("id") { - Some(id) => id.to_string(), - None => continue, - }; + let href = node.attribute("href").or_else(|| node.attribute(("http://www.w3.org/1999/xlink", "href")))?; + let referenced_id = href.strip_prefix('#')?; + if seen.contains(&referenced_id) { + return None; + } + let referenced_node = *gradient_nodes.get(referenced_id)?; + seen.push(referenced_id); + resolve_units(referenced_node, gradient_nodes, seen) + } + for (&gradient_id, &node) in &gradient_nodes { let mut real_stops = Vec::new(); let mut has_any_midpoint = false; @@ -555,11 +564,14 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap } if has_any_midpoint && !real_stops.is_empty() { - result.insert(gradient_id, GradientStops::new(real_stops)); + stops.insert(gradient_id.to_string(), GradientStops::new(real_stops)); } + + let resolved_units = resolve_units(node, &gradient_nodes, &mut vec![gradient_id]).unwrap_or(GradientUnits::ObjectBoundingBox); + units.insert(gradient_id.to_string(), resolved_units); } - result + ImportedGradientInfo { stops, units } } fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option { @@ -579,14 +591,7 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option { /// interact with any existing layers in the parent stack. All descendant layers use a lightweight /// O(n) import path that skips collision detection and instead calculates positions directly from /// the known tree structure. -fn import_usvg_node( - modify_inputs: &mut ModifyInputsContext, - node: &usvg::Node, - id: NodeId, - parent: LayerNodeIdentifier, - insert_index: usize, - graphite_gradient_stops: &HashMap, -) { +fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, imported_gradient_info: &ImportedGradientInfo) { let layer = modify_inputs.create_layer(id); modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); @@ -606,7 +611,7 @@ fn import_usvg_node( modify_inputs.import = true; for child in group.children() { - let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, &mut group_extents_map); + let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, imported_gradient_info, &mut group_extents_map); child_extents_svg_order.push(extent); } @@ -625,7 +630,7 @@ fn import_usvg_node( modify_inputs.network_interface.unload_all_nodes_bounding_box(&[]); } usvg::Node::Path(path) => { - import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops); + import_usvg_path(modify_inputs, node, path, layer, imported_gradient_info); } usvg::Node::Image(_image) => { warn!("Skip image"); @@ -649,7 +654,7 @@ fn import_usvg_node_inner( id: NodeId, parent: LayerNodeIdentifier, insert_index: usize, - graphite_gradient_stops: &HashMap, + imported_gradient_info: &ImportedGradientInfo, group_extents_map: &mut HashMap>, ) -> u32 { let layer = modify_inputs.create_layer(id); @@ -660,7 +665,7 @@ fn import_usvg_node_inner( usvg::Node::Group(group) => { let mut child_extents: Vec = Vec::new(); for child in group.children() { - let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, graphite_gradient_stops, group_extents_map); + let extent = import_usvg_node_inner(modify_inputs, child, NodeId::new(), layer, 0, imported_gradient_info, group_extents_map); child_extents.push(extent); } modify_inputs.layer_node = Some(layer); @@ -675,7 +680,7 @@ fn import_usvg_node_inner( total_extent } usvg::Node::Path(path) => { - import_usvg_path(modify_inputs, node, path, layer, graphite_gradient_stops); + import_usvg_path(modify_inputs, node, path, layer, imported_gradient_info); 0 } usvg::Node::Image(_image) => { @@ -692,7 +697,7 @@ fn import_usvg_node_inner( } /// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer. -fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap) { +fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, imported_gradient_info: &ImportedGradientInfo) { let subpaths = convert_usvg_path(path); // Skip creating a Transform node entirely when the SVG-native transform is identity. @@ -706,7 +711,7 @@ fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, } if let Some(fill) = path.fill() { - apply_usvg_fill(fill, modify_inputs, graphite_gradient_stops); + apply_usvg_fill(fill, modify_inputs, imported_gradient_info); } if let Some(stroke) = path.stroke() { apply_usvg_stroke(stroke, modify_inputs, node_transform); @@ -807,7 +812,7 @@ fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMet } } -fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap) { +fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, imported_gradient_info: &ImportedGradientInfo) { match &fill.paint() { usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))), usvg::Paint::LinearGradient(linear) => { @@ -819,7 +824,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g let gradient_type = GradientType::Linear; - let gradient = match graphite_gradient_stops.get(linear.id()) { + let gradient = match imported_gradient_info.stops.get(linear.id()) { Some(graphite_stops) => graphite_stops.clone(), None => { let stops = linear.stops().iter().map(|stop| GradientStop { @@ -831,7 +836,8 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g } }; let spread_method = convert_spread_method(linear.spread_method()); - modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform); + let gradient_units = imported_gradient_info.units.get(linear.id()).copied().unwrap_or(GradientUnits::ObjectBoundingBox); + modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, gradient_units, transform); } usvg::Paint::RadialGradient(radial) => { let gradient_transform = usvg_transform(radial.transform()); @@ -843,7 +849,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g let gradient_type = GradientType::Radial; - let gradient = match graphite_gradient_stops.get(radial.id()) { + let gradient = match imported_gradient_info.stops.get(radial.id()) { Some(graphite_stops) => graphite_stops.clone(), None => { let stops = radial.stops().iter().map(|stop| GradientStop { @@ -855,8 +861,9 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g } }; let spread_method = convert_spread_method(radial.spread_method()); + let gradient_units = imported_gradient_info.units.get(radial.id()).copied().unwrap_or(GradientUnits::ObjectBoundingBox); - modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform); + modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, gradient_units, transform); } usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"), }; diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 4920f0aeb5..626103b930 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -15,7 +15,7 @@ use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke}; +use graphene_std::vector::style::{GradientSpreadMethod, GradientType, GradientUnits, Stroke}; use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType}; use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration}; @@ -464,7 +464,7 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false); } - pub fn fill_gradient_set(&mut self, gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: DAffine2) { + pub fn fill_gradient_set(&mut self, gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, gradient_units: GradientUnits, transform: DAffine2) { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { return; }; @@ -504,6 +504,12 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh( InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX), NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false), + true, + ); + + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientUnitsInput::INDEX), + NodeInput::value(TaggedValue::GradientUnits(gradient_units), false), false, ); } 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 36eb417ca3..99b5ca34b4 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -34,7 +34,7 @@ use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform}; use graphene_std::vector::misc::BooleanOperation; use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType}; use graphene_std::vector::style::{ - FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation, + FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, GradientUnits, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation, }; use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification}; @@ -2445,6 +2445,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, + gradient_units: GradientUnits, transform: DAffine2, /// Whether the transform input holds a plain value (so the "Reverse Direction" button may write to it) rather than a wire. transform_is_value: bool, @@ -2488,6 +2489,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte gradient: gradient.stops, gradient_type: gradient.gradient_type, spread_method: gradient.spread_method, + gradient_units: gradient.gradient_units, transform: gradient.transform, transform_is_value: gradient.transform_is_value, } @@ -2624,6 +2626,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte if let ResolvedFill::Gradient { gradient_type, spread_method, + gradient_units, transform, transform_is_value, .. @@ -2650,6 +2653,31 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte widgets.push(LayoutGroup::row(row)); + // Units radio: blank assist + let mut units_row = vec![TextLabel::new("").widget_instance()]; + add_blank_assist(&mut units_row); + + let units_entries = [GradientUnits::UserSpaceOnUse, GradientUnits::ObjectBoundingBox] + .iter() + .map(|&units| { + let label = match units { + GradientUnits::UserSpaceOnUse => "User Space", + GradientUnits::ObjectBoundingBox => "Bounding Box", + }; + RadioEntryData::new(format!("{:?}", units)) + .label(label) + .on_update(update_value(move |_| TaggedValue::GradientUnits(units), node_id, GradientUnitsInput::INDEX)) + .on_commit(commit_value) + }) + .collect(); + + units_row.extend_from_slice(&[ + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + RadioInput::new(units_entries).selected_index(Some(gradient_units as u32)).widget_instance(), + ]); + + widgets.push(LayoutGroup::row(units_row)); + // "Reverse Direction" button (assist) plus the Pad/Reflect/Repeat radio. Icon orientation is resolved in viewport // space so canvas tilt and layer transforms behave the same as in the Gradient tool's control bar. let mut spread_methods_row = vec![TextLabel::new("").widget_instance()]; diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index c68bc5ac56..cfba9e2a9a 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -15,7 +15,7 @@ use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box}; -use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, PointId, SegmentId, VectorModificationType}; +use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, GradientUnits, PointId, SegmentId, VectorModificationType}; use graphene_std::{Color, Graphic}; use std::collections::VecDeque; @@ -629,6 +629,7 @@ pub struct FillNodeGradient { pub stops: GradientStops, pub gradient_type: GradientType, pub spread_method: GradientSpreadMethod, + pub gradient_units: GradientUnits, pub transform: DAffine2, /// Whether the transform input holds a plain value (so it may be written to) rather than a wire. pub transform_is_value: bool, @@ -649,6 +650,10 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn Some(&TaggedValue::GradientSpreadMethod(value)) => value, _ => GradientSpreadMethod::default(), }; + let gradient_units = match fill_node.inputs.get(fill::GradientUnitsInput::INDEX).and_then(|input| input.as_value()) { + Some(&TaggedValue::GradientUnits(value)) => value, + _ => GradientUnits::default(), + }; let transform_input = fill_node.inputs.get(fill::TransformInput::INDEX).and_then(|input| input.as_value()); let transform = match transform_input { Some(&TaggedValue::OptionalDAffine2(value)) => value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(bounding_box())), @@ -659,6 +664,7 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn stops: stops.clone(), gradient_type, spread_method, + gradient_units, transform, transform_is_value: transform_input.is_some(), }) @@ -799,6 +805,10 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document Some(TaggedValue::GradientSpreadMethod(value)) => *value, _ => GradientSpreadMethod::default(), }; + let gradient_units = match read(graphene_std::vector::fill::GradientUnitsInput::INDEX) { + Some(TaggedValue::GradientUnits(value)) => *value, + _ => GradientUnits::default(), + }; let transform = match read(graphene_std::vector::fill::TransformInput::INDEX) { Some(TaggedValue::OptionalDAffine2(value)) => { value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(document.network_interface.document_metadata().nonzero_bounding_box(layer))) @@ -811,6 +821,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document gradient: stops.clone(), gradient_type, spread_method, + gradient_units, transform, }); } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index cd3d6672f0..9ee96cd0bf 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -16,7 +16,7 @@ use glam::DMat2; use graph_craft::document::value::TaggedValue; use graphene_std::color::SRGBA8; use graphene_std::raster::color::Color; -use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStop, GradientStops, GradientStopsUI, GradientType, build_transform_with_y_preservation}; +use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStop, GradientStops, GradientStopsUI, GradientType, GradientUnits, build_transform_with_y_preservation}; #[derive(Default, ExtractField)] pub struct GradientTool { @@ -401,6 +401,7 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI GradientAppearance { gradient_type: gradient.gradient_type, spread_method: gradient.spread_method, + gradient_units: gradient.gradient_units, transform: gradient.transform, }, GradientSource::Direct, @@ -420,6 +421,7 @@ struct GradientAppearance { transform: DAffine2, gradient_type: GradientType, spread_method: GradientSpreadMethod, + gradient_units: GradientUnits, } /// Resolve the gradient transform, type, and spread method by walking the chain feeding the layer. Transform composes all @@ -468,6 +470,7 @@ fn read_gradient_chain_state(layer: LayerNodeIdentifier, network_interface: &Nod transform: composed_transform, gradient_type: gradient_type.unwrap_or_default(), spread_method: spread_method.unwrap_or_default(), + gradient_units: GradientUnits::default(), } } @@ -804,6 +807,7 @@ impl SelectedGradient { gradient: self.gradient.clone(), gradient_type: self.appearance.gradient_type, spread_method: self.appearance.spread_method, + gradient_units: self.appearance.gradient_units, transform: self.appearance.transform, }); } @@ -1512,6 +1516,7 @@ impl Fsm for GradientToolFsmState { transform: DAffine2::IDENTITY, gradient_type: tool_options.gradient_type, spread_method: tool_options.spread_method, + gradient_units: GradientUnits::default(), }, GradientSource::Direct, ), @@ -1865,6 +1870,7 @@ fn apply_gradient_update( gradient, gradient_type: appearance.gradient_type, spread_method: appearance.spread_method, + gradient_units: appearance.gradient_units, transform: appearance.transform, }); } @@ -1911,6 +1917,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa gradient: new_gradient.clone(), gradient_type: appearance.gradient_type, spread_method: appearance.spread_method, + gradient_units: appearance.gradient_units, transform: appearance.transform, }); updated_any_layer = true; diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index c3af90cf23..4b13cc99fc 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -446,6 +446,7 @@ tagged_value! { PaintOrder(vector::style::PaintOrder), GradientType(vector::style::GradientType), GradientSpreadMethod(vector::style::GradientSpreadMethod), + GradientUnits(vector::style::GradientUnits), ReferencePoint(vector::ReferencePoint), CentroidType(vector::misc::CentroidType), BooleanOperation(vector::misc::BooleanOperation), diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 878e7f5360..9378e20d91 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -25,8 +25,8 @@ pub use graphene_hash; pub use graphene_hash::CacheHash; pub use list::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END, - ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, - ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE, + ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_GRADIENT_UNITS, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, + ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE, }; pub use memo::MemoHash; pub use no_std_types::AsU32; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index b0d5888115..49a81b1e6d 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -58,6 +58,8 @@ pub const ATTR_CLIP: &str = "clip"; pub const ATTR_SPREAD_METHOD: &str = "spread_method"; /// Gradient's `GradientType` (`Linear` or `Radial`). pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; +/// Gradient's SVG coordinate system (`userSpaceOnUse` or `objectBoundingBox`). +pub const ATTR_GRADIENT_UNITS: &str = "gradient_units"; /// Vector graphics object's filled area paint, of type List where T is any graphic type. pub const ATTR_FILL: &str = "fill"; /// Vector graphics object's stroke paint, of type List where T is any graphic type. diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index f25bb069ff..93dae17c39 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -3,10 +3,10 @@ use crate::{Render, RenderSvgSegmentList, SvgRender}; use core_types::color::SRGBA8; use core_types::list::List; use core_types::uuid::generate_uuid; -use core_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color}; +use core_types::{ATTR_GRADIENT_TYPE, ATTR_GRADIENT_UNITS, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color}; use glam::{DAffine2, DVec2}; use graphic_types::Graphic; -use graphic_types::vector_types::gradient::GradientType; +use graphic_types::vector_types::gradient::{GradientType, GradientUnits}; use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use std::fmt::Write; use vector_types::GradientStops; @@ -18,6 +18,16 @@ pub enum PaintTarget { Stroke, } +fn svg_gradient_transform(transform: DAffine2, bounds: DAffine2, units: GradientUnits) -> (GradientUnits, String) { + let (units, transform) = match units { + GradientUnits::UserSpaceOnUse => (GradientUnits::UserSpaceOnUse, transform), + GradientUnits::ObjectBoundingBox if transform_is_invertible(bounds) => (GradientUnits::ObjectBoundingBox, bounds.inverse() * transform), + GradientUnits::ObjectBoundingBox => (GradientUnits::UserSpaceOnUse, transform), + }; + + (units, format_transform_matrix(transform)) +} + impl PaintTarget { fn paint_attr(self) -> &'static str { match self { @@ -86,7 +96,7 @@ impl RenderExt for List { item_transform: DAffine2, element_transform: DAffine2, _stroke_transform: DAffine2, - _bounds: DAffine2, + bounds: DAffine2, _render_params: &RenderParams, _target: PaintTarget, ) -> Self::Output { @@ -94,6 +104,7 @@ impl RenderExt for List { let Some(stops) = self.element(0) else { return 0 }; let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0); + let gradient_units: GradientUnits = self.attribute_cloned_or_default(ATTR_GRADIENT_UNITS, 0); let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0); @@ -122,7 +133,7 @@ impl RenderExt for List { let document_transform = item_transform * local_gradient_transform; let placement = gradient_placement(document_transform, gradient_type); - let gradient_transform = format_transform_matrix(element_transform_inverse * placement); + let (gradient_units, gradient_transform) = svg_gradient_transform(element_transform_inverse * placement, bounds, gradient_units); let gradient_transform = if gradient_transform.is_empty() { String::new() } else { @@ -141,15 +152,19 @@ impl RenderExt for List { GradientType::Linear => { let _ = write!( svg_defs, - r#"{}"#, - gradient_id, stop + r#"{}"#, + gradient_id, + gradient_units.svg_name(), + stop ); } GradientType::Radial => { let _ = write!( svg_defs, - r#"{}"#, - gradient_id, stop + r#"{}"#, + gradient_id, + gradient_units.svg_name(), + stop ); } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6b23eb940a..ba73e7e47f 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -14,8 +14,8 @@ use core_types::transform::Footprint; use core_types::uuid::{NodeId, generate_uuid}; use core_types::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT, - ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, - ATTR_TEXT_ALIGN, ATTR_TRANSFORM, + ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_GRADIENT_UNITS, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, + ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, }; use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; @@ -39,7 +39,7 @@ use std::fmt::Write; use std::hash::Hash; use std::ops::Deref; use std::sync::{Arc, LazyLock}; -use vector_types::gradient::GradientSpreadMethod; +use vector_types::gradient::{GradientSpreadMethod, GradientUnits}; use vello::*; #[derive(Clone, Copy, Debug, PartialEq)] @@ -2062,6 +2062,11 @@ impl Render for List { for index in 0..self.len() { let Some(gradient) = self.element(index) else { continue }; let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let mut gradient_units: GradientUnits = self.attribute_cloned_or_default(ATTR_GRADIENT_UNITS, index); + if gradient_units == GradientUnits::ObjectBoundingBox { + // Standalone gradient path has no shape bbox, so objectBoundingBox falls back to userSpaceOnUse + gradient_units = GradientUnits::UserSpaceOnUse; + } let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); @@ -2115,13 +2120,15 @@ impl Render for List { GradientType::Linear => { let _ = write!( &mut attributes.0.svg_defs, - r#"{stop_string}"# + r#"{stop_string}"#, + gradient_units.svg_name() ); } GradientType::Radial => { let _ = write!( &mut attributes.0.svg_defs, - r#"{stop_string}"# + r#"{stop_string}"#, + gradient_units.svg_name() ); } } diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index 00dc7ea9d6..2107cd14c5 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -14,6 +14,25 @@ pub enum GradientType { Radial, } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[widget(Radio)] +pub enum GradientUnits { + #[default] + UserSpaceOnUse, + ObjectBoundingBox, +} + +impl GradientUnits { + pub fn svg_name(self) -> &'static str { + match self { + GradientUnits::UserSpaceOnUse => "userSpaceOnUse", + GradientUnits::ObjectBoundingBox => "objectBoundingBox", + } + } +} + // TODO: Someday we could switch this to a Box[T] to avoid over-allocation /// A list of colors (linear, unassociated alpha) associated with positions (in the range 0 to 1) along a gradient. /// diff --git a/node-graph/libraries/vector-types/src/lib.rs b/node-graph/libraries/vector-types/src/lib.rs index 6ff415e567..8ad13f836e 100644 --- a/node-graph/libraries/vector-types/src/lib.rs +++ b/node-graph/libraries/vector-types/src/lib.rs @@ -8,7 +8,7 @@ pub mod vector; // Re-export commonly used types at the crate root pub use core_types as gcore; -pub use gradient::{GradientSpreadMethod, GradientStop, GradientStops, GradientType}; +pub use gradient::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, GradientUnits}; pub use math::{QuadExt, RectExt}; pub use subpath::Subpath; pub use vector::Vector; diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 3b08ac33d1..c30ce6b84d 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -8,8 +8,8 @@ use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLe use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; use core_types::{ - ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, CloneVarArgs, - Color, Context, Ctx, ExtractAll, OwnedContextImpl, + ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_GRADIENT_UNITS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, + ATTR_TRANSFORM, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl, }; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; @@ -33,7 +33,7 @@ use vector_types::vector::misc::{ }; use vector_types::vector::style::{GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; -use vector_types::{GradientSpreadMethod, GradientType}; +use vector_types::{GradientSpreadMethod, GradientType, GradientUnits}; /// Implemented for types that contain vector items reachable via mutable access. /// Used for the fill and stroke nodes so they can apply to either `List` or `List`. @@ -175,6 +175,7 @@ async fn fill, _gradient_type: GradientType, _spread_method: GradientSpreadMethod, + _gradient_units: GradientUnits, _transform: Option, ) -> V { if let Some(gradient) = (&mut fill as &mut dyn std::any::Any).downcast_mut::>() { @@ -190,6 +191,12 @@ async fn fill(ATTR_GRADIENT_UNITS).is_none() { + for value in gradient.iter_attribute_values_mut_or_default::(ATTR_GRADIENT_UNITS) { + *value = _gradient_units; + } + } + if gradient.iter_attribute_values::(ATTR_TRANSFORM).is_none() { let transform = _transform.unwrap_or_else(|| { // Construct a transform that covers the bounding box of the paint target