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 2a3fef21cb..b359d6cc6c 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 @@ -8,7 +8,7 @@ use graphene_std::Color; use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke}; +use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, GradientUnits, PaintOrder, Stroke}; use graphene_std::vector::{Gradient, VectorModificationType}; #[impl_message(Message, DocumentMessage, GraphOperation)] @@ -28,6 +28,7 @@ pub enum GraphOperationMessage { gradient: Gradient, gradient_form: GradientForm, gradient_settings: GradientSettings, + 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 c3741f2c65..2699914d89 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 @@ -14,7 +14,7 @@ use graph_craft::document::{NodeId, NodeInput}; use graph_craft::list; use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, GradientUnits, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::{Artboard, Color}; #[derive(ExtractField)] @@ -533,6 +533,7 @@ impl MessageHandler> for let gradient_info = SvgGradientInfo { graphite_stops: extract_graphite_gradient_stops(&svg), spaces: extract_gradient_spaces(&svg), + units: extract_gradient_units(&svg), }; // Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`. @@ -575,6 +576,8 @@ struct SvgGradientInfo { graphite_stops: HashMap, /// Gradient spaces, keyed by gradient element `id`, resolved from the `color-interpolation` property. spaces: HashMap, + /// Gradient units, keyed by gradient element `id`, resolved from the `gradientUnits` attribute. + units: HashMap, } /// Pre-parses the raw SVG XML to resolve each gradient's inherited `color-interpolation` property, which usvg's @@ -620,6 +623,59 @@ fn extract_gradient_spaces(svg: &str) -> HashMap { result } +fn extract_gradient_units(svg: &str) -> HashMap { + let mut result = HashMap::new(); + + let doc = match usvg::roxmltree::Document::parse(svg) { + Ok(doc) => doc, + Err(_) => return result, + }; + + // Collect gradient nodes by id for href resolution + let mut gradient_nodes: HashMap<&str, usvg::roxmltree::Node> = HashMap::new(); + for node in doc.descendants() { + if matches!(node.tag_name().name(), "linearGradient" | "radialGradient") { + if let Some(id) = node.attribute("id") { + gradient_nodes.insert(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 { + if let Some(units) = node.attribute("gradientUnits") { + match units { + "userSpaceOnUse" => return Some(GradientUnits::UserSpaceOnUse), + "objectBoundingBox" => return Some(GradientUnits::ObjectBoundingBox), + _ => {} + } + } + if let Some(href) = node.attribute("href").or_else(|| node.attribute("xlink:href")) { + let id = href.trim_start_matches('#'); + if seen.contains(&id) { + return None; + } + seen.push(id); + if let Some(referenced) = gradient_nodes.get(id) { + if let Some(units) = resolve_units(*referenced, gradient_nodes, seen) { + return Some(units); + } + } + } + None + } + + for node in doc.descendants() { + if !matches!(node.tag_name().name(), "linearGradient" | "radialGradient") { + continue; + } + let Some(gradient_id) = node.attribute("id") else { continue }; + let units = resolve_units(node, &gradient_nodes, &mut Vec::new()).unwrap_or(GradientUnits::ObjectBoundingBox); + result.insert(gradient_id.to_string(), units); + } + + result +} + /// The `color-interpolation` in effect for an element: the nearest self-or-ancestor declaration, taking each /// element's own winning declaration per [`declared_color_interpolation`]'s cascade order. fn resolve_color_interpolation(element: usvg::roxmltree::Node, stylesheet: &simplecss::StyleSheet) -> Option { @@ -1033,7 +1089,8 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g space: gradient_info.spaces.get(linear.id()).copied().unwrap_or(GradientSpace::RgbGamma), ..Default::default() }; - modify_inputs.fill_gradient_set(gradient, gradient_form, settings, transform); + let gradient_units = gradient_info.units.get(linear.id()).copied().unwrap_or(GradientUnits::ObjectBoundingBox); + modify_inputs.fill_gradient_set(gradient, gradient_form, settings, gradient_units, transform); } usvg::Paint::RadialGradient(radial) => { let gradient_transform = usvg_transform(radial.transform()); @@ -1061,7 +1118,8 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g space: gradient_info.spaces.get(radial.id()).copied().unwrap_or(GradientSpace::RgbGamma), ..Default::default() }; - modify_inputs.fill_gradient_set(gradient, gradient_form, settings, transform); + let gradient_units = gradient_info.units.get(radial.id()).copied().unwrap_or(GradientUnits::ObjectBoundingBox); + modify_inputs.fill_gradient_set(gradient, gradient_form, settings, 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 3149b1d717..99e5b59a14 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -16,7 +16,7 @@ use graph_craft::{ProtoNodeIdentifier, list}; use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke}; +use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, GradientUnits, PaintOrder, Stroke}; use graphene_std::vector::{Gradient, GradientRamp, Vector, VectorModification, VectorModificationType}; use graphene_std::{Artboard, Color, Graphic}; use kurbo::BezPath; @@ -449,7 +449,7 @@ impl<'a> ModifyInputsContext<'a> { } } - pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) { + pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, gradient_units: GradientUnits, transform: DAffine2) { let existing_fill_node_id = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, false); let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else { return; @@ -490,6 +490,12 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh( InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientFormInput), NodeInput::value(TaggedValue::GradientForm(gradient_form), false), + true, + ); + + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientUnitsInput), + NodeInput::value(TaggedValue::GradientUnits(gradient_units), false), false, ); diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index aec941840e..e72d594962 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -569,6 +569,7 @@ tagged_value! { GradientSpace(vector::style::GradientSpace), GradientHueDirection(vector::style::GradientHueDirection), GradientInterpolation(vector::style::GradientInterpolation), + 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 01b53ca24d..2944c14208 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -26,8 +26,9 @@ 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_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, 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_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE, + ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, 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_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 37acdad0a4..0f68a943eb 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -71,6 +71,8 @@ pub const ATTR_GRADIENT_INTERPOLATION: &str = "gradient_interpolation"; /// Gradient's `bool` (implicit default `false`) for treating the stop list as a cycle, where a wrapped interval /// interpolates from the last stop through the 1|0 boundary back to the first. pub const ATTR_GRADIENT_CYCLIC: &str = "gradient_cyclic"; +/// Gradient's SVG coordinate system (`userSpaceOnUse` or `objectBoundingBox`). +pub const ATTR_GRADIENT_UNITS: &str = "gradient_units"; /// Gradient stop's `f64` position from 0 to 1 along the gradient, on the `List` inside a `Gradient`. /// When the attribute is absent, stops distribute evenly across the 0 to 1 range. pub const ATTR_POSITION: &str = "position"; diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index c0207ff279..af408d849d 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -6,15 +6,24 @@ 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_FORM, ATTR_TRANSFORM, Color}; +use core_types::{ATTR_GRADIENT_FORM, ATTR_GRADIENT_UNITS, ATTR_TRANSFORM, Color}; use glam::{DAffine2, DVec2}; use graphic_types::Graphic; -use graphic_types::vector_types::gradient::GradientForm; +use graphic_types::vector_types::gradient::{GradientForm, GradientUnits}; use graphic_types::vector_types::vector::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use std::fmt::Write; use vector_types::Gradient; use vector_types::gradient::GradientSpread; +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)) +} + #[derive(Copy, Clone, PartialEq)] pub enum PaintTarget { Fill, @@ -90,6 +99,7 @@ fn render_gradient_paint(item: Option>, svg_defs: &mut Str let item = item?; let stops = item.element()?; let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM); + let gradient_units: GradientUnits = item.attribute_cloned_or_default(ATTR_GRADIENT_UNITS); let local_gradient_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); let settings = gradient_settings_from_item(item); @@ -155,15 +165,19 @@ fn render_gradient_paint(item: Option>, svg_defs: &mut Str GradientForm::Linear => { let _ = write!( svg_defs, - r#"{}"#, - gradient_id, stop + r#"{}"#, + gradient_id, + gradient_units.svg_name(), + stop ); } GradientForm::Radial => { let _ = write!( svg_defs, - r#"{}"#, - gradient_id, stop + r#"{}"#, + gradient_id, + gradient_units.svg_name(), + stop ); } } diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index 9ba8099e34..4ceb9bb7b0 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -17,6 +17,25 @@ pub enum GradientForm { 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", + } + } +} + /// A gradient's stops: a list of colors (linear, unassociated alpha) whose optional `position` and `midpoint` /// attributes place each stop along the 0 to 1 range. Stops lacking the `position` attribute distribute evenly, /// and stops lacking the `midpoint` attribute interpolate linearly (`0.5`). diff --git a/node-graph/libraries/vector-types/src/lib.rs b/node-graph/libraries/vector-types/src/lib.rs index a025562eb0..7403344cde 100644 --- a/node-graph/libraries/vector-types/src/lib.rs +++ b/node-graph/libraries/vector-types/src/lib.rs @@ -7,7 +7,7 @@ pub mod vector; // Re-export commonly used types at the crate root pub use core_types as gcore; -pub use gradient::{Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStop}; +pub use gradient::{Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStop, GradientUnits}; pub use math::QuadExt; pub use vector::Vector; pub use vector::reference_point::ReferencePoint;