Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -562,6 +562,51 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
result
}

#[allow(dead_code)]

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: Imported SVG gradientUnits still have no effect because the new extractor is never called or wired into apply_usvg_fill; #[allow(dead_code)] masks that the feature path is currently inactive. Consider either integrating this map into gradient fill creation or leaving the helper out until the refactor can consume it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs, line 565:

<comment>Imported SVG `gradientUnits` still have no effect because the new extractor is never called or wired into `apply_usvg_fill`; `#[allow(dead_code)]` masks that the feature path is currently inactive. Consider either integrating this map into gradient fill creation or leaving the helper out until the refactor can consume it.</comment>

<file context>
@@ -562,6 +562,51 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
 	result
 }
 
+#[allow(dead_code)]
+fn extract_svg_gradient_units(svg: &str) -> HashMap<String, GradientUnits> {
+	let mut result = HashMap::new();
</file context>

fn extract_svg_gradient_units(svg: &str) -> HashMap<String, GradientUnits> {
let mut result = HashMap::new();
let mut gradient_nodes = HashMap::new();

let doc = match usvg::roxmltree::Document::parse(svg) {
Ok(doc) => doc,
Err(_) => return result,
};
Comment on lines +566 to +573

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.

medium

Redundant XML Parsing

Both extract_graphite_gradient_stops and extract_svg_gradient_units parse the same raw SVG XML string independently using usvg::roxmltree::Document::parse. For large SVG files, parsing the XML tree multiple times introduces unnecessary CPU and memory overhead.

Consider refactoring these functions to accept a shared reference to the parsed roxmltree::Document, or combining them into a single pass over the XML descendants to extract both gradient stops and units simultaneously.


for node in doc.descendants() {
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<GradientUnits> {
match node.attribute("gradientUnits") {
Some("userSpaceOnUse") => return Some(GradientUnits::UserSpaceOnUse),
Some("objectBoundingBox") => return Some(GradientUnits::ObjectBoundingBox),
Some(_) => return None,
None => {}
}

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 {
// Per SVG spec, the default gradientUnits is objectBoundingBox.
let units = resolve_units(node, &gradient_nodes, &mut vec![gradient_id]).unwrap_or(GradientUnits::ObjectBoundingBox);
result.insert(gradient_id.to_string(), units);
}

result
}

fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
let hex = hex.strip_prefix('#')?;
if hex.len() != 6 {
Expand Down
4 changes: 2 additions & 2 deletions node-graph/libraries/core-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_LEGACY, 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;
Expand Down
6 changes: 6 additions & 0 deletions node-graph/libraries/core-types/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ 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";
// TODO: Eventually remove this document upgrade code
/// `bool` runtime marker (never serialized) flagging a gradient that came from the legacy bounding-box-relative `Fill::Gradient`,
/// so the renderer reproduces the pre-#4241 positioning instead of the new absolute path.
pub const ATTR_GRADIENT_LEGACY: &str = "gradient_legacy";
/// Vector graphics object's filled area paint, of type List<T> where T is any graphic type.
pub const ATTR_FILL: &str = "fill";
/// Vector graphics object's stroke paint, of type List<T> where T is any graphic type.
Expand Down
29 changes: 22 additions & 7 deletions node-graph/libraries/rendering/src/render_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -94,6 +104,7 @@ impl RenderExt for List<GradientStops> {

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);

Expand Down Expand Up @@ -122,7 +133,7 @@ impl RenderExt for List<GradientStops> {
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);

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.

medium

Used Variable with Underscore Prefix

The _bounds variable is now used in the call to svg_gradient_transform. By Rust convention, variables prefixed with an underscore should remain unused. Since it is now used, consider renaming _bounds to bounds in the function signature of RenderExt::render for List<GradientStops> to improve code clarity and adhere to standard style guidelines.

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.

P3: The _bounds parameter is now used in the call to svg_gradient_transform, but the underscore prefix indicates an intentionally-unused binding. Rename it to bounds in the function signature to follow Rust naming conventions.

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 136:

<comment>The `_bounds` parameter is now used in the call to `svg_gradient_transform`, but the underscore prefix indicates an intentionally-unused binding. Rename it to `bounds` in the function signature to follow Rust naming conventions.</comment>

<file context>
@@ -122,7 +133,7 @@ impl RenderExt for List<GradientStops> {
 
 		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()
</file context>

let gradient_transform = if gradient_transform.is_empty() {
String::new()
} else {
Expand All @@ -141,15 +152,19 @@ impl RenderExt for List<GradientStops> {
GradientType::Linear => {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method}{gradient_transform}>{}</linearGradient>"#,
gradient_id, stop
r#"<linearGradient id="{}" gradientUnits="{}" x1="0" y1="0" x2="1" y2="0"{spread_method}{gradient_transform}>{}</linearGradient>"#,
gradient_id,
gradient_units.svg_name(),
stop
);
}
GradientType::Radial => {
let _ = write!(
svg_defs,
r#"<radialGradient id="{}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method}{gradient_transform}>{}</radialGradient>"#,
gradient_id, stop
r#"<radialGradient id="{}" gradientUnits="{}" cx="0" cy="0" r="1"{spread_method}{gradient_transform}>{}</radialGradient>"#,
gradient_id,
gradient_units.svg_name(),
stop
);
}
}
Expand Down
26 changes: 19 additions & 7 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
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_LEGACY, ATTR_GRADIENT_TYPE, ATTR_GRADIENT_UNITS, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH,

Check failure on line 17 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / test

unused import: `ATTR_GRADIENT_LEGACY`

Check warning on line 17 in node-graph/libraries/rendering/src/renderer.rs

View workflow job for this annotation

GitHub Actions / build / web

unused import: `ATTR_GRADIENT_LEGACY`
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
};
use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2};
Expand All @@ -39,7 +39,7 @@
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)]
Expand Down Expand Up @@ -2064,12 +2064,14 @@
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);
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.);
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, index);
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, index);
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
const MAX: f64 = 1e7;
render.leaf_tag(tag, |attributes| {
if let Some((min, size)) = thumbnail_rect {
attributes.push("x", min.x.to_string());
Expand All @@ -2080,7 +2082,6 @@
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
// 1e7 stays under that limit while still being far larger than any practical document extent.
const MAX: f64 = 1e7;
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
}

Expand All @@ -2097,7 +2098,16 @@
}

// render_thumbnail already added the footprint transform
let gradient_transform = if render_params.thumbnail { transform } else { render_params.footprint.transform * transform };
let mut gradient_transform = if render_params.thumbnail { transform } else { render_params.footprint.transform * transform };
if gradient_units == GradientUnits::ObjectBoundingBox {
let (min, size) = thumbnail_rect.unwrap_or((DVec2::splat(-MAX), DVec2::splat(MAX * 2.)));
let bounds = DAffine2::from_scale_angle_translation(size, 0., min);
if transform_is_invertible(bounds) {
gradient_transform = bounds.inverse() * gradient_transform;
} else {
gradient_units = GradientUnits::UserSpaceOnUse;
}
}
let gradient_transform_matrix = format_transform_matrix(gradient_transform);
let gradient_transform_attribute = if gradient_transform_matrix.is_empty() {
String::new()
Expand All @@ -2117,13 +2127,15 @@
GradientType::Linear => {
let _ = write!(
&mut attributes.0.svg_defs,
r#"<linearGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</linearGradient>"#
r#"<linearGradient id="{gradient_id}" gradientUnits="{}" x1="0" y1="0" x2="1" y2="0"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</linearGradient>"#,
gradient_units.svg_name()
);
}
GradientType::Radial => {
let _ = write!(
&mut attributes.0.svg_defs,
r#"<radialGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</radialGradient>"#
r#"<radialGradient id="{gradient_id}" gradientUnits="{}" cx="0" cy="0" r="1"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</radialGradient>"#,
gradient_units.svg_name()
);
}
}
Expand Down
18 changes: 18 additions & 0 deletions node-graph/libraries/vector-types/src/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ pub enum GradientType {
Radial,
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny)]

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: GradientUnits enum is missing node_macro::ChoiceType derive and #[widget(Radio)] attribute used by sibling enums (GradientType, GradientSpreadMethod), likely preventing it from being rendered in the property panel.

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

<comment>`GradientUnits` enum is missing `node_macro::ChoiceType` derive and `#[widget(Radio)]` attribute used by sibling enums (`GradientType`, `GradientSpreadMethod`), likely preventing it from being rendered in the property panel.</comment>

<file context>
@@ -14,6 +14,24 @@ pub enum GradientType {
 }
 
+#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
+#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+pub enum GradientUnits {
</file context>

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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.
///
Expand Down
2 changes: 1 addition & 1 deletion node-graph/libraries/vector-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading