Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ vello = "0.8"
vello_encoding = "0.8"
resvg = "0.47"
usvg = "0.47"
svgtypes = "0.16"
parley = "0.6"
skrifa = "0.40"
polycool = "0.4"
Expand Down
1 change: 1 addition & 0 deletions editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -15,7 +16,7 @@ use graphene_std::renderer::Quad;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, GradientUnits, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};

#[derive(ExtractField)]
pub struct GraphOperationMessageContext<'a> {
Expand Down Expand Up @@ -502,7 +503,6 @@ const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops> {
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;
}
Expand All @@ -512,6 +512,8 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
Err(_) => return result,
};

let mut raw_stops: HashMap<String, Vec<GradientStop>> = HashMap::new();

for node in doc.descendants() {
match node.tag_name().name() {
"linearGradient" | "radialGradient" => {}
Expand All @@ -523,7 +525,7 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
None => continue,
};

let mut real_stops = Vec::new();
let mut stops = Vec::new();
let mut has_any_midpoint = false;

for child in node.children() {
Expand All @@ -532,35 +534,95 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
}

let midpoint = child.attribute((GRAPHITE_NAMESPACE, "midpoint")).and_then(|v| v.parse::<f64>().ok());

if let Some(midpoint) = midpoint {
has_any_midpoint = true;

let offset = child.attribute("offset").and_then(|v| v.parse::<f64>().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::<f32>().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());
}
}
}
Comment thread
jsjgdh marked this conversation as resolved.

for (id, stops) in raw_stops {
result.insert(id, GradientStops::new(stops));
}

result
}

fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
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::<f64>().unwrap_or(0.) / 100.
} else {
s.trim().parse::<f64>().unwrap_or(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))
}

fn parse_stop_color(value: &str, opacity: f32) -> Option<Color> {
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<Color> {
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<f32>)> {
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))
Comment thread
jsjgdh marked this conversation as resolved.
Outdated
}
Comment thread
jsjgdh marked this conversation as resolved.

/// Import a usvg node as the root of an SVG import operation.
Expand Down Expand Up @@ -826,17 +888,27 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
end,
gradient_type,
stops,
focal_center: start,
focal_radius: 0.,
gradient_units: GradientUnits::UserSpaceOnUse,
spread_method,
})
}
usvg::Paint::RadialGradient(radial) => {
let gradient_transform = usvg_transform(radial.transform());
let inv_bounds = bounds_transform.inverse();

let center = DVec2::new(radial.cx() as f64, radial.cy() as f64);
let edge = center + DVec2::X * radial.r().get() as f64;
let (start, end) = (gradient_transform.transform_point2(center), gradient_transform.transform_point2(edge));
let (start, end) = (bounds_transform.inverse().transform_point2(start), bounds_transform.inverse().transform_point2(end));
let start = inv_bounds.transform_point2(gradient_transform.transform_point2(center));
let end = inv_bounds.transform_point2(gradient_transform.transform_point2(edge));

let gradient_type = GradientType::Radial;
let focal = DVec2::new(radial.fx() as f64, radial.fy() as f64);
let focal_center = inv_bounds.transform_point2(gradient_transform.transform_point2(focal));

let focal_edge = focal + DVec2::X * radial.fr().get() as f64;
let focal_edge_transformed = inv_bounds.transform_point2(gradient_transform.transform_point2(focal_edge));
let focal_radius = focal_center.distance(focal_edge_transformed);

let stops = match graphite_gradient_stops.get(radial.id()) {
Some(graphite_stops) => graphite_stops.clone(),
Expand All @@ -854,8 +926,11 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
Fill::Gradient(Gradient {
start,
end,
gradient_type,
gradient_type: GradientType::Radial,
stops,
focal_center,
focal_radius,
gradient_units: GradientUnits::UserSpaceOnUse,
spread_method,
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use graphene_std::text_nodes::StringCapitalization;
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::{Fill, FillChoice, FillType, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, GradientUnits};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};

pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
Expand Down Expand Up @@ -2663,6 +2663,55 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
]);

widgets.push(LayoutGroup::row(spread_methods_row));

// Gradient Units
let mut gradient_units_row = vec![TextLabel::new("").widget_instance()];
add_blank_assist(&mut gradient_units_row);

let gradient_units_entries = [GradientUnits::UserSpaceOnUse, GradientUnits::ObjectBoundingBox]
.iter()
.map(|&gradient_units| {
let gradient_for_input = gradient_for_closure.clone();
let gradient_for_backup = gradient_for_closure.clone();

let set_input_value = update_value(
move |_: &()| {
let mut new_gradient = gradient_for_input.clone();
new_gradient.gradient_units = gradient_units;
TaggedValue::Fill(Fill::Gradient(new_gradient))
},
node_id,
FillInput::<Color>::INDEX,
);

let set_backup_value = update_value(
move |_: &()| {
let mut new_gradient = gradient_for_backup.clone();
new_gradient.gradient_units = gradient_units;
TaggedValue::Gradient(new_gradient)
},
node_id,
BackupGradientInput::INDEX,
);

RadioEntryData::new(format!("{:?}", gradient_units))
.label(format!("{:?}", gradient_units))
.on_update(move |_| Message::Batched {
messages: Box::new([
set_input_value(&()),
set_backup_value(&()),
]),
})
.on_commit(commit_value)
})
.collect();

gradient_units_row.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
RadioInput::new(gradient_units_entries).selected_index(Some(gradient.gradient_units as u32)).widget_instance(),
]);

widgets.push(LayoutGroup::row(gradient_units_row));
}

widgets
Expand Down
3 changes: 3 additions & 0 deletions editor/src/messages/tool/tool_messages/gradient_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
spread_method,
start: transform.transform_point2(DVec2::ZERO),
end: transform.transform_point2(DVec2::X),
focal_center: transform.transform_point2(DVec2::ZERO),
focal_radius: 0.,
gradient_units: Default::default(),
});
}
graph_modification_utils::get_gradient(layer, network_interface)
Expand Down
28 changes: 23 additions & 5 deletions node-graph/libraries/rendering/src/render_ext.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::renderer::{RenderParams, format_transform_matrix};
use core_types::uuid::generate_uuid;
use glam::DAffine2;
use glam::{DAffine2, DVec2};
use graphic_types::vector_types::gradient::{Gradient, GradientType};
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use std::fmt::Write;
use vector_types::gradient::GradientSpreadMethod;
use vector_types::gradient::{GradientSpreadMethod, GradientUnits};

pub trait RenderExt {
type Output;
Expand Down Expand Up @@ -54,21 +54,39 @@ impl RenderExt for Gradient {
format!(r#" spreadMethod="{}""#, self.spread_method.svg_name())
};

let gradient_units = if self.gradient_units == GradientUnits::UserSpaceOnUse {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: gradientUnits emission condition is inverted: UserSpaceOnUse (non-default) is omitted, causing SVG consumers to misinterpret the gradient coordinate space

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/rendering/src/render_ext.rs, line 57:

<comment>`gradientUnits` emission condition is inverted: `UserSpaceOnUse` (non-default) is omitted, causing SVG consumers to misinterpret the gradient coordinate space</comment>

<file context>
@@ -54,21 +54,39 @@ impl RenderExt for Gradient {
 			format!(r#" spreadMethod="{}""#, self.spread_method.svg_name())
 		};
 
+		let gradient_units = if self.gradient_units == GradientUnits::UserSpaceOnUse {
+			String::new()
+		} else {
</file context>

String::new()
} else {
format!(r#" gradientUnits="{}""#, self.gradient_units.svg_name())
};

let gradient_id = generate_uuid();

match self.gradient_type {
GradientType::Linear => {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{spread_method}{gradient_transform}>{}</linearGradient>"#,
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{gradient_units}{spread_method}{gradient_transform}>{}</linearGradient>"#,
gradient_id, start.x, start.y, end.x, end.y, stop
);
}
GradientType::Radial => {
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
let radius = start.distance(end);
let focal = transform_points.transform_point2(self.focal_center);
let focal_edge = transform_points.transform_point2(self.focal_center + DVec2::X * self.focal_radius);
let focal_radius = focal.distance(focal_edge);

let mut focal_attrs = String::new();
if (focal.x - start.x).abs() > 1e-9 || (focal.y - start.y).abs() > 1e-9 {
let _ = write!(focal_attrs, r#" fx="{}" fy="{}""#, focal.x, focal.y);
}
if focal_radius > 1e-9 {
let _ = write!(focal_attrs, r#" fr="{}""#, focal_radius);
}

let _ = write!(
svg_defs,
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{spread_method}{gradient_transform}>{}</radialGradient>"#,
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{focal_attrs}{gradient_units}{spread_method}{gradient_transform}>{}</radialGradient>"#,
gradient_id, start.x, start.y, radius, stop
);
}
Expand Down
5 changes: 3 additions & 2 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,9 +1202,10 @@ impl Render for Table<Vector> {
.into(),
GradientType::Radial => {
let radius = start.distance(end);
let focal = mod_points.transform_point2(gradient.focal_center);
peniko::RadialGradientPosition {
start_center: to_point(start),
start_radius: 0.,
start_center: to_point(focal),
start_radius: gradient.focal_radius as f32,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Radial gradient focal radius is not transformed with the rest of the gradient, so scaled/transformed gradients can render with the wrong inner radius.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/rendering/src/renderer.rs, line 1208:

<comment>Radial gradient focal radius is not transformed with the rest of the gradient, so scaled/transformed gradients can render with the wrong inner radius.</comment>

<file context>
@@ -1202,9 +1202,10 @@ impl Render for Table<Vector> {
-									start_center: to_point(start),
-									start_radius: 0.,
+									start_center: to_point(focal),
+									start_radius: gradient.focal_radius as f32,
 									end_center: to_point(start),
 									end_radius: radius as f32,
</file context>

end_center: to_point(start),
end_radius: radius as f32,
}
Expand Down
Loading