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 d074781c14..c213d2de3d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -27,7 +27,7 @@ use graphene_std::vector::misc::GridType; use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm}; use graphene_std::vector::misc::{CentroidType, PointSpacingType}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops}; -use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{GradientType, PaintOrder, ShapeRenderingModes, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::{GraphicGroupTable, NodeInputDecleration}; pub(crate) fn string_properties(text: &str) -> Vec { @@ -243,6 +243,7 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), // ===== // OTHER // ===== diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index c1901ce905..8ed338d481 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -468,17 +468,42 @@ impl Default for Stroke { } } +/// The shape-rendering attribute provides hints to the renderer about what tradeoffs to make when rendering shapes like paths, circles, or rectangles. +/// See https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/shape-rendering +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Dropdown)] +pub enum ShapeRenderingModes { + #[default] + Auto, + OptimizeSpeed, + CrispEdges, + GeometricPrecision, +} + +impl ShapeRenderingModes { + pub fn svg_name(&self) -> &'static str { + match self { + ShapeRenderingModes::Auto => "auto", + ShapeRenderingModes::OptimizeSpeed => "optimizeSpeed", + ShapeRenderingModes::CrispEdges => "crispEdges", + ShapeRenderingModes::GeometricPrecision => "geometricPrecision", + } + } +} + #[repr(C)] #[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] pub struct PathStyle { pub stroke: Option, pub fill: Fill, + pub shape_rendering: ShapeRenderingModes, } impl std::hash::Hash for PathStyle { fn hash(&self, state: &mut H) { self.stroke.hash(state); self.fill.hash(state); + self.shape_rendering.hash(state); } } @@ -491,13 +516,15 @@ impl std::fmt::Display for PathStyle { None => "None".to_string(), }; - write!(f, "Fill: {fill}\nStroke: {stroke}") + let shape_rendering = &self.shape_rendering; + + write!(f, "Fill: {fill}\nStroke: {stroke}\nShape-Rendering: {shape_rendering}") } } impl PathStyle { - pub const fn new(stroke: Option, fill: Fill) -> Self { - Self { stroke, fill } + pub const fn new(stroke: Option, fill: Fill, shape_rendering: ShapeRenderingModes) -> Self { + Self { stroke, fill, shape_rendering } } pub fn lerp(&self, other: &Self, time: f64) -> Self { @@ -521,6 +548,7 @@ impl PathStyle { } (None, None) => None, }, + shape_rendering: self.shape_rendering, } } @@ -598,6 +626,11 @@ impl PathStyle { self.stroke = Some(stroke); } + /// Replace the path's [ShapeRenderingModes] with a provided one. + pub fn set_shape_rendering(&mut self, shape_rendering: ShapeRenderingModes) { + self.shape_rendering = shape_rendering; + } + /// Set the path's fill to None. /// /// # Example diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 4a6a66034a..225633c751 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -96,7 +96,7 @@ pub struct VectorData { impl Default for VectorData { fn default() -> Self { Self { - style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None), + style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None, super::style::ShapeRenderingModes::Auto), colinear_manipulators: Vec::new(), point_domain: PointDomain::new(), segment_domain: SegmentDomain::new(), diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 009d7ab357..5312dc755d 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -2,7 +2,7 @@ use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_po use super::algorithms::offset_subpath::offset_subpath; use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open}; use super::misc::{CentroidType, point_to_dvec2}; -use super::style::{Fill, Gradient, GradientStops, Stroke}; +use super::style::{Fill, Gradient, GradientStops, ShapeRenderingModes, Stroke}; use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataExt, VectorDataTable}; use crate::bounds::BoundingBox; use crate::instances::{Instance, InstanceMut, Instances}; @@ -211,6 +211,35 @@ where vector_data } +/// Set the shape-render SVG property for all input vectors. Determines anti-aliasing, but output varies by renderer. +/// Not implimented in Vello +#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))] +async fn shape_render_mode( + _: impl Ctx, + #[implementations( + VectorDataTable, + VectorDataTable, + VectorDataTable, + VectorDataTable, + GraphicGroupTable, + GraphicGroupTable, + GraphicGroupTable, + GraphicGroupTable + )] + /// The vector elements, or group of vector elements, to apply the shape render mode to. + mut vector_data: V, + render_mode: ShapeRenderingModes, +) -> V +where + V: VectorDataTableIterMut + 'n + Send, +{ + for vector in vector_data.vector_iter_mut() { + vector.instance.style.set_shape_rendering(render_mode); + } + + vector_data +} + #[node_macro::node(category("Instancing"), path(graphene_core::vector))] async fn repeat( _: impl Ctx, diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 742b155557..c39127b318 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -248,6 +248,7 @@ tagged_value! { ReferencePoint(graphene_core::transform::ReferencePoint), CentroidType(graphene_core::vector::misc::CentroidType), BooleanOperation(graphene_path_bool::BooleanOperation), + ShapeRenderingModes(graphene_core::vector::style::ShapeRenderingModes), } impl TaggedValue { diff --git a/node-graph/gsvg-renderer/src/render_ext.rs b/node-graph/gsvg-renderer/src/render_ext.rs index d0d7c16cc8..8268b53b5b 100644 --- a/node-graph/gsvg-renderer/src/render_ext.rs +++ b/node-graph/gsvg-renderer/src/render_ext.rs @@ -3,7 +3,7 @@ use glam::{DAffine2, DVec2}; use graphene_core::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; use graphene_core::gradient::{Gradient, GradientType}; use graphene_core::uuid::generate_uuid; -use graphene_core::vector::style::{Fill, PaintOrder, PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin, ViewMode}; +use graphene_core::vector::style::{Fill, PaintOrder, PathStyle, ShapeRenderingModes, Stroke, StrokeAlign, StrokeCap, StrokeJoin, ViewMode}; use std::fmt::Write; pub trait RenderExt { @@ -200,6 +200,26 @@ impl RenderExt for Stroke { } } +impl RenderExt for ShapeRenderingModes { + type Output = String; + + /// Provide the SVG attributes for the stroke. + fn render( + &self, + _svg_defs: &mut String, + _element_transform: DAffine2, + _stroke_transform: DAffine2, + _bounds: [DVec2; 2], + _transformed_bounds: [DVec2; 2], + _aligned_strokes: bool, + _override_paint_order: bool, + _render_params: &RenderParams, + ) -> Self::Output { + let svg_name = self.svg_name(); + format!(" shape-rendering=\"{svg_name}\"") + } +} + impl RenderExt for PathStyle { type Output = String; @@ -271,7 +291,19 @@ impl RenderExt for PathStyle { ) }) .unwrap_or_default(); - format!("{fill_attribute}{stroke_attribute}") + + let shape_rendering_attribute = self.shape_rendering.render( + svg_defs, + element_transform, + stroke_transform, + bounds, + transformed_bounds, + aligned_strokes, + override_paint_order, + render_params, + ); + + format!("{fill_attribute}{stroke_attribute}{shape_rendering_attribute}") } } }