From 75725fe86fe8cd5e5ff2b23a38ff7b2335786728 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Sun, 10 May 2026 16:52:00 +0000 Subject: [PATCH 1/3] SVG2 Graident --- Cargo.lock | 1 + Cargo.toml | 1 + editor/Cargo.toml | 1 + .../graph_operation_message_handler.rs | 110 ++++++++++++++---- .../document/graph_operation/utility_types.rs | 14 ++- .../graph_modification_utils.rs | 12 ++ .../tool/tool_messages/gradient_tool.rs | 4 + node-graph/libraries/core-types/src/lib.rs | 2 +- node-graph/libraries/core-types/src/list.rs | 4 + .../libraries/rendering/src/render_ext.rs | 15 ++- .../libraries/rendering/src/renderer.rs | 20 ++-- .../libraries/vector-types/src/gradient.rs | 20 ++++ node-graph/nodes/raster/shaders/build.rs | 12 ++ node-graph/nodes/vector/src/vector_nodes.rs | 18 ++- 14 files changed, 200 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1deb08a70..8974516bb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2357,6 +2357,7 @@ dependencies = [ "serde_bytes", "serde_json", "spin", + "svgtypes", "thiserror 2.0.18", "tokio", "tsify", diff --git a/Cargo.toml b/Cargo.toml index 5685e06aba..1c5d6bfc09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,7 @@ vello = "0.9" vello_encoding = "0.9" resvg = "0.47" usvg = "0.47" +svgtypes = "0.16" parley = { version = "0.9", default-features = false, features = ["std"] } skrifa = "0.42" polycool = "0.4" diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 7db04a35b0..ccca64b457 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -44,6 +44,7 @@ tsify = { workspace = true } dyn-any = { workspace = true } num_enum = { workspace = true } usvg = { workspace = true } +svgtypes = { workspace = true } once_cell = { workspace = true } web-sys = { workspace = true } vello = { workspace = true } 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..c92070b69e 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 @@ -1,3 +1,4 @@ +use std::str::FromStr; use super::transform_utils; use super::utility_types::ModifyInputsContext; use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP}; @@ -512,7 +513,6 @@ const GRAPHITE_NAMESPACE: &str = "https://graphite.art"; 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; } @@ -522,6 +522,8 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap Err(_) => return result, }; + let mut raw_stops: HashMap> = HashMap::new(); + for node in doc.descendants() { match node.tag_name().name() { "linearGradient" | "radialGradient" => {} @@ -533,7 +535,7 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap None => continue, }; - let mut real_stops = Vec::new(); + let mut stops = Vec::new(); let mut has_any_midpoint = false; for child in node.children() { @@ -542,35 +544,95 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap } let midpoint = child.attribute((GRAPHITE_NAMESPACE, "midpoint")).and_then(|v| v.parse::().ok()); - if let Some(midpoint) = midpoint { has_any_midpoint = true; - let offset = child.attribute("offset").and_then(|v| v.parse::().ok()).unwrap_or(0.); + let offset = parse_stop_offset(child.attribute("offset").unwrap_or("0")); let opacity = child.attribute("stop-opacity").and_then(|v| v.parse::().ok()).unwrap_or(1.); - let color = child.attribute("stop-color").and_then(|hex| parse_hex_stop_color(hex, opacity)).unwrap_or(Color::BLACK); + let color = child.attribute("stop-color").and_then(|c| parse_stop_color(c, opacity)).unwrap_or(Color::BLACK); - real_stops.push(GradientStop { position: offset, midpoint, color }); + stops.push(GradientStop { + position: offset, + midpoint, + color, + }); } } - if has_any_midpoint && !real_stops.is_empty() { - result.insert(gradient_id, GradientStops::new(real_stops)); + if has_any_midpoint && !stops.is_empty() { + raw_stops.insert(gradient_id, stops); + } + } + + for node in doc.descendants() { + match node.tag_name().name() { + "linearGradient" | "radialGradient" => {} + _ => continue, + } + + let gradient_id = match node.attribute("id") { + Some(id) => id.to_string(), + None => continue, + }; + + if raw_stops.contains_key(&gradient_id) { + continue; + } + + let href = node.attribute("href").or_else(|| node.attribute(("http://www.w3.org/1999/xlink", "href"))); + if let Some(referenced_id) = href.and_then(|h| h.strip_prefix('#')) { + if let Some(inherited) = raw_stops.get(referenced_id) { + raw_stops.insert(gradient_id, inherited.clone()); + } } } + for (id, stops) in raw_stops { + result.insert(id, GradientStops::new(stops)); + } + result } -fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option { - let hex = hex.strip_prefix('#')?; - if hex.len() != 6 { - return None; +fn parse_stop_offset(s: &str) -> f64 { + if let Some(pct) = s.strip_suffix('%') { + pct.trim().parse::().unwrap_or(0.) / 100. + } else { + s.trim().parse::().unwrap_or(0.) + } +} + +fn parse_stop_color(value: &str, opacity: f32) -> Option { + let value = value.trim(); + if let Some(hex) = value.strip_prefix('#') { + return parse_hex_color(hex, opacity); + } + named_css_color(value).map(|(r, g, b, alpha)| Color::from_rgbaf32_unchecked(r as f32 / 255., g as f32 / 255., b as f32 / 255., alpha.unwrap_or(opacity))) +} + +fn parse_hex_color(hex: &str, opacity: f32) -> Option { + let (r, g, b) = match hex.len() { + 6 => ( + u8::from_str_radix(&hex[0..2], 16).ok()?, + u8::from_str_radix(&hex[2..4], 16).ok()?, + u8::from_str_radix(&hex[4..6], 16).ok()?, + ), + 3 => { + let r = u8::from_str_radix(&hex[0..1], 16).ok()?; + let g = u8::from_str_radix(&hex[1..2], 16).ok()?; + let b = u8::from_str_radix(&hex[2..3], 16).ok()?; + (r * 17, g * 17, b * 17) + } + _ => return None, + }; + Some(Color::from_rgbaf32_unchecked(r as f32 / 255., g as f32 / 255., b as f32 / 255., opacity)) +} + +fn named_css_color(name: &str) -> Option<(u8, u8, u8, Option)> { + if name.to_ascii_lowercase() == "transparent" { + return Some((0, 0, 0, Some(0.))); } - let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.; - let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.; - let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.; - Some(Color::from_rgbaf32_unchecked(r, g, b, opacity)) + svgtypes::Color::from_str(name).ok().map(|c| (c.red, c.green, c.blue, None)) } /// Import a usvg node as the root of an SVG import operation. @@ -831,18 +893,24 @@ 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); + modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform, DVec2::ZERO, 0.); } usvg::Paint::RadialGradient(radial) => { let gradient_transform = usvg_transform(radial.transform()); + let center = DVec2::new(radial.cx() as f64, radial.cy() as f64); - let edge = center + DVec2::X * radial.r().get() as f64; + let radius = radial.r().get() as f64; + let focal = DVec2::new(radial.fx() as f64, radial.fy() as f64); + let focal_radius_raw = radial.fr().get() as f64; + + let focal_center = (focal - center) / radius; + let focal_radius = focal_radius_raw / radius; + + let edge = center + DVec2::X * radius; let (start, end) = (gradient_transform.transform_point2(center), gradient_transform.transform_point2(edge)); let direction = end - start; let transform = DAffine2::from_cols(direction, direction.perp(), start); - let gradient_type = GradientType::Radial; - let gradient = match graphite_gradient_stops.get(radial.id()) { Some(graphite_stops) => graphite_stops.clone(), None => { @@ -856,7 +924,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g }; let spread_method = convert_spread_method(radial.spread_method()); - modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform); + modify_inputs.fill_gradient_set(gradient, GradientType::Radial, spread_method, transform, focal_center, focal_radius); } 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..b6bb4919d8 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -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, transform: DAffine2, focal_center: DVec2, focal_radius: f64) { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { return; }; @@ -501,6 +501,18 @@ impl<'a> ModifyInputsContext<'a> { true, ); + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::FocalCenterInput::INDEX), + NodeInput::value(TaggedValue::DVec2(focal_center), false), + true, + ); + + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::FocalRadiusInput::INDEX), + NodeInput::value(TaggedValue::F64(focal_radius), false), + true, + ); + self.set_input_with_refresh( InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX), NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false), 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..d9c0b7dba9 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -632,6 +632,8 @@ pub struct FillNodeGradient { 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, + pub focal_center: DVec2, + pub focal_radius: f64, } /// Decode a Fill node's gradient metadata inputs, resolving an unset transform to the default over `bounding_box`. Returns `None` when the fill input isn't a gradient value. @@ -654,6 +656,14 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn Some(&TaggedValue::OptionalDAffine2(value)) => value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(bounding_box())), _ => DAffine2::IDENTITY, }; + let focal_center = match fill_node.inputs.get(fill::FocalCenterInput::INDEX).and_then(|input| input.as_value()) { + Some(&TaggedValue::DVec2(value)) => value, + _ => DVec2::ZERO, + }; + let focal_radius = match fill_node.inputs.get(fill::FocalRadiusInput::INDEX).and_then(|input| input.as_value()) { + Some(&TaggedValue::F64(value)) => value, + _ => 0., + }; Some(FillNodeGradient { stops: stops.clone(), @@ -661,6 +671,8 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn spread_method, transform, transform_is_value: transform_input.is_some(), + focal_center, + focal_radius, }) } /// Returns the stroke color from a layer's upstream Stroke node. diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index cd3d6672f0..ddc3e1cff3 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -402,6 +402,8 @@ fn resolve_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI gradient_type: gradient.gradient_type, spread_method: gradient.spread_method, transform: gradient.transform, + focal_center: gradient.focal_center, + focal_radius: gradient.focal_radius, }, GradientSource::Direct, )); @@ -420,6 +422,8 @@ struct GradientAppearance { transform: DAffine2, gradient_type: GradientType, spread_method: GradientSpreadMethod, + focal_center: DVec2, + focal_radius: f64, } /// Resolve the gradient transform, type, and spread method by walking the chain feeding the layer. Transform composes all diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 878e7f5360..d2d1cce288 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -26,7 +26,7 @@ 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_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE, ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, }; 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..63325263f9 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -58,6 +58,10 @@ 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"; +/// Radial gradient's `DVec2` focal center relative to the start/center. +pub const ATTR_FOCAL_CENTER: &str = "focal_center"; +/// Radial gradient's `f64` focal radius relative to the radius. +pub const ATTR_FOCAL_RADIUS: &str = "focal_radius"; /// 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..7985d330d0 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -3,7 +3,7 @@ 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_SPREAD_METHOD, ATTR_TRANSFORM, ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, Color}; use glam::{DAffine2, DVec2}; use graphic_types::Graphic; use graphic_types::vector_types::gradient::GradientType; @@ -146,9 +146,20 @@ impl RenderExt for List { ); } GradientType::Radial => { + let focal_center: DVec2 = self.attribute_cloned_or_default(ATTR_FOCAL_CENTER, 0); + let focal_radius: f64 = self.attribute_cloned_or_default(ATTR_FOCAL_RADIUS, 0); + + let mut focal_attrs = String::new(); + if focal_center.x.abs() > 1e-9 || focal_center.y.abs() > 1e-9 { + let _ = write!(focal_attrs, r#" fx="{}" fy="{}""#, focal_center.x, focal_center.y); + } + if focal_radius > 1e-9 { + let _ = write!(focal_attrs, r#" fr="{}""#, focal_radius); + } + let _ = write!( svg_defs, - r#"{}"#, + r#"{}"#, gradient_id, stop ); } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6b23eb940a..e3206d394a 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -15,7 +15,7 @@ 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_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, }; use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; @@ -419,13 +419,17 @@ fn create_peniko_gradient_brush(gradient_list: &List, multiplied_ end: to_point(end), } .into(), - GradientType::Radial => peniko::RadialGradientPosition { - start_center: to_point(start), - start_radius: 0., - end_center: to_point(start), - end_radius: start.distance(end) as f32, - } - .into(), + GradientType::Radial => { + let focal_center: DVec2 = gradient_list.attribute_cloned_or_default(ATTR_FOCAL_CENTER, 0); + let focal_radius: f64 = gradient_list.attribute_cloned_or_default(ATTR_FOCAL_RADIUS, 0); + peniko::RadialGradientPosition { + start_center: to_point(focal_center), + start_radius: focal_radius as f32, + end_center: to_point(start), + end_radius: start.distance(end) as f32, + } + .into() + }, }, extend: match spread_method { GradientSpreadMethod::Pad => peniko::Extend::Pad, diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index 00dc7ea9d6..9edc0c1cd9 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -469,6 +469,26 @@ impl GradientStops { } } +#[repr(C)] +#[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", + } + } +} + #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] diff --git a/node-graph/nodes/raster/shaders/build.rs b/node-graph/nodes/raster/shaders/build.rs index f80677a347..55d254ae70 100644 --- a/node-graph/nodes/raster/shaders/build.rs +++ b/node-graph/nodes/raster/shaders/build.rs @@ -3,6 +3,18 @@ use cargo_gpu_install::spirv_builder::SpirvMetadata; use std::path::PathBuf; pub fn main() -> Result<(), Box> { + // SAFETY: Build scripts are single-threaded. + unsafe { + std::env::remove_var("TARGET"); + std::env::remove_var("CARGO_BUILD_TARGET"); + std::env::remove_var("RUSTFLAGS"); + std::env::remove_var("CARGO_ENCODED_RUSTFLAGS"); + for (key, _) in std::env::vars_os() { + if key.to_string_lossy().starts_with("CARGO_CFG_TARGET_") { + std::env::remove_var(key); + } + } + } env_logger::builder().filter_level(log::LevelFilter::Debug).init(); // Skip building the shaders if they are provided externally diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 3b08ac33d1..988a640805 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -8,7 +8,7 @@ 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, + 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, ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl, }; use glam::{DAffine2, DMat2, DVec2}; @@ -176,6 +176,10 @@ async fn fill, + #[default(DVec2::ZERO)] + _focal_center: DVec2, + #[default(0.)] + _focal_radius: f64, ) -> V { if let Some(gradient) = (&mut fill as &mut dyn std::any::Any).downcast_mut::>() { if gradient.iter_attribute_values::(ATTR_GRADIENT_TYPE).is_none() { @@ -218,6 +222,18 @@ async fn fill(ATTR_FOCAL_CENTER).is_none() { + for value in gradient.iter_attribute_values_mut_or_default::(ATTR_FOCAL_CENTER) { + *value = _focal_center; + } + } + + if gradient.iter_attribute_values::(ATTR_FOCAL_RADIUS).is_none() { + for value in gradient.iter_attribute_values_mut_or_default::(ATTR_FOCAL_RADIUS) { + *value = _focal_radius; + } + } } let fill = fill.into_graphic_list(); From 545dc7fefed377615e0ca7be7937a2a9d892c450 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Sat, 16 May 2026 10:59:24 +0000 Subject: [PATCH 2/3] Cleanup --- .../graph_operation_message_handler.rs | 39 ++++--------------- .../document/graph_operation/utility_types.rs | 26 +++++++------ .../tool/tool_messages/gradient_tool.rs | 2 + 3 files changed, 25 insertions(+), 42 deletions(-) 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 c92070b69e..b2e0c1f727 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 @@ -48,7 +48,7 @@ impl MessageHandler> for 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, transform, None, None); } } GraphOperationMessage::BlendingFillSet { layer, fill } => { @@ -604,35 +604,12 @@ fn parse_stop_offset(s: &str) -> f64 { fn parse_stop_color(value: &str, opacity: f32) -> Option { let value = value.trim(); - if let Some(hex) = value.strip_prefix('#') { - return parse_hex_color(hex, opacity); + if value.to_ascii_lowercase() == "transparent" { + return Some(Color::from_rgbaf32_unchecked(0., 0., 0., 0.)); } - named_css_color(value).map(|(r, g, b, alpha)| Color::from_rgbaf32_unchecked(r as f32 / 255., g as f32 / 255., b as f32 / 255., alpha.unwrap_or(opacity))) -} - -fn parse_hex_color(hex: &str, opacity: f32) -> Option { - let (r, g, b) = match hex.len() { - 6 => ( - u8::from_str_radix(&hex[0..2], 16).ok()?, - u8::from_str_radix(&hex[2..4], 16).ok()?, - u8::from_str_radix(&hex[4..6], 16).ok()?, - ), - 3 => { - let r = u8::from_str_radix(&hex[0..1], 16).ok()?; - let g = u8::from_str_radix(&hex[1..2], 16).ok()?; - let b = u8::from_str_radix(&hex[2..3], 16).ok()?; - (r * 17, g * 17, b * 17) - } - _ => return None, - }; - Some(Color::from_rgbaf32_unchecked(r as f32 / 255., g as f32 / 255., b as f32 / 255., opacity)) -} - -fn named_css_color(name: &str) -> Option<(u8, u8, u8, Option)> { - if name.to_ascii_lowercase() == "transparent" { - return Some((0, 0, 0, Some(0.))); - } - svgtypes::Color::from_str(name).ok().map(|c| (c.red, c.green, c.blue, None)) + svgtypes::Color::from_str(value).ok().map(|c| { + Color::from_rgbaf32_unchecked(c.red as f32 / 255., c.green as f32 / 255., c.blue as f32 / 255., (c.alpha as f32 / 255.) * opacity) + }) } /// Import a usvg node as the root of an SVG import operation. @@ -893,7 +870,7 @@ 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, DVec2::ZERO, 0.); + modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform, Some(DVec2::ZERO), Some(0.)); } usvg::Paint::RadialGradient(radial) => { let gradient_transform = usvg_transform(radial.transform()); @@ -924,7 +901,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g }; let spread_method = convert_spread_method(radial.spread_method()); - modify_inputs.fill_gradient_set(gradient, GradientType::Radial, spread_method, transform, focal_center, focal_radius); + modify_inputs.fill_gradient_set(gradient, GradientType::Radial, spread_method, transform, Some(focal_center), Some(focal_radius)); } 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 b6bb4919d8..e5f05238ef 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -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, focal_center: DVec2, focal_radius: f64) { + pub fn fill_gradient_set(&mut self, gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: DAffine2, focal_center: Option, focal_radius: Option) { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { return; }; @@ -501,17 +501,21 @@ impl<'a> ModifyInputsContext<'a> { true, ); - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::FocalCenterInput::INDEX), - NodeInput::value(TaggedValue::DVec2(focal_center), false), - true, - ); + if let Some(focal_center) = focal_center { + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::FocalCenterInput::INDEX), + NodeInput::value(TaggedValue::DVec2(focal_center), false), + true, + ); + } - self.set_input_with_refresh( - InputConnector::node(fill_node_id, graphene_std::vector::fill::FocalRadiusInput::INDEX), - NodeInput::value(TaggedValue::F64(focal_radius), false), - true, - ); + if let Some(focal_radius) = focal_radius { + self.set_input_with_refresh( + InputConnector::node(fill_node_id, graphene_std::vector::fill::FocalRadiusInput::INDEX), + NodeInput::value(TaggedValue::F64(focal_radius), false), + true, + ); + } self.set_input_with_refresh( InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX), diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index ddc3e1cff3..64f91c9f22 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -472,6 +472,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(), + ..Default::default() } } @@ -1516,6 +1517,7 @@ impl Fsm for GradientToolFsmState { transform: DAffine2::IDENTITY, gradient_type: tool_options.gradient_type, spread_method: tool_options.spread_method, + ..Default::default() }, GradientSource::Direct, ), From 877d05dafb775a9b2c9bc6190b14d212453aea69 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Wed, 15 Jul 2026 22:11:49 +0000 Subject: [PATCH 3/3] format --- .../graph_operation_message_handler.rs | 14 +++++--------- .../document/graph_operation/utility_types.rs | 10 +++++++++- node-graph/libraries/core-types/src/lib.rs | 4 ++-- node-graph/libraries/rendering/src/render_ext.rs | 2 +- node-graph/libraries/rendering/src/renderer.rs | 8 ++++---- node-graph/nodes/vector/src/vector_nodes.rs | 10 ++++------ 6 files changed, 25 insertions(+), 23 deletions(-) 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 b2e0c1f727..bddab059ee 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 @@ -1,4 +1,3 @@ -use std::str::FromStr; use super::transform_utils; use super::utility_types::ModifyInputsContext; use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP}; @@ -16,6 +15,7 @@ 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::{Artboard, Color}; +use std::str::FromStr; #[derive(ExtractField)] pub struct GraphOperationMessageContext<'a> { @@ -551,11 +551,7 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap let opacity = child.attribute("stop-opacity").and_then(|v| v.parse::().ok()).unwrap_or(1.); let color = child.attribute("stop-color").and_then(|c| parse_stop_color(c, opacity)).unwrap_or(Color::BLACK); - stops.push(GradientStop { - position: offset, - midpoint, - color, - }); + stops.push(GradientStop { position: offset, midpoint, color }); } } @@ -607,9 +603,9 @@ fn parse_stop_color(value: &str, opacity: f32) -> Option { if value.to_ascii_lowercase() == "transparent" { return Some(Color::from_rgbaf32_unchecked(0., 0., 0., 0.)); } - svgtypes::Color::from_str(value).ok().map(|c| { - Color::from_rgbaf32_unchecked(c.red as f32 / 255., c.green as f32 / 255., c.blue as f32 / 255., (c.alpha as f32 / 255.) * opacity) - }) + svgtypes::Color::from_str(value) + .ok() + .map(|c| Color::from_rgbaf32_unchecked(c.red as f32 / 255., c.green as f32 / 255., c.blue as f32 / 255., (c.alpha as f32 / 255.) * opacity)) } /// Import a usvg node as the root of an SVG import operation. 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 e5f05238ef..cbd9fbfe99 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -464,7 +464,15 @@ 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, focal_center: Option, focal_radius: Option) { + pub fn fill_gradient_set( + &mut self, + gradient: GradientStops, + gradient_type: GradientType, + spread_method: GradientSpreadMethod, + transform: DAffine2, + focal_center: Option, + focal_radius: Option, + ) { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { return; }; diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index d2d1cce288..32cbfd5426 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_FOCAL_CENTER, ATTR_FOCAL_RADIUS, + ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, 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, }; pub use memo::MemoHash; pub use no_std_types::AsU32; diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index 7985d330d0..bd1ee29a6c 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -3,7 +3,7 @@ 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, ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, Color}; +use core_types::{ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color}; use glam::{DAffine2, DVec2}; use graphic_types::Graphic; use graphic_types::vector_types::gradient::GradientType; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index e3206d394a..3b5ee42991 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -13,9 +13,9 @@ use core_types::render_complexity::RenderComplexity; 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_FOCAL_CENTER, ATTR_FOCAL_RADIUS, + 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_FOCAL_CENTER, ATTR_FOCAL_RADIUS, 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, }; use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; @@ -429,7 +429,7 @@ fn create_peniko_gradient_brush(gradient_list: &List, multiplied_ end_radius: start.distance(end) as f32, } .into() - }, + } }, extend: match spread_method { GradientSpreadMethod::Pad => peniko::Extend::Pad, diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 988a640805..0fd46de471 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, ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, CloneVarArgs, - Color, Context, Ctx, ExtractAll, OwnedContextImpl, + ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, ATTR_GRADIENT_TYPE, 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; @@ -176,10 +176,8 @@ async fn fill, - #[default(DVec2::ZERO)] - _focal_center: DVec2, - #[default(0.)] - _focal_radius: f64, + #[default(DVec2::ZERO)] _focal_center: DVec2, + #[default(0.)] _focal_radius: f64, ) -> V { if let Some(gradient) = (&mut fill as &mut dyn std::any::Any).downcast_mut::>() { if gradient.iter_attribute_values::(ATTR_GRADIENT_TYPE).is_none() {