From 248d75f3f3f1254d7a3c63a7444ba4d96cbe34ab Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 21 Apr 2026 16:27:08 -0700 Subject: [PATCH 1/7] Feature-gate serde derives behind cfg_attr in all runtime node graph type crates --- node-graph/graph-craft/Cargo.toml | 18 ++++---- .../libraries/application-io/Cargo.toml | 6 ++- .../libraries/application-io/src/lib.rs | 14 ++++-- node-graph/libraries/core-types/Cargo.toml | 1 + .../libraries/core-types/src/context.rs | 9 ++-- node-graph/libraries/core-types/src/misc.rs | 4 +- node-graph/libraries/core-types/src/table.rs | 10 ++-- node-graph/libraries/core-types/src/text.rs | 6 ++- .../libraries/core-types/src/transform.rs | 9 ++-- node-graph/libraries/core-types/src/types.rs | 22 +++++---- node-graph/libraries/core-types/src/uuid.rs | 6 ++- node-graph/libraries/graphic-types/Cargo.toml | 1 + .../libraries/graphic-types/src/artboard.rs | 15 +++--- .../libraries/graphic-types/src/graphic.rs | 23 ++++++---- node-graph/libraries/graphic-types/src/lib.rs | 17 ++++--- node-graph/libraries/raster-types/Cargo.toml | 1 + .../libraries/raster-types/src/image.rs | 46 +++++++++++-------- node-graph/libraries/rendering/Cargo.toml | 11 +++-- .../libraries/rendering/src/renderer.rs | 6 ++- node-graph/libraries/vector-types/Cargo.toml | 1 + .../libraries/vector-types/src/gradient.rs | 18 +++++--- .../vector-types/src/vector/click_target.rs | 11 +++-- .../libraries/vector-types/src/vector/misc.rs | 39 ++++++++++------ .../src/vector/reference_point.rs | 3 +- .../vector-types/src/vector/style.rs | 44 +++++++++++------- .../src/vector/vector_attributes.rs | 17 ++++--- .../src/vector/vector_modification.rs | 31 +++++++------ .../vector-types/src/vector/vector_types.rs | 5 +- node-graph/nodes/brush/Cargo.toml | 2 +- node-graph/nodes/brush/src/brush_cache.rs | 18 ++++---- node-graph/nodes/brush/src/brush_stroke.rs | 9 ++-- node-graph/nodes/gcore/Cargo.toml | 1 + node-graph/nodes/gcore/src/animation.rs | 3 +- node-graph/nodes/gcore/src/extract_xy.rs | 3 +- node-graph/nodes/raster/Cargo.toml | 3 +- node-graph/nodes/raster/src/adjustments.rs | 30 ++++++++---- node-graph/nodes/raster/src/curve.rs | 12 +++-- node-graph/nodes/text/Cargo.toml | 1 + node-graph/nodes/text/src/font_cache.rs | 12 +++-- node-graph/nodes/text/src/lib.rs | 6 ++- node-graph/nodes/vector/Cargo.toml | 1 + .../nodes/vector/src/generator_nodes.rs | 3 +- 42 files changed, 310 insertions(+), 188 deletions(-) diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index db2fdd5756..c0904e8f77 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -22,15 +22,15 @@ wasm = [ [dependencies] # Local dependencies dyn-any = { workspace = true } -core-types = { workspace = true } -brush-nodes = { workspace = true } -graphene-core = { workspace = true } -graphene-application-io = { workspace = true } -rendering = { workspace = true } -raster-nodes = { workspace = true } -vector-nodes = { workspace = true } -graphic-types = { workspace = true } -text-nodes = { workspace = true } +core-types = { workspace = true, features = ["serde"] } +brush-nodes = { workspace = true, features = ["serde"] } +graphene-core = { workspace = true, features = ["serde"] } +graphene-application-io = { workspace = true, features = ["serde"] } +rendering = { workspace = true, features = ["serde"] } +raster-nodes = { workspace = true, features = ["serde"] } +vector-nodes = { workspace = true, features = ["serde"] } +graphic-types = { workspace = true, features = ["serde"] } +text-nodes = { workspace = true, features = ["serde"] } # Workspace dependencies log = { workspace = true } diff --git a/node-graph/libraries/application-io/Cargo.toml b/node-graph/libraries/application-io/Cargo.toml index 2546f65de5..4c6d129697 100644 --- a/node-graph/libraries/application-io/Cargo.toml +++ b/node-graph/libraries/application-io/Cargo.toml @@ -7,6 +7,8 @@ authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" [features] +default = ["serde"] +serde = ["dep:serde", "core-types/serde", "vector-types/serde", "text-nodes/serde"] wasm = ["dep:web-sys"] wgpu = ["dep:wgpu"] @@ -19,9 +21,11 @@ text-nodes = { workspace = true } # Workspace dependencies glam = { workspace = true } -serde = { workspace = true } log = { workspace = true } +# Optional workspace dependencies +serde = { workspace = true, optional = true } + # Optional workspace dependencies web-sys = { workspace = true, optional = true } wgpu = { workspace = true, optional = true } diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index c28a1baa7b..7228af2880 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -73,7 +73,8 @@ pub enum ApplicationError { InvalidUrl, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum NodeGraphUpdateMessage {} pub trait NodeGraphUpdateSender { @@ -90,26 +91,29 @@ pub trait GetEditorPreferences { fn max_render_region_area(&self) -> u32; } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ExportFormat { #[default] Svg, Raster, } -#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TimingInformation { pub time: f64, pub animation_time: Duration, } -#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct RenderConfig { pub viewport: Footprint, pub scale: f64, pub time: TimingInformation, pub pointer: DVec2, - #[serde(alias = "view_mode")] + #[cfg_attr(feature = "serde", serde(alias = "view_mode"))] pub render_mode: RenderMode, pub export_format: ExportFormat, pub for_export: bool, diff --git a/node-graph/libraries/core-types/Cargo.toml b/node-graph/libraries/core-types/Cargo.toml index d6bc01c4db..7a25ce2790 100644 --- a/node-graph/libraries/core-types/Cargo.toml +++ b/node-graph/libraries/core-types/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +serde = ["dep:serde"] nightly = [] type_id_logging = [] dealloc_nodes = [] diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index 443d2f8b2f..9611625ab9 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -130,7 +130,8 @@ impl ModifyVarArgs for T {} // ================ // Public enum for flexible node macro codegen -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ContextFeature { ExtractFootprint, ExtractRealTime, @@ -151,7 +152,8 @@ pub enum ContextFeature { // Internal bitflags for fast compiler analysis use bitflags::bitflags; bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, Default)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ContextFeatures: u32 { const FOOTPRINT = 1 << 0; const REAL_TIME = 1 << 1; @@ -182,7 +184,8 @@ impl ContextFeatures { // CONTEXT DEPENDENCIES // ==================== -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ContextDependencies { pub extract: ContextFeatures, pub inject: ContextFeatures, diff --git a/node-graph/libraries/core-types/src/misc.rs b/node-graph/libraries/core-types/src/misc.rs index 45012110d9..8ec42702a4 100644 --- a/node-graph/libraries/core-types/src/misc.rs +++ b/node-graph/libraries/core-types/src/misc.rs @@ -67,8 +67,8 @@ pub fn migrate_color<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resul use no_std_types::color::Color; use serde::Deserialize; - #[derive(serde::Serialize, serde::Deserialize)] - #[serde(untagged)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(untagged))] enum ColorFormat { Color(Color), OptionalColor(Option), diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 4a814aaf58..e107da59ae 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -6,9 +6,10 @@ use dyn_any::{StaticType, StaticTypeSized}; use glam::DAffine2; use std::hash::Hash; -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Table { - #[serde(alias = "instances", alias = "instance")] + #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] element: Vec, transform: Vec, alpha_blending: Vec, @@ -248,9 +249,10 @@ impl FromIterator> for Table { } } -#[derive(Copy, Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Copy, Clone, Default, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TableRow { - #[serde(alias = "instance")] + #[cfg_attr(feature = "serde", serde(alias = "instance"))] pub element: T, pub transform: DAffine2, pub alpha_blending: AlphaBlending, diff --git a/node-graph/libraries/core-types/src/text.rs b/node-graph/libraries/core-types/src/text.rs index 29917694b6..81adab3526 100644 --- a/node-graph/libraries/core-types/src/text.rs +++ b/node-graph/libraries/core-types/src/text.rs @@ -11,7 +11,8 @@ pub use to_path::*; /// Alignment of lines of type within a text block. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum TextAlign { #[default] @@ -34,7 +35,8 @@ impl From for parley::Alignment { } } -#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] +#[derive(PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TypesettingConfig { pub font_size: f64, pub line_height_ratio: f64, diff --git a/node-graph/libraries/core-types/src/transform.rs b/node-graph/libraries/core-types/src/transform.rs index f92965ab97..e3fb7ab029 100644 --- a/node-graph/libraries/core-types/src/transform.rs +++ b/node-graph/libraries/core-types/src/transform.rs @@ -6,7 +6,8 @@ use glam::{DAffine2, DMat2, DVec2, UVec2}; /// Controls whether the Decompose Scale node returns axis-length magnitudes or pure scale factors. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum ScaleType { /// The visual length of each axis (always positive, includes any skew contribution). @@ -141,7 +142,8 @@ impl TransformMut for Footprint { } } -#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum RenderQuality { /// Low quality, fast rendering Preview, @@ -154,7 +156,8 @@ pub enum RenderQuality { /// Render at full quality Full, } -#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Footprint { /// Inverse of the transform which will be applied to the node output during the rendering process pub transform: DAffine2, diff --git a/node-graph/libraries/core-types/src/types.rs b/node-graph/libraries/core-types/src/types.rs index ac0c5a6687..42eab1f3d6 100644 --- a/node-graph/libraries/core-types/src/types.rs +++ b/node-graph/libraries/core-types/src/types.rs @@ -77,7 +77,8 @@ macro_rules! fn_type_fut { } // TODO: Rename to NodeSignatureMonomorphization -#[derive(Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NodeIOTypes { pub call_argument: Type, pub return_value: Type, @@ -126,7 +127,8 @@ impl std::fmt::Debug for NodeIOTypes { } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ProtoNodeIdentifier { name: Cow<'static, str>, } @@ -180,17 +182,18 @@ fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer: } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, Debug, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TypeDescriptor { - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] pub id: Option, - #[serde(deserialize_with = "migrate_type_descriptor_names")] + #[cfg_attr(feature = "serde", serde(deserialize_with = "migrate_type_descriptor_names"))] pub name: Cow<'static, str>, - #[serde(default)] + #[cfg_attr(feature = "serde", serde(default))] pub alias: Option>, - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] pub size: usize, - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] pub align: usize, } @@ -222,7 +225,8 @@ impl PartialEq for TypeDescriptor { /// Graph runtime type information used for type inference. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Type { /// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type. Generic(Cow<'static, str>), diff --git a/node-graph/libraries/core-types/src/uuid.rs b/node-graph/libraries/core-types/src/uuid.rs index 9ddab56d4c..4d47df0b74 100644 --- a/node-graph/libraries/core-types/src/uuid.rs +++ b/node-graph/libraries/core-types/src/uuid.rs @@ -2,7 +2,8 @@ use dyn_any::DynAny; pub use uuid_generation::*; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, Copy, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Uuid(#[serde(with = "u64_string")] u64); mod u64_string { @@ -68,7 +69,8 @@ mod uuid_generation { #[repr(transparent)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NodeId(pub u64); impl NodeId { diff --git a/node-graph/libraries/graphic-types/Cargo.toml b/node-graph/libraries/graphic-types/Cargo.toml index 1dac611f07..8b0963c3ee 100644 --- a/node-graph/libraries/graphic-types/Cargo.toml +++ b/node-graph/libraries/graphic-types/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +serde = ["dep:serde", "core-types/serde", "vector-types/serde", "raster-types/serde"] wasm = [ "core-types/wasm", "vector-types/wasm", diff --git a/node-graph/libraries/graphic-types/src/artboard.rs b/node-graph/libraries/graphic-types/src/artboard.rs index 7595f2cd52..eca77b66a0 100644 --- a/node-graph/libraries/graphic-types/src/artboard.rs +++ b/node-graph/libraries/graphic-types/src/artboard.rs @@ -12,7 +12,8 @@ use glam::{DAffine2, DVec2, IVec2}; use std::hash::Hash; /// Some [`ArtboardData`] with some optional clipping bounds that can be exported. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Hash, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Artboard { pub content: Table, pub label: String, @@ -76,22 +77,24 @@ impl Transform for Artboard { pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { use serde::Deserialize; - #[derive(Clone, Default, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, Hash, PartialEq, DynAny)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ArtboardGroup { pub artboards: Vec<(Artboard, Option)>, } - #[derive(serde::Serialize, serde::Deserialize)] - #[serde(untagged)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(untagged))] enum ArtboardFormat { ArtboardGroup(ArtboardGroup), OldArtboardTable(OldTable), ArtboardTable(Table), } - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OldTable { - #[serde(alias = "instances", alias = "instance")] + #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] element: Vec, transform: Vec, alpha_blending: Vec, diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 878d702fdf..b0617ae7ac 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -15,7 +15,8 @@ use vector_types::GradientStops; pub type Vector = vector_types::Vector>>; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Hash, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Graphic { Graphic(Table), Vector(Table), @@ -436,35 +437,39 @@ impl OmitIndex for Table { pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { use serde::Deserialize; - #[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, PartialEq, DynAny, Default)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OldGraphicGroup { elements: Vec<(Graphic, Option)>, transform: DAffine2, alpha_blending: AlphaBlending, } - #[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, PartialEq, DynAny, Default)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct GraphicGroup { elements: Vec<(Graphic, Option)>, } - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OlderTable { id: Vec, - #[serde(alias = "instances", alias = "instance")] + #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] element: Vec, } - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OldTable { id: Vec, - #[serde(alias = "instances", alias = "instance")] + #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] element: Vec, transform: Vec, alpha_blending: Vec, } - #[derive(serde::Serialize, serde::Deserialize)] - #[serde(untagged)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(untagged))] enum GraphicFormat { OldGraphicGroup(OldGraphicGroup), OlderTableOldGraphicGroup(OlderTable), diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 54e4302bca..598f82fda7 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -25,7 +25,8 @@ pub mod migrations { pub fn migrate_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { use serde::Deserialize; - #[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, PartialEq, DynAny)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OldVectorData { pub transform: DAffine2, pub alpha_blending: AlphaBlending, @@ -41,23 +42,25 @@ pub mod migrations { pub upstream_graphic_group: Option>, } - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OldTable { - #[serde(alias = "instances", alias = "instance")] + #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] element: Vec, transform: Vec, alpha_blending: Vec, } - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OlderTable { id: Vec, - #[serde(alias = "instances", alias = "instance")] + #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] element: Vec, } - #[derive(serde::Serialize, serde::Deserialize)] - #[serde(untagged)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(untagged))] #[allow(clippy::large_enum_variant)] enum VectorFormat { Vector(Vector), diff --git a/node-graph/libraries/raster-types/Cargo.toml b/node-graph/libraries/raster-types/Cargo.toml index 0039481667..d733fcadf5 100644 --- a/node-graph/libraries/raster-types/Cargo.toml +++ b/node-graph/libraries/raster-types/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +serde = ["dep:serde", "core-types/serde"] wgpu = ["dep:wgpu"] wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] diff --git a/node-graph/libraries/raster-types/src/image.rs b/node-graph/libraries/raster-types/src/image.rs index 420b2439af..7caac14be7 100644 --- a/node-graph/libraries/raster-types/src/image.rs +++ b/node-graph/libraries/raster-types/src/image.rs @@ -40,15 +40,16 @@ mod base64_serde { } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, Eq, Default, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Eq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Image { pub width: u32, pub height: u32, - #[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")] + #[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))] pub data: Vec

, /// Optional: Stores a base64 string representation of the image which can be used to speed up the conversion /// to an svg string. This is used as a cache in order to not have to encode the data on every graph evaluation. - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] pub base64_string: Option, // TODO: Add an `origin` field to store where in the local space the image is anchored. // TODO: Currently it is always anchored at the top left corner at (0, 0). The bottom right corner of the new origin field would correspond to (1, 1). @@ -61,7 +62,8 @@ impl PartialEq for Image

{ } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TransformImage(pub DAffine2); impl Hash for TransformImage { @@ -237,13 +239,15 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> } } - #[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, Hash, PartialEq, DynAny)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum GraphicElement { GraphicGroup(Table), RasterFrame(RasterFrame), } - #[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ImageFrame { pub image: Image

, } @@ -271,15 +275,16 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> type Static = ImageFrame; } - #[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OldImageFrame { image: Image

, transform: DAffine2, alpha_blending: AlphaBlending, } - #[derive(serde::Serialize, serde::Deserialize)] - #[serde(untagged)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(untagged))] enum FormatVersions { Image(Image), OldImageFrame(OldImageFrame), @@ -292,18 +297,20 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> RasterTable(Table>), } - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OldTable { - #[serde(alias = "instances", alias = "instance")] + #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] element: Vec, transform: Vec, alpha_blending: Vec, } - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OlderTable { id: Vec, - #[serde(alias = "instances", alias = "instance")] + #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] element: Vec, } @@ -390,14 +397,16 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D } } - #[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, Hash, PartialEq, DynAny)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum GraphicElement { /// Equivalent to the SVG tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g GraphicGroup(Table), RasterFrame(RasterFrame), } - #[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ImageFrame { pub image: Image

, } @@ -425,15 +434,16 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D type Static = ImageFrame; } - #[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OldImageFrame { image: Image

, transform: DAffine2, alpha_blending: AlphaBlending, } - #[derive(serde::Serialize, serde::Deserialize)] - #[serde(untagged)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serde", serde(untagged))] enum FormatVersions { Image(Image), OldImageFrame(OldImageFrame), diff --git a/node-graph/libraries/rendering/Cargo.toml b/node-graph/libraries/rendering/Cargo.toml index e33ce052fd..430996aabe 100644 --- a/node-graph/libraries/rendering/Cargo.toml +++ b/node-graph/libraries/rendering/Cargo.toml @@ -6,6 +6,10 @@ description = "SVG rendering for Graphene" authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" +[features] +default = ["serde"] +serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/serde"] + [dependencies] # Local dependencies dyn-any = { workspace = true } @@ -13,7 +17,6 @@ core-types = { workspace = true } # Workspace dependencies glam = { workspace = true } -serde = { workspace = true } base64 = { workspace = true } log = { workspace = true } num-traits = { workspace = true } @@ -21,7 +24,7 @@ usvg = { workspace = true } kurbo = { workspace = true } vector-types = { workspace = true } graphic-types = { workspace = true } - - -# Workspace dependencies vello = { workspace = true } + +# Optional workspace dependencies +serde = { workspace = true, optional = true } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6f1690ad37..04ae29d034 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -67,7 +67,8 @@ pub fn checkerboard_brush() -> peniko::Brush { }) } -#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] enum MaskType { Clip, Mask, @@ -333,7 +334,8 @@ fn draw_raster_outline(scene: &mut Scene, outline_transform: &DAffine2, render_p // TODO: Click targets can be removed from the render output, since the vector data is available in the vector modify data from Monitor nodes. // This will require that the transform for child layers into that layer space be calculated, or it could be returned from the RenderOutput instead of click targets. -#[derive(Debug, Default, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct RenderMetadata { pub upstream_footprints: HashMap, pub local_transforms: HashMap, diff --git a/node-graph/libraries/vector-types/Cargo.toml b/node-graph/libraries/vector-types/Cargo.toml index 5212fb2386..c6d0bf29f8 100644 --- a/node-graph/libraries/vector-types/Cargo.toml +++ b/node-graph/libraries/vector-types/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +serde = ["dep:serde", "core-types/serde"] wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index f5c241c2f0..b80423d037 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -3,7 +3,8 @@ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum GradientType { #[default] @@ -15,7 +16,8 @@ pub enum GradientType { // TODO: Use linear not gamma colors /// A list of colors associated with positions (in the range 0 to 1) along a gradient. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, serde::Serialize, DynAny)] +#[derive(Debug, Clone, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct GradientStops { /// The position of this stop, a factor from 0-1 along the length of the full gradient. pub position: Vec, @@ -36,7 +38,7 @@ impl<'de> serde::Deserialize<'de> for GradientStops { } #[derive(serde::Deserialize)] - #[serde(untagged)] + #[cfg_attr(feature = "serde", serde(untagged))] enum GradientStopsFormat { New(NewFormat), Old(Vec<(f64, Color)>), @@ -336,7 +338,8 @@ impl GradientStops { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum GradientSpreadMethod { #[default] @@ -360,13 +363,14 @@ impl GradientSpreadMethod { /// Contains the start and end points, along with the colors at varying points along the length. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Gradient { pub stops: GradientStops, pub gradient_type: GradientType, pub start: DVec2, pub end: DVec2, - #[serde(default)] + #[cfg_attr(feature = "serde", serde(default))] pub spread_method: GradientSpreadMethod, } @@ -494,7 +498,7 @@ pub fn migrate_gradient_stops<'de, D: serde::Deserializer<'de>>(deserializer: D) use serde::Deserialize; #[derive(serde::Deserialize)] - #[serde(untagged)] + #[cfg_attr(feature = "serde", serde(untagged))] enum GradientStopsFormat { GradientStops(GradientStops), GradientTable(Table), diff --git a/node-graph/libraries/vector-types/src/vector/click_target.rs b/node-graph/libraries/vector-types/src/vector/click_target.rs index ce9eeb16f4..56a1dedaf3 100644 --- a/node-graph/libraries/vector-types/src/vector/click_target.rs +++ b/node-graph/libraries/vector-types/src/vector/click_target.rs @@ -13,7 +13,8 @@ use kurbo::{Affine, BezPath, ParamCurve, PathSeg, Shape}; type BoundingBox = Option<[DVec2; 2]>; -#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct FreePoint { pub id: PointId, pub position: DVec2, @@ -29,7 +30,8 @@ impl FreePoint { } } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ClickTargetType { Subpath(Subpath), FreePoint(FreePoint), @@ -115,12 +117,13 @@ impl BoundingBoxCache { } /// Represents a clickable target for the layer -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ClickTarget { target_type: ClickTargetType, stroke_width: f64, bounding_box: BoundingBox, - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] bounding_box_cache: Arc>, } diff --git a/node-graph/libraries/vector-types/src/vector/misc.rs b/node-graph/libraries/vector-types/src/vector/misc.rs index 7f5711da75..a662069aaf 100644 --- a/node-graph/libraries/vector-types/src/vector/misc.rs +++ b/node-graph/libraries/vector-types/src/vector/misc.rs @@ -8,7 +8,8 @@ use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point use std::ops::Sub; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum BooleanOperation { #[default] @@ -26,7 +27,8 @@ pub enum BooleanOperation { /// Represents different geometric interpretations of calculating the centroid (center of mass). #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum CentroidType { /// The center of mass for the area of a solid shape's interior, as if made out of an infinitely flat material. @@ -38,7 +40,8 @@ pub enum CentroidType { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum RowsOrColumns { #[default] @@ -85,7 +88,8 @@ impl AsI64 for f64 { } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum GridType { #[default] @@ -95,7 +99,8 @@ pub enum GridType { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum ArcType { #[default] @@ -106,7 +111,8 @@ pub enum ArcType { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum MergeByDistanceAlgorithm { #[default] @@ -116,7 +122,8 @@ pub enum MergeByDistanceAlgorithm { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum ExtrudeJoiningAlgorithm { All, @@ -127,7 +134,8 @@ pub enum ExtrudeJoiningAlgorithm { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum PointSpacingType { #[default] @@ -368,7 +376,8 @@ impl Tangent for kurbo::PathSeg { } /// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature). -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ManipulatorPointId { /// A control anchor - the start or end point of a bézier. Anchor(PointId), @@ -479,7 +488,8 @@ impl ManipulatorPointId { } /// The type of handle found on a bézier curve. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum HandleType { /// The first handle on a cubic bézier or the only handle on a quadratic bézier. Primary, @@ -488,7 +498,8 @@ pub enum HandleType { } /// Represents a primary or end handle found in a particular segment. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct HandleId { pub ty: HandleType, pub segment: SegmentId, @@ -547,7 +558,8 @@ impl HandleId { } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Dropdown)] pub enum SpiralType { #[default] @@ -557,7 +569,8 @@ pub enum SpiralType { /// Controls how the morph/blend progression spends its time along the interpolation path, allowing for constant speed/spacing with respect to different parameters of change. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Dropdown)] pub enum InterpolationDistribution { /// All objects occupy an equal portion of the progression range, regardless of their changing distances, angles, sizes, or slants. diff --git a/node-graph/libraries/vector-types/src/vector/reference_point.rs b/node-graph/libraries/vector-types/src/vector/reference_point.rs index 094155918c..f134477844 100644 --- a/node-graph/libraries/vector-types/src/vector/reference_point.rs +++ b/node-graph/libraries/vector-types/src/vector/reference_point.rs @@ -2,7 +2,8 @@ use core_types::math::bbox::AxisAlignedBbox; use glam::DVec2; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ReferencePoint { #[default] None, diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index 0828c4e6f2..d26bd9333f 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -16,7 +16,8 @@ use std::f64::consts::{PI, TAU}; /// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash)] +#[derive(Default, Debug, Clone, PartialEq, DynAny, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Fill { #[default] None, @@ -161,7 +162,8 @@ impl From for Fill { /// In the future we'll probably also add a pattern fill. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash)] +#[derive(Default, Debug, Clone, PartialEq, DynAny, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum FillChoice { #[default] None, @@ -209,7 +211,8 @@ impl From for FillChoice { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, DynAny, Hash, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum FillType { #[default] @@ -220,7 +223,8 @@ pub enum FillType { /// The stroke (outline) style of an SVG element. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum StrokeCap { #[default] @@ -241,7 +245,8 @@ impl StrokeCap { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum StrokeJoin { #[default] @@ -262,7 +267,8 @@ impl StrokeJoin { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum StrokeAlign { #[default] @@ -279,7 +285,8 @@ impl StrokeAlign { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum PaintOrder { #[default] @@ -299,8 +306,9 @@ fn daffine2_identity() -> DAffine2 { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny)] -#[serde(default)] +#[derive(Debug, Clone, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(default))] pub struct Stroke { /// Stroke color pub color: Option, @@ -308,17 +316,17 @@ pub struct Stroke { pub weight: f64, pub dash_lengths: Vec, pub dash_offset: f64, - #[serde(alias = "line_cap")] + #[cfg_attr(feature = "serde", serde(alias = "line_cap"))] pub cap: StrokeCap, - #[serde(alias = "line_join")] + #[cfg_attr(feature = "serde", serde(alias = "line_join"))] pub join: StrokeJoin, - #[serde(alias = "line_join_miter_limit")] + #[cfg_attr(feature = "serde", serde(alias = "line_join_miter_limit"))] pub join_miter_limit: f64, - #[serde(default)] + #[cfg_attr(feature = "serde", serde(default))] pub align: StrokeAlign, - #[serde(default = "daffine2_identity")] + #[cfg_attr(feature = "serde", serde(default = "daffine2_identity"))] pub transform: DAffine2, - #[serde(default)] + #[cfg_attr(feature = "serde", serde(default))] pub paint_order: PaintOrder, } @@ -512,7 +520,8 @@ impl Default for Stroke { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, PartialEq, Default, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PathStyle { pub stroke: Option, pub fill: Fill, @@ -680,7 +689,8 @@ impl PathStyle { /// Ways the user can choose to view the artwork in the viewport. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum RenderMode { /// Render with normal coloration at the current viewport resolution #[default] diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index a2f8edd188..8a81a2c3ef 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -14,7 +14,7 @@ macro_rules! create_ids { ($($id:ident),*) => { $( #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, DynAny)] - #[derive(serde::Serialize, serde::Deserialize)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// A strongly typed ID pub struct $id(u64); @@ -79,11 +79,12 @@ impl std::hash::BuildHasher for NoHashBuilder { } } -#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Stores data which is per-point. Each point is merely a position and can be used in a point cloud or to for a bézier path. In future this will be extendable at runtime with custom attributes. pub struct PointDomain { id: Vec, - #[serde(alias = "positions")] + #[cfg_attr(feature = "serde", serde(alias = "positions"))] pub(crate) position: Vec, } @@ -212,10 +213,11 @@ impl PointDomain { } } -#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Stores data which is per-segment. A segment is a bézier curve between two end points with a stroke. In future this will be extendable at runtime with custom attributes. pub struct SegmentDomain { - #[serde(alias = "ids")] + #[cfg_attr(feature = "serde", serde(alias = "ids"))] id: Vec, start_point: Vec, end_point: Vec, @@ -594,11 +596,12 @@ impl SegmentDomain { } } -#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Stores data which is per-region. A region is an enclosed area composed of a range of segments from the /// [`SegmentDomain`] that can be given a fill. In future this will be extendable at runtime with custom attributes. pub struct RegionDomain { - #[serde(alias = "ids")] + #[cfg_attr(feature = "serde", serde(alias = "ids"))] id: Vec, segment_range: Vec>, fill: Vec, diff --git a/node-graph/libraries/vector-types/src/vector/vector_modification.rs b/node-graph/libraries/vector-types/src/vector/vector_modification.rs index f9d094223f..69423da7c0 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_modification.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_modification.rs @@ -9,11 +9,12 @@ use std::collections::{HashMap, HashSet}; use std::hash::BuildHasher; /// Represents a procedural change to the [`PointDomain`] in [`Vector`]. -#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PointModification { add: Vec, remove: HashSet, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))] delta: HashMap, } @@ -80,19 +81,20 @@ impl PointModification { } /// Represents a procedural change to the [`SegmentDomain`] in [`Vector`]. -#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SegmentModification { add: Vec, remove: HashSet, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))] start_point: HashMap, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))] end_point: HashMap, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))] handle_primary: HashMap>, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))] handle_end: HashMap>, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))] stroke: HashMap, } @@ -250,13 +252,14 @@ impl SegmentModification { } /// Represents a procedural change to the [`RegionDomain`] in [`Vector`]. -#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct RegionModification { add: Vec, remove: HashSet, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))] segment_range: HashMap>, - #[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")] + #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap"))] fill: HashMap, } @@ -294,7 +297,8 @@ impl RegionModification { } /// Represents a procedural change to the [`Vector`]. -#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct VectorModification { points: PointModification, segments: SegmentModification, @@ -304,7 +308,8 @@ pub struct VectorModification { } /// A modification type that can be added to a [`VectorModification`]. -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(PartialEq, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum VectorModificationType { InsertSegment { id: SegmentId, points: [PointId; 2], handles: [Option; 2] }, InsertPoint { id: PointId, position: DVec2 }, diff --git a/node-graph/libraries/vector-types/src/vector/vector_types.rs b/node-graph/libraries/vector-types/src/vector/vector_types.rs index e9b8bbb670..8f74a6f158 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_types.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_types.rs @@ -20,7 +20,8 @@ use std::collections::HashMap; /// Generic over `Upstream` to avoid circular dependency with the Graphic type. /// - Use `Vector<()>` for basic vectors without upstream tracking /// - Use `Vector>>` in the graphic crate for vectors with upstream layers -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Vector { pub style: PathStyle, @@ -34,7 +35,7 @@ pub struct Vector { /// Used to store the upstream group/folder of nested layers during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved for the child layers. /// Without this, the tools would be working with a collapsed version of the data which has no reference to the original child layers that were booleaned together, resulting in the inner layers not being editable. - #[serde(alias = "upstream_group")] + #[cfg_attr(feature = "serde", serde(alias = "upstream_group"))] pub upstream_data: Upstream, } unsafe impl StaticType for Vector { diff --git a/node-graph/nodes/brush/Cargo.toml b/node-graph/nodes/brush/Cargo.toml index 59e0852567..48580c2b5e 100644 --- a/node-graph/nodes/brush/Cargo.toml +++ b/node-graph/nodes/brush/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -serde = ["dep:serde"] +serde = ["dep:serde", "core-types/serde", "raster-types/serde", "raster-nodes/serde"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/brush/src/brush_cache.rs b/node-graph/nodes/brush/src/brush_cache.rs index f90618830b..b19c427eb4 100644 --- a/node-graph/nodes/brush/src/brush_cache.rs +++ b/node-graph/nodes/brush/src/brush_cache.rs @@ -13,24 +13,25 @@ use std::sync::{Arc, Mutex}; // TODO: This is a temporary hack, be sure to not reuse this when the brush system is replaced/rewritten. static NEXT_BRUSH_CACHE_IMPL_ID: AtomicU64 = AtomicU64::new(0); -#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] struct BrushCacheImpl { - #[serde(default = "new_unique_id")] + #[cfg_attr(feature = "serde", serde(default = "new_unique_id"))] unique_id: u64, // The full previous input that was cached. - #[serde(default)] + #[cfg_attr(feature = "serde", serde(default))] prev_input: Vec, // The strokes that have been fully processed and blended into the background. - #[serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row")] + #[cfg_attr(feature = "serde", serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row"))] background: TableRow>, - #[serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row")] + #[cfg_attr(feature = "serde", serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row"))] blended_image: TableRow>, - #[serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row")] + #[cfg_attr(feature = "serde", serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row"))] last_stroke_texture: TableRow>, // A cache for brush textures. - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] brush_texture_cache: HashMap>, } @@ -134,7 +135,8 @@ pub struct BrushPlan { pub first_stroke_point_skip: usize, } -#[derive(Debug, Default, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BrushCache(Arc>); // A bit of a cursed implementation to work around the current node system. diff --git a/node-graph/nodes/brush/src/brush_stroke.rs b/node-graph/nodes/brush/src/brush_stroke.rs index c69360148d..d6f7f294d2 100644 --- a/node-graph/nodes/brush/src/brush_stroke.rs +++ b/node-graph/nodes/brush/src/brush_stroke.rs @@ -6,7 +6,8 @@ use glam::DVec2; use std::hash::{Hash, Hasher}; /// The style of a brush. -#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BrushStyle { pub color: Color, pub diameter: f64, @@ -54,7 +55,8 @@ impl PartialEq for BrushStyle { } /// A single sample of brush parameters across the brush stroke. -#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BrushInputSample { // The position of the sample in layer space, in pixels. // The origin of layer space is not specified. @@ -70,7 +72,8 @@ impl Hash for BrushInputSample { } /// The parameters for a single stroke brush. -#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BrushStroke { pub style: BrushStyle, pub trace: Vec, diff --git a/node-graph/nodes/gcore/Cargo.toml b/node-graph/nodes/gcore/Cargo.toml index 740a021b59..dc6e9ffa53 100644 --- a/node-graph/nodes/gcore/Cargo.toml +++ b/node-graph/nodes/gcore/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +serde = ["dep:serde", "core-types/serde", "raster-types/serde", "graphic-types/serde"] wasm = [ "core-types/wasm", "raster-types/wasm", diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index 50c682d0b3..eaacb89162 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -9,7 +9,8 @@ use raster_types::{CPU, GPU, Raster}; const DAY: f64 = 1000. * 3600. * 24.; -#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum RealTimeMode { #[label("UTC")] Utc, diff --git a/node-graph/nodes/gcore/src/extract_xy.rs b/node-graph/nodes/gcore/src/extract_xy.rs index ffe13e26f2..a78d242c41 100644 --- a/node-graph/nodes/gcore/src/extract_xy.rs +++ b/node-graph/nodes/gcore/src/extract_xy.rs @@ -15,7 +15,8 @@ fn extract_xy>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2 /// The X or Y component of a vec2. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum XY { #[default] diff --git a/node-graph/nodes/raster/Cargo.toml b/node-graph/nodes/raster/Cargo.toml index 55c7e06a03..2daf3b374d 100644 --- a/node-graph/nodes/raster/Cargo.toml +++ b/node-graph/nodes/raster/Cargo.toml @@ -11,8 +11,10 @@ workspace = true [features] default = ["std"] +serde = ["dep:serde", "core-types?/serde", "raster-types?/serde", "vector-types?/serde"] shader-nodes = ["std", "dep:raster-nodes-shaders", "dep:wgpu-executor"] std = [ + "serde", "dep:core-types", "dep:dyn-any", "dep:raster-types", @@ -22,7 +24,6 @@ std = [ "dep:rand", "dep:rand_chacha", "dep:fastnoise-lite", - "dep:serde", "dep:kurbo", ] wasm = [ diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index 2dd182c06a..717e561c68 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -34,7 +34,8 @@ use vector_types::GradientStops; // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Color%20Lookup%20(Photoshop%20CS6 #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)] #[widget(Dropdown)] #[repr(u32)] @@ -565,7 +566,8 @@ fn vibrance>( #[repr(u32)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] #[widget(Radio)] pub enum RedGreenBlue { @@ -576,7 +578,8 @@ pub enum RedGreenBlue { } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)] #[widget(Radio)] #[repr(u32)] @@ -590,7 +593,8 @@ pub enum RedGreenBlueAlpha { /// Style of noise pattern. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] #[widget(Dropdown)] pub enum NoiseType { @@ -608,7 +612,8 @@ pub enum NoiseType { /// Style of layered levels of the noise pattern. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] pub enum FractalType { #[default] @@ -625,7 +630,8 @@ pub enum FractalType { /// Distance function used by the cellular noise. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] pub enum CellularDistanceFunction { #[default] @@ -637,7 +643,8 @@ pub enum CellularDistanceFunction { } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] pub enum CellularReturnType { CellValue, @@ -657,7 +664,8 @@ pub enum CellularReturnType { } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] #[widget(Dropdown)] pub enum DomainWarpType { @@ -771,7 +779,8 @@ fn channel_mixer>( #[repr(u32)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] #[widget(Radio)] pub enum RelativeAbsolute { @@ -782,7 +791,8 @@ pub enum RelativeAbsolute { #[repr(u32)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] pub enum SelectiveColorChoice { #[default] diff --git a/node-graph/nodes/raster/src/curve.rs b/node-graph/nodes/raster/src/curve.rs index 2ba1d84cbf..61ee41fc9e 100644 --- a/node-graph/nodes/raster/src/curve.rs +++ b/node-graph/nodes/raster/src/curve.rs @@ -5,13 +5,14 @@ use std::hash::{Hash, Hasher}; use std::ops::{Add, Mul, Sub}; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Curve { - #[serde(rename = "manipulatorGroups")] + #[cfg_attr(feature = "serde", serde(rename = "manipulatorGroups"))] pub manipulator_groups: Vec, - #[serde(rename = "firstHandle")] + #[cfg_attr(feature = "serde", serde(rename = "firstHandle"))] pub first_handle: [f32; 2], - #[serde(rename = "lastHandle")] + #[cfg_attr(feature = "serde", serde(rename = "lastHandle"))] pub last_handle: [f32; 2], } @@ -33,7 +34,8 @@ impl Hash for Curve { } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct CurveManipulatorGroup { pub anchor: [f32; 2], pub handles: [[f32; 2]; 2], diff --git a/node-graph/nodes/text/Cargo.toml b/node-graph/nodes/text/Cargo.toml index e5558c741d..801e6b973c 100644 --- a/node-graph/nodes/text/Cargo.toml +++ b/node-graph/nodes/text/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +serde = ["dep:serde", "core-types/serde", "vector-types/serde"] wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs index 58111bda21..20ab303be9 100644 --- a/node-graph/nodes/text/src/font_cache.rs +++ b/node-graph/nodes/text/src/font_cache.rs @@ -5,13 +5,14 @@ use std::sync::Arc; /// A font type (storing font family and font style and an optional preview URL) #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, DynAny)] +#[derive(Debug, Clone, Eq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Font { - #[serde(rename = "fontFamily")] + #[cfg_attr(feature = "serde", serde(rename = "fontFamily"))] pub font_family: String, - #[serde(rename = "fontStyle", deserialize_with = "migrate_font_style")] + #[cfg_attr(feature = "serde", serde(rename = "fontStyle", deserialize_with = "migrate_font_style"))] pub font_style: String, - #[serde(skip)] + #[cfg_attr(feature = "serde", serde(skip))] pub font_style_to_restore: Option, } @@ -63,7 +64,8 @@ impl Default for Font { } /// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`) -#[derive(Clone, serde::Serialize, serde::Deserialize, Default, DynAny)] +#[derive(Clone, Default, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct FontCache { /// Actual font file data used for rendering a font font_file_data: HashMap>, diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index e877c1b89b..d92a04cdfe 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -25,7 +25,8 @@ pub use vector_types; /// Alignment of lines of type within a text block. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum TextAlign { #[default] @@ -48,7 +49,8 @@ impl From for parley::Alignment { } } -#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] +#[derive(PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TypesettingConfig { pub font_size: f64, pub line_height_ratio: f64, diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index bd976b84e7..02ea5f0fc8 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/serde"] wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 66c8009e02..914b928dcf 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -188,7 +188,8 @@ fn star( } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum QRCodeErrorCorrectionLevel { /// Allows recovery from up to 7% data loss. From dac0e6b914ed8a859083b953ae92c5c8068d1fd3 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 21 Apr 2026 17:35:55 -0700 Subject: [PATCH 2/7] Refactor Table to move its hard-coded fields into an attributes field --- node-graph/libraries/core-types/src/table.rs | 393 ++++++++++++++++--- 1 file changed, 341 insertions(+), 52 deletions(-) diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index e107da59ae..34ccabf716 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -6,14 +6,123 @@ use dyn_any::{StaticType, StaticTypeSized}; use glam::DAffine2; use std::hash::Hash; +// ATTRIBUTE VALUE TRAIT +// Enables type-erased storage that supports Clone, Send, Sync, and downcasting. + +trait AttributeValue: std::any::Any + Send + Sync { + fn clone_box(&self) -> Box; + fn as_any(&self) -> &dyn std::any::Any; + fn as_any_mut(&mut self) -> &mut dyn std::any::Any; + fn into_any(self: Box) -> Box; +} + +// The `Sized` bound ensures this blanket impl does not apply to `dyn AttributeValue` itself, +// which would cause infinite recursion in the `Clone for Box` impl. +impl AttributeValue for T { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn into_any(self: Box) -> Box { + self + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + (**self).clone_box() + } +} + +// ATTRIBUTES + +/// A small ordered map of type-erased attribute columns, keyed by string name. +/// Linear search preserves insertion order and is likely faster than a HashMap for small attribute counts. +#[derive(Clone, Default)] +pub struct Attributes { + entries: Vec<(String, Box)>, +} + +impl std::fmt::Debug for Attributes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let keys: Vec<&str> = self.entries.iter().map(|(k, _)| k.as_str()).collect(); + f.debug_struct("Attributes").field("keys", &keys).finish() + } +} + +impl Attributes { + pub fn new() -> Self { + Self::default() + } + + /// Inserts an attribute with the given key and value, replacing any existing entry with the same key. + pub fn insert(&mut self, key: String, value: T) { + for (k, v) in &mut self.entries { + if *k == key { + *v = Box::new(value); + return; + } + } + self.entries.push((key, Box::new(value))); + } + + /// Gets a reference to the value of the attribute with the given key, if it exists and can be downcast to the requested type. + pub fn get(&self, key: &str) -> Option<&T> { + // Explicit deref `(**v)` reaches `dyn AttributeValue` (which is !Sized and thus dispatches + // through the vtable to the concrete type) rather than resolving to the blanket + // `impl AttributeValue for Box` which would return the wrong TypeId. + self.entries.iter().find_map(|(k, v)| if k == key { (**v).as_any().downcast_ref::() } else { None }) + } + + /// Gets a mutable reference to the value of the attribute with the given key, if it exists and can be downcast to the requested type. + pub fn get_mut(&mut self, key: &str) -> Option<&mut T> { + self.entries.iter_mut().find_map(|(k, v)| if k == key { (**v).as_any_mut().downcast_mut::() } else { None }) + } + + /// Gets a mutable reference to the value, inserting a default if it doesn't exist or has the wrong type. + pub fn get_or_insert_default_mut(&mut self, key: &str) -> &mut T { + // Remove any existing entry with the wrong type, then insert a correctly-typed default + let needs_insert = match self.entries.iter().position(|(k, _)| k == key) { + Some(index) => { + if (*self.entries[index].1).as_any().downcast_ref::().is_some() { + false + } else { + self.entries.remove(index); + true + } + } + None => true, + }; + + if needs_insert { + self.entries.push((key.to_string(), Box::new(T::default()))); + } + + self.get_mut::(key).expect("attribute was just ensured to exist with correct type") + } + + /// Removes and returns the value for the given key, if it exists and can be downcast to the requested type. + pub fn remove(&mut self, key: &str) -> Option { + let index = self.entries.iter().position(|(k, _)| k == key)?; + let (_, value) = self.entries.remove(index); + value.into_any().downcast::().ok().map(|b| *b) + } +} + +// TABLE + #[derive(Clone, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Table { - #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] element: Vec, - transform: Vec, - alpha_blending: Vec, - source_node_id: Vec>, + attributes: Attributes, } impl Table { @@ -22,44 +131,52 @@ impl Table { } pub fn with_capacity(capacity: usize) -> Self { + let mut attributes = Attributes::new(); + attributes.insert("transform".to_string(), Vec::::with_capacity(capacity)); + attributes.insert("alpha_blending".to_string(), Vec::::with_capacity(capacity)); + attributes.insert("source_node_id".to_string(), Vec::>::with_capacity(capacity)); + Self { element: Vec::with_capacity(capacity), - transform: Vec::with_capacity(capacity), - alpha_blending: Vec::with_capacity(capacity), - source_node_id: Vec::with_capacity(capacity), + attributes, } } pub fn new_from_element(element: T) -> Self { - Self { - element: vec![element], - transform: vec![DAffine2::IDENTITY], - alpha_blending: vec![AlphaBlending::default()], - source_node_id: vec![None], - } + let mut attributes = Attributes::new(); + attributes.insert("transform".to_string(), vec![DAffine2::IDENTITY]); + attributes.insert("alpha_blending".to_string(), vec![AlphaBlending::default()]); + attributes.insert("source_node_id".to_string(), vec![Option::::None]); + + Self { element: vec![element], attributes } } pub fn new_from_row(row: TableRow) -> Self { + let mut attributes = Attributes::new(); + attributes.insert("transform".to_string(), vec![row.transform]); + attributes.insert("alpha_blending".to_string(), vec![row.alpha_blending]); + attributes.insert("source_node_id".to_string(), vec![row.source_node_id]); + Self { element: vec![row.element], - transform: vec![row.transform], - alpha_blending: vec![row.alpha_blending], - source_node_id: vec![row.source_node_id], + attributes, } } pub fn push(&mut self, row: TableRow) { self.element.push(row.element); - self.transform.push(row.transform); - self.alpha_blending.push(row.alpha_blending); - self.source_node_id.push(row.source_node_id); + self.transforms_mut().push(row.transform); + self.alpha_blendings_mut().push(row.alpha_blending); + self.source_node_ids_mut().push(row.source_node_id); } pub fn extend(&mut self, table: Table) { + let mut other_attributes = table.attributes; + self.element.extend(table.element); - self.transform.extend(table.transform); - self.alpha_blending.extend(table.alpha_blending); - self.source_node_id.extend(table.source_node_id); + self.transforms_mut().extend(other_attributes.remove::>("transform").unwrap_or_default()); + self.alpha_blendings_mut().extend(other_attributes.remove::>("alpha_blending").unwrap_or_default()); + self.source_node_ids_mut().extend(other_attributes.remove::>>("source_node_id").unwrap_or_default()); } pub fn get(&self, index: usize) -> Option> { @@ -69,9 +186,9 @@ impl Table { Some(TableRowRef { element: &self.element[index], - transform: &self.transform[index], - alpha_blending: &self.alpha_blending[index], - source_node_id: &self.source_node_id[index], + transform: &self.transforms()[index], + alpha_blending: &self.alpha_blendings()[index], + source_node_id: &self.source_node_ids()[index], }) } @@ -80,11 +197,21 @@ impl Table { return None; } + // Split borrows: element from the vec, attributes from the Attributes map + let element = &mut self.element[index] as *mut T; + let transforms = self.transforms_mut(); + let transform = &mut transforms[index] as *mut DAffine2; + let alpha_blendings = self.alpha_blendings_mut(); + let alpha_blending = &mut alpha_blendings[index] as *mut AlphaBlending; + let source_node_ids = self.source_node_ids_mut(); + let source_node_id = &mut source_node_ids[index] as *mut Option; + + // SAFETY: All pointers come from distinct Vecs in self, so they don't alias Some(TableRowMut { - element: &mut self.element[index], - transform: &mut self.transform[index], - alpha_blending: &mut self.alpha_blending[index], - source_node_id: &mut self.source_node_id[index], + element: unsafe { &mut *element }, + transform: unsafe { &mut *transform }, + alpha_blending: unsafe { &mut *alpha_blending }, + source_node_id: unsafe { &mut *source_node_id }, }) } @@ -100,9 +227,9 @@ impl Table { pub fn iter(&self) -> impl DoubleEndedIterator> + Clone { self.element .iter() - .zip(self.transform.iter()) - .zip(self.alpha_blending.iter()) - .zip(self.source_node_id.iter()) + .zip(self.transforms().iter()) + .zip(self.alpha_blendings().iter()) + .zip(self.source_node_ids().iter()) .map(|(((element, transform), alpha_blending), source_node_id)| TableRowRef { element, transform, @@ -113,11 +240,16 @@ impl Table { /// Mutably borrows a [`Table`] and returns an iterator of [`TableRowMut`]s, each containing mutable references to the data of the respective row from the table. pub fn iter_mut(&mut self) -> impl DoubleEndedIterator> { + let transforms = self.transforms_mut() as *mut Vec; + let alpha_blendings = self.alpha_blendings_mut() as *mut Vec; + let source_node_ids = self.source_node_ids_mut() as *mut Vec>; + + // SAFETY: Each Vec is a distinct allocation within Attributes, so mutable references to their elements don't alias self.element .iter_mut() - .zip(self.transform.iter_mut()) - .zip(self.alpha_blending.iter_mut()) - .zip(self.source_node_id.iter_mut()) + .zip(unsafe { &mut *transforms }.iter_mut()) + .zip(unsafe { &mut *alpha_blendings }.iter_mut()) + .zip(unsafe { &mut *source_node_ids }.iter_mut()) .map(|(((element, transform), alpha_blending), source_node_id)| TableRowMut { element, transform, @@ -125,8 +257,96 @@ impl Table { source_node_id, }) } + + // Convenience accessors for the well-known attribute columns + + pub fn transforms(&self) -> &[DAffine2] { + self.attributes.get::>("transform").map(Vec::as_slice).unwrap_or(&[]) + } + + pub fn transforms_mut(&mut self) -> &mut Vec { + self.attributes.get_or_insert_default_mut::>("transform") + } + + pub fn alpha_blendings(&self) -> &[AlphaBlending] { + self.attributes.get::>("alpha_blending").map(Vec::as_slice).unwrap_or(&[]) + } + + pub fn alpha_blendings_mut(&mut self) -> &mut Vec { + self.attributes.get_or_insert_default_mut::>("alpha_blending") + } + + pub fn source_node_ids(&self) -> &[Option] { + self.attributes.get::>>("source_node_id").map(Vec::as_slice).unwrap_or(&[]) + } + + pub fn source_node_ids_mut(&mut self) -> &mut Vec> { + self.attributes.get_or_insert_default_mut::>>("source_node_id") + } } +// CUSTOM SERDE + +#[cfg(feature = "serde")] +impl serde::Serialize for Table { + fn serialize(&self, serializer: S) -> Result { + #[derive(serde::Serialize)] + struct TableHelper<'a, T: serde::Serialize> { + element: &'a Vec, + transform: &'a [DAffine2], + alpha_blending: &'a [AlphaBlending], + source_node_id: &'a [Option], + } + + TableHelper { + element: &self.element, + transform: self.transforms(), + alpha_blending: self.alpha_blendings(), + source_node_id: self.source_node_ids(), + } + .serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Table { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct TableHelper { + #[serde(alias = "instances", alias = "instance")] + element: Vec, + #[serde(default)] + transform: Vec, + #[serde(default)] + alpha_blending: Vec, + #[serde(default)] + source_node_id: Vec>, + } + + let helper = TableHelper::deserialize(deserializer)?; + let length = helper.element.len(); + + // Pad attribute vecs to match element length if they're shorter (e.g., from older save formats) + let mut transform = helper.transform; + transform.resize(length, DAffine2::IDENTITY); + + let mut alpha_blending = helper.alpha_blending; + alpha_blending.resize(length, AlphaBlending::default()); + + let mut source_node_id = helper.source_node_id; + source_node_id.resize(length, None); + + let mut attributes = Attributes::new(); + attributes.insert("transform".to_string(), transform); + attributes.insert("alpha_blending".to_string(), alpha_blending); + attributes.insert("source_node_id".to_string(), source_node_id); + + Ok(Table { element: helper.element, attributes }) + } +} + +// TRAIT IMPLS + impl BoundingBox for Table { fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { let mut combined_bounds = None; @@ -155,11 +375,13 @@ impl IntoIterator for Table { /// Consumes a [`Table`] and returns an iterator of [`TableRow`]s, each containing the owned data of the respective row from the original table. fn into_iter(self) -> Self::IntoIter { + let mut attributes = self.attributes; + TableRowIter { element: self.element.into_iter(), - transform: self.transform.into_iter(), - alpha_blending: self.alpha_blending.into_iter(), - source_node_id: self.source_node_id.into_iter(), + transform: attributes.remove::>("transform").unwrap_or_default().into_iter(), + alpha_blending: attributes.remove::>("alpha_blending").unwrap_or_default().into_iter(), + source_node_id: attributes.remove::>>("source_node_id").unwrap_or_default().into_iter(), } } } @@ -170,6 +392,7 @@ pub struct TableRowIter { alpha_blending: std::vec::IntoIter, source_node_id: std::vec::IntoIter>, } + impl Iterator for TableRowIter { type Item = TableRow; @@ -188,14 +411,30 @@ impl Iterator for TableRowIter { } } +impl DoubleEndedIterator for TableRowIter { + fn next_back(&mut self) -> Option { + let element = self.element.next_back()?; + let transform = self.transform.next_back()?; + let alpha_blending = self.alpha_blending.next_back()?; + let source_node_id = self.source_node_id.next_back()?; + + Some(TableRow { + element, + transform, + alpha_blending, + source_node_id, + }) + } +} + impl Default for Table { fn default() -> Self { - Self { - element: Vec::new(), - transform: Vec::new(), - alpha_blending: Vec::new(), - source_node_id: Vec::new(), - } + let mut attributes = Attributes::new(); + attributes.insert("transform".to_string(), Vec::::new()); + attributes.insert("alpha_blending".to_string(), Vec::::new()); + attributes.insert("source_node_id".to_string(), Vec::>::new()); + + Self { element: Vec::new(), attributes } } } @@ -204,10 +443,10 @@ impl Hash for Table { for element in &self.element { element.hash(state); } - for transform in &self.transform { + for transform in self.transforms() { transform.to_cols_array().map(|x| x.to_bits()).hash(state); } - for alpha_blending in &self.alpha_blending { + for alpha_blending in self.alpha_blendings() { alpha_blending.hash(state); } } @@ -215,19 +454,19 @@ impl Hash for Table { impl PartialEq for Table { fn eq(&self, other: &Self) -> bool { - self.element == other.element && self.transform == other.transform && self.alpha_blending == other.alpha_blending + self.element == other.element && self.transforms() == other.transforms() && self.alpha_blendings() == other.alpha_blendings() } } impl ApplyTransform for Table { fn apply_transform(&mut self, modification: &DAffine2) { - for transform in &mut self.transform { + for transform in self.transforms_mut() { *transform *= *modification; } } fn left_apply_transform(&mut self, modification: &DAffine2) { - for transform in &mut self.transform { + for transform in self.transforms_mut() { *transform = *modification * *transform; } } @@ -249,16 +488,66 @@ impl FromIterator> for Table { } } +// TABLE ROW TYPES + #[derive(Copy, Clone, Default, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TableRow { - #[cfg_attr(feature = "serde", serde(alias = "instance"))] pub element: T, pub transform: DAffine2, pub alpha_blending: AlphaBlending, pub source_node_id: Option, } +#[cfg(feature = "serde")] +impl serde::Serialize for TableRow { + fn serialize(&self, serializer: S) -> Result { + #[derive(serde::Serialize)] + struct TableRowHelper<'a, T: serde::Serialize> { + element: &'a T, + transform: &'a DAffine2, + alpha_blending: &'a AlphaBlending, + source_node_id: &'a Option, + } + + TableRowHelper { + element: &self.element, + transform: &self.transform, + alpha_blending: &self.alpha_blending, + source_node_id: &self.source_node_id, + } + .serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for TableRow { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + struct TableRowHelper { + #[serde(alias = "instance")] + element: T, + #[serde(default = "default_transform")] + transform: DAffine2, + #[serde(default)] + alpha_blending: AlphaBlending, + #[serde(default)] + source_node_id: Option, + } + + fn default_transform() -> DAffine2 { + DAffine2::IDENTITY + } + + let helper = TableRowHelper::deserialize(deserializer)?; + Ok(TableRow { + element: helper.element, + transform: helper.transform, + alpha_blending: helper.alpha_blending, + source_node_id: helper.source_node_id, + }) + } +} + impl TableRow { pub fn new_from_element(element: T) -> Self { Self { From 4530a157819180c4d1c0144bd6b40b5435c712c2 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 21 Apr 2026 21:39:49 -0700 Subject: [PATCH 3/7] Encapsulate TableRow/TableRowRef/TableRowMut attribute fields behind accessor methods --- .../data_panel/data_panel_message_handler.rs | 6 +- .../document/overlays/utility_types_native.rs | 2 +- node-graph/libraries/core-types/src/ops.rs | 10 +- node-graph/libraries/core-types/src/table.rs | 179 +++++++++++------ .../libraries/graphic-types/src/artboard.rs | 14 +- .../libraries/graphic-types/src/graphic.rs | 77 +++---- node-graph/libraries/graphic-types/src/lib.rs | 18 +- .../libraries/raster-types/src/image.rs | 46 ++--- .../libraries/rendering/src/renderer.rs | 126 ++++++------ .../vector-types/src/vector/style.rs | 2 +- .../per_pixel_adjust_runtime.rs | 7 +- .../wgpu-executor/src/texture_conversion.rs | 27 +-- node-graph/nodes/blending/src/lib.rs | 80 ++++---- node-graph/nodes/brush/src/brush.rs | 28 +-- node-graph/nodes/brush/src/brush_cache.rs | 6 +- node-graph/nodes/graphic/src/graphic.rs | 26 ++- .../nodes/gstd/src/platform_application_io.rs | 10 +- node-graph/nodes/path-bool/src/lib.rs | 55 ++--- node-graph/nodes/raster/src/std_nodes.rs | 76 ++++--- node-graph/nodes/repeat/src/repeat_nodes.rs | 23 ++- node-graph/nodes/text/src/path_builder.rs | 12 +- .../nodes/transform/src/transform_nodes.rs | 20 +- .../vector/src/vector_modification_nodes.rs | 16 +- node-graph/nodes/vector/src/vector_nodes.rs | 190 ++++++++---------- 24 files changed, 508 insertions(+), 548 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index e58d93cd8d..b832c12efd 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -265,9 +265,9 @@ impl TableRowLayout for Table { vec![ TextLabel::new(format!("{index}")).narrow(true).widget_instance(), row.element.element_widget(index), - TextLabel::new(format_transform_matrix(row.transform)).narrow(true).widget_instance(), - TextLabel::new(format!("{}", row.alpha_blending)).narrow(true).widget_instance(), - TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))) + TextLabel::new(format_transform_matrix(row.transform())).narrow(true).widget_instance(), + TextLabel::new(format!("{}", row.alpha_blending())).narrow(true).widget_instance(), + TextLabel::new(row.source_node_id().map_or_else(|| "-".to_string(), |id| format!("{}", id.0))) .narrow(true) .widget_instance(), ] diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 9991d39c57..0e922259d7 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -1175,7 +1175,7 @@ impl OverlayContextInternal { let move_to = last_point != Some(start_id); last_point = Some(end_id); - self.bezier_to_path(bezier, *row.transform, move_to, &mut path); + self.bezier_to_path(bezier, *row.transform(), move_to, &mut path); } // Render the path diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index 9221f6dc6d..456a90ffed 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -62,11 +62,11 @@ impl + Send> Convert, ()> for Table { async fn convert(self, _: Footprint, _: ()) -> Table { let table: Table = self .into_iter() - .map(|row| TableRow { - element: row.element.convert_row(), - transform: row.transform, - alpha_blending: row.alpha_blending, - source_node_id: row.source_node_id, + .map(|row| { + let transform = *row.transform(); + let alpha_blending = *row.alpha_blending(); + let source_node_id = *row.source_node_id(); + TableRow::new(row.element.convert_row(), transform, alpha_blending, source_node_id) }) .collect(); table diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 34ccabf716..9ae50f7055 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -152,10 +152,15 @@ impl Table { } pub fn new_from_row(row: TableRow) -> Self { + let mut row_attributes = row.attributes; + let transform = row_attributes.remove::("transform").unwrap_or(DAffine2::IDENTITY); + let alpha_blending = row_attributes.remove::("alpha_blending").unwrap_or_default(); + let source_node_id = row_attributes.remove::>("source_node_id").unwrap_or(None); + let mut attributes = Attributes::new(); - attributes.insert("transform".to_string(), vec![row.transform]); - attributes.insert("alpha_blending".to_string(), vec![row.alpha_blending]); - attributes.insert("source_node_id".to_string(), vec![row.source_node_id]); + attributes.insert("transform".to_string(), vec![transform]); + attributes.insert("alpha_blending".to_string(), vec![alpha_blending]); + attributes.insert("source_node_id".to_string(), vec![source_node_id]); Self { element: vec![row.element], @@ -164,10 +169,11 @@ impl Table { } pub fn push(&mut self, row: TableRow) { + let mut attributes = row.attributes; self.element.push(row.element); - self.transforms_mut().push(row.transform); - self.alpha_blendings_mut().push(row.alpha_blending); - self.source_node_ids_mut().push(row.source_node_id); + self.transforms_mut().push(attributes.remove::("transform").unwrap_or(DAffine2::IDENTITY)); + self.alpha_blendings_mut().push(attributes.remove::("alpha_blending").unwrap_or_default()); + self.source_node_ids_mut().push(attributes.remove::>("source_node_id").unwrap_or(None)); } pub fn extend(&mut self, table: Table) { @@ -352,7 +358,7 @@ impl BoundingBox for Table { let mut combined_bounds = None; for row in self.iter() { - match row.element.bounding_box(transform * *row.transform, include_stroke) { + match row.element.bounding_box(transform * *row.transform(), include_stroke) { RenderBoundingBox::None => continue, RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite, RenderBoundingBox::Rectangle(bounds) => match combined_bounds { @@ -402,12 +408,7 @@ impl Iterator for TableRowIter { let alpha_blending = self.alpha_blending.next()?; let source_node_id = self.source_node_id.next()?; - Some(TableRow { - element, - transform, - alpha_blending, - source_node_id, - }) + Some(TableRow::new(element, transform, alpha_blending, source_node_id)) } } @@ -418,12 +419,7 @@ impl DoubleEndedIterator for TableRowIter { let alpha_blending = self.alpha_blending.next_back()?; let source_node_id = self.source_node_id.next_back()?; - Some(TableRow { - element, - transform, - alpha_blending, - source_node_id, - }) + Some(TableRow::new(element, transform, alpha_blending, source_node_id)) } } @@ -490,12 +486,22 @@ impl FromIterator> for Table { // TABLE ROW TYPES -#[derive(Copy, Clone, Default, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct TableRow { pub element: T, - pub transform: DAffine2, - pub alpha_blending: AlphaBlending, - pub source_node_id: Option, + attributes: Attributes, +} + +impl Default for TableRow { + fn default() -> Self { + Self::new_from_element(T::default()) + } +} + +impl PartialEq for TableRow { + fn eq(&self, other: &Self) -> bool { + self.element == other.element && self.transform() == other.transform() && self.alpha_blending() == other.alpha_blending() && self.source_node_id() == other.source_node_id() + } } #[cfg(feature = "serde")] @@ -511,9 +517,9 @@ impl serde::Serialize for TableRow { TableRowHelper { element: &self.element, - transform: &self.transform, - alpha_blending: &self.alpha_blending, - source_node_id: &self.source_node_id, + transform: self.transform(), + alpha_blending: self.alpha_blending(), + source_node_id: self.source_node_id(), } .serialize(serializer) } @@ -539,40 +545,56 @@ impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for TableRow { } let helper = TableRowHelper::deserialize(deserializer)?; - Ok(TableRow { - element: helper.element, - transform: helper.transform, - alpha_blending: helper.alpha_blending, - source_node_id: helper.source_node_id, - }) + Ok(TableRow::new(helper.element, helper.transform, helper.alpha_blending, helper.source_node_id)) } } impl TableRow { + pub fn new(element: T, transform: DAffine2, alpha_blending: AlphaBlending, source_node_id: Option) -> Self { + let mut attributes = Attributes::new(); + attributes.insert("transform".to_string(), transform); + attributes.insert("alpha_blending".to_string(), alpha_blending); + attributes.insert("source_node_id".to_string(), source_node_id); + Self { element, attributes } + } + pub fn new_from_element(element: T) -> Self { - Self { - element, - transform: DAffine2::IDENTITY, - alpha_blending: AlphaBlending::default(), - source_node_id: None, - } + Self::new(element, DAffine2::IDENTITY, AlphaBlending::default(), None) + } + + pub fn transform(&self) -> &DAffine2 { + static DEFAULT: DAffine2 = DAffine2::IDENTITY; + self.attributes.get::("transform").unwrap_or(&DEFAULT) + } + + pub fn transform_mut(&mut self) -> &mut DAffine2 { + self.attributes.get_or_insert_default_mut::("transform") + } + + pub fn alpha_blending(&self) -> &AlphaBlending { + static DEFAULT: AlphaBlending = AlphaBlending::new(); + self.attributes.get::("alpha_blending").unwrap_or(&DEFAULT) + } + + pub fn alpha_blending_mut(&mut self) -> &mut AlphaBlending { + self.attributes.get_or_insert_default_mut::("alpha_blending") + } + + pub fn source_node_id(&self) -> &Option { + static DEFAULT: Option = None; + self.attributes.get::>("source_node_id").unwrap_or(&DEFAULT) + } + + pub fn source_node_id_mut(&mut self) -> &mut Option { + self.attributes.get_or_insert_default_mut::>("source_node_id") } pub fn as_ref(&self) -> TableRowRef<'_, T> { TableRowRef { element: &self.element, - transform: &self.transform, - alpha_blending: &self.alpha_blending, - source_node_id: &self.source_node_id, - } - } - - pub fn as_mut(&mut self) -> TableRowMut<'_, T> { - TableRowMut { - element: &mut self.element, - transform: &mut self.transform, - alpha_blending: &mut self.alpha_blending, - source_node_id: &mut self.source_node_id, + transform: self.transform(), + alpha_blending: self.alpha_blending(), + source_node_id: self.source_node_id(), } } } @@ -580,31 +602,64 @@ impl TableRow { #[derive(Copy, Clone, Debug, PartialEq)] pub struct TableRowRef<'a, T> { pub element: &'a T, - pub transform: &'a DAffine2, - pub alpha_blending: &'a AlphaBlending, - pub source_node_id: &'a Option, + transform: &'a DAffine2, + alpha_blending: &'a AlphaBlending, + source_node_id: &'a Option, } impl TableRowRef<'_, T> { + pub fn transform(&self) -> &DAffine2 { + self.transform + } + + pub fn alpha_blending(&self) -> &AlphaBlending { + self.alpha_blending + } + + pub fn source_node_id(&self) -> &Option { + self.source_node_id + } + pub fn into_cloned(self) -> TableRow where T: Clone, { - TableRow { - element: self.element.clone(), - transform: *self.transform, - alpha_blending: *self.alpha_blending, - source_node_id: *self.source_node_id, - } + TableRow::new(self.element.clone(), *self.transform, *self.alpha_blending, *self.source_node_id) } } #[derive(Debug)] pub struct TableRowMut<'a, T> { pub element: &'a mut T, - pub transform: &'a mut DAffine2, - pub alpha_blending: &'a mut AlphaBlending, - pub source_node_id: &'a mut Option, + transform: &'a mut DAffine2, + alpha_blending: &'a mut AlphaBlending, + source_node_id: &'a mut Option, +} + +impl TableRowMut<'_, T> { + pub fn transform(&self) -> &DAffine2 { + self.transform + } + + pub fn transform_mut(&mut self) -> &mut DAffine2 { + self.transform + } + + pub fn alpha_blending(&self) -> &AlphaBlending { + self.alpha_blending + } + + pub fn alpha_blending_mut(&mut self) -> &mut AlphaBlending { + self.alpha_blending + } + + pub fn source_node_id(&self) -> &Option { + self.source_node_id + } + + pub fn source_node_id_mut(&mut self) -> &mut Option { + self.source_node_id + } } // Conversion from Table to Option - extracts first element diff --git a/node-graph/libraries/graphic-types/src/artboard.rs b/node-graph/libraries/graphic-types/src/artboard.rs index eca77b66a0..119caba646 100644 --- a/node-graph/libraries/graphic-types/src/artboard.rs +++ b/node-graph/libraries/graphic-types/src/artboard.rs @@ -104,12 +104,7 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re ArtboardFormat::ArtboardGroup(artboard_group) => { let mut table = Table::new(); for (artboard, source_node_id) in artboard_group.artboards { - table.push(TableRow { - element: artboard, - transform: DAffine2::IDENTITY, - alpha_blending: AlphaBlending::default(), - source_node_id, - }); + table.push(TableRow::new(artboard, DAffine2::IDENTITY, AlphaBlending::default(), source_node_id)); } table } @@ -117,12 +112,7 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re .element .into_iter() .zip(old_table.transform.into_iter().zip(old_table.alpha_blending)) - .map(|(element, (transform, alpha_blending))| TableRow { - element, - transform, - alpha_blending, - source_node_id: None, - }) + .map(|(element, (transform, alpha_blending))| TableRow::new(element, transform, alpha_blending, None)) .collect(), ArtboardFormat::ArtboardTable(artboard_table) => artboard_table, }) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index b0617ae7ac..8490bebab5 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -119,14 +119,16 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic fn flatten_recursive(output: &mut Table, current_graphic_table: Table, extract_variant: fn(Graphic) -> Option>) { for current_graphic_row in current_graphic_table.into_iter() { - let source_node_id = current_graphic_row.source_node_id; + let source_node_id = *current_graphic_row.source_node_id(); + let current_transform = *current_graphic_row.transform(); + let current_alpha_blending = *current_graphic_row.alpha_blending(); match current_graphic_row.element { // Recurse into nested graphic tables, composing the parent's transform onto each child Graphic::Graphic(mut sub_table) => { - for graphic in sub_table.iter_mut() { - *graphic.transform = current_graphic_row.transform * *graphic.transform; - *graphic.alpha_blending = compose_alpha_blending(current_graphic_row.alpha_blending, *graphic.alpha_blending); + for mut graphic in sub_table.iter_mut() { + *graphic.transform_mut() = current_transform * *graphic.transform(); + *graphic.alpha_blending_mut() = compose_alpha_blending(current_alpha_blending, *graphic.alpha_blending()); } flatten_recursive(output, sub_table, extract_variant); @@ -135,12 +137,9 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic other => { if let Some(typed_table) = extract_variant(other) { for row in typed_table.into_iter() { - output.push(TableRow { - element: row.element, - transform: current_graphic_row.transform * row.transform, - alpha_blending: compose_alpha_blending(current_graphic_row.alpha_blending, row.alpha_blending), - source_node_id, - }); + let transform = current_transform * *row.transform(); + let alpha_blending = compose_alpha_blending(current_alpha_blending, *row.alpha_blending()); + output.push(TableRow::new(row.element, transform, alpha_blending, source_node_id)); } } } @@ -291,12 +290,12 @@ impl Graphic { pub fn had_clip_enabled(&self) -> bool { match self { - Graphic::Vector(vector) => vector.iter().all(|row| row.alpha_blending.clip), - Graphic::Graphic(graphic) => graphic.iter().all(|row| row.alpha_blending.clip), - Graphic::RasterCPU(raster) => raster.iter().all(|row| row.alpha_blending.clip), - Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending.clip), - Graphic::Color(color) => color.iter().all(|row| row.alpha_blending.clip), - Graphic::Gradient(gradient) => gradient.iter().all(|row| row.alpha_blending.clip), + Graphic::Vector(vector) => vector.iter().all(|row| row.alpha_blending().clip), + Graphic::Graphic(graphic) => graphic.iter().all(|row| row.alpha_blending().clip), + Graphic::RasterCPU(raster) => raster.iter().all(|row| row.alpha_blending().clip), + Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending().clip), + Graphic::Color(color) => color.iter().all(|row| row.alpha_blending().clip), + Graphic::Gradient(gradient) => gradient.iter().all(|row| row.alpha_blending().clip), } } @@ -304,7 +303,7 @@ impl Graphic { match self { Graphic::Vector(vector) => vector.iter().all(|row| { let style = &row.element.style; - let alpha_blending = &row.alpha_blending; + let alpha_blending = row.alpha_blending(); (alpha_blending.opacity > 1. - f32::EPSILON) && style.fill().is_opaque() && style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) }), _ => false, @@ -482,12 +481,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res GraphicFormat::OldGraphicGroup(old) => { let mut graphic_table = Table::new(); for (graphic, source_node_id) in old.elements { - graphic_table.push(TableRow { - element: graphic, - transform: old.transform, - alpha_blending: old.alpha_blending, - source_node_id, - }); + graphic_table.push(TableRow::new(graphic, old.transform, old.alpha_blending, source_node_id)); } graphic_table } @@ -495,36 +489,30 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res .element .into_iter() .flat_map(|element| { - element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow { - element: graphic, - transform: element.transform, - alpha_blending: element.alpha_blending, - source_node_id, - }) + element + .elements + .into_iter() + .map(move |(graphic, source_node_id)| TableRow::new(graphic, element.transform, element.alpha_blending, source_node_id)) }) .collect(), GraphicFormat::OldTableOldGraphicGroup(old) => old .element .into_iter() .flat_map(|element| { - element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow { - element: graphic, - transform: element.transform, - alpha_blending: element.alpha_blending, - source_node_id, - }) + element + .elements + .into_iter() + .map(move |(graphic, source_node_id)| TableRow::new(graphic, element.transform, element.alpha_blending, source_node_id)) }) .collect(), GraphicFormat::OldTableGraphicGroup(old) => old .element .into_iter() .flat_map(|element| { - element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow { - element: graphic, - transform: Default::default(), - alpha_blending: Default::default(), - source_node_id, - }) + element + .elements + .into_iter() + .map(move |(graphic, source_node_id)| TableRow::new(graphic, Default::default(), Default::default(), source_node_id)) }) .collect(), GraphicFormat::Table(value) => { @@ -533,12 +521,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res let mut graphic_table = Table::new(); for row in old_table.iter() { for (graphic, source_node_id) in &row.element.elements { - graphic_table.push(TableRow { - element: graphic.clone(), - transform: *row.transform, - alpha_blending: *row.alpha_blending, - source_node_id: *source_node_id, - }); + graphic_table.push(TableRow::new(graphic.clone(), *row.transform(), *row.alpha_blending(), *source_node_id)); } } graphic_table diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 598f82fda7..953826c195 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -81,21 +81,21 @@ pub mod migrations { region_domain: old.region_domain, upstream_data: old.upstream_graphic_group, }); - *vector_table.iter_mut().next().unwrap().transform = old.transform; - *vector_table.iter_mut().next().unwrap().alpha_blending = old.alpha_blending; + let mut row = vector_table.iter_mut().next().unwrap(); + *row.transform_mut() = old.transform; + *row.alpha_blending_mut() = old.alpha_blending; vector_table } - VectorFormat::OlderVectorTable(older_table) => older_table.element.into_iter().map(|element| TableRow { element, ..Default::default() }).collect(), + VectorFormat::OlderVectorTable(older_table) => older_table + .element + .into_iter() + .map(|element| TableRow::new(element, DAffine2::IDENTITY, AlphaBlending::default(), None)) + .collect(), VectorFormat::OldVectorTable(old_table) => old_table .element .into_iter() .zip(old_table.transform.into_iter().zip(old_table.alpha_blending)) - .map(|(element, (transform, alpha_blending))| TableRow { - element, - transform, - alpha_blending, - source_node_id: None, - }) + .map(|(element, (transform, alpha_blending))| TableRow::new(element, transform, alpha_blending, None)) .collect(), VectorFormat::VectorTable(vector_table) => vector_table, }) diff --git a/node-graph/libraries/raster-types/src/image.rs b/node-graph/libraries/raster-types/src/image.rs index 7caac14be7..7eebe2b7ef 100644 --- a/node-graph/libraries/raster-types/src/image.rs +++ b/node-graph/libraries/raster-types/src/image.rs @@ -323,12 +323,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> .element .into_iter() .zip(old_table.transform.into_iter().zip(old_table.alpha_blending)) - .map(|(element, (transform, alpha_blending))| TableRow { - element, - transform, - alpha_blending, - source_node_id: None, - }) + .map(|(element, (transform, alpha_blending))| TableRow::new(element, transform, alpha_blending, None)) .collect() } @@ -336,12 +331,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> old_table .element .into_iter() - .map(|element| TableRow { - element, - transform: DAffine2::IDENTITY, - alpha_blending: AlphaBlending::default(), - source_node_id: None, - }) + .map(|element| TableRow::new(element, DAffine2::IDENTITY, AlphaBlending::default(), None)) .collect() } @@ -361,8 +351,9 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> FormatVersions::Image(image) => Table::new_from_element(Raster::new_cpu(image)), FormatVersions::OldImageFrame(OldImageFrame { image, transform, alpha_blending }) => { let mut image_frame_table = Table::new_from_element(Raster::new_cpu(image)); - *image_frame_table.iter_mut().next().unwrap().transform = transform; - *image_frame_table.iter_mut().next().unwrap().alpha_blending = alpha_blending; + let mut row = image_frame_table.iter_mut().next().unwrap(); + *row.transform_mut() = transform; + *row.alpha_blending_mut() = alpha_blending; image_frame_table } FormatVersions::OlderImageFrameTable(old_table) => from_image_frame_table(older_table_to_new_table(old_table)), @@ -453,20 +444,19 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D } Ok(match FormatVersions::deserialize(deserializer)? { - FormatVersions::Image(image) => TableRow { - element: Raster::new_cpu(image), - ..Default::default() - }, - FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => TableRow { - element: Raster::new_cpu(image_frame_with_transform_and_blending.image), - transform: image_frame_with_transform_and_blending.transform, - alpha_blending: image_frame_with_transform_and_blending.alpha_blending, - source_node_id: None, - }, - FormatVersions::ImageFrameTable(image_frame) => TableRow { - element: Raster::new_cpu(image_frame.iter().next().unwrap().element.image.clone()), - ..Default::default() - }, + FormatVersions::Image(image) => TableRow::new(Raster::new_cpu(image), DAffine2::IDENTITY, AlphaBlending::default(), None), + FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => TableRow::new( + Raster::new_cpu(image_frame_with_transform_and_blending.image), + image_frame_with_transform_and_blending.transform, + image_frame_with_transform_and_blending.alpha_blending, + None, + ), + FormatVersions::ImageFrameTable(image_frame) => TableRow::new( + Raster::new_cpu(image_frame.iter().next().unwrap().element.image.clone()), + DAffine2::IDENTITY, + AlphaBlending::default(), + None, + ), FormatVersions::RasterTable(image_frame_table) => image_frame_table.into_iter().next().unwrap_or_default(), FormatVersions::RasterTableRow(image_table_row) => image_table_row, }) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 04ae29d034..7d6a66d757 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -428,8 +428,8 @@ impl Render for Graphic { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.first_element_source_id.insert(element_id, *row.source_node_id); - metadata.local_transforms.insert(element_id, *row.transform); + metadata.first_element_source_id.insert(element_id, *row.source_node_id()); + metadata.local_transforms.insert(element_id, *row.transform()); } } Graphic::RasterCPU(table) => { @@ -437,7 +437,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.local_transforms.insert(element_id, *row.transform); + metadata.local_transforms.insert(element_id, *row.transform()); } } Graphic::RasterGPU(table) => { @@ -445,7 +445,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.local_transforms.insert(element_id, *row.transform); + metadata.local_transforms.insert(element_id, *row.transform()); } } Graphic::Color(table) => { @@ -453,7 +453,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.local_transforms.insert(element_id, *row.transform); + metadata.local_transforms.insert(element_id, *row.transform()); } } Graphic::Gradient(table) => { @@ -461,7 +461,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.local_transforms.insert(element_id, *row.transform); + metadata.local_transforms.insert(element_id, *row.transform()); } } } @@ -668,7 +668,7 @@ impl Render for Table { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { for row in self.iter() { - row.element.collect_metadata(metadata, footprint, *row.source_node_id); + row.element.collect_metadata(metadata, footprint, *row.source_node_id()); } } @@ -692,18 +692,18 @@ impl Render for Table { render.parent_tag( "g", |attributes| { - let matrix = format_transform_matrix(*row.transform); + let matrix = format_transform_matrix(*row.transform()); if !matrix.is_empty() { attributes.push("transform", matrix); } - let opacity = row.alpha_blending.opacity(render_params.for_mask); + let opacity = row.alpha_blending().opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending.blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending.blend_mode.render()); + if row.alpha_blending().blend_mode != BlendMode::default() { + attributes.push("style", row.alpha_blending().blend_mode.render()); } let next_clips = iter.peek().is_some_and(|next_row| next_row.element.had_clip_enabled()); @@ -740,8 +740,8 @@ impl Render for Table { let mut mask_element_and_transform = None; while let Some(row) = iter.next() { - let transform = transform * *row.transform; - let alpha_blending = *row.alpha_blending; + let transform = transform * *row.transform(); + let alpha_blending = *row.alpha_blending(); let mut layer = false; @@ -751,7 +751,7 @@ impl Render for Table { }; let mut bounds = RenderBoundingBox::None; - let opacity = row.alpha_blending.opacity(render_params.for_mask); + let opacity = row.alpha_blending().opacity(render_params.for_mask); if opacity < 1. || (render_params.render_mode != RenderMode::Outline && alpha_blending.blend_mode != BlendMode::default()) { bounds = row.element.bounding_box(transform, true); @@ -813,9 +813,9 @@ impl Render for Table { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for row in self.iter() { let mut footprint = footprint; - footprint.transform *= *row.transform; + footprint.transform *= *row.transform(); - if let Some(element_id) = row.source_node_id { + if let Some(element_id) = row.source_node_id() { row.element.collect_metadata(metadata, footprint, Some(*element_id)); } else { // Recurse through anonymous wrapper rows to reach nested content with source_node_ids @@ -831,7 +831,7 @@ impl Render for Table { row.element.add_upstream_click_targets(&mut new_click_targets); for click_target in new_click_targets.iter_mut() { - click_target.apply_transform(*row.transform) + click_target.apply_transform(*row.transform()) } all_upstream_click_targets.extend(new_click_targets); @@ -848,7 +848,7 @@ impl Render for Table { row.element.add_upstream_click_targets(&mut new_click_targets); for click_target in new_click_targets.iter_mut() { - click_target.apply_transform(*row.transform) + click_target.apply_transform(*row.transform()) } click_targets.extend(new_click_targets); @@ -861,7 +861,7 @@ impl Render for Table { fn new_ids_from_hash(&mut self, _reference: Option) { for row in self.iter_mut() { - row.element.new_ids_from_hash(*row.source_node_id); + row.element.new_ids_from_hash(*row.source_node_id()); } } } @@ -869,12 +869,12 @@ impl Render for Table { impl Render for Table { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for row in self.iter() { - let multiplied_transform = *row.transform; + let multiplied_transform = *row.transform(); let vector = &row.element; // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform let has_real_stroke = vector.style.stroke().filter(|stroke| stroke.weight() > 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.); - let applied_stroke_transform = set_stroke_transform.unwrap_or(*row.transform); + let applied_stroke_transform = set_stroke_transform.unwrap_or(*row.transform()); let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform); let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse()); let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY); @@ -932,12 +932,7 @@ impl Render for Table { element.style.clear_stroke(); element.style.set_fill(Fill::solid(Color::BLACK)); - let vector_row = Table::new_from_row(TableRow { - element, - alpha_blending: *row.alpha_blending, - transform: *row.transform, - source_node_id: None, - }); + let vector_row = Table::new_from_row(TableRow::new(element, *row.transform(), *row.alpha_blending(), None)); (id, mask_type, vector_row) }); @@ -1022,13 +1017,13 @@ impl Render for Table { attributes.push("fill-rule", "evenodd"); } - let opacity = row.alpha_blending.opacity(render_params.for_mask); + let opacity = row.alpha_blending().opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending.blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending.blend_mode.render()); + if row.alpha_blending().blend_mode != BlendMode::default() { + attributes.push("style", row.alpha_blending().blend_mode.render()); } }); @@ -1062,7 +1057,7 @@ impl Render for Table { for row in self.iter() { use graphic_types::vector_types::vector; - let multiplied_transform = parent_transform * *row.transform; + let multiplied_transform = parent_transform * *row.transform(); let has_real_stroke = row.element.style.stroke().filter(|stroke| stroke.weight() > 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.); let mut applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform); @@ -1091,12 +1086,12 @@ impl Render for Table { // If we're using opacity or a blend mode, we need to push a layer let blend_mode = match render_params.render_mode { RenderMode::Outline => peniko::Mix::Normal, - _ => row.alpha_blending.blend_mode.to_peniko(), + _ => row.alpha_blending().blend_mode.to_peniko(), }; let mut layer = false; - let opacity = row.alpha_blending.opacity(render_params.for_mask); - if opacity < 1. || row.alpha_blending.blend_mode != BlendMode::default() { + let opacity = row.alpha_blending().opacity(render_params.for_mask); + if opacity < 1. || row.alpha_blending().blend_mode != BlendMode::default() { layer = true; let weight = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width); let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform)); @@ -1247,12 +1242,7 @@ impl Render for Table { element.style.clear_stroke(); element.style.set_fill(Fill::solid(Color::BLACK)); - let vector_table = Table::new_from_row(TableRow { - element, - alpha_blending: *row.alpha_blending, - transform: *row.transform, - source_node_id: None, - }); + let vector_table = Table::new_from_row(TableRow::new(element, *row.transform(), *row.alpha_blending(), None)); let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); let weight = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width); @@ -1321,10 +1311,10 @@ impl Render for Table { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option) { for row in self.iter() { - let transform = *row.transform; + let transform = *row.transform(); let vector = row.element; - if let Some(element_id) = caller_element_id.or(*row.source_node_id) { + if let Some(element_id) = caller_element_id.or(*row.source_node_id()) { // When recovering element_id from the row's source_node_id (because the caller // passed None), also store the transform metadata that Graphic::collect_metadata // normally provides but skipped due to the None element_id. @@ -1386,7 +1376,7 @@ impl Render for Table { }; click_targets.extend(row.element.stroke_bezier_paths().map(fill).map(|subpath| { let mut click_target = ClickTarget::new_with_subpath(subpath, stroke_width); - click_target.apply_transform(*row.transform); + click_target.apply_transform(*row.transform()); click_target })); @@ -1400,7 +1390,7 @@ impl Render for Table { let point = FreePoint::new(point_id, anchor); let mut click_target = ClickTarget::new_with_free_point(point); - click_target.apply_transform(*row.transform); + click_target.apply_transform(*row.transform()); Some(click_target) }); click_targets.extend(single_anchors_targets); @@ -1419,7 +1409,7 @@ impl Render for Table> { for row in self.iter() { let image = row.element; - let transform = *row.transform; + let transform = *row.transform(); if image.data.is_empty() { continue; @@ -1446,13 +1436,13 @@ impl Render for Table> { attributes.push("width", size.x.to_string()); attributes.push("height", size.y.to_string()); - let opacity = row.alpha_blending.opacity(render_params.for_mask); + let opacity = row.alpha_blending().opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending.blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending.blend_mode.render()); + if row.alpha_blending().blend_mode != BlendMode::default() { + attributes.push("style", row.alpha_blending().blend_mode.render()); } }, |render| { @@ -1486,12 +1476,12 @@ impl Render for Table> { attributes.push("transform", matrix); } - let opacity = row.alpha_blending.opacity(render_params.for_mask); + let opacity = row.alpha_blending().opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending.blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending.blend_mode.render()); + if row.alpha_blending().blend_mode != BlendMode::default() { + attributes.push("style", row.alpha_blending().blend_mode.render()); } }); } @@ -1505,7 +1495,7 @@ impl Render for Table> { continue; } - let alpha_blending = *row.alpha_blending; + let alpha_blending = *row.alpha_blending(); let blend_mode = alpha_blending.blend_mode.to_peniko(); let opacity = alpha_blending.opacity(render_params.for_mask); @@ -1521,7 +1511,7 @@ impl Render for Table> { } if let RenderMode::Outline = render_params.render_mode { - let outline_transform = transform * *row.transform; + let outline_transform = transform * *row.transform(); draw_raster_outline(scene, &outline_transform, render_params); if layer { @@ -1531,7 +1521,7 @@ impl Render for Table> { continue; } - let image_transform = transform * *row.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); + let image_transform = transform * *row.transform() * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); let image_brush = peniko::ImageBrush::new(peniko::ImageData { data: image.to_flat_u8().0.into(), @@ -1558,7 +1548,7 @@ impl Render for Table> { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than one row of the raster table if let Some(raster) = self.iter().next() { - metadata.local_transforms.insert(element_id, *raster.transform); + metadata.local_transforms.insert(element_id, *raster.transform()); } } @@ -1577,7 +1567,7 @@ impl Render for Table> { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { for row in self.iter() { - let alpha_blending = *row.alpha_blending; + let alpha_blending = *row.alpha_blending(); let blend_mode = match render_params.render_mode { RenderMode::Outline => peniko::Mix::Normal, _ => alpha_blending.blend_mode.to_peniko(), @@ -1595,7 +1585,7 @@ impl Render for Table> { } if let RenderMode::Outline = render_params.render_mode { - let outline_transform = transform * *row.transform; + let outline_transform = transform * *row.transform(); draw_raster_outline(scene, &outline_transform, render_params); if layer { @@ -1615,7 +1605,7 @@ impl Render for Table> { alpha_type: peniko::ImageAlphaType::Alpha, }) .with_extend(peniko::Extend::Repeat); - let image_transform = transform * *row.transform * DAffine2::from_scale(1. / DVec2::new(width as f64, height as f64)); + let image_transform = transform * *row.transform() * DAffine2::from_scale(1. / DVec2::new(width as f64, height as f64)); scene.draw_image(&image, kurbo::Affine::new(image_transform.to_cols_array())); context.resource_overrides.push((image, row.element.data().clone())); @@ -1633,7 +1623,7 @@ impl Render for Table> { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than one row of the raster table if let Some(raster) = self.iter().next() { - metadata.local_transforms.insert(element_id, *raster.transform); + metadata.local_transforms.insert(element_id, *raster.transform()); } } @@ -1663,13 +1653,13 @@ impl Render for Table { attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string()); } - let opacity = row.alpha_blending.opacity(render_params.for_mask); + let opacity = row.alpha_blending().opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending.blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending.blend_mode.render()); + if row.alpha_blending().blend_mode != BlendMode::default() { + attributes.push("style", row.alpha_blending().blend_mode.render()); } }); } @@ -1679,7 +1669,7 @@ impl Render for Table { use vello::peniko; for row in self.iter() { - let alpha_blending = *row.alpha_blending; + let alpha_blending = *row.alpha_blending(); let blend_mode = alpha_blending.blend_mode.to_peniko(); let opacity = alpha_blending.opacity(render_params.for_mask); @@ -1725,7 +1715,7 @@ impl Render for Table { stop_string.push_str(" />"); } - let gradient_transform = render_params.footprint.transform * *row.transform; + let gradient_transform = render_params.footprint.transform * *row.transform(); let gradient_transform_matrix = format_transform_matrix(gradient_transform); let gradient_transform_attribute = if gradient_transform_matrix.is_empty() { String::new() @@ -1758,13 +1748,13 @@ impl Render for Table { attributes.push("fill", format!("url('#{gradient_id}')")); - let opacity = row.alpha_blending.opacity(render_params.for_mask); + let opacity = row.alpha_blending().opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending.blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending.blend_mode.render()); + if row.alpha_blending().blend_mode != BlendMode::default() { + attributes.push("style", row.alpha_blending().blend_mode.render()); } }); } @@ -1775,7 +1765,7 @@ impl Render for Table { use vello::peniko; for row in self.iter() { - let alpha_blending = *row.alpha_blending; + let alpha_blending = *row.alpha_blending(); let blend_mode = alpha_blending.blend_mode.to_peniko(); let opacity = alpha_blending.opacity(render_params.for_mask); diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index d26bd9333f..9809c57929 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -134,7 +134,7 @@ impl From> for Fill { impl From> for Fill { fn from(color: Table) -> Fill { - let alpha = color.get(0).map(|c| c.alpha_blending.opacity).unwrap_or(1.); + let alpha = color.get(0).map(|c| c.alpha_blending().opacity).unwrap_or(1.); let color: Option = color.into(); Fill::solid_or_none(color.map(|c| c.with_alpha(c.alpha() * alpha))) } diff --git a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs index d6fcf75d3b..b2f8a71a7f 100644 --- a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs +++ b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs @@ -236,12 +236,7 @@ impl PerPixelAdjustGraphicsPipeline { rp.set_bind_group(0, Some(&bind_group), &[]); rp.draw(0..3, 0..1); - TableRow { - element: Raster::new(GPU { texture: tex_out }), - transform: *instance.transform, - alpha_blending: *instance.alpha_blending, - source_node_id: *instance.source_node_id, - } + TableRow::new(Raster::new(GPU { texture: tex_out }), *instance.transform(), *instance.alpha_blending(), *instance.source_node_id()) }) .collect::>(); context.queue.submit([cmd.finish()]); diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index 4e8ed8461a..bc15db155a 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -155,12 +155,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { let image = row.element; let texture = upload_to_texture(device, queue, image); - TableRow { - element: Raster::new_gpu(texture), - transform: *row.transform, - alpha_blending: *row.alpha_blending, - source_node_id: *row.source_node_id, - } + TableRow::new(Raster::new_gpu(texture), *row.transform(), *row.alpha_blending(), *row.source_node_id()) }) .collect(); @@ -204,14 +199,11 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { let mut rows_meta = Vec::new(); for row in self { - let gpu_raster = row.element; - converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, gpu_raster)); - rows_meta.push(TableRow { - element: (), - transform: row.transform, - alpha_blending: row.alpha_blending, - source_node_id: row.source_node_id, - }); + let transform = *row.transform(); + let alpha_blending = *row.alpha_blending(); + let source_node_id = *row.source_node_id(); + converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, row.element)); + rows_meta.push(TableRow::new((), transform, alpha_blending, source_node_id)); } queue.submit([encoder.finish()]); @@ -229,12 +221,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { map_results .into_iter() .zip(rows_meta.into_iter()) - .map(|(element, row)| TableRow { - element, - transform: row.transform, - alpha_blending: row.alpha_blending, - source_node_id: row.source_node_id, - }) + .map(|(element, row)| TableRow::new(element, *row.transform(), *row.alpha_blending(), *row.source_node_id())) .collect() } } diff --git a/node-graph/nodes/blending/src/lib.rs b/node-graph/nodes/blending/src/lib.rs index c277fc9b44..81a5c78963 100644 --- a/node-graph/nodes/blending/src/lib.rs +++ b/node-graph/nodes/blending/src/lib.rs @@ -17,36 +17,36 @@ impl MultiplyAlpha for Color { } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.opacity *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().opacity *= factor as f32; } } } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.opacity *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().opacity *= factor as f32; } } } impl MultiplyAlpha for Table> { fn multiply_alpha(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.opacity *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().opacity *= factor as f32; } } } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.opacity *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().opacity *= factor as f32; } } } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.opacity *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().opacity *= factor as f32; } } } @@ -61,36 +61,36 @@ impl MultiplyFill for Color { } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.fill *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().fill *= factor as f32; } } } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.fill *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().fill *= factor as f32; } } } impl MultiplyFill for Table> { fn multiply_fill(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.fill *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().fill *= factor as f32; } } } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.fill *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().fill *= factor as f32; } } } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { - for row in self.iter_mut() { - row.alpha_blending.fill *= factor as f32; + for mut row in self.iter_mut() { + row.alpha_blending_mut().fill *= factor as f32; } } } @@ -101,36 +101,36 @@ trait SetBlendMode { impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for row in self.iter_mut() { - row.alpha_blending.blend_mode = blend_mode; + for mut row in self.iter_mut() { + row.alpha_blending_mut().blend_mode = blend_mode; } } } impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for row in self.iter_mut() { - row.alpha_blending.blend_mode = blend_mode; + for mut row in self.iter_mut() { + row.alpha_blending_mut().blend_mode = blend_mode; } } } impl SetBlendMode for Table> { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for row in self.iter_mut() { - row.alpha_blending.blend_mode = blend_mode; + for mut row in self.iter_mut() { + row.alpha_blending_mut().blend_mode = blend_mode; } } } impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for row in self.iter_mut() { - row.alpha_blending.blend_mode = blend_mode; + for mut row in self.iter_mut() { + row.alpha_blending_mut().blend_mode = blend_mode; } } } impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for row in self.iter_mut() { - row.alpha_blending.blend_mode = blend_mode; + for mut row in self.iter_mut() { + row.alpha_blending_mut().blend_mode = blend_mode; } } } @@ -141,36 +141,36 @@ trait SetClip { impl SetClip for Table { fn set_clip(&mut self, clip: bool) { - for row in self.iter_mut() { - row.alpha_blending.clip = clip; + for mut row in self.iter_mut() { + row.alpha_blending_mut().clip = clip; } } } impl SetClip for Table { fn set_clip(&mut self, clip: bool) { - for row in self.iter_mut() { - row.alpha_blending.clip = clip; + for mut row in self.iter_mut() { + row.alpha_blending_mut().clip = clip; } } } impl SetClip for Table> { fn set_clip(&mut self, clip: bool) { - for row in self.iter_mut() { - row.alpha_blending.clip = clip; + for mut row in self.iter_mut() { + row.alpha_blending_mut().clip = clip; } } } impl SetClip for Table { fn set_clip(&mut self, clip: bool) { - for row in self.iter_mut() { - row.alpha_blending.clip = clip; + for mut row in self.iter_mut() { + row.alpha_blending_mut().clip = clip; } } } impl SetClip for Table { fn set_clip(&mut self, clip: bool) { - for row in self.iter_mut() { - row.alpha_blending.clip = clip; + for mut row in self.iter_mut() { + row.alpha_blending_mut().clip = clip; } } } diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index 0716ca25a5..f01b1d7909 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -96,7 +96,7 @@ where let texture_size = DVec2::new(texture.width as f64, texture.height as f64); - let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * table_row.transform.inverse(); + let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * table_row.transform().inverse(); for position in &positions { let start = document_to_target.transform_point2(*position).round(); @@ -274,11 +274,7 @@ async fn brush( let has_erase_or_restore_strokes = strokes.iter().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)); if has_erase_or_restore_strokes { let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE); - let mut erase_restore_mask = TableRow { - element: Raster::new_cpu(opaque_image), - transform: background_bounds, - ..Default::default() - }; + let mut erase_restore_mask = TableRow::new(Raster::new_cpu(opaque_image), background_bounds, Default::default(), None); for stroke in strokes { let mut brush_texture = cache.get_cached_brush(&stroke.style); @@ -310,11 +306,15 @@ async fn brush( actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b))); } - let first_row = image.iter_mut().next().unwrap(); + let transform = *actual_image.transform(); + let alpha_blending = *actual_image.alpha_blending(); + let source_node_id = *actual_image.source_node_id(); + + let mut first_row = image.iter_mut().next().unwrap(); *first_row.element = actual_image.element; - *first_row.transform = actual_image.transform; - *first_row.alpha_blending = actual_image.alpha_blending; - *first_row.source_node_id = actual_image.source_node_id; + *first_row.transform_mut() = transform; + *first_row.alpha_blending_mut() = alpha_blending; + *first_row.source_node_id_mut() = source_node_id; image } @@ -324,10 +324,10 @@ pub fn blend_image_closure(foreground: TableRow>, mut background: Ta let background_size = DVec2::new(background.element.width as f64, background.element.height as f64); // Transforms a point from the background image to the foreground image - let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground.transform.inverse() * background.transform * DAffine2::from_scale(1. / background_size); + let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground.transform().inverse() * *background.transform() * DAffine2::from_scale(1. / background_size); // Footprint of the foreground image (0, 0)..(1, 1) in the background image space - let background_aabb = Bbox::unit().affine_transform(background.transform.inverse() * foreground.transform).to_axis_aligned_bbox(); + let background_aabb = Bbox::unit().affine_transform(background.transform().inverse() * *foreground.transform()).to_axis_aligned_bbox(); // Clamp the foreground image to the background image let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2(); @@ -352,10 +352,10 @@ pub fn blend_stamp_closure(foreground: BrushStampGenerator, mut backgroun let background_size = DVec2::new(background.element.width as f64, background.element.height as f64); // Transforms a point from the background image to the foreground image - let background_to_foreground = background.transform * DAffine2::from_scale(1. / background_size); + let background_to_foreground = *background.transform() * DAffine2::from_scale(1. / background_size); // Footprint of the foreground image (0, 0)..(1, 1) in the background image space - let background_aabb = Bbox::unit().affine_transform(background.transform.inverse() * foreground.transform).to_axis_aligned_bbox(); + let background_aabb = Bbox::unit().affine_transform(background.transform().inverse() * foreground.transform()).to_axis_aligned_bbox(); // Clamp the foreground image to the background image let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2(); diff --git a/node-graph/nodes/brush/src/brush_cache.rs b/node-graph/nodes/brush/src/brush_cache.rs index b19c427eb4..2af9bcb087 100644 --- a/node-graph/nodes/brush/src/brush_cache.rs +++ b/node-graph/nodes/brush/src/brush_cache.rs @@ -63,11 +63,7 @@ impl BrushCacheImpl { background = std::mem::take(&mut self.blended_image); // Check if the first non-blended stroke is an extension of the last one. - let mut first_stroke_texture = TableRow { - element: Raster::::default(), - transform: glam::DAffine2::ZERO, - ..Default::default() - }; + let mut first_stroke_texture = TableRow::new(Raster::::default(), glam::DAffine2::ZERO, Default::default(), None); let mut first_stroke_point_skip = 0; let strokes = input[num_blended_strokes..].to_vec(); if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes { diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index c370b25a5f..0f4c3379e8 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -169,7 +169,7 @@ where // Create and add mirrored instance for mut row in content.into_iter() { - row.transform = reflected_transform * row.transform; + *row.transform_mut() = reflected_transform * *row.transform(); result_table.push(row); } @@ -199,8 +199,8 @@ pub async fn source_node_id( let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied(); let mut content = content; - for row in content.iter_mut() { - *row.source_node_id = source_node_id; + for mut row in content.iter_mut() { + *row.source_node_id_mut() = source_node_id; } content @@ -241,8 +241,9 @@ pub async fn legacy_layer_extend( let source_node_id = nested_node_path.get(nested_node_path.len().wrapping_sub(2)).copied(); let mut base = base; - for row in new.into_iter() { - base.push(TableRow { source_node_id, ..row }); + for mut row in new.into_iter() { + *row.source_node_id_mut() = source_node_id; + base.push(row); } base @@ -292,7 +293,9 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table, fully_flatten fn flatten_table(output_graphic_table: &mut Table, current_graphic_table: Table, fully_flatten: bool, recursion_depth: usize) { for current_row in current_graphic_table.iter() { let current_element = current_row.element.clone(); - let reference = *current_row.source_node_id; + let reference = *current_row.source_node_id(); + let current_transform = *current_row.transform(); + let current_alpha_blending = *current_row.alpha_blending(); let recurse = fully_flatten || recursion_depth == 0; @@ -300,20 +303,15 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table, fully_flatten // If we're allowed to recurse, flatten any graphics we encounter Graphic::Graphic(mut current_element) if recurse => { // Apply the parent graphic's transform to all child elements - for graphic in current_element.iter_mut() { - *graphic.transform = *current_row.transform * *graphic.transform; + for mut graphic in current_element.iter_mut() { + *graphic.transform_mut() = current_transform * *graphic.transform(); } flatten_table(output_graphic_table, current_element, fully_flatten, recursion_depth + 1); } // Push any leaf Graphic elements we encounter, which can be either Graphic table elements beyond the recursion depth, or table elements other than Graphic tables _ => { - output_graphic_table.push(TableRow { - element: current_element, - transform: *current_row.transform, - alpha_blending: *current_row.alpha_blending, - source_node_id: reference, - }); + output_graphic_table.push(TableRow::new(current_element, current_transform, current_alpha_blending, reference)); } } } diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index c72828a9d7..5fa946b539 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -203,8 +203,8 @@ where ..Default::default() }; - for row in data.iter_mut() { - *row.transform = glam::DAffine2::from_translation(-aabb.start) * *row.transform; + for mut row in data.iter_mut() { + *row.transform_mut() = glam::DAffine2::from_translation(-aabb.start) * *row.transform(); } data.render_svg(&mut render, &render_params); render.format_svg(glam::DVec2::ZERO, size); @@ -228,9 +228,5 @@ where let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap(); let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32); - Table::new_from_row(TableRow { - element: Raster::new_cpu(image), - transform: footprint.transform, - ..Default::default() - }) + Table::new_from_row(TableRow::new(Raster::new_cpu(image), footprint.transform, Default::default(), None)) } diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 3a80a44189..f83b549045 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -37,16 +37,16 @@ async fn boolean_operation(vector: impl DoubleEndedIterator::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) { @@ -153,16 +153,17 @@ fn flatten_vector(graphic_table: &Table) -> Table { match element.element.clone() { Graphic::Vector(vector) => { // Apply the parent graphic's transform to each element of the vector table + let parent_transform = *element.transform(); vector .into_iter() .map(|mut sub_vector| { - sub_vector.transform = *element.transform * sub_vector.transform; - + *sub_vector.transform_mut() = parent_transform * *sub_vector.transform(); sub_vector }) .collect::>() } Graphic::RasterCPU(image) => { + let parent_transform = *element.transform(); let make_row = |transform| { // Convert the image frame into a rectangular subpath with the image's transform let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); @@ -172,13 +173,14 @@ fn flatten_vector(graphic_table: &Table) -> Table { let mut element = Vector::from_subpath(subpath); element.style.set_fill(Fill::Solid(Color::BLACK)); - TableRow { element, ..Default::default() } + TableRow::new_from_element(element) }; // Apply the parent graphic's transform to each raster element - image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::>() + image.iter().map(|row| make_row(parent_transform * *row.transform())).collect::>() } Graphic::RasterGPU(image) => { + let parent_transform = *element.transform(); let make_row = |transform| { // Convert the image frame into a rectangular subpath with the image's transform let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); @@ -188,16 +190,17 @@ fn flatten_vector(graphic_table: &Table) -> Table { let mut element = Vector::from_subpath(subpath); element.style.set_fill(Fill::Solid(Color::BLACK)); - TableRow { element, ..Default::default() } + TableRow::new_from_element(element) }; // Apply the parent graphic's transform to each raster element - image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::>() + image.iter().map(|row| make_row(parent_transform * *row.transform())).collect::>() } Graphic::Graphic(mut graphic) => { + let parent_transform = *element.transform(); // Apply the parent graphic's transform to each element of inner table - for sub_element in graphic.iter_mut() { - *sub_element.transform = *element.transform * *sub_element.transform; + for mut sub_element in graphic.iter_mut() { + *sub_element.transform_mut() = parent_transform * *sub_element.transform(); } // Recursively flatten the inner table into the output vector table @@ -208,21 +211,24 @@ fn flatten_vector(graphic_table: &Table) -> Table { Graphic::Color(color) => color .into_iter() .map(|row| { + let transform = *row.transform(); + let alpha_blending = *row.alpha_blending(); + let source_node_id = *row.source_node_id(); + let mut element = Vector::default(); element.style.set_fill(Fill::Solid(row.element)); element.style.set_stroke_transform(DAffine2::IDENTITY); - TableRow { - element, - transform: row.transform, - alpha_blending: row.alpha_blending, - source_node_id: row.source_node_id, - } + TableRow::new(element, transform, alpha_blending, source_node_id) }) .collect::>(), Graphic::Gradient(gradient) => gradient .into_iter() .map(|row| { + let transform = *row.transform(); + let alpha_blending = *row.alpha_blending(); + let source_node_id = *row.source_node_id(); + let mut element = Vector::default(); element.style.set_fill(Fill::Gradient(graphic_types::vector_types::gradient::Gradient { stops: row.element, @@ -230,12 +236,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { })); element.style.set_stroke_transform(DAffine2::IDENTITY); - TableRow { - element, - transform: row.transform, - alpha_blending: row.alpha_blending, - source_node_id: row.source_node_id, - } + TableRow::new(element, transform, alpha_blending, source_node_id) }) .collect::>(), } diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index b362074ecb..fe3f696414 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -33,8 +33,10 @@ impl From for Error { pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Table>) -> Table> { image_frame .into_iter() - .filter_map(|mut row| { - let image_frame_transform = row.transform; + .filter_map(|row| { + let image_frame_transform = *row.transform(); + let alpha_blending = *row.alpha_blending(); + let source_node_id = *row.source_node_id(); let image = row.element; // Resize the image using the image crate @@ -87,9 +89,7 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Tabl let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size); - row.transform = new_transform; - row.element = Raster::new_cpu(image); - Some(row) + Some(TableRow::new(Raster::new_cpu(image), new_transform, alpha_blending, source_node_id)) }) .collect() } @@ -123,7 +123,7 @@ pub fn combine_channels( let (transform, alpha_blending, source_node_id) = [&red, &green, &blue, &alpha] .iter() .find_map(|i| i.as_ref()) - .map(|i| (i.transform, i.alpha_blending, i.source_node_id))?; + .map(|i| (*i.transform(), *i.alpha_blending(), *i.source_node_id()))?; // Get the common width and height of the channels, which must have equal dimensions let channel_dimensions = [ @@ -173,12 +173,7 @@ pub fn combine_channels( } } - Some(TableRow { - element: Raster::new_cpu(image), - transform, - alpha_blending, - source_node_id, - }) + Some(TableRow::new(Raster::new_cpu(image), transform, alpha_blending, source_node_id)) }) .collect() } @@ -203,23 +198,23 @@ pub fn mask( .into_iter() .filter_map(|mut row| { let image_size = DVec2::new(row.element.width as f64, row.element.height as f64); - let mask_size = stencil.transform.scale_magnitudes(); + let mask_size = stencil.transform().scale_magnitudes(); if mask_size == DVec2::ZERO { return None; } // Transforms a point from the background image to the foreground image - let bg_to_fg = row.transform * DAffine2::from_scale(1. / image_size); - let stencil_transform_inverse = stencil.transform.inverse(); + let bg_to_fg = *row.transform() * DAffine2::from_scale(1. / image_size); + let stencil_transform_inverse = stencil.transform().inverse(); for y in 0..row.element.height { for x in 0..row.element.width { let image_point = DVec2::new(x as f64, y as f64); let mask_point = bg_to_fg.transform_point2(image_point); let local_mask_point = stencil_transform_inverse.transform_point2(mask_point); - let mask_point = stencil.transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE)); - let mask_point = (DAffine2::from_scale(stencil_size) * stencil.transform.inverse()).transform_point2(mask_point); + let mask_point = stencil.transform().transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE)); + let mask_point = (DAffine2::from_scale(stencil_size) * stencil.transform().inverse()).transform_point2(mask_point); let image_pixel = row.element.data_mut().get_pixel_mut(x, y).unwrap(); let mask_pixel = stencil.element.sample(mask_point); @@ -237,7 +232,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Table>, bounds: DA image .into_iter() .map(|mut row| { - let image_aabb = Bbox::unit().affine_transform(row.transform).to_axis_aligned_bbox(); + let image_aabb = Bbox::unit().affine_transform(*row.transform()).to_axis_aligned_bbox(); let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox(); if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) { return row; @@ -250,7 +245,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Table>, bounds: DA } let orig_image_scale = DVec2::new(image_width as f64, image_height as f64); - let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row.transform.inverse(); + let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row.transform().inverse(); let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox(); let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO); @@ -270,10 +265,10 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Table>, bounds: DA // Compute new transform. // let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse(); - let new_texture_to_layer_space = row.transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale); + let new_texture_to_layer_space = *row.transform() * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale); row.element = Raster::new_cpu(new_image); - row.transform = new_texture_to_layer_space; + *row.transform_mut() = new_texture_to_layer_space; row }) .collect() @@ -288,9 +283,9 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Table) -> Tab let image = Image::new(width, height, color.unwrap_or(Color::WHITE)); let mut result_table = Table::new_from_element(Raster::new_cpu(image)); - let row = result_table.get_mut(0).unwrap(); - *row.transform = transform; - *row.alpha_blending = AlphaBlending::default(); + let mut row = result_table.get_mut(0).unwrap(); + *row.transform_mut() = transform; + *row.alpha_blending_mut() = AlphaBlending::default(); // Callers of empty_image can safely unwrap on returned table result_table @@ -383,11 +378,12 @@ pub fn noise_pattern( } } - return Table::new_from_row(TableRow { - element: Raster::new_cpu(image), - transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size), - ..Default::default() - }); + return Table::new_from_row(TableRow::new( + Raster::new_cpu(image), + DAffine2::from_translation(offset) * DAffine2::from_scale(size), + AlphaBlending::default(), + None, + )); } }; noise.set_noise_type(Some(noise_type)); @@ -445,11 +441,12 @@ pub fn noise_pattern( } } - Table::new_from_row(TableRow { - element: Raster::new_cpu(image), - transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size), - ..Default::default() - }) + Table::new_from_row(TableRow::new( + Raster::new_cpu(image), + DAffine2::from_translation(offset) * DAffine2::from_scale(size), + AlphaBlending::default(), + None, + )) } #[node_macro::node(category("Raster: Pattern"))] @@ -487,16 +484,17 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Table> { } } - Table::new_from_row(TableRow { - element: Raster::new_cpu(Image { + Table::new_from_row(TableRow::new( + Raster::new_cpu(Image { width, height, data, ..Default::default() }), - transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size), - ..Default::default() - }) + DAffine2::from_translation(offset) * DAffine2::from_scale(size), + AlphaBlending::default(), + None, + )) } #[inline(always)] diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index c06e5aa716..fd3d795286 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -1,7 +1,7 @@ use crate::gcore::Context; use core::f64::consts::TAU; use core_types::registry::types::{Angle, PixelSize}; -use core_types::table::{Table, TableRowRef}; +use core_types::table::Table; use core_types::{CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::{Graphic, Vector}; @@ -80,9 +80,9 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( for row in generated_instance.iter() { let mut row = row.into_cloned(); - let local_translation = DAffine2::from_translation(row.transform.translation); - let local_matrix = DAffine2::from_mat2(row.transform.matrix2); - row.transform = local_translation * transform * local_matrix; + let local_translation = DAffine2::from_translation(row.transform().translation); + let local_matrix = DAffine2::from_mat2(row.transform().matrix2); + *row.transform_mut() = local_translation * transform * local_matrix; result_table.push(row); } @@ -125,9 +125,9 @@ async fn repeat_radial + Default + Send + Clone + 'static>( for row in generated_instance.iter() { let mut row = row.into_cloned(); - let local_translation = DAffine2::from_translation(row.transform.translation); - let local_matrix = DAffine2::from_mat2(row.transform.matrix2); - row.transform = local_translation * transform * local_matrix; + let local_translation = DAffine2::from_translation(row.transform().translation); + let local_matrix = DAffine2::from_mat2(row.transform().matrix2); + *row.transform_mut() = local_translation * transform * local_matrix; result_table.push(row); } @@ -152,7 +152,10 @@ async fn repeat_on_points + Default + Send + Clone + 'static>( ) -> Table { let mut result_table = Table::new(); - for TableRowRef { element: points, transform, .. } in points.iter() { + for points_row in points.iter() { + let points = points_row.element; + let transform = points_row.transform(); + let mut iteration = async |index, point| { let transformed_point = transform.transform_point2(point); @@ -160,7 +163,7 @@ async fn repeat_on_points + Default + Send + Clone + 'static>( let generated_instance = instance.eval(new_ctx.into_context()).await; for mut generated_row in generated_instance.into_iter() { - generated_row.transform.translation = transformed_point; + generated_row.transform_mut().translation = transformed_point; result_table.push(generated_row); } }; @@ -229,7 +232,7 @@ mod test { let generated = super::repeat_on_points(context, points, &rect, false).await; assert_eq!(generated.len(), positions.len()); for (position, generated_row) in positions.into_iter().zip(generated.iter()) { - let bounds = generated_row.element.bounding_box_with_transform(*generated_row.transform).unwrap(); + let bounds = generated_row.element.bounding_box_with_transform(*generated_row.transform()).unwrap(); assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10)); assert_eq!((bounds[1] - bounds[0]).x, position.y); } diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index c5ba250409..11a667c7ca 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -1,3 +1,4 @@ +use core_types::AlphaBlending; use core_types::table::{Table, TableRow}; use glam::{DAffine2, DVec2}; use parley::GlyphRun; @@ -51,11 +52,12 @@ impl PathBuilder { } if per_glyph_instances { - self.vector_table.push(TableRow { - element: Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false), - transform: DAffine2::from_translation(glyph_offset), - ..Default::default() - }); + self.vector_table.push(TableRow::new( + Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false), + DAffine2::from_translation(glyph_offset), + AlphaBlending::default(), + None, + )); } else { for subpath in self.glyph_subpaths.drain(..) { // Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index 93de7153fb..0634dae9f1 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -66,24 +66,24 @@ fn reset_transform( reset_rotation: bool, reset_scale: bool, ) -> Table { - for row in content.iter_mut() { + for mut row in content.iter_mut() { // Translation if reset_translation { - row.transform.translation = DVec2::ZERO; + row.transform_mut().translation = DVec2::ZERO; } // (Rotation, Scale) match (reset_rotation, reset_scale) { (true, true) => { - row.transform.matrix2 = DMat2::IDENTITY; + row.transform_mut().matrix2 = DMat2::IDENTITY; } (true, false) => { - let scale = row.transform.scale_magnitudes(); - row.transform.matrix2 = DMat2::from_diagonal(scale); + let scale = row.transform().scale_magnitudes(); + row.transform_mut().matrix2 = DMat2::from_diagonal(scale); } (false, true) => { - let rotation = row.transform.decompose_rotation(); + let rotation = row.transform().decompose_rotation(); let rotation_matrix = DMat2::from_angle(rotation); - row.transform.matrix2 = rotation_matrix; + row.transform_mut().matrix2 = rotation_matrix; } (false, false) => {} } @@ -106,8 +106,8 @@ fn replace_transform( mut content: Table, transform: DAffine2, ) -> Table { - for row in content.iter_mut() { - *row.transform = transform.transform(); + for mut row in content.iter_mut() { + *row.transform_mut() = transform.transform(); } content } @@ -127,7 +127,7 @@ async fn extract_transform( )] content: Table, ) -> DAffine2 { - content.iter().next().map(|row| *row.transform).unwrap_or_default() + content.iter().next().map(|row| *row.transform()).unwrap_or_default() } /// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform. diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index a0b611f003..91a3db772a 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -13,12 +13,13 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table, modification: Bo if vector.is_empty() { vector.push(TableRow::default()); } - let row = vector.get_mut(0).expect("push should give one item"); + let mut row = vector.get_mut(0).expect("push should give one item"); modification.apply(row.element); // Update the source node id let this_node_path = node_path.iter().rev().nth(1).copied(); - *row.source_node_id = row.source_node_id.or(this_node_path); + let existing = *row.source_node_id(); + *row.source_node_id_mut() = existing.or(this_node_path); if vector.len() > 1 { warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len()); @@ -29,16 +30,15 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table, modification: Bo /// Applies the vector path's local transformation to its geometry and resets the transform to the identity. #[node_macro::node(category("Vector"))] async fn apply_transform(_ctx: impl Ctx, mut vector: Table) -> Table { - for row in vector.iter_mut() { - let vector = row.element; - let transform = *row.transform; + for mut row in vector.iter_mut() { + let transform = *row.transform(); - for (_, point) in vector.point_domain.positions_mut() { + for (_, point) in row.element.point_domain.positions_mut() { *point = transform.transform_point2(*point); } - vector.segment_domain.transform(transform); + row.element.segment_domain.transform(transform); - *row.transform = DAffine2::IDENTITY; + *row.transform_mut() = DAffine2::IDENTITY; } vector diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 40fb786b06..b2e235eb34 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -217,7 +217,7 @@ where for vector in content.vector_iter_mut() { let mut stroke = stroke.clone(); - stroke.transform *= *vector.transform; + stroke.transform *= *vector.transform(); vector.element.style.set_stroke(stroke); } @@ -264,7 +264,7 @@ async fn copy_to_points( let do_scale = random_scale_difference.abs() > 1e-6; let do_rotation = random_rotation.abs() > 1e-6; - let points_transform = row.transform; + let points_transform = *row.transform(); for &point in row.element.point_domain.positions() { let translation = points_transform.transform_point2(point); @@ -292,7 +292,7 @@ async fn copy_to_points( let transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation); for mut row in instance.iter().map(|row| row.into_cloned()) { - row.transform = transform * row.transform; + *row.transform_mut() = transform * *row.transform(); result_table.push(row); } @@ -324,9 +324,9 @@ async fn round_corners( source .iter() .map(|source| { - let source_transform = *source.transform; + let source_transform = *source.transform(); let source_transform_inverse = source_transform.inverse(); - let source_node_id = source.source_node_id; + let source_node_id = *source.source_node_id(); let source = source.element; let upstream_nested_layers = source.upstream_data.clone(); @@ -416,12 +416,7 @@ async fn round_corners( result.upstream_data = upstream_nested_layers; - TableRow { - element: result, - transform: source_transform, - alpha_blending: Default::default(), - source_node_id: *source_node_id, - } + TableRow::new(result, source_transform, Default::default(), source_node_id) }) .collect() } @@ -439,7 +434,8 @@ pub fn merge_by_distance( MergeByDistanceAlgorithm::Spatial => content .into_iter() .map(|mut row| { - row.element.merge_by_distance_spatial(row.transform, distance); + let transform = *row.transform(); + row.element.merge_by_distance_spatial(transform, distance); row }) .collect(), @@ -660,14 +656,14 @@ async fn extrude(_: impl Ctx, mut source: Table, direction: DVec2, joini #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] async fn box_warp(_: impl Ctx, content: Table, #[expose] rectangle: Table) -> Table { - let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.element, rect.transform)) else { + let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.element, *rect.transform())) else { return content; }; content .into_iter() .map(|mut row| { - let transform = row.transform; + let transform = *row.transform(); let vector = row.element; // Get the bounding box of the source vector geometry @@ -727,7 +723,7 @@ async fn box_warp(_: impl Ctx, content: Table, #[expose] rectangle: Tabl // Add this to the table and reset the transform since we've applied it directly to the points row.element = result; - row.transform = DAffine2::IDENTITY; + *row.transform_mut() = DAffine2::IDENTITY; row }) .collect() @@ -836,7 +832,7 @@ where RowsOrColumns::Rows => DVec2::new(strip.along_position, strip.cross_position), RowsOrColumns::Columns => DVec2::new(strip.cross_position, strip.along_position), }; - row.transform = DAffine2::from_translation(target_position - top_left) * row.transform; + *row.transform_mut() = DAffine2::from_translation(target_position - top_left) * *row.transform(); strip.along_position += along + separation; } else { @@ -847,7 +843,7 @@ where RowsOrColumns::Rows => DVec2::new(0., new_cross), RowsOrColumns::Columns => DVec2::new(new_cross, 0.), }; - row.transform = DAffine2::from_translation(target_position - top_left) * row.transform; + *row.transform_mut() = DAffine2::from_translation(target_position - top_left) * *row.transform(); strips.push(Strip { along_position: along + separation, @@ -879,9 +875,9 @@ async fn auto_tangents( source .iter() .map(|source| { - let transform = *source.transform; - let alpha_blending = *source.alpha_blending; - let source_node_id = *source.source_node_id; + let transform = *source.transform(); + let alpha_blending = *source.alpha_blending(); + let source_node_id = *source.source_node_id(); let source = source.element; let mut result = Vector { @@ -1014,12 +1010,7 @@ async fn auto_tangents( } } - TableRow { - element: result, - transform, - alpha_blending, - source_node_id, - } + TableRow::new(result, transform, alpha_blending, source_node_id) }) .collect() } @@ -1053,7 +1044,7 @@ async fn bounding_box(_: impl Ctx, content: Table) -> Table { async fn dimensions(_: impl Ctx, content: Table) -> DVec2 { content .iter() - .filter_map(|vector| vector.element.bounding_box_with_transform(*vector.transform)) + .filter_map(|vector| vector.element.bounding_box_with_transform(*vector.transform())) .reduce(|[acc_top_left, acc_bottom_right], [top_left, bottom_right]| [acc_top_left.min(top_left), acc_bottom_right.max(bottom_right)]) .map(|[top_left, bottom_right]| bottom_right - top_left) .unwrap_or_default() @@ -1068,10 +1059,7 @@ async fn vec2_to_point(_: impl Ctx, vec2: DVec2) -> Table { let mut point_domain = PointDomain::new(); point_domain.push(PointId::generate(), vec2); - Table::new_from_row(TableRow { - element: Vector { point_domain, ..Default::default() }, - ..Default::default() - }) + Table::new_from_row(TableRow::new_from_element(Vector { point_domain, ..Default::default() })) } /// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist. @@ -1108,7 +1096,7 @@ async fn offset_path(_: impl Ctx, content: Table, distance: f64, join: S content .into_iter() .map(|mut row| { - let transform = Affine::new(row.transform.to_cols_array()); + let transform = Affine::new(row.transform().to_cols_array()); let vector = row.element; let bezpaths = vector.stroke_bezpath_iter(); @@ -1153,10 +1141,11 @@ async fn solidify_stroke(_: impl Ctx, content: Table) -> Table { content .into_iter() .flat_map(|row| { + let transform = *row.transform(); + let alpha_blending = *row.alpha_blending(); + let source_node_id = *row.source_node_id(); + let mut vector = row.element; - let transform = row.transform; - let alpha_blending = row.alpha_blending; - let source_node_id = row.source_node_id; let stroke = vector.style.stroke().clone().unwrap_or_default(); let bezpaths = vector.stroke_bezpath_iter(); @@ -1205,23 +1194,13 @@ async fn solidify_stroke(_: impl Ctx, content: Table) -> Table { solidified_stroke.style.set_fill(Fill::solid_or_none(stroke.color)); } - let stroke_row = TableRow { - element: solidified_stroke, - transform, - alpha_blending, - source_node_id, - }; + let stroke_row = TableRow::new(solidified_stroke, transform, alpha_blending, source_node_id); // If the original vector has a fill, preserve it as a separate row with the stroke cleared. let has_fill = !vector.style.fill().is_none(); let fill_row = has_fill.then(move || { vector.style.clear_stroke(); - TableRow { - element: vector, - transform, - alpha_blending, - source_node_id, - } + TableRow::new(vector, transform, alpha_blending, source_node_id) }); // Ordering based on the paint order. The first row in the table is rendered below the second. @@ -1239,9 +1218,9 @@ async fn separate_subpaths(_: impl Ctx, content: Table) -> Table .into_iter() .flat_map(|row| { let style = row.element.style.clone(); - let transform = row.transform; - let alpha_blending = row.alpha_blending; - let source_node_id = row.source_node_id; + let transform = *row.transform(); + let alpha_blending = *row.alpha_blending(); + let source_node_id = *row.source_node_id(); row.element .stroke_bezpath_iter() @@ -1250,12 +1229,7 @@ async fn separate_subpaths(_: impl Ctx, content: Table) -> Table vector.append_bezpath(bezpath); vector.style = style.clone(); - TableRow { - element: vector, - transform, - alpha_blending, - source_node_id, - } + TableRow::new(vector, transform, alpha_blending, source_node_id) }) .collect::>>() }) @@ -1304,13 +1278,13 @@ pub async fn flatten_path(_: impl Ctx, #[implem // Concatenate every vector element's subpaths into the single output compound path for (index, row) in content.into_flattened_table().iter().enumerate() { - let node_id = row.source_node_id.map(|node_id| node_id.0).unwrap_or_default(); + let node_id = row.source_node_id().map(|node_id| node_id.0).unwrap_or_default(); let mut hasher = DefaultHasher::new(); (index, node_id).hash(&mut hasher); let collision_hash_seed = hasher.finish(); - output.element.concat(row.element, *row.transform, collision_hash_seed); + output.element.concat(row.element, *row.transform(), collision_hash_seed); // TODO: Make this instead use the first encountered style // Use the last encountered style as the output style @@ -1345,7 +1319,7 @@ async fn sample_polyline( upstream_data: std::mem::take(&mut row.element.upstream_data), }; // Transfer the stroke transform from the input vector content to the result. - result.style.set_stroke_transform(row.transform); + result.style.set_stroke_transform(*row.transform()); // Using `stroke_bezpath_iter` so that the `subpath_segment_lengths` is aligned to the segments of each bezpath. // So we can index into `subpath_segment_lengths` to get the length of the segments. @@ -1358,7 +1332,7 @@ async fn sample_polyline( for local_bezpath in bezpaths { // Apply the transform to compute sample locations in world space (for correct distance-based spacing) let mut world_bezpath = local_bezpath.clone(); - world_bezpath.apply_affine(Affine::new(row.transform.to_cols_array())); + world_bezpath.apply_affine(Affine::new(row.transform().to_cols_array())); let segment_count = world_bezpath.segments().count(); @@ -1425,7 +1399,7 @@ async fn simplify( content .into_iter() .map(|mut row| { - let transform = Affine::new(row.transform.to_cols_array()); + let transform = Affine::new(row.transform().to_cols_array()); let inverse_transform = transform.inverse(); let mut result = Vector { @@ -1521,7 +1495,7 @@ async fn decimate( content .into_iter() .map(|mut row| { - let transform = Affine::new(row.transform.to_cols_array()); + let transform = Affine::new(row.transform().to_cols_array()); let inverse_transform = transform.inverse(); let mut result = Vector { @@ -1705,7 +1679,7 @@ async fn position_on_path( let mut bezpaths = content .iter() .flat_map(|vector| { - let transform = *vector.transform; + let transform = *vector.transform(); vector.element.stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)) }) .collect::>(); @@ -1746,7 +1720,7 @@ async fn tangent_on_path( let mut bezpaths = content .iter() .flat_map(|vector| { - let transform = *vector.transform; + let transform = *vector.transform(); vector.element.stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)) }) .collect::>(); @@ -1837,7 +1811,7 @@ async fn subpath_segment_lengths(_: impl Ctx, content: Table) -> Vec = (0..row.element.point_domain.positions().len()) .map(|point_index| { @@ -1989,7 +1963,8 @@ async fn jitter_points( }) .collect(); - apply_point_deltas(&mut row.element, &deltas, row.transform); + let transform = *row.transform(); + apply_point_deltas(&mut row.element, &deltas, transform); row }) @@ -2011,7 +1986,7 @@ async fn offset_points( content .into_iter() .map(|mut row| { - let inverse_linear = inverse_linear_or_repair(row.transform.matrix2); + let inverse_linear = inverse_linear_or_repair(row.transform().matrix2); let deltas: Vec<_> = (0..row.element.point_domain.positions().len()) .map(|point_index| { @@ -2023,7 +1998,8 @@ async fn offset_points( }) .collect(); - apply_point_deltas(&mut row.element, &deltas, row.transform); + let transform = *row.transform(); + apply_point_deltas(&mut row.element, &deltas, transform); row }) @@ -2163,7 +2139,7 @@ async fn morph( let default_polyline = || { let mut default_path = BezPath::new(); for (i, row) in content.iter().enumerate() { - let origin = row.transform.translation; + let origin = row.transform().translation; let point = kurbo::Point::new(origin.x, origin.y); if i == 0 { default_path.move_to(point); @@ -2181,7 +2157,7 @@ async fn morph( let paths: Vec = path .iter() .flat_map(|vector| { - let transform = *vector.transform; + let transform = *vector.transform(); vector.element.stroke_bezpath_iter().map(move |mut bezpath| { bezpath.apply_affine(Affine::new(transform.to_cols_array())); bezpath @@ -2251,8 +2227,8 @@ async fn morph( let (Some(source), Some(target)) = (content.get(source_index), content.get(target_index)) else { return 0.; }; - let (s_angle, s_scale, s_skew) = source.transform.decompose_rotation_scale_skew(); - let (t_angle, t_scale, t_skew) = target.transform.decompose_rotation_scale_skew(); + let (s_angle, s_scale, s_skew) = source.transform().decompose_rotation_scale_skew(); + let (t_angle, t_scale, t_skew) = target.transform().decompose_rotation_scale_skew(); match distribution { InterpolationDistribution::Angles => { @@ -2322,7 +2298,7 @@ async fn morph( }; // Lerp styles - let vector_alpha_blending = source_row.alpha_blending.lerp(target_row.alpha_blending, time as f32); + let vector_alpha_blending = source_row.alpha_blending().lerp(target_row.alpha_blending(), time as f32); // Evaluate the spatial position on the control path for the translation component. // When the segment has zero arc length (e.g., two objects at the same position), inv_arclen @@ -2339,8 +2315,8 @@ async fn morph( // This decomposition must match the one used in Stroke::lerp so the renderer's stroke_transform.inverse() // correctly cancels the element transform, keeping the stroke uniform when Stroke is after Transform. let lerped_transform = { - let (s_angle, s_scale, s_skew) = source_row.transform.decompose_rotation_scale_skew(); - let (t_angle, t_scale, t_skew) = target_row.transform.decompose_rotation_scale_skew(); + let (s_angle, s_scale, s_skew) = source_row.transform().decompose_rotation_scale_skew(); + let (t_angle, t_scale, t_skew) = target_row.transform().decompose_rotation_scale_skew(); let lerp = |a: f64, b: f64| a + (b - a) * time; @@ -2367,8 +2343,8 @@ async fn morph( // in which case we skip pre-compensation to avoid propagating NaN through upstream_data transforms. if lerped_transform.matrix2.determinant().abs() > f64::EPSILON { let lerped_inverse = lerped_transform.inverse(); - for row in graphic_table_content.iter_mut() { - *row.transform = lerped_inverse * *row.transform; + for mut row in graphic_table_content.iter_mut() { + *row.transform_mut() = lerped_inverse * *row.transform(); } } @@ -2376,15 +2352,15 @@ async fn morph( // instead of extracting manipulator groups, subdividing, interpolating, and rebuilding. if time == 0. || time == 1. { let row = if time == 0. { source_row } else { target_row }; - return Table::new_from_row(TableRow { - element: Vector { + return Table::new_from_row(TableRow::new( + Vector { upstream_data: Some(graphic_table_content), ..row.element.clone() }, - alpha_blending: *row.alpha_blending, - transform: lerped_transform, - ..Default::default() - }); + lerped_transform, + *row.alpha_blending(), + None, + )); } let mut vector = Vector { @@ -2536,12 +2512,7 @@ async fn morph( push_manipulators_to_vector(&mut vector, &manips, closed, &mut point_id, &mut segment_id); } - Table::new_from_row(TableRow { - element: vector, - transform: lerped_transform, - alpha_blending: vector_alpha_blending, - ..Default::default() - }) + Table::new_from_row(TableRow::new(vector, lerped_transform, vector_alpha_blending, None)) } fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Vector { @@ -2818,9 +2789,11 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve fn bevel(_: impl Ctx, source: Table, #[default(10.)] distance: Length) -> Table { source .into_iter() - .map(|row| TableRow { - element: bevel_algorithm(row.element, row.transform, distance), - ..row + .map(|row| { + let transform = *row.transform(); + let alpha_blending = *row.alpha_blending(); + let source_node_id = *row.source_node_id(); + TableRow::new(bevel_algorithm(row.element, transform, distance), transform, alpha_blending, source_node_id) }) .collect() } @@ -2838,7 +2811,10 @@ fn close_path(_: impl Ctx, source: Table) -> Table { #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] fn point_inside(_: impl Ctx, source: Table, point: DVec2) -> bool { - source.into_iter().any(|row| row.element.check_point_inside_shape(row.transform, point)) + source.into_iter().any(|row| { + let transform = *row.transform(); + row.element.check_point_inside_shape(transform, point) + }) } trait Count { @@ -2922,7 +2898,7 @@ async fn path_length(_: impl Ctx, source: Table) -> f64 { source .into_iter() .map(|row| { - let transform = row.transform; + let transform = row.transform(); row.element .stroke_bezpath_iter() .map(|mut bezpath| { @@ -2942,7 +2918,7 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node() }) .sum() @@ -2969,7 +2945,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< CentroidType::Length => subpath.length_centroid_and_length(None, true), }; if let Some((subpath_centroid, area_or_length)) = partial { - let subpath_centroid = row.transform.transform_point2(subpath_centroid); + let subpath_centroid = row.transform().transform_point2(subpath_centroid); sum += area_or_length; centroid += area_or_length * subpath_centroid; @@ -2986,7 +2962,10 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< let summed_positions = vector .iter() - .flat_map(|row| row.element.point_domain.positions().iter().map(|&p| row.transform.transform_point2(p))) + .flat_map(|row| { + let transform = *row.transform(); + row.element.point_domain.positions().iter().map(move |&p| transform.transform_point2(p)) + }) .inspect(|_| count += 1) .sum::(); @@ -2997,6 +2976,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< #[cfg(test)] mod test { use super::*; + use core_types::AlphaBlending; use core_types::Node; use kurbo::{CubicBez, Ellipse, Point, Rect}; use std::future::Future; @@ -3022,11 +3002,7 @@ mod test { fn create_vector_row(bezpath: BezPath, transform: DAffine2) -> TableRow { let mut row = Vector::default(); row.append_bezpath(bezpath); - TableRow { - element: row, - transform, - ..Default::default() - } + TableRow::new(row, transform, AlphaBlending::default(), None) } #[tokio::test] @@ -3048,7 +3024,7 @@ mod test { // Test a rectangular path with non-zero rotation let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let mut square = Table::new_from_element(square); - *square.get_mut(0).unwrap().transform *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4); + *square.get_mut(0).unwrap().transform_mut() *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4); let bounding_box = BoundingBoxNode { content: FutureWrapperNode(square) }.eval(Footprint::default()).await; let bounding_box = bounding_box.iter().next().unwrap().element; assert_eq!(bounding_box.region_manipulator_groups().count(), 1); @@ -3157,7 +3133,7 @@ mod test { async fn morph() { let mut rectangles = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); let mut second_rectangle = rectangles.get(0).unwrap().into_cloned(); - second_rectangle.transform *= DAffine2::from_translation((-100., -100.).into()); + *second_rectangle.transform_mut() *= DAffine2::from_translation((-100., -100.).into()); rectangles.push(second_rectangle); let morphed = super::morph(Footprint::default(), rectangles, 0.5, false, InterpolationDistribution::default(), Table::default()).await; @@ -3168,7 +3144,7 @@ mod test { vec![DVec2::new(0., 0.), DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)] ); // The interpolated transform carries the midpoint translation (approximate due to arc-length parameterization) - assert!((row.transform.translation - DVec2::new(-50., -50.)).length() < 1e-3); + assert!((row.transform().translation - DVec2::new(-50., -50.)).length() < 1e-3); } #[track_caller] @@ -3244,7 +3220,7 @@ mod test { let vector = Vector::from_bezpath(source); let mut vector_table = Table::new_from_element(vector.clone()); - *vector_table.get_mut(0).unwrap().transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); + *vector_table.get_mut(0).unwrap().transform_mut() = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); let beveled = super::bevel((), Table::new_from_element(vector), 2_f64.sqrt() * 10.); let beveled = beveled.iter().next().unwrap().element; From fa8193cd2b5d7201c24efda9b85a714d444dc407 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 21 Apr 2026 23:31:37 -0700 Subject: [PATCH 4/7] Remove TaggedValue::GraphicUnused --- node-graph/graph-craft/src/document/value.rs | 1 - .../libraries/graphic-types/src/graphic.rs | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index d89e8f7996..8d0ce41bd8 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -188,7 +188,6 @@ tagged_value! { // =========== // TABLE TYPES // =========== - GraphicUnused(Graphic), // TODO: This is unused but removing it causes `cargo test` to infinitely recurse its type solving; figure out why and then remove this #[serde(deserialize_with = "graphic_types::migrations::migrate_vector")] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "VectorData")] Vector(Table), diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 8490bebab5..05f26b1ffe 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -32,6 +32,28 @@ impl Default for Graphic { } } +// Explicit `Send`/`Sync` impls. All fields are themselves `Send`/`Sync`, so these would normally +// be inferred, but the type participates in two mutually recursive cycles through `Table` +// and `Table` (where `Vector = vector_types::Vector>>`). The second +// path, wrapped in `Option<_>` and a generic type parameter, produces a distinct auto-trait +// obligation that the solver cannot recognize as the same cycle node, causing +// `overflow evaluating the requirement` errors at the workspace's `once_cell::sync::Lazy` statics. +// Providing these impls explicitly anchors the proof and lets the coinductive cache close both cycles. +// +// These can be removed (reverting to auto-derived `Send`/`Sync`) once any of the following holds: +// - We remove the TaggedValue or its variants that contain tables. +// - The `Vector` alias no longer references `Graphic` through a generic type parameter, breaking +// the second cycle so only the direct `Table` self-cycle remains (which the solver +// already handles on its own). +// - `Graphic` stops containing `Table` directly, e.g. by boxing children through a trait +// object or opaque handle so the recursion is no longer structural. +// - A future rustc release improves the auto-trait solver to recognize cycles across generic- +// parameter substitutions. Try deleting these impls and running: +// `cargo check --tests -p graphite-editor` +// If no `overflow evaluating the requirement` errors appear, they're no longer needed). +unsafe impl Send for Graphic {} +unsafe impl Sync for Graphic {} + // Graphic impl From> for Graphic { fn from(graphic: Table) -> Self { From 749dcbdf4804ed127b384b3385e8667bf5704135 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 23 Apr 2026 12:06:35 -0700 Subject: [PATCH 5/7] Refactor Table to use dynamic attributes instead fixed names --- .../data_panel/data_panel_message_handler.rs | 64 +- .../graph_operation_message_handler.rs | 4 +- .../document/node_graph/node_properties.rs | 4 +- .../document/overlays/utility_types_native.rs | 4 +- .../utility_types/document_metadata.rs | 2 +- .../utility_types/network_interface.rs | 4 +- .../messages/portfolio/document_migration.rs | 4 +- .../tool/tool_messages/artboard_tool.rs | 2 +- editor/src/node_graph_executor/runtime.rs | 5 +- node-graph/libraries/core-types/src/ops.rs | 6 +- .../core-types/src/render_complexity.rs | 2 +- node-graph/libraries/core-types/src/table.rs | 1089 ++++++++++++----- .../libraries/graphic-types/src/artboard.rs | 35 +- .../libraries/graphic-types/src/graphic.rs | 104 +- node-graph/libraries/graphic-types/src/lib.rs | 18 +- .../libraries/no-std-types/src/blending.rs | 13 - .../libraries/raster-types/src/image.rs | 51 +- .../libraries/rendering/src/renderer.rs | 281 +++-- .../vector-types/src/vector/style.rs | 7 +- .../per_pixel_adjust_runtime.rs | 5 +- .../wgpu-executor/src/texture_conversion.rs | 18 +- node-graph/nodes/blending/src/lib.rs | 41 +- node-graph/nodes/brush/src/brush.rs | 66 +- node-graph/nodes/brush/src/brush_cache.rs | 5 +- node-graph/nodes/graphic/src/artboard.rs | 2 +- node-graph/nodes/graphic/src/graphic.rs | 33 +- .../nodes/gstd/src/platform_application_io.rs | 10 +- node-graph/nodes/math/src/lib.rs | 2 +- node-graph/nodes/path-bool/src/lib.rs | 80 +- node-graph/nodes/raster/src/adjust.rs | 12 +- node-graph/nodes/raster/src/blending_nodes.rs | 23 +- node-graph/nodes/raster/src/dehaze.rs | 4 +- node-graph/nodes/raster/src/filter.rs | 8 +- node-graph/nodes/raster/src/gradient_map.rs | 2 +- .../nodes/raster/src/image_color_palette.rs | 2 +- node-graph/nodes/raster/src/std_nodes.rs | 123 +- node-graph/nodes/repeat/src/repeat_nodes.rs | 28 +- node-graph/nodes/text/src/path_builder.rs | 14 +- .../nodes/transform/src/transform_nodes.rs | 18 +- .../nodes/vector/src/generator_nodes.rs | 16 +- .../vector/src/vector_modification_nodes.rs | 14 +- node-graph/nodes/vector/src/vector_nodes.rs | 433 ++++--- 42 files changed, 1670 insertions(+), 988 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index b832c12efd..e6e6ef392e 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -4,9 +4,8 @@ use crate::messages::portfolio::document::data_panel::DataPanelMessage; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::prelude::*; use crate::messages::tool::tool_messages::tool_prelude::*; -use glam::{Affine2, Vec2}; +use glam::{Affine2, DAffine2, Vec2}; use graph_craft::document::NodeId; -use graphene_std::Color; use graphene_std::Context; use graphene_std::gradient::GradientStops; use graphene_std::memo::IORecord; @@ -14,6 +13,7 @@ use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::table::Table; use graphene_std::vector::Vector; use graphene_std::vector::style::{Fill, FillChoice}; +use graphene_std::{AlphaBlending, Color}; use graphene_std::{Artboard, Graphic}; use std::any::Any; use std::sync::Arc; @@ -249,7 +249,7 @@ impl TableRowLayout for Table { if let Some(index) = data.desired_path.get(data.current_depth).copied() { if let Some(row) = self.get(index) { data.current_depth += 1; - let result = row.element.layout_with_breadcrumb(data); + let result = row.element().layout_with_breadcrumb(data); data.current_depth -= 1; return result; } else { @@ -258,23 +258,42 @@ impl TableRowLayout for Table { } } + // Collect all unique attribute keys across all rows, preserving first-seen order + let mut attribute_keys: Vec = Vec::new(); + for row in self.iter() { + for key in row.attribute_keys() { + if !attribute_keys.iter().any(|existing| existing == key) { + attribute_keys.push(key.to_string()); + } + } + } + let mut rows = self .iter() .enumerate() .map(|(index, row)| { - vec![ - TextLabel::new(format!("{index}")).narrow(true).widget_instance(), - row.element.element_widget(index), - TextLabel::new(format_transform_matrix(row.transform())).narrow(true).widget_instance(), - TextLabel::new(format!("{}", row.alpha_blending())).narrow(true).widget_instance(), - TextLabel::new(row.source_node_id().map_or_else(|| "-".to_string(), |id| format!("{}", id.0))) - .narrow(true) - .widget_instance(), - ] + let mut cells = vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), row.element().element_widget(index)]; + for key in &attribute_keys { + let value = row + .attribute_display_value(key, |ty| { + Some(match () { + () if let Some(&value) = ty.downcast_ref::() => format_transform_matrix(value), + () if let Some(&value) = ty.downcast_ref::() => format_dvec2(value), + () if let Some(&value) = ty.downcast_ref::() => format_alpha_blending(value), + () if let Some(&value) = ty.downcast_ref::>() => value.map_or_else(|| "-".to_string(), |id| id.to_string()), + _ => return None, + }) + }) + .unwrap_or_else(|| "-".to_string()); + cells.push(TextLabel::new(value).narrow(true).widget_instance()); + } + cells }) .collect::>(); - rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"])); + let mut column_names = vec!["", "element"]; + column_names.extend(attribute_keys.iter().map(|s| s.as_str())); + rows.insert(0, column_headings(&column_names)); vec![LayoutGroup::table(rows, false)] } @@ -430,7 +449,7 @@ impl TableRowLayout for Vector { ]); table_rows.push(vec![ TextLabel::new("Stroke Transform").narrow(true).widget_instance(), - TextLabel::new(format_transform_matrix(&stroke.transform)).narrow(true).widget_instance(), + TextLabel::new(format_transform_matrix(stroke.transform)).narrow(true).widget_instance(), ]); table_rows.push(vec![ TextLabel::new("Stroke Paint Order").narrow(true).widget_instance(), @@ -695,7 +714,7 @@ impl TableRowLayout for DAffine2 { "Transform".to_string() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_instance()]; + let widgets = vec![TextLabel::new(format_transform_matrix(*self)).widget_instance()]; vec![LayoutGroup::row(widgets)] } } @@ -709,12 +728,12 @@ impl TableRowLayout for Affine2 { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64)); - let widgets = vec![TextLabel::new(format_transform_matrix(&matrix)).widget_instance()]; + let widgets = vec![TextLabel::new(format_transform_matrix(matrix)).widget_instance()]; vec![LayoutGroup::row(widgets)] } } -fn format_transform_matrix(transform: &DAffine2) -> String { +fn format_transform_matrix(transform: DAffine2) -> String { let (scale, angle, translation) = if transform.matrix2.determinant().abs() <= f64::EPSILON { let [col_0, col_1] = transform.matrix2.to_cols_array_2d().map(|[x, y]| DVec2::new(x, y)); @@ -748,3 +767,14 @@ fn format_dvec2(value: DVec2) -> String { let round = |x: f64| (x * 1e3).round() / 1e3; format!("({} px, {} px)", round(value.x), round(value.y)) } + +fn format_alpha_blending(value: AlphaBlending) -> String { + let round = |x: f32| (x * 1e3).round() / 1e3; + format!( + "Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}", + value.blend_mode, + round(value.opacity * 100.), + round(value.fill * 100.), + if value.clip { "Yes" } else { "No" } + ) +} 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 db6148934c..65e14a3f70 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 @@ -713,10 +713,10 @@ fn set_import_child_positions( let child_pos = IVec2::new(child_x, current_y); if i == 0 { - // Top of stack — set to `Absolute` position + // Top of stack: set to `Absolute` position network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Absolute(child_pos), &[]); } else { - // Below top — set `Stack` with `y_offset` based on previous sibling's subtree extent + // Below top: set `Stack` with `y_offset` based on previous sibling's subtree extent let prev_sibling_svg_index = n - i; let y_offset = child_extents_svg_order[prev_sibling_svg_index] + STACK_VERTICAL_GAP as u32; network_interface.set_layer_position_for_import(&child_layer.to_node(), LayerPosition::Stack(y_offset), &[]); 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 3edd4411c8..62638d9edb 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1179,7 +1179,7 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: TaggedValue::Color(color_table) => widgets.push( color_button .value(match color_table.iter().next() { - Some(color) => FillChoice::Solid(*color.element), + Some(color) => FillChoice::Solid(*color.element()), None => FillChoice::None, }) .on_update(update_value( @@ -1193,7 +1193,7 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: TaggedValue::GradientTable(gradient_table) => widgets.push( color_button .value(match gradient_table.iter().next() { - Some(row) => FillChoice::Gradient(row.element.clone()), + Some(row) => FillChoice::Gradient(row.element().clone()), None => FillChoice::Gradient(GradientStops::default()), }) .on_update(update_value( diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 0e922259d7..fe87361bee 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -1171,11 +1171,11 @@ impl OverlayContextInternal { let mut path = BezPath::new(); let mut last_point = None; - for (_, bezier, start_id, end_id) in row.element.segment_iter() { + for (_, bezier, start_id, end_id) in row.element().segment_iter() { let move_to = last_point != Some(start_id); last_point = Some(end_id); - self.bezier_to_path(bezier, *row.transform(), move_to, &mut path); + self.bezier_to_path(bezier, row.attribute_cloned_or_default("transform"), move_to, &mut path); } // Render the path diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 328477a3c7..4787ebf0c5 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -104,7 +104,7 @@ impl DocumentMetadata { .any(|upstream| Some(upstream) == source) { use_local = false; - info!("Local transform is invalid — using the identity for the local transform instead") + info!("Local transform is invalid. Using the identity for the local transform instead."); } let local_transform = use_local.then(|| self.local_transforms.get(&layer.to_node()).copied()).flatten().unwrap_or_default(); diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 7fc992b459..aa009e74f6 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -5521,11 +5521,11 @@ impl NodeNetworkInterface { match post_node_input { NodeInput::Value { .. } | NodeInput::Scope(_) | NodeInput::Inline(_) | NodeInput::Reflection(_) => { - // First child in the stack — wire layer output to the post_node input + // First child in the stack: wire layer output to the post_node input self.set_input_for_import(&post_node, layer_output, network_path); } NodeInput::Node { .. } => { - // Subsequent child — insert layer between post_node and its current upstream: + // Subsequent child: insert layer between post_node and its current upstream... // 1. Disconnect old upstream from post_node, wire layer output to post_node self.set_input_for_import(&post_node, layer_output, network_path); // 2. Wire old upstream into layer's primary (stack) input diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index f55c41ae80..a66c06ed70 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1962,7 +1962,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], && let TaggedValue::Vector(vector_table) = &**tagged_value && !vector_table.is_empty() { - let vector = vector_table.iter().next()?.element; + let vector = vector_table.iter().next()?.element(); let modification = Box::new(graphene_std::vector::VectorModification::create_from_vector(vector)); // Reset input 0 to the default exposed state @@ -1983,7 +1983,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], && let TaggedValue::Raster(raster_table) = &**tagged_value && let Some(row) = raster_table.iter().next() { - let image = row.element.data().clone(); + let image = row.element().data().clone(); document .network_interface diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index fa06962f00..afe8c82b92 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -649,7 +649,7 @@ mod test_artboard { let artboards = get_artboards(editor) .await .iter() - .map(|row| ArtboardLayoutDocument::new(row.element.location, row.element.dimensions)) + .map(|row| ArtboardLayoutDocument::new(row.element().location, row.element().dimensions)) .collect::>(); assert_eq!(artboards.len(), expected.len(), "incorrect len: actual {:?}, expected {:?}", artboards, expected); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 70ceaefec0..2711d98181 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -15,7 +15,7 @@ use graphene_std::ops::Convert; use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; use graphene_std::raster_types::Raster; use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; -use graphene_std::table::{Table, TableRow}; +use graphene_std::table::Table; use graphene_std::text::FontCache; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; @@ -441,9 +441,8 @@ impl NodeRuntime { // Vector table: vector modifications else if let Some(io) = introspected_data.downcast_ref::>>() { // Insert the vector modify - let default = TableRow::default(); self.vector_modify - .insert(parent_network_node_id, io.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone()); + .insert(parent_network_node_id, io.output.iter().next().map(|row| row.element().clone()).unwrap_or_default()); } // Other else { diff --git a/node-graph/libraries/core-types/src/ops.rs b/node-graph/libraries/core-types/src/ops.rs index 456a90ffed..9e0a3e3ffa 100644 --- a/node-graph/libraries/core-types/src/ops.rs +++ b/node-graph/libraries/core-types/src/ops.rs @@ -63,10 +63,8 @@ impl + Send> Convert, ()> for Table { let table: Table = self .into_iter() .map(|row| { - let transform = *row.transform(); - let alpha_blending = *row.alpha_blending(); - let source_node_id = *row.source_node_id(); - TableRow::new(row.element.convert_row(), transform, alpha_blending, source_node_id) + let (element, attributes) = row.into_parts(); + TableRow::from_parts(element.convert_row(), attributes) }) .collect(); table diff --git a/node-graph/libraries/core-types/src/render_complexity.rs b/node-graph/libraries/core-types/src/render_complexity.rs index b3491e07b9..4daca26eef 100644 --- a/node-graph/libraries/core-types/src/render_complexity.rs +++ b/node-graph/libraries/core-types/src/render_complexity.rs @@ -10,7 +10,7 @@ pub trait RenderComplexity { impl RenderComplexity for Table { fn render_complexity(&self) -> usize { - self.iter().map(|row| row.element.render_complexity()).fold(0, usize::saturating_add) + self.iter().map(|row| row.element().render_complexity()).fold(0, usize::saturating_add) } } diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 9ae50f7055..8ee6882e17 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -1,39 +1,70 @@ use crate::bounds::{BoundingBox, RenderBoundingBox}; +use crate::math::quad::Quad; use crate::transform::ApplyTransform; -use crate::uuid::NodeId; -use crate::{AlphaBlending, math::quad::Quad}; use dyn_any::{StaticType, StaticTypeSized}; use glam::DAffine2; +use std::fmt::Debug; use std::hash::Hash; -// ATTRIBUTE VALUE TRAIT -// Enables type-erased storage that supports Clone, Send, Sync, and downcasting. +// ===================== +// TRAIT: AttributeValue +// ===================== +/// Enables type-erased scalar storage that supports Clone, Send, Sync, and downcasting. +/// Used for individual attribute values in a TableRow. trait AttributeValue: std::any::Any + Send + Sync { + /// Clones this value into a new boxed trait object. fn clone_box(&self) -> Box; + + /// Returns a shared reference to the underlying concrete type for downcasting. fn as_any(&self) -> &dyn std::any::Any; + + /// Returns a mutable reference to the underlying concrete type for downcasting. fn as_any_mut(&mut self) -> &mut dyn std::any::Any; + + /// Consumes the box and returns the underlying concrete type for downcasting. fn into_any(self: Box) -> Box; + + /// Returns a debug-formatted string representation of this value. + fn display_string(&self) -> String; + + /// Wraps this scalar value into a new column for columnar storage, + /// with `preceding_defaults` default values before this value. + fn into_column(self: Box, preceding_defaults: usize) -> Box; } -// The `Sized` bound ensures this blanket impl does not apply to `dyn AttributeValue` itself, -// which would cause infinite recursion in the `Clone for Box` impl. -impl AttributeValue for T { +impl AttributeValue for T { + /// Clones this value into a new boxed trait object. fn clone_box(&self) -> Box { Box::new(self.clone()) } + /// Returns a shared reference to the underlying concrete type for downcasting. fn as_any(&self) -> &dyn std::any::Any { self } + /// Returns a mutable reference to the underlying concrete type for downcasting. fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } + /// Consumes the box and returns the underlying concrete type for downcasting. fn into_any(self: Box) -> Box { self } + + /// Returns a debug-formatted string representation of this value. + fn display_string(&self) -> String { + format!("{:?}", self) + } + + /// Wraps this scalar value into a new column, padded with `preceding_defaults` default values before it. + fn into_column(self: Box, preceding_defaults: usize) -> Box { + let mut data = vec![T::default(); preceding_defaults]; + data.push(*self); + Box::new(Column(data)) + } } impl Clone for Box { @@ -42,60 +73,192 @@ impl Clone for Box { } } -// ATTRIBUTES +// ====================== +// TRAIT: AttributeColumn +// ====================== + +/// Enables type-erased columnar storage for parallel attribute lists in a Table. +trait AttributeColumn: std::any::Any + Send + Sync { + /// Clones this column into a new boxed trait object. + fn clone_box(&self) -> Box; + + /// Returns a shared reference to the underlying concrete type for downcasting. + fn as_any(&self) -> &dyn std::any::Any; + + /// Returns a mutable reference to the underlying concrete type for downcasting. + fn as_any_mut(&mut self) -> &mut dyn std::any::Any; + + /// Pushes a scalar attribute value onto the end of this column. + fn push(&mut self, value: Box); + + /// Pushes a default value onto the end of this column. + fn push_default(&mut self); + + /// Creates a new column of the same type filled with `count` number of default values. + fn new_with_defaults(&self, count: usize) -> Box; + + /// Appends all values from another column of the same type. + fn extend(&mut self, other: Box); + + /// Returns a shared reference to the value at the requested index. + fn get_any(&self, index: usize) -> Option<&dyn std::any::Any>; + + /// Returns a mutable reference to the value at the requested index. + fn get_any_mut(&mut self, index: usize) -> Option<&mut dyn std::any::Any>; + + /// Returns a debug-formatted display string for the value at the requested index. + fn display_at(&self, index: usize) -> Option; + + /// Clones a single value from this column into a boxed scalar attribute value. + fn clone_cell(&self, index: usize) -> Option>; + + /// Drains all values out of this column into a Vec of scalar attribute values. + fn drain(self: Box) -> Vec>; +} + +impl Clone for Box { + fn clone(&self) -> Self { + (**self).clone_box() + } +} + +// ========= +// Column +// ========= + +/// Wraps a Vec for column-major attribute storage in a Table. +struct Column(Vec); + +impl AttributeColumn for Column { + /// Clones this column into a new boxed trait object. + fn clone_box(&self) -> Box { + Box::new(Column(self.0.clone())) + } + + /// Returns a shared reference to the underlying concrete type for downcasting. + fn as_any(&self) -> &dyn std::any::Any { + self + } + + /// Returns a mutable reference to the underlying concrete type for downcasting. + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + /// Pushes a scalar attribute value onto the end of this column, downcasting it to `T`. + fn push(&mut self, value: Box) { + if let Ok(value) = value.into_any().downcast::() { + self.0.push(*value); + } + } + + /// Pushes a default `T` value onto the end of this column. + fn push_default(&mut self) { + self.0.push(T::default()); + } + + /// Creates a new column filled with `count` default `T` values. + fn new_with_defaults(&self, count: usize) -> Box { + Box::new(Column(vec![T::default(); count])) + } + + /// Appends all values from another column, downcasting it to the same `Column` type. + fn extend(&mut self, other: Box) { + if let Ok(other) = (other as Box).downcast::() { + self.0.extend(other.0); + } + } + + /// Returns a shared reference to the value at the given index as a type-erased `Any`. + fn get_any(&self, index: usize) -> Option<&dyn std::any::Any> { + self.0.get(index).map(|v| v as &dyn std::any::Any) + } + + /// Returns a mutable reference to the value at the given index as a type-erased `Any`. + fn get_any_mut(&mut self, index: usize) -> Option<&mut dyn std::any::Any> { + self.0.get_mut(index).map(|v| v as &mut dyn std::any::Any) + } + + /// Returns a debug-formatted string for the value at the given index. + fn display_at(&self, index: usize) -> Option { + self.0.get(index).map(|v| format!("{v:?}")) + } + + /// Clones the value at the given index into a boxed scalar attribute value. + fn clone_cell(&self, index: usize) -> Option> { + self.0.get(index).map(|v| Box::new(v.clone()) as Box) + } + + /// Consumes this column and returns all values as a Vec of boxed scalar attribute values. + fn drain(self: Box) -> Vec> { + self.0.into_iter().map(|v| Box::new(v) as Box).collect() + } +} + +// =============== +// AttributeValues +// =============== -/// A small ordered map of type-erased attribute columns, keyed by string name. +/// Scalar attribute storage. +/// +/// A small ordered map of type-erased scalar attribute values, keyed by string name. +/// Used for individual attribute values in a TableRow. /// Linear search preserves insertion order and is likely faster than a HashMap for small attribute counts. #[derive(Clone, Default)] -pub struct Attributes { - entries: Vec<(String, Box)>, -} +pub struct AttributeValues(Vec<(String, Box)>); -impl std::fmt::Debug for Attributes { +impl Debug for AttributeValues { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let keys: Vec<&str> = self.entries.iter().map(|(k, _)| k.as_str()).collect(); + let keys: Vec<&str> = self.0.iter().map(|(k, _)| k.as_str()).collect(); f.debug_struct("Attributes").field("keys", &keys).finish() } } -impl Attributes { +impl AttributeValues { + /// Creates an empty set of attributes. pub fn new() -> Self { Self::default() } /// Inserts an attribute with the given key and value, replacing any existing entry with the same key. - pub fn insert(&mut self, key: String, value: T) { - for (k, v) in &mut self.entries { - if *k == key { - *v = Box::new(value); + pub fn insert(&mut self, key: impl Into, value: T) { + let key = key.into(); + + for (existing_key, existing_value) in &mut self.0 { + if *existing_key == key { + *existing_value = Box::new(value); return; } } - self.entries.push((key, Box::new(value))); + + self.0.push((key, Box::new(value))); } /// Gets a reference to the value of the attribute with the given key, if it exists and can be downcast to the requested type. pub fn get(&self, key: &str) -> Option<&T> { - // Explicit deref `(**v)` reaches `dyn AttributeValue` (which is !Sized and thus dispatches + // Explicit deref `(**value)` reaches `dyn AttributeValue` (which is !Sized and thus dispatches // through the vtable to the concrete type) rather than resolving to the blanket // `impl AttributeValue for Box` which would return the wrong TypeId. - self.entries.iter().find_map(|(k, v)| if k == key { (**v).as_any().downcast_ref::() } else { None }) + self.0 + .iter() + .find_map(|(existing_key, value)| if existing_key == key { (**value).as_any().downcast_ref::() } else { None }) } /// Gets a mutable reference to the value of the attribute with the given key, if it exists and can be downcast to the requested type. pub fn get_mut(&mut self, key: &str) -> Option<&mut T> { - self.entries.iter_mut().find_map(|(k, v)| if k == key { (**v).as_any_mut().downcast_mut::() } else { None }) + self.0 + .iter_mut() + .find_map(|(existing_key, value)| if existing_key == key { (**value).as_any_mut().downcast_mut::() } else { None }) } /// Gets a mutable reference to the value, inserting a default if it doesn't exist or has the wrong type. - pub fn get_or_insert_default_mut(&mut self, key: &str) -> &mut T { - // Remove any existing entry with the wrong type, then insert a correctly-typed default - let needs_insert = match self.entries.iter().position(|(k, _)| k == key) { + pub fn get_or_insert_default_mut(&mut self, key: &str) -> &mut T { + let needs_insert = match self.0.iter().position(|(existing_key, _)| existing_key == key) { Some(index) => { - if (*self.entries[index].1).as_any().downcast_ref::().is_some() { + if (*self.0[index].1).as_any().downcast_ref::().is_some() { false } else { - self.entries.remove(index); + self.0.remove(index); true } } @@ -103,262 +266,386 @@ impl Attributes { }; if needs_insert { - self.entries.push((key.to_string(), Box::new(T::default()))); + self.0.push((key.to_string(), Box::new(T::default()))); } - self.get_mut::(key).expect("attribute was just ensured to exist with correct type") + self.get_mut::(key).expect("Attribute was just ensured to exist with correct type") } /// Removes and returns the value for the given key, if it exists and can be downcast to the requested type. pub fn remove(&mut self, key: &str) -> Option { - let index = self.entries.iter().position(|(k, _)| k == key)?; - let (_, value) = self.entries.remove(index); - value.into_any().downcast::().ok().map(|b| *b) + let index = self.0.iter().position(|(existing_key, _)| existing_key == key)?; + let (_, value) = self.0.remove(index); + value.into_any().downcast::().ok().map(|boxed| *boxed) + } + + /// Returns an iterator over the keys of all stored attributes, in insertion order. + pub fn keys(&self) -> impl Iterator { + self.0.iter().map(|(key, _)| key.as_str()) + } + + /// Returns a debug-formatted string representation of the attribute value for the given key, if it exists. + /// The `overrides` function can provide custom formatting for specific type. + pub fn display_value(&self, key: &str, overrides: fn(&dyn std::any::Any) -> Option) -> Option { + self.0.iter().find_map(|(k, value)| { + if k == key { + if let Some(text) = overrides(value.as_any()) { Some(text) } else { Some(value.display_string()) } + } else { + None + } + }) } } -// TABLE +// ================ +// AttributeColumns +// ================ +/// Columnar attribute storage. +/// +/// A collection of type-erased parallel attribute columns, keyed by string name. +/// Used for columnar attribute storage in a Table. +/// Not public. All access goes through Table, TableRowRef, and TableRowMut. +/// Invariant: every column in `columns` has exactly `len` elements. +#[derive(Clone, Default)] +struct AttributeColumns { + columns: Vec<(String, Box)>, + len: usize, +} + +impl Debug for AttributeColumns { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let keys: Vec<&str> = self.columns.iter().map(|(k, _)| k.as_str()).collect(); + f.debug_struct("AttributeColumns").field("keys", &keys).field("len", &self.len).finish() + } +} + +impl AttributeColumns { + /// Creates an empty column store with no columns and zero length. + fn new() -> Self { + Self::default() + } + + /// Creates an empty column store with no columns but a pre-set row count. + fn with_len(len: usize) -> Self { + Self { columns: Vec::new(), len } + } + + /// Pushes a row's scalar attributes into this column store. + /// Existing columns that the row lacks receive a default value. + /// New attribute keys create a new column padded with defaults for all prior rows. + fn push_row(&mut self, row: AttributeValues) { + let mut row_entries = row.0; + + // Push values into existing columns, or a default if the row lacks that attribute + for (column_key, column) in &mut self.columns { + if let Some(position) = row_entries.iter().position(|(k, _)| k == column_key) { + let (_, cell_value) = row_entries.swap_remove(position); + column.push(cell_value); + } else { + column.push_default(); + } + } + + // Create new columns for any remaining row entries, padded with defaults for prior rows + for (key, value) in row_entries { + self.columns.push((key, value.into_column(self.len))); + } + + self.len += 1; + } + + /// Appends all column data from another column store into this one. + /// Columns present in only one side are padded with defaults for the other side's rows. + fn extend(&mut self, other: AttributeColumns) { + let other_len = other.len; + let mut other_entries = other.columns; + + // Extend matching columns, or pad self's columns with defaults for the other's row count + for (key, self_column) in &mut self.columns { + if let Some(position) = other_entries.iter().position(|(k, _)| k == key) { + let (_, other_column) = other_entries.swap_remove(position); + self_column.extend(other_column); + } else { + for _ in 0..other_len { + self_column.push_default(); + } + } + } + + // Remaining other columns are new, pad with defaults for self's existing rows + for (key, other_column) in other_entries { + let mut combined = other_column.new_with_defaults(self.len); + combined.extend(other_column); + self.columns.push((key, combined)); + } + + self.len += other_len; + } + + /// Gets a reference to a cell value at the given index from the column for the given key. + fn get_cell(&self, key: &str, index: usize) -> Option<&T> { + self.columns.iter().find_map(|(k, column)| if k == key { column.get_any(index)?.downcast_ref::() } else { None }) + } + + /// Gets a mutable reference to a cell value at the given index from the column for the given key. + fn get_cell_mut(&mut self, key: &str, index: usize) -> Option<&mut T> { + self.columns + .iter_mut() + .find_map(|(k, column)| if k == key { column.get_any_mut(index)?.downcast_mut::() } else { None }) + } + + /// Finds or creates a column for the given key and type, returning its position. + /// If a column with the key exists but has the wrong type, it is removed and replaced with a new column of the correct type, padded with defaults. + /// A newly created column is filled with `T::default()` for all existing rows. + fn find_or_create_column(&mut self, key: &str) -> usize { + match self.columns.iter().position(|(k, _)| k == key) { + Some(position) => { + if (*self.columns[position].1).as_any().downcast_ref::>().is_some() { + position + } else { + self.columns.remove(position); + self.columns.push((key.to_string(), Box::new(Column::(vec![T::default(); self.len])))); + self.columns.len() - 1 + } + } + None => { + self.columns.push((key.to_string(), Box::new(Column::(vec![T::default(); self.len])))); + self.columns.len() - 1 + } + } + } + + /// Gets a mutable reference to a cell value at the given index, creating the column if it doesn't exist or has the wrong type. + fn get_or_insert_default_cell(&mut self, key: &str, index: usize) -> &mut T { + let column_position = self.find_or_create_column::(key); + let column = (*self.columns[column_position].1).as_any_mut().downcast_mut::>().unwrap(); + &mut column.0[index] + } + + /// Sets a cell value at the given index in the column for the given key. + /// Creates the column with defaults if it doesn't exist. + fn set_cell(&mut self, key: impl Into, index: usize, value: T) { + let key = key.into(); + let column_position = self.find_or_create_column::(&key); + let column = (*self.columns[column_position].1).as_any_mut().downcast_mut::>().unwrap(); + column.0[index] = value; + } + + /// Returns a debug-formatted string for a cell at the given index in the column for the given key. + fn display_cell_value(&self, key: &str, index: usize, overrides: fn(&dyn std::any::Any) -> Option) -> Option { + self.columns.iter().find_map(|(k, column)| { + if k == key { + if let Some(cell) = column.get_any(index) + && let Some(text) = overrides(cell) + { + return Some(text); + } + column.display_at(index) + } else { + None + } + }) + } + + /// Returns an iterator over the keys of all stored attribute columns, in insertion order. + fn keys(&self) -> impl Iterator { + self.columns.iter().map(|(key, _)| key.as_str()) + } + + /// Clones all attribute values at the given row index into a new scalar Attributes. + fn clone_row(&self, index: usize) -> AttributeValues { + let mut attributes = AttributeValues::new(); + + for (key, column) in &self.columns { + if let Some(cell) = column.clone_cell(index) { + attributes.0.push((key.clone(), cell)); + } + } + + attributes + } + + /// Drains all column data into a Vec of per-row scalar Attributes. + fn into_row_vec(self) -> Vec { + let mut rows: Vec = (0..self.len).map(|_| AttributeValues::new()).collect(); + + for (key, column) in self.columns { + for (i, cell) in column.drain().into_iter().enumerate() { + rows[i].0.push((key.clone(), cell)); + } + } + + rows + } +} + +// ======== +// Table +// ======== + +/// A struct-of-arrays collection where each row holds an element of type `T` alongside +/// a set of type-erased, dynamically-typed attributes stored in parallel columns. +/// +/// Elements are stored contiguously in a `Vec`, while attributes live in an internal +/// [`AttributeColumns`] store that keeps one column per attribute key. Rows are accessed +/// by index through [`TableRowRef`] (shared) or [`TableRowMut`] (mutable) views, or +/// consumed as owned [`TableRow`]s via iteration. #[derive(Clone, Debug)] pub struct Table { element: Vec, - attributes: Attributes, + attributes: AttributeColumns, } impl Table { + /// Creates an empty table with no rows. pub fn new() -> Self { Self::default() } + /// Creates an empty table with pre-allocated capacity for the given number of rows. pub fn with_capacity(capacity: usize) -> Self { - let mut attributes = Attributes::new(); - attributes.insert("transform".to_string(), Vec::::with_capacity(capacity)); - attributes.insert("alpha_blending".to_string(), Vec::::with_capacity(capacity)); - attributes.insert("source_node_id".to_string(), Vec::>::with_capacity(capacity)); - Self { element: Vec::with_capacity(capacity), - attributes, + attributes: AttributeColumns::new(), } } + /// Creates a table containing a single row with the given element and no attributes. pub fn new_from_element(element: T) -> Self { - let mut attributes = Attributes::new(); - attributes.insert("transform".to_string(), vec![DAffine2::IDENTITY]); - attributes.insert("alpha_blending".to_string(), vec![AlphaBlending::default()]); - attributes.insert("source_node_id".to_string(), vec![Option::::None]); - - Self { element: vec![element], attributes } + Self { + element: vec![element], + attributes: AttributeColumns::with_len(1), + } } + /// Creates a table containing a single row from the given [`TableRow`], preserving its attributes. pub fn new_from_row(row: TableRow) -> Self { - let mut row_attributes = row.attributes; - let transform = row_attributes.remove::("transform").unwrap_or(DAffine2::IDENTITY); - let alpha_blending = row_attributes.remove::("alpha_blending").unwrap_or_default(); - let source_node_id = row_attributes.remove::>("source_node_id").unwrap_or(None); - - let mut attributes = Attributes::new(); - attributes.insert("transform".to_string(), vec![transform]); - attributes.insert("alpha_blending".to_string(), vec![alpha_blending]); - attributes.insert("source_node_id".to_string(), vec![source_node_id]); - + let mut attributes = AttributeColumns::new(); + attributes.push_row(row.attributes); Self { element: vec![row.element], attributes, } } + /// Appends a row to the end of this table. pub fn push(&mut self, row: TableRow) { - let mut attributes = row.attributes; self.element.push(row.element); - self.transforms_mut().push(attributes.remove::("transform").unwrap_or(DAffine2::IDENTITY)); - self.alpha_blendings_mut().push(attributes.remove::("alpha_blending").unwrap_or_default()); - self.source_node_ids_mut().push(attributes.remove::>("source_node_id").unwrap_or(None)); + self.attributes.push_row(row.attributes); } + /// Appends all rows from another table into this one. pub fn extend(&mut self, table: Table) { - let mut other_attributes = table.attributes; - self.element.extend(table.element); - self.transforms_mut().extend(other_attributes.remove::>("transform").unwrap_or_default()); - self.alpha_blendings_mut().extend(other_attributes.remove::>("alpha_blending").unwrap_or_default()); - self.source_node_ids_mut().extend(other_attributes.remove::>>("source_node_id").unwrap_or_default()); + self.attributes.extend(table.attributes); } + /// Returns a shared reference to the row at the given index, or `None` if out of bounds. pub fn get(&self, index: usize) -> Option> { - if index >= self.element.len() { - return None; - } - Some(TableRowRef { - element: &self.element[index], - transform: &self.transforms()[index], - alpha_blending: &self.alpha_blendings()[index], - source_node_id: &self.source_node_ids()[index], + element: self.element.get(index)?, + index, + columns: &self.attributes, }) } + /// Returns a mutable reference to the row at the given index, or `None` if out of bounds. pub fn get_mut(&mut self, index: usize) -> Option> { if index >= self.element.len() { return None; } - // Split borrows: element from the vec, attributes from the Attributes map let element = &mut self.element[index] as *mut T; - let transforms = self.transforms_mut(); - let transform = &mut transforms[index] as *mut DAffine2; - let alpha_blendings = self.alpha_blendings_mut(); - let alpha_blending = &mut alpha_blendings[index] as *mut AlphaBlending; - let source_node_ids = self.source_node_ids_mut(); - let source_node_id = &mut source_node_ids[index] as *mut Option; - - // SAFETY: All pointers come from distinct Vecs in self, so they don't alias + let columns = &mut self.attributes as *mut AttributeColumns; + + // SAFETY: `element` points into the `Vec` while `columns` points to the `AttributeColumns`. + // These are distinct fields in `self`, so they do not alias. Some(TableRowMut { element: unsafe { &mut *element }, - transform: unsafe { &mut *transform }, - alpha_blending: unsafe { &mut *alpha_blending }, - source_node_id: unsafe { &mut *source_node_id }, + index, + columns, + _marker: std::marker::PhantomData, }) } + /// Returns the number of rows in this table. pub fn len(&self) -> usize { self.element.len() } + /// Returns `true` if this table contains no rows. pub fn is_empty(&self) -> bool { self.element.is_empty() } + /// Returns an iterator over all attribute keys in this table, in insertion order. + pub fn attribute_keys(&self) -> impl Iterator { + self.attributes.keys() + } + /// Borrows a [`Table`] and returns an iterator of [`TableRowRef`]s, each containing references to the data of the respective row from the table. pub fn iter(&self) -> impl DoubleEndedIterator> + Clone { - self.element - .iter() - .zip(self.transforms().iter()) - .zip(self.alpha_blendings().iter()) - .zip(self.source_node_ids().iter()) - .map(|(((element, transform), alpha_blending), source_node_id)| TableRowRef { - element, - transform, - alpha_blending, - source_node_id, - }) + self.element.iter().enumerate().map(|(index, element)| TableRowRef { + element, + index, + columns: &self.attributes, + }) } /// Mutably borrows a [`Table`] and returns an iterator of [`TableRowMut`]s, each containing mutable references to the data of the respective row from the table. - pub fn iter_mut(&mut self) -> impl DoubleEndedIterator> { - let transforms = self.transforms_mut() as *mut Vec; - let alpha_blendings = self.alpha_blendings_mut() as *mut Vec; - let source_node_ids = self.source_node_ids_mut() as *mut Vec>; - - // SAFETY: Each Vec is a distinct allocation within Attributes, so mutable references to their elements don't alias - self.element - .iter_mut() - .zip(unsafe { &mut *transforms }.iter_mut()) - .zip(unsafe { &mut *alpha_blendings }.iter_mut()) - .zip(unsafe { &mut *source_node_ids }.iter_mut()) - .map(|(((element, transform), alpha_blending), source_node_id)| TableRowMut { - element, - transform, - alpha_blending, - source_node_id, - }) - } - - // Convenience accessors for the well-known attribute columns - - pub fn transforms(&self) -> &[DAffine2] { - self.attributes.get::>("transform").map(Vec::as_slice).unwrap_or(&[]) - } - - pub fn transforms_mut(&mut self) -> &mut Vec { - self.attributes.get_or_insert_default_mut::>("transform") - } - - pub fn alpha_blendings(&self) -> &[AlphaBlending] { - self.attributes.get::>("alpha_blending").map(Vec::as_slice).unwrap_or(&[]) - } - - pub fn alpha_blendings_mut(&mut self) -> &mut Vec { - self.attributes.get_or_insert_default_mut::>("alpha_blending") - } - - pub fn source_node_ids(&self) -> &[Option] { - self.attributes.get::>>("source_node_id").map(Vec::as_slice).unwrap_or(&[]) - } - - pub fn source_node_ids_mut(&mut self) -> &mut Vec> { - self.attributes.get_or_insert_default_mut::>>("source_node_id") + pub fn iter_mut(&mut self) -> TableRowIterMut<'_, T> { + let columns = &mut self.attributes as *mut AttributeColumns; + TableRowIterMut { + inner: self.element.iter_mut().enumerate(), + columns, + _marker: std::marker::PhantomData, + } } } -// CUSTOM SERDE - #[cfg(feature = "serde")] impl serde::Serialize for Table { + /// Serializes only the element vec, omitting type-erased attributes which are not serializable. fn serialize(&self, serializer: S) -> Result { #[derive(serde::Serialize)] struct TableHelper<'a, T: serde::Serialize> { element: &'a Vec, - transform: &'a [DAffine2], - alpha_blending: &'a [AlphaBlending], - source_node_id: &'a [Option], } - TableHelper { - element: &self.element, - transform: self.transforms(), - alpha_blending: self.alpha_blendings(), - source_node_id: self.source_node_ids(), - } - .serialize(serializer) + TableHelper { element: &self.element }.serialize(serializer) } } #[cfg(feature = "serde")] impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Table { + /// Deserializes the element vec and initializes an empty attribute column store with the matching row count. fn deserialize>(deserializer: D) -> Result { #[derive(serde::Deserialize)] struct TableHelper { #[serde(alias = "instances", alias = "instance")] element: Vec, - #[serde(default)] - transform: Vec, - #[serde(default)] - alpha_blending: Vec, - #[serde(default)] - source_node_id: Vec>, } let helper = TableHelper::deserialize(deserializer)?; - let length = helper.element.len(); + let len = helper.element.len(); - // Pad attribute vecs to match element length if they're shorter (e.g., from older save formats) - let mut transform = helper.transform; - transform.resize(length, DAffine2::IDENTITY); - - let mut alpha_blending = helper.alpha_blending; - alpha_blending.resize(length, AlphaBlending::default()); - - let mut source_node_id = helper.source_node_id; - source_node_id.resize(length, None); - - let mut attributes = Attributes::new(); - attributes.insert("transform".to_string(), transform); - attributes.insert("alpha_blending".to_string(), alpha_blending); - attributes.insert("source_node_id".to_string(), source_node_id); - - Ok(Table { element: helper.element, attributes }) + Ok(Table { + element: helper.element, + attributes: AttributeColumns::with_len(len), + }) } } -// TRAIT IMPLS - impl BoundingBox for Table { + /// Computes the combined bounding box of all rows, composing each row's transform attribute with the given transform. fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { let mut combined_bounds = None; for row in self.iter() { - match row.element.bounding_box(transform * *row.transform(), include_stroke) { + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + + match row.element.bounding_box(transform * row_transform, include_stroke) { RenderBoundingBox::None => continue, RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite, RenderBoundingBox::Rectangle(bounds) => match combined_bounds { @@ -381,56 +668,20 @@ impl IntoIterator for Table { /// Consumes a [`Table`] and returns an iterator of [`TableRow`]s, each containing the owned data of the respective row from the original table. fn into_iter(self) -> Self::IntoIter { - let mut attributes = self.attributes; - + let row_attributes = self.attributes.into_row_vec(); TableRowIter { element: self.element.into_iter(), - transform: attributes.remove::>("transform").unwrap_or_default().into_iter(), - alpha_blending: attributes.remove::>("alpha_blending").unwrap_or_default().into_iter(), - source_node_id: attributes.remove::>>("source_node_id").unwrap_or_default().into_iter(), + attributes: row_attributes.into_iter(), } } } -pub struct TableRowIter { - element: std::vec::IntoIter, - transform: std::vec::IntoIter, - alpha_blending: std::vec::IntoIter, - source_node_id: std::vec::IntoIter>, -} - -impl Iterator for TableRowIter { - type Item = TableRow; - - fn next(&mut self) -> Option { - let element = self.element.next()?; - let transform = self.transform.next()?; - let alpha_blending = self.alpha_blending.next()?; - let source_node_id = self.source_node_id.next()?; - - Some(TableRow::new(element, transform, alpha_blending, source_node_id)) - } -} - -impl DoubleEndedIterator for TableRowIter { - fn next_back(&mut self) -> Option { - let element = self.element.next_back()?; - let transform = self.transform.next_back()?; - let alpha_blending = self.alpha_blending.next_back()?; - let source_node_id = self.source_node_id.next_back()?; - - Some(TableRow::new(element, transform, alpha_blending, source_node_id)) - } -} - impl Default for Table { fn default() -> Self { - let mut attributes = Attributes::new(); - attributes.insert("transform".to_string(), Vec::::new()); - attributes.insert("alpha_blending".to_string(), Vec::::new()); - attributes.insert("source_node_id".to_string(), Vec::>::new()); - - Self { element: Vec::new(), attributes } + Self { + element: Vec::new(), + attributes: AttributeColumns::new(), + } } } @@ -439,31 +690,28 @@ impl Hash for Table { for element in &self.element { element.hash(state); } - for transform in self.transforms() { - transform.to_cols_array().map(|x| x.to_bits()).hash(state); - } - for alpha_blending in self.alpha_blendings() { - alpha_blending.hash(state); - } } } impl PartialEq for Table { fn eq(&self, other: &Self) -> bool { - self.element == other.element && self.transforms() == other.transforms() && self.alpha_blendings() == other.alpha_blendings() + self.element == other.element } } impl ApplyTransform for Table { + /// Right-multiplies the modification into each row's transform attribute. fn apply_transform(&mut self, modification: &DAffine2) { - for transform in self.transforms_mut() { - *transform *= *modification; + for mut row in self.iter_mut() { + *row.attribute_mut_or_insert_default::("transform") *= *modification; } } + /// Left-multiplies the modification into each row's transform attribute. fn left_apply_transform(&mut self, modification: &DAffine2) { - for transform in self.transforms_mut() { - *transform = *modification * *transform; + for mut row in self.iter_mut() { + let current_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + *row.attribute_mut_or_insert_default("transform") = *modification * current_transform; } } } @@ -473,23 +721,33 @@ unsafe impl StaticType for Table { } impl FromIterator> for Table { + /// Collects an iterator of [`TableRow`]s into a [`Table`], pre-allocating based on the iterator's size hint. fn from_iter>>(iter: I) -> Self { let iter = iter.into_iter(); - let (lower, _) = iter.size_hint(); - let mut table = Self::with_capacity(lower); + let (lower_bound, _) = iter.size_hint(); + let mut table = Self::with_capacity(lower_bound); + for row in iter { table.push(row); } + table } } -// TABLE ROW TYPES +// =========== +// TableRow +// =========== +/// An owned row containing an element of type `T` and a set of type-erased scalar attributes. +/// +/// Used to build rows before pushing them into a [`Table`], or when consuming rows out of a +/// table via [`IntoIterator`]. Attribute values use scalar [`AttributeValues`] storage rather +/// than the columnar layout inside a [`Table`]. #[derive(Clone, Debug)] pub struct TableRow { - pub element: T, - attributes: Attributes, + element: T, + attributes: AttributeValues, } impl Default for TableRow { @@ -500,171 +758,360 @@ impl Default for TableRow { impl PartialEq for TableRow { fn eq(&self, other: &Self) -> bool { - self.element == other.element && self.transform() == other.transform() && self.alpha_blending() == other.alpha_blending() && self.source_node_id() == other.source_node_id() + self.element == other.element } } #[cfg(feature = "serde")] impl serde::Serialize for TableRow { + /// Serializes only the element, omitting type-erased attributes which are not serializable. fn serialize(&self, serializer: S) -> Result { #[derive(serde::Serialize)] struct TableRowHelper<'a, T: serde::Serialize> { element: &'a T, - transform: &'a DAffine2, - alpha_blending: &'a AlphaBlending, - source_node_id: &'a Option, } - TableRowHelper { - element: &self.element, - transform: self.transform(), - alpha_blending: self.alpha_blending(), - source_node_id: self.source_node_id(), - } - .serialize(serializer) + TableRowHelper { element: &self.element }.serialize(serializer) } } #[cfg(feature = "serde")] impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for TableRow { + /// Deserializes the element and initializes an empty set of attributes. fn deserialize>(deserializer: D) -> Result { #[derive(serde::Deserialize)] struct TableRowHelper { #[serde(alias = "instance")] element: T, - #[serde(default = "default_transform")] - transform: DAffine2, - #[serde(default)] - alpha_blending: AlphaBlending, - #[serde(default)] - source_node_id: Option, - } - - fn default_transform() -> DAffine2 { - DAffine2::IDENTITY } let helper = TableRowHelper::deserialize(deserializer)?; - Ok(TableRow::new(helper.element, helper.transform, helper.alpha_blending, helper.source_node_id)) + Ok(TableRow::new_from_element(helper.element)) } } impl TableRow { - pub fn new(element: T, transform: DAffine2, alpha_blending: AlphaBlending, source_node_id: Option) -> Self { - let mut attributes = Attributes::new(); - attributes.insert("transform".to_string(), transform); - attributes.insert("alpha_blending".to_string(), alpha_blending); - attributes.insert("source_node_id".to_string(), source_node_id); + /// Constructs a row from a pre-built element and attributes pair. + pub fn from_parts(element: T, attributes: AttributeValues) -> Self { Self { element, attributes } } + /// Constructs a row with the given element and an empty set of attributes. pub fn new_from_element(element: T) -> Self { - Self::new(element, DAffine2::IDENTITY, AlphaBlending::default(), None) + Self::from_parts(element, AttributeValues::new()) + } + + /// Returns a shared reference to this row's element. + pub fn element(&self) -> &T { + &self.element } - pub fn transform(&self) -> &DAffine2 { - static DEFAULT: DAffine2 = DAffine2::IDENTITY; - self.attributes.get::("transform").unwrap_or(&DEFAULT) + /// Returns a mutable reference to this row's element. + pub fn element_mut(&mut self) -> &mut T { + &mut self.element } - pub fn transform_mut(&mut self) -> &mut DAffine2 { - self.attributes.get_or_insert_default_mut::("transform") + /// Consumes this row and returns the owned element, discarding attributes. + pub fn into_element(self) -> T { + self.element } - pub fn alpha_blending(&self) -> &AlphaBlending { - static DEFAULT: AlphaBlending = AlphaBlending::new(); - self.attributes.get::("alpha_blending").unwrap_or(&DEFAULT) + /// Consumes this row and returns its element and attributes as separate owned values. + pub fn into_parts(self) -> (T, AttributeValues) { + (self.element, self.attributes) } - pub fn alpha_blending_mut(&mut self) -> &mut AlphaBlending { - self.attributes.get_or_insert_default_mut::("alpha_blending") + /// Returns a shared reference to all attributes of this row. + pub fn attributes(&self) -> &AttributeValues { + &self.attributes } - pub fn source_node_id(&self) -> &Option { - static DEFAULT: Option = None; - self.attributes.get::>("source_node_id").unwrap_or(&DEFAULT) + /// Returns a mutable reference to all attributes of this row. + pub fn attributes_mut(&mut self) -> &mut AttributeValues { + &mut self.attributes } - pub fn source_node_id_mut(&mut self) -> &mut Option { - self.attributes.get_or_insert_default_mut::>("source_node_id") + /// Returns a reference to the attribute value for the given key, if it exists and is of the requested type. + pub fn attribute(&self, key: &str) -> Option<&U> { + self.attributes.get(key) } - pub fn as_ref(&self) -> TableRowRef<'_, T> { - TableRowRef { - element: &self.element, - transform: self.transform(), - alpha_blending: self.alpha_blending(), - source_node_id: self.source_node_id(), - } + /// Returns the attribute value for the given key, or the provided default if absent or of a different type. + pub fn attribute_or<'a, U: 'static>(&'a self, key: &str, default: &'a U) -> &'a U { + self.attribute(key).unwrap_or(default) + } + + /// Returns a clone of the attribute value for the given key, or the provided default if absent or of a different type. + pub fn attribute_cloned_or(&self, key: &str, default: U) -> U { + self.attribute(key).cloned().unwrap_or(default) + } + + /// Returns a clone of the attribute value for the given key, or `U`'s default value if absent or of a different type. + pub fn attribute_cloned_or_default(&self, key: &str) -> U { + self.attribute(key).cloned().unwrap_or_default() + } + + /// Returns a mutable reference to the attribute value for the given key, if it exists and is of the requested type. + pub fn attribute_mut(&mut self, key: &str) -> Option<&mut U> { + self.attributes.get_mut(key) + } + + /// Returns a mutable reference to the attribute value for the given key, inserting a default value if absent or of a different type. + pub fn attribute_mut_or_insert_default(&mut self, key: &str) -> &mut U { + self.attributes.get_or_insert_default_mut(key) + } + + /// Sets the attribute value for the given key, replacing any existing entry with the same key. + pub fn set_attribute(&mut self, key: impl Into, value: U) { + self.attributes.insert(key, value); + } + + /// Sets the attribute value for the given key and returns the row, enabling builder-style chaining. + pub fn with_attribute(mut self, key: impl Into, value: U) -> Self { + self.set_attribute(key, value); + self + } + + /// Removes and returns the attribute value for the given key, if it exists and is of the requested type. + pub fn remove_attribute(&mut self, key: &str) -> Option { + self.attributes.remove(key) } } -#[derive(Copy, Clone, Debug, PartialEq)] +// ============== +// TableRowRef +// ============== + +/// A shared view into a single row of a [`Table`], providing read access to the element and its attributes. +/// +/// Holds a reference to the element and a reference to the table's shared column store +/// together with this row's index, so attribute lookups are forwarded to the correct column slot. +#[derive(Copy, Clone, Debug)] pub struct TableRowRef<'a, T> { - pub element: &'a T, - transform: &'a DAffine2, - alpha_blending: &'a AlphaBlending, - source_node_id: &'a Option, + element: &'a T, + index: usize, + columns: &'a AttributeColumns, } -impl TableRowRef<'_, T> { - pub fn transform(&self) -> &DAffine2 { - self.transform +impl<'a, T> TableRowRef<'a, T> { + /// Returns a shared reference to this row's element. + pub fn element(&self) -> &'a T { + self.element + } + + /// Returns an iterator over all attribute keys for this table. + pub fn attribute_keys(&self) -> impl Iterator { + self.columns.keys() + } + + /// Returns a debug-formatted display string for the attribute at the given key for this row. + pub fn attribute_display_value(&self, key: &str, overrides: fn(&dyn std::any::Any) -> Option) -> Option { + self.columns.display_cell_value(key, self.index, overrides) + } + + /// Returns a reference to the attribute value for the given key, if it exists and is of the requested type. + pub fn attribute(&self, key: &str) -> Option<&U> { + self.columns.get_cell(key, self.index) + } + + /// Returns the attribute value for the given key, or the provided default if absent or of a different type. + pub fn attribute_or<'b, U: 'static>(&'b self, key: &str, default: &'b U) -> &'b U { + self.attribute(key).unwrap_or(default) } - pub fn alpha_blending(&self) -> &AlphaBlending { - self.alpha_blending + /// Returns a clone of the attribute value for the given key, or the provided default if absent or of a different type. + pub fn attribute_cloned_or(&self, key: &str, default: U) -> U { + self.attribute(key).cloned().unwrap_or(default) } - pub fn source_node_id(&self) -> &Option { - self.source_node_id + /// Returns a clone of the attribute value for the given key, or `U`'s default value if absent or of a different type. + pub fn attribute_cloned_or_default(&self, key: &str) -> U { + self.attribute(key).cloned().unwrap_or_default() } + /// Clones both the element and its row attributes into a new owned [`TableRow`]. pub fn into_cloned(self) -> TableRow where T: Clone, { - TableRow::new(self.element.clone(), *self.transform, *self.alpha_blending, *self.source_node_id) + TableRow { + element: self.element.clone(), + attributes: self.columns.clone_row(self.index), + } } } -#[derive(Debug)] +// ============== +// TableRowMut +// ============== + +/// A mutable view into a single row of a [`Table`], providing read-write access to the element and its attributes. +/// +/// Uses a raw pointer to the column store in order to split the borrow between the element +/// (which lives in the `Vec`) and the attribute columns (a separate field). The `PhantomData` +/// marker ties the pointer's validity to the `'a` lifetime of the originating table borrow. pub struct TableRowMut<'a, T> { - pub element: &'a mut T, - transform: &'a mut DAffine2, - alpha_blending: &'a mut AlphaBlending, - source_node_id: &'a mut Option, + element: &'a mut T, + index: usize, + columns: *mut AttributeColumns, + _marker: std::marker::PhantomData<&'a mut AttributeColumns>, } -impl TableRowMut<'_, T> { - pub fn transform(&self) -> &DAffine2 { - self.transform +impl std::fmt::Debug for TableRowMut<'_, T> +where + T: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TableRowMut").field("element", &self.element).field("index", &self.index).finish() + } +} + +impl<'a, T> TableRowMut<'a, T> { + /// Returns a shared reference to the element, bound by the lifetime of this borrow. + pub fn element(&self) -> &T { + self.element + } + + /// Returns a mutable reference to the element, bound by the lifetime of this borrow. + pub fn element_mut(&mut self) -> &mut T { + self.element } - pub fn transform_mut(&mut self) -> &mut DAffine2 { - self.transform + /// Consumes this row reference and returns the underlying mutable reference with its full `'a` lifetime. + /// Use this instead of [`element_mut`](Self::element_mut) when the reference must outlive the row borrow itself, + /// such as when returning it from a closure or storing it past the row's scope. + pub fn into_element_mut(self) -> &'a mut T { + self.element } - pub fn alpha_blending(&self) -> &AlphaBlending { - self.alpha_blending + /// Returns a shared reference to the column store backing this row's attributes. + /// + // SAFETY: The raw pointer `self.columns` is guaranteed valid for the lifetime `'a` by the + // PhantomData marker and by the Table methods that construct TableRowMut. + fn columns(&self) -> &AttributeColumns { + unsafe { &*self.columns } } - pub fn alpha_blending_mut(&mut self) -> &mut AlphaBlending { - self.alpha_blending + /// Returns a mutable reference to the column store backing this row's attributes. + /// + // SAFETY: The raw pointer `self.columns` is guaranteed valid for the lifetime `'a` by the + // PhantomData marker and by the Table methods that construct TableRowMut. + fn columns_mut(&mut self) -> &mut AttributeColumns { + unsafe { &mut *self.columns } } - pub fn source_node_id(&self) -> &Option { - self.source_node_id + /// Returns a reference to the attribute value for the given key, if it exists and is of the requested type. + pub fn attribute(&self, key: &str) -> Option<&U> { + self.columns().get_cell(key, self.index) } - pub fn source_node_id_mut(&mut self) -> &mut Option { - self.source_node_id + /// Returns the attribute value for the given key, or the provided default if absent or of a different type. + pub fn attribute_or<'b, U: 'static>(&'b self, key: &str, default: &'b U) -> &'b U { + self.attribute(key).unwrap_or(default) + } + + /// Returns a clone of the attribute value for the given key, or the provided default if absent or of a different type. + pub fn attribute_cloned_or(&self, key: &str, default: U) -> U { + self.attribute(key).cloned().unwrap_or(default) + } + + /// Returns a clone of the attribute value for the given key, or a default constructed value if absent or of a different type. + pub fn attribute_cloned_or_default(&self, key: &str) -> U { + self.attribute(key).cloned().unwrap_or_default() + } + + /// Returns a mutable reference to the attribute value for the given key, if it exists and is of the requested type. + pub fn attribute_mut(&mut self, key: &str) -> Option<&mut U> { + let index = self.index; + self.columns_mut().get_cell_mut(key, index) + } + + /// Returns a mutable reference to the attribute value for the given key, inserting a default value if absent or of a different type. + pub fn attribute_mut_or_insert_default(&mut self, key: &str) -> &mut U { + let index = self.index; + self.columns_mut().get_or_insert_default_cell(key, index) + } + + /// Sets the attribute value for the given key, replacing any existing entry with the same key. + pub fn set_attribute(&mut self, key: impl Into, value: U) { + let index = self.index; + self.columns_mut().set_cell(key, index, value); + } +} + +// =============== +// TableRowIter +// =============== + +/// Owning iterator over the rows of a consumed [`Table`], yielding [`TableRow`]s. +/// +/// Created by [`Table::into_iter`]. The table's columnar attributes are converted into +/// per-row scalar [`AttributeValues`] during construction so each yielded row is self-contained. +pub struct TableRowIter { + element: std::vec::IntoIter, + attributes: std::vec::IntoIter, +} + +impl Iterator for TableRowIter { + type Item = TableRow; + + fn next(&mut self) -> Option { + Some(TableRow { + element: self.element.next()?, + attributes: self.attributes.next()?, + }) + } +} + +impl DoubleEndedIterator for TableRowIter { + fn next_back(&mut self) -> Option { + Some(TableRow { + element: self.element.next_back()?, + attributes: self.attributes.next_back()?, + }) + } +} + +// ================== +// TableRowIterMut +// ================== + +/// Mutable iterator over table rows. Each yielded [`TableRowMut`] provides mutable access to one +/// element and the shared column store at that row's index. +pub struct TableRowIterMut<'a, T> { + inner: std::iter::Enumerate>, + columns: *mut AttributeColumns, + _marker: std::marker::PhantomData<&'a mut AttributeColumns>, +} + +impl<'a, T> Iterator for TableRowIterMut<'a, T> { + type Item = TableRowMut<'a, T>; + + fn next(&mut self) -> Option { + let (index, element) = self.inner.next()?; + Some(TableRowMut { + element, + index, + columns: self.columns, + _marker: std::marker::PhantomData, + }) } } -// Conversion from Table to Option - extracts first element -impl From> for Option { - fn from(table: Table) -> Self { - table.iter().nth(0).map(|row| row.element).copied() +impl DoubleEndedIterator for TableRowIterMut<'_, T> { + fn next_back(&mut self) -> Option { + let (index, element) = self.inner.next_back()?; + Some(TableRowMut { + element, + index, + columns: self.columns, + _marker: std::marker::PhantomData, + }) } } + +// SAFETY: The raw `*mut AttributeColumns` pointer is derived from a `&mut Table` borrow that lives +// for `'a`, and `AttributeColumns` is `Send`. The pointer is only used to split the borrow between +// the element slice and the column store, which are disjoint fields. +unsafe impl Send for TableRowIterMut<'_, T> {} +unsafe impl Send for TableRowMut<'_, T> {} diff --git a/node-graph/libraries/graphic-types/src/artboard.rs b/node-graph/libraries/graphic-types/src/artboard.rs index 119caba646..7b99be742f 100644 --- a/node-graph/libraries/graphic-types/src/artboard.rs +++ b/node-graph/libraries/graphic-types/src/artboard.rs @@ -50,9 +50,24 @@ impl BoundingBox for Artboard { return RenderBoundingBox::Rectangle(artboard_bounds()); } - match self.content.bounding_box(transform, include_stroke) { - RenderBoundingBox::Rectangle(content_bounds) => RenderBoundingBox::Rectangle(Quad::combine_bounds(content_bounds, artboard_bounds())), - other => other, + let mut combined_bounds = None; + + for row in self.content.iter() { + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + + match row.element().bounding_box(transform * row_transform, include_stroke) { + RenderBoundingBox::None => continue, + RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite, + RenderBoundingBox::Rectangle(bounds) => match combined_bounds { + Some(existing) => combined_bounds = Some(Quad::combine_bounds(existing, bounds)), + None => combined_bounds = Some(bounds), + }, + } + } + + match combined_bounds { + Some(content_bounds) => RenderBoundingBox::Rectangle(Quad::combine_bounds(content_bounds, artboard_bounds())), + None => RenderBoundingBox::Rectangle(artboard_bounds()), } } } @@ -104,7 +119,12 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re ArtboardFormat::ArtboardGroup(artboard_group) => { let mut table = Table::new(); for (artboard, source_node_id) in artboard_group.artboards { - table.push(TableRow::new(artboard, DAffine2::IDENTITY, AlphaBlending::default(), source_node_id)); + table.push( + TableRow::new_from_element(artboard) + .with_attribute("transform", DAffine2::IDENTITY) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", source_node_id), + ); } table } @@ -112,7 +132,12 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re .element .into_iter() .zip(old_table.transform.into_iter().zip(old_table.alpha_blending)) - .map(|(element, (transform, alpha_blending))| TableRow::new(element, transform, alpha_blending, None)) + .map(|(element, (transform, alpha_blending))| { + TableRow::new_from_element(element) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", None::) + }) .collect(), ArtboardFormat::ArtboardTable(artboard_table) => artboard_table, }) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 05f26b1ffe..d028f2423c 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -141,16 +141,19 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic fn flatten_recursive(output: &mut Table, current_graphic_table: Table, extract_variant: fn(Graphic) -> Option>) { for current_graphic_row in current_graphic_table.into_iter() { - let source_node_id = *current_graphic_row.source_node_id(); - let current_transform = *current_graphic_row.transform(); - let current_alpha_blending = *current_graphic_row.alpha_blending(); + let source_node_id: Option = current_graphic_row.attribute_cloned_or_default("source_node_id"); + let current_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default("transform"); + let current_alpha_blending: AlphaBlending = current_graphic_row.attribute_cloned_or_default("alpha_blending"); - match current_graphic_row.element { + match current_graphic_row.into_element() { // Recurse into nested graphic tables, composing the parent's transform onto each child Graphic::Graphic(mut sub_table) => { for mut graphic in sub_table.iter_mut() { - *graphic.transform_mut() = current_transform * *graphic.transform(); - *graphic.alpha_blending_mut() = compose_alpha_blending(current_alpha_blending, *graphic.alpha_blending()); + let child_transform: DAffine2 = graphic.attribute_cloned_or_default("transform"); + let child_alpha_blending: AlphaBlending = graphic.attribute_cloned_or_default("alpha_blending"); + + *graphic.attribute_mut_or_insert_default("transform") = current_transform * child_transform; + *graphic.attribute_mut_or_insert_default("alpha_blending") = compose_alpha_blending(current_alpha_blending, child_alpha_blending); } flatten_recursive(output, sub_table, extract_variant); @@ -159,9 +162,15 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic other => { if let Some(typed_table) = extract_variant(other) { for row in typed_table.into_iter() { - let transform = current_transform * *row.transform(); - let alpha_blending = compose_alpha_blending(current_alpha_blending, *row.alpha_blending()); - output.push(TableRow::new(row.element, transform, alpha_blending, source_node_id)); + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let row_alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + let (element, mut attributes) = row.into_parts(); + + attributes.insert("transform", current_transform * row_transform); + attributes.insert("alpha_blending", compose_alpha_blending(current_alpha_blending, row_alpha_blending)); + attributes.insert("source_node_id", source_node_id); + + output.push(TableRow::from_parts(element, attributes)); } } } @@ -312,20 +321,20 @@ impl Graphic { pub fn had_clip_enabled(&self) -> bool { match self { - Graphic::Vector(vector) => vector.iter().all(|row| row.alpha_blending().clip), - Graphic::Graphic(graphic) => graphic.iter().all(|row| row.alpha_blending().clip), - Graphic::RasterCPU(raster) => raster.iter().all(|row| row.alpha_blending().clip), - Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending().clip), - Graphic::Color(color) => color.iter().all(|row| row.alpha_blending().clip), - Graphic::Gradient(gradient) => gradient.iter().all(|row| row.alpha_blending().clip), + Graphic::Vector(vector) => vector.iter().all(|row| row.attribute_cloned_or_default::("alpha_blending").clip), + Graphic::Graphic(graphic) => graphic.iter().all(|row| row.attribute_cloned_or_default::("alpha_blending").clip), + Graphic::RasterCPU(raster) => raster.iter().all(|row| row.attribute_cloned_or_default::("alpha_blending").clip), + Graphic::RasterGPU(raster) => raster.iter().all(|row| row.attribute_cloned_or_default::("alpha_blending").clip), + Graphic::Color(color) => color.iter().all(|row| row.attribute_cloned_or_default::("alpha_blending").clip), + Graphic::Gradient(gradient) => gradient.iter().all(|row| row.attribute_cloned_or_default::("alpha_blending").clip), } } pub fn can_reduce_to_clip_path(&self) -> bool { match self { Graphic::Vector(vector) => vector.iter().all(|row| { - let style = &row.element.style; - let alpha_blending = row.alpha_blending(); + let style = &row.element().style; + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); (alpha_blending.opacity > 1. - f32::EPSILON) && style.fill().is_opaque() && style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) }), _ => false, @@ -336,12 +345,12 @@ impl Graphic { impl BoundingBox for Graphic { fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { match self { - Graphic::Vector(vector) => vector.bounding_box(transform, include_stroke), - Graphic::RasterCPU(raster) => raster.bounding_box(transform, include_stroke), - Graphic::RasterGPU(raster) => raster.bounding_box(transform, include_stroke), - Graphic::Graphic(graphic) => graphic.bounding_box(transform, include_stroke), - Graphic::Color(color) => color.bounding_box(transform, include_stroke), - Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke), + Graphic::Vector(table) => table.bounding_box(transform, include_stroke), + Graphic::RasterCPU(table) => table.bounding_box(transform, include_stroke), + Graphic::RasterGPU(table) => table.bounding_box(transform, include_stroke), + Graphic::Graphic(table) => table.bounding_box(transform, include_stroke), + Graphic::Color(table) => table.bounding_box(transform, include_stroke), + Graphic::Gradient(table) => table.bounding_box(transform, include_stroke), } } } @@ -503,7 +512,12 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res GraphicFormat::OldGraphicGroup(old) => { let mut graphic_table = Table::new(); for (graphic, source_node_id) in old.elements { - graphic_table.push(TableRow::new(graphic, old.transform, old.alpha_blending, source_node_id)); + graphic_table.push( + TableRow::new_from_element(graphic) + .with_attribute("transform", old.transform) + .with_attribute("alpha_blending", old.alpha_blending) + .with_attribute("source_node_id", source_node_id), + ); } graphic_table } @@ -511,30 +525,36 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res .element .into_iter() .flat_map(|element| { - element - .elements - .into_iter() - .map(move |(graphic, source_node_id)| TableRow::new(graphic, element.transform, element.alpha_blending, source_node_id)) + element.elements.into_iter().map(move |(graphic, source_node_id)| { + TableRow::new_from_element(graphic) + .with_attribute("transform", element.transform) + .with_attribute("alpha_blending", element.alpha_blending) + .with_attribute("source_node_id", source_node_id) + }) }) .collect(), GraphicFormat::OldTableOldGraphicGroup(old) => old .element .into_iter() .flat_map(|element| { - element - .elements - .into_iter() - .map(move |(graphic, source_node_id)| TableRow::new(graphic, element.transform, element.alpha_blending, source_node_id)) + element.elements.into_iter().map(move |(graphic, source_node_id)| { + TableRow::new_from_element(graphic) + .with_attribute("transform", element.transform) + .with_attribute("alpha_blending", element.alpha_blending) + .with_attribute("source_node_id", source_node_id) + }) }) .collect(), GraphicFormat::OldTableGraphicGroup(old) => old .element .into_iter() .flat_map(|element| { - element - .elements - .into_iter() - .map(move |(graphic, source_node_id)| TableRow::new(graphic, Default::default(), Default::default(), source_node_id)) + element.elements.into_iter().map(move |(graphic, source_node_id)| { + TableRow::new_from_element(graphic) + .with_attribute("transform", DAffine2::IDENTITY) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", source_node_id) + }) }) .collect(), GraphicFormat::Table(value) => { @@ -542,8 +562,16 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res if let Ok(old_table) = serde_json::from_value::>(value.clone()) { let mut graphic_table = Table::new(); for row in old_table.iter() { - for (graphic, source_node_id) in &row.element.elements { - graphic_table.push(TableRow::new(graphic.clone(), *row.transform(), *row.alpha_blending(), *source_node_id)); + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let row_alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + + for (graphic, source_node_id) in &row.element().elements { + graphic_table.push( + TableRow::new_from_element(graphic.clone()) + .with_attribute("transform", row_transform) + .with_attribute("alpha_blending", row_alpha_blending) + .with_attribute("source_node_id", *source_node_id), + ); } } graphic_table diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 953826c195..c0bc9eee2f 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -82,20 +82,30 @@ pub mod migrations { upstream_data: old.upstream_graphic_group, }); let mut row = vector_table.iter_mut().next().unwrap(); - *row.transform_mut() = old.transform; - *row.alpha_blending_mut() = old.alpha_blending; + *row.attribute_mut_or_insert_default("transform") = old.transform; + *row.attribute_mut_or_insert_default("alpha_blending") = old.alpha_blending; vector_table } VectorFormat::OlderVectorTable(older_table) => older_table .element .into_iter() - .map(|element| TableRow::new(element, DAffine2::IDENTITY, AlphaBlending::default(), None)) + .map(|element| { + TableRow::new_from_element(element) + .with_attribute("transform", DAffine2::IDENTITY) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::) + }) .collect(), VectorFormat::OldVectorTable(old_table) => old_table .element .into_iter() .zip(old_table.transform.into_iter().zip(old_table.alpha_blending)) - .map(|(element, (transform, alpha_blending))| TableRow::new(element, transform, alpha_blending, None)) + .map(|(element, (transform, alpha_blending))| { + TableRow::new_from_element(element) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", None::) + }) .collect(), VectorFormat::VectorTable(vector_table) => vector_table, }) diff --git a/node-graph/libraries/no-std-types/src/blending.rs b/node-graph/libraries/no-std-types/src/blending.rs index f6bb2965af..f7abe42306 100644 --- a/node-graph/libraries/no-std-types/src/blending.rs +++ b/node-graph/libraries/no-std-types/src/blending.rs @@ -28,19 +28,6 @@ impl Hash for AlphaBlending { self.clip.hash(state); } } -impl Display for AlphaBlending { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let round = |x: f32| (x * 1e3).round() / 1e3; - write!( - f, - "Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}", - self.blend_mode, - round(self.opacity * 100.), - round(self.fill * 100.), - if self.clip { "Yes" } else { "No" } - ) - } -} impl AlphaBlending { pub const fn new() -> Self { diff --git a/node-graph/libraries/raster-types/src/image.rs b/node-graph/libraries/raster-types/src/image.rs index 7eebe2b7ef..4f7da07fbd 100644 --- a/node-graph/libraries/raster-types/src/image.rs +++ b/node-graph/libraries/raster-types/src/image.rs @@ -260,7 +260,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> fn from(element: GraphicElement) -> Self { match element { GraphicElement::RasterFrame(RasterFrame::ImageFrame(image)) => Self { - image: image.iter().next().unwrap().element.clone(), + image: image.iter().next().unwrap().element().clone(), }, _ => panic!("Expected Image, found {element:?}"), } @@ -315,7 +315,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> } fn from_image_table(table: Table>) -> Table> { - Table::new_from_element(Raster::new_cpu(table.iter().next().unwrap().element.clone())) + Table::new_from_element(Raster::new_cpu(table.iter().next().unwrap().element().clone())) } fn old_table_to_new_table(old_table: OldTable) -> Table { @@ -323,7 +323,12 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> .element .into_iter() .zip(old_table.transform.into_iter().zip(old_table.alpha_blending)) - .map(|(element, (transform, alpha_blending))| TableRow::new(element, transform, alpha_blending, None)) + .map(|(element, (transform, alpha_blending))| { + TableRow::new_from_element(element) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", None::) + }) .collect() } @@ -331,7 +336,12 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> old_table .element .into_iter() - .map(|element| TableRow::new(element, DAffine2::IDENTITY, AlphaBlending::default(), None)) + .map(|element| { + TableRow::new_from_element(element) + .with_attribute("transform", DAffine2::IDENTITY) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::) + }) .collect() } @@ -341,7 +351,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> .iter() .next() .unwrap_or(Table::new_from_element(ImageFrame::default()).iter().next().unwrap()) - .element + .element() .image .clone(), )) @@ -352,8 +362,8 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> FormatVersions::OldImageFrame(OldImageFrame { image, transform, alpha_blending }) => { let mut image_frame_table = Table::new_from_element(Raster::new_cpu(image)); let mut row = image_frame_table.iter_mut().next().unwrap(); - *row.transform_mut() = transform; - *row.alpha_blending_mut() = alpha_blending; + *row.attribute_mut_or_insert_default("transform") = transform; + *row.attribute_mut_or_insert_default("alpha_blending") = alpha_blending; image_frame_table } FormatVersions::OlderImageFrameTable(old_table) => from_image_frame_table(older_table_to_new_table(old_table)), @@ -410,7 +420,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D fn from(element: GraphicElement) -> Self { match element { GraphicElement::RasterFrame(RasterFrame::ImageFrame(image)) => Self { - image: image.iter().next().unwrap().element.clone(), + image: image.iter().next().unwrap().element().clone(), }, _ => panic!("Expected Image, found {element:?}"), } @@ -444,19 +454,18 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D } Ok(match FormatVersions::deserialize(deserializer)? { - FormatVersions::Image(image) => TableRow::new(Raster::new_cpu(image), DAffine2::IDENTITY, AlphaBlending::default(), None), - FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => TableRow::new( - Raster::new_cpu(image_frame_with_transform_and_blending.image), - image_frame_with_transform_and_blending.transform, - image_frame_with_transform_and_blending.alpha_blending, - None, - ), - FormatVersions::ImageFrameTable(image_frame) => TableRow::new( - Raster::new_cpu(image_frame.iter().next().unwrap().element.image.clone()), - DAffine2::IDENTITY, - AlphaBlending::default(), - None, - ), + FormatVersions::Image(image) => TableRow::new_from_element(Raster::new_cpu(image)) + .with_attribute("transform", DAffine2::IDENTITY) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::), + FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => TableRow::new_from_element(Raster::new_cpu(image_frame_with_transform_and_blending.image)) + .with_attribute("transform", image_frame_with_transform_and_blending.transform) + .with_attribute("alpha_blending", image_frame_with_transform_and_blending.alpha_blending) + .with_attribute("source_node_id", None::), + FormatVersions::ImageFrameTable(image_frame) => TableRow::new_from_element(Raster::new_cpu(image_frame.iter().next().unwrap().element().image.clone())) + .with_attribute("transform", DAffine2::IDENTITY) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::), FormatVersions::RasterTable(image_frame_table) => image_frame_table.into_iter().next().unwrap_or_default(), FormatVersions::RasterTableRow(image_table_row) => image_table_row, }) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 7d6a66d757..cab0c52b5a 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -1,5 +1,6 @@ use crate::render_ext::RenderExt; use crate::to_peniko::BlendModeExt; +use core_types::AlphaBlending; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::color::{Alpha, Color}; @@ -428,8 +429,11 @@ impl Render for Graphic { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.first_element_source_id.insert(element_id, *row.source_node_id()); - metadata.local_transforms.insert(element_id, *row.transform()); + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + + metadata.first_element_source_id.insert(element_id, source_node_id); + metadata.local_transforms.insert(element_id, transform); } } Graphic::RasterCPU(table) => { @@ -437,7 +441,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.local_transforms.insert(element_id, *row.transform()); + metadata.local_transforms.insert(element_id, row.attribute_cloned_or_default("transform")); } } Graphic::RasterGPU(table) => { @@ -445,7 +449,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.local_transforms.insert(element_id, *row.transform()); + metadata.local_transforms.insert(element_id, row.attribute_cloned_or_default("transform")); } } Graphic::Color(table) => { @@ -453,7 +457,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.local_transforms.insert(element_id, *row.transform()); + metadata.local_transforms.insert(element_id, row.attribute_cloned_or_default("transform")); } } Graphic::Gradient(table) => { @@ -461,7 +465,7 @@ impl Render for Graphic { // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { - metadata.local_transforms.insert(element_id, *row.transform()); + metadata.local_transforms.insert(element_id, row.attribute_cloned_or_default("transform")); } } } @@ -656,25 +660,26 @@ impl Render for Artboard { impl Render for Table { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for artboard in self.iter() { - artboard.element.render_svg(render, render_params); + artboard.element().render_svg(render, render_params); } } fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { for row in self.iter() { - row.element.render_to_vello(scene, transform, context, render_params); + row.element().render_to_vello(scene, transform, context, render_params); } } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { for row in self.iter() { - row.element.collect_metadata(metadata, footprint, *row.source_node_id()); + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); + row.element().collect_metadata(metadata, footprint, source_node_id); } } fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for row in self.iter() { - row.element.add_upstream_click_targets(click_targets); + row.element().add_upstream_click_targets(click_targets); } } @@ -689,31 +694,34 @@ impl Render for Table { let mut mask_state = None; while let Some(row) = iter.next() { + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + render.parent_tag( "g", |attributes| { - let matrix = format_transform_matrix(*row.transform()); + let matrix = format_transform_matrix(transform); if !matrix.is_empty() { attributes.push("transform", matrix); } - let opacity = row.alpha_blending().opacity(render_params.for_mask); + let opacity = alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending().blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending().blend_mode.render()); + if alpha_blending.blend_mode != BlendMode::default() { + attributes.push("style", alpha_blending.blend_mode.render()); } - let next_clips = iter.peek().is_some_and(|next_row| next_row.element.had_clip_enabled()); + let next_clips = iter.peek().is_some_and(|next_row| next_row.element().had_clip_enabled()); if next_clips && mask_state.is_none() { let uuid = generate_uuid(); - let mask_type = if row.element.can_reduce_to_clip_path() { MaskType::Clip } else { MaskType::Mask }; + let mask_type = if row.element().can_reduce_to_clip_path() { MaskType::Clip } else { MaskType::Mask }; mask_state = Some((uuid, mask_type)); let mut svg = SvgRender::new(); - row.element.render_svg(&mut svg, &render_params.for_clipper()); + row.element().render_svg(&mut svg, &render_params.for_clipper()); write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap(); mask_type.write_to_defs(&mut attributes.0.svg_defs, uuid, svg.svg.to_svg_string()); @@ -729,7 +737,7 @@ impl Render for Table { } }, |render| { - row.element.render_svg(render, render_params); + row.element().render_svg(render, render_params); }, ); } @@ -740,8 +748,9 @@ impl Render for Table { let mut mask_element_and_transform = None; while let Some(row) = iter.next() { - let transform = transform * *row.transform(); - let alpha_blending = *row.alpha_blending(); + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let transform = transform * row_transform; + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); let mut layer = false; @@ -751,9 +760,9 @@ impl Render for Table { }; let mut bounds = RenderBoundingBox::None; - let opacity = row.alpha_blending().opacity(render_params.for_mask); + let opacity = alpha_blending.opacity(render_params.for_mask); if opacity < 1. || (render_params.render_mode != RenderMode::Outline && alpha_blending.blend_mode != BlendMode::default()) { - bounds = row.element.bounding_box(transform, true); + bounds = row.element().bounding_box(transform, true); if let RenderBoundingBox::Rectangle(bounds) = bounds { scene.push_layer( @@ -767,17 +776,17 @@ impl Render for Table { } } - let next_clips = iter.peek().is_some_and(|next_row| next_row.element.had_clip_enabled()); + let next_clips = iter.peek().is_some_and(|next_row| next_row.element().had_clip_enabled()); if next_clips && mask_element_and_transform.is_none() { - mask_element_and_transform = Some((row.element, transform)); + mask_element_and_transform = Some((row.element(), transform)); - row.element.render_to_vello(scene, transform, context, render_params); + row.element().render_to_vello(scene, transform, context, render_params); } else if let Some((mask_element, transform_mask)) = mask_element_and_transform { if !next_clips { mask_element_and_transform = None; } if !layer { - bounds = row.element.bounding_box(transform, true); + bounds = row.element().bounding_box(transform, true); } if let RenderBoundingBox::Rectangle(bounds) = bounds { @@ -794,14 +803,14 @@ impl Render for Table { ); } - row.element.render_to_vello(scene, transform, context, render_params); + row.element().render_to_vello(scene, transform, context, render_params); if matches!(bounds, RenderBoundingBox::Rectangle(_)) { scene.pop_layer(); scene.pop_layer(); } } else { - row.element.render_to_vello(scene, transform, context, render_params); + row.element().render_to_vello(scene, transform, context, render_params); } if layer { @@ -812,14 +821,17 @@ impl Render for Table { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for row in self.iter() { + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); + let mut footprint = footprint; - footprint.transform *= *row.transform(); + footprint.transform *= row_transform; - if let Some(element_id) = row.source_node_id() { - row.element.collect_metadata(metadata, footprint, Some(*element_id)); + if let Some(element_id) = source_node_id { + row.element().collect_metadata(metadata, footprint, Some(element_id)); } else { // Recurse through anonymous wrapper rows to reach nested content with source_node_ids - row.element.collect_metadata(metadata, footprint, None); + row.element().collect_metadata(metadata, footprint, None); } } @@ -827,11 +839,12 @@ impl Render for Table { let mut all_upstream_click_targets = Vec::new(); for row in self.iter() { + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); let mut new_click_targets = Vec::new(); - row.element.add_upstream_click_targets(&mut new_click_targets); + row.element().add_upstream_click_targets(&mut new_click_targets); for click_target in new_click_targets.iter_mut() { - click_target.apply_transform(*row.transform()) + click_target.apply_transform(row_transform) } all_upstream_click_targets.extend(new_click_targets); @@ -843,12 +856,13 @@ impl Render for Table { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for row in self.iter() { + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); let mut new_click_targets = Vec::new(); - row.element.add_upstream_click_targets(&mut new_click_targets); + row.element().add_upstream_click_targets(&mut new_click_targets); for click_target in new_click_targets.iter_mut() { - click_target.apply_transform(*row.transform()) + click_target.apply_transform(row_transform) } click_targets.extend(new_click_targets); @@ -856,12 +870,13 @@ impl Render for Table { } fn contains_artboard(&self) -> bool { - self.iter().any(|row| row.element.contains_artboard()) + self.iter().any(|row| row.element().contains_artboard()) } fn new_ids_from_hash(&mut self, _reference: Option) { - for row in self.iter_mut() { - row.element.new_ids_from_hash(*row.source_node_id()); + for mut row in self.iter_mut() { + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); + row.element_mut().new_ids_from_hash(source_node_id); } } } @@ -869,12 +884,13 @@ impl Render for Table { impl Render for Table { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for row in self.iter() { - let multiplied_transform = *row.transform(); - let vector = &row.element; + let multiplied_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + let vector = &row.element(); // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform let has_real_stroke = vector.style.stroke().filter(|stroke| stroke.weight() > 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.); - let applied_stroke_transform = set_stroke_transform.unwrap_or(*row.transform()); + let applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform); let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform); let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse()); let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY); @@ -886,7 +902,7 @@ impl Render for Table { let mut path = String::new(); - for mut bezpath in row.element.stroke_bezpath_iter() { + for mut bezpath in row.element().stroke_bezpath_iter() { bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); path.push_str(bezpath.to_svg().as_str()); } @@ -899,7 +915,7 @@ impl Render for Table { let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let can_draw_aligned_stroke = path_is_closed && vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()); - let can_use_paint_order = !(row.element.style.fill().is_none() || !row.element.style.fill().is_opaque() || mask_type == MaskType::Clip); + let can_use_paint_order = !(row.element().style.fill().is_none() || !row.element().style.fill().is_opaque() || mask_type == MaskType::Clip); let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order; let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); @@ -911,7 +927,7 @@ impl Render for Table { if !matrix.is_empty() { attributes.push("transform", matrix); } - let mut style = row.element.style.clone(); + let mut style = row.element().style.clone(); style.clear_stroke(); let fill_and_stroke = style.render( &mut attributes.0.svg_defs, @@ -928,11 +944,16 @@ impl Render for Table { let push_id = needs_separate_alignment_fill.then_some({ let id = format!("alignment-{}", generate_uuid()); - let mut element = row.element.clone(); + let mut element = row.element().clone(); element.style.clear_stroke(); element.style.set_fill(Fill::solid(Color::BLACK)); - let vector_row = Table::new_from_row(TableRow::new(element, *row.transform(), *row.alpha_blending(), None)); + let vector_row = Table::new_from_row( + TableRow::new_from_element(element) + .with_attribute("transform", multiplied_transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", None::), + ); (id, mask_type, vector_row) }); @@ -949,7 +970,7 @@ impl Render for Table { if !matrix.is_empty() { attributes.push("transform", matrix); } - let mut style = row.element.style.clone(); + let mut style = row.element().style.clone(); style.clear_stroke(); let fill_only = style.render( &mut attributes.0.svg_defs, @@ -975,7 +996,7 @@ impl Render for Table { if let Some((ref id, mask_type, ref vector_row)) = push_id { let mut svg = SvgRender::new(); vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform)); - let stroke = row.element.style.stroke().unwrap(); + let stroke = row.element().style.stroke().unwrap(); let weight = stroke.effective_width() * max_scale(applied_stroke_transform); let quad = Quad::from_box(transformed_bounds).inflate(weight); let (x, y) = quad.top_left().into(); @@ -1000,7 +1021,7 @@ impl Render for Table { render_params.aligned_strokes = can_draw_aligned_stroke; render_params.override_paint_order = can_draw_aligned_stroke && can_use_paint_order; - let mut style = row.element.style.clone(); + let mut style = row.element().style.clone(); if needs_separate_alignment_fill || use_face_fill { style.clear_fill(); } @@ -1017,13 +1038,13 @@ impl Render for Table { attributes.push("fill-rule", "evenodd"); } - let opacity = row.alpha_blending().opacity(render_params.for_mask); + let opacity = alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending().blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending().blend_mode.render()); + if alpha_blending.blend_mode != BlendMode::default() { + attributes.push("style", alpha_blending.blend_mode.render()); } }); @@ -1035,7 +1056,7 @@ impl Render for Table { if !matrix.is_empty() { attributes.push("transform", matrix); } - let mut style = row.element.style.clone(); + let mut style = row.element().style.clone(); style.clear_stroke(); let fill_and_stroke = style.render( &mut attributes.0.svg_defs, @@ -1057,8 +1078,10 @@ impl Render for Table { for row in self.iter() { use graphic_types::vector_types::vector; - let multiplied_transform = parent_transform * *row.transform(); - let has_real_stroke = row.element.style.stroke().filter(|stroke| stroke.weight() > 0.); + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + let multiplied_transform = parent_transform * row_transform; + let has_real_stroke = row.element().style.stroke().filter(|stroke| stroke.weight() > 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.); let mut applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform); let mut element_transform = set_stroke_transform @@ -1072,11 +1095,11 @@ impl Render for Table { multiplied_transform }; } - let layer_bounds = row.element.bounding_box().unwrap_or_default(); + let layer_bounds = row.element().bounding_box().unwrap_or_default(); let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y); let mut path = kurbo::BezPath::new(); - for mut bezpath in row.element.stroke_bezpath_iter() { + for mut bezpath in row.element().stroke_bezpath_iter() { bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); for element in bezpath { path.push(element); @@ -1086,14 +1109,14 @@ impl Render for Table { // If we're using opacity or a blend mode, we need to push a layer let blend_mode = match render_params.render_mode { RenderMode::Outline => peniko::Mix::Normal, - _ => row.alpha_blending().blend_mode.to_peniko(), + _ => alpha_blending.blend_mode.to_peniko(), }; let mut layer = false; - let opacity = row.alpha_blending().opacity(render_params.for_mask); - if opacity < 1. || row.alpha_blending().blend_mode != BlendMode::default() { + let opacity = alpha_blending.opacity(render_params.for_mask); + if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { layer = true; - let weight = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width); + let weight = row.element().style.stroke().as_ref().map_or(0., Stroke::effective_width); let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform)); let layer_bounds = quad.bounding_box(); scene.push_layer( @@ -1106,13 +1129,13 @@ impl Render for Table { } let can_draw_aligned_stroke = - row.element.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && row.element.stroke_bezier_paths().all(|path| path.closed()); + row.element().style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && row.element().stroke_bezier_paths().all(|path| path.closed()); let use_layer = can_draw_aligned_stroke; - let wants_stroke_below = row.element.style.stroke().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); + let wants_stroke_below = row.element().style.stroke().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); // Closures to avoid duplicated fill/stroke drawing logic - let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| match row.element.style.fill() { + let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| match row.element().style.fill() { Fill::Solid(color) => { let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path); @@ -1126,7 +1149,7 @@ impl Render for Table { }); } - let bounds = row.element.nonzero_bounding_box(); + let bounds = row.element().nonzero_bounding_box(); let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); let inverse_parent_transform = if parent_transform.matrix2.determinant() != 0. { @@ -1178,10 +1201,10 @@ impl Render for Table { }; // Branching vectors without regions (e.g. mesh grids) need face-by-face fill rendering. - let use_face_fill = row.element.use_face_fill(); + let use_face_fill = row.element().use_face_fill(); let do_fill = |scene: &mut Scene| { if use_face_fill { - for mut face_path in row.element.construct_faces().filter(|face| face.area() >= 0.) { + for mut face_path in row.element().construct_faces().filter(|face| face.area() >= 0.) { face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); let mut kurbo_path = kurbo::BezPath::new(); for element in face_path { @@ -1189,7 +1212,7 @@ impl Render for Table { } do_fill_path(scene, &kurbo_path, peniko::Fill::NonZero); } - } else if row.element.is_branching() { + } else if row.element().is_branching() { do_fill_path(scene, &path, peniko::Fill::EvenOdd); } else { do_fill_path(scene, &path, peniko::Fill::NonZero); @@ -1197,7 +1220,7 @@ impl Render for Table { }; let do_stroke = |scene: &mut Scene, width_scale: f64| { - if let Some(stroke) = row.element.style.stroke() { + if let Some(stroke) = row.element().style.stroke() { let color = match stroke.color { Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]), None => peniko::Color::TRANSPARENT, @@ -1238,19 +1261,24 @@ impl Render for Table { } _ => { if use_layer { - let mut element = row.element.clone(); + let mut element = row.element().clone(); element.style.clear_stroke(); element.style.set_fill(Fill::solid(Color::BLACK)); - let vector_table = Table::new_from_row(TableRow::new(element, *row.transform(), *row.alpha_blending(), None)); + let vector_table = Table::new_from_row( + TableRow::new_from_element(element) + .with_attribute("transform", row_transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", None::), + ); - let bounds = row.element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); - let weight = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width); + let bounds = row.element().bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); + let weight = row.element().style.stroke().as_ref().map_or(0., Stroke::effective_width); let quad = Quad::from_box(bounds).inflate(weight * max_scale(applied_stroke_transform)); let bounds = quad.bounding_box(); let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); - let compose = if row.element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) { + let compose = if row.element().style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) { peniko::Compose::SrcOut } else { peniko::Compose::SrcIn @@ -1287,7 +1315,7 @@ impl Render for Table { Stroke, } - let order = match row.element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) { + let order = match row.element().style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) { true => [Op::Stroke, Op::Fill], false => [Op::Fill, Op::Stroke], // Default }; @@ -1311,10 +1339,11 @@ impl Render for Table { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option) { for row in self.iter() { - let transform = *row.transform(); - let vector = row.element; + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); + let vector = row.element(); - if let Some(element_id) = caller_element_id.or(*row.source_node_id()) { + if let Some(element_id) = caller_element_id.or(source_node_id) { // When recovering element_id from the row's source_node_id (because the caller // passed None), also store the transform metadata that Graphic::collect_metadata // normally provides but skipped due to the None element_id. @@ -1366,31 +1395,31 @@ impl Render for Table { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for row in self.iter() { - let stroke_width = row.element.style.stroke().as_ref().map_or(0., Stroke::effective_width); - let filled = row.element.style.fill() != &Fill::None; + let stroke_width = row.element().style.stroke().as_ref().map_or(0., Stroke::effective_width); + let filled = row.element().style.fill() != &Fill::None; let fill = |mut subpath: Subpath<_>| { if filled { subpath.set_closed(true); } subpath }; - click_targets.extend(row.element.stroke_bezier_paths().map(fill).map(|subpath| { + click_targets.extend(row.element().stroke_bezier_paths().map(fill).map(|subpath| { let mut click_target = ClickTarget::new_with_subpath(subpath, stroke_width); - click_target.apply_transform(*row.transform()); + click_target.apply_transform(row.attribute_cloned_or_default("transform")); click_target })); // For free-floating anchors, we need to add a click target for each - let single_anchors_targets = row.element.point_domain.ids().iter().filter_map(|&point_id| { - if row.element.any_connected(point_id) { + let single_anchors_targets = row.element().point_domain.ids().iter().filter_map(|&point_id| { + if row.element().any_connected(point_id) { return None; } - let anchor = row.element.point_domain.position_from_id(point_id).unwrap_or_default(); + let anchor = row.element().point_domain.position_from_id(point_id).unwrap_or_default(); let point = FreePoint::new(point_id, anchor); let mut click_target = ClickTarget::new_with_free_point(point); - click_target.apply_transform(*row.transform()); + click_target.apply_transform(row.attribute_cloned_or_default("transform")); Some(click_target) }); click_targets.extend(single_anchors_targets); @@ -1398,8 +1427,8 @@ impl Render for Table { } fn new_ids_from_hash(&mut self, reference: Option) { - for row in self.iter_mut() { - row.element.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default()); + for mut row in self.iter_mut() { + row.element_mut().vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default()); } } } @@ -1407,9 +1436,10 @@ impl Render for Table { impl Render for Table> { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for row in self.iter() { - let image = row.element; + let image = row.element(); - let transform = *row.transform(); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); if image.data.is_empty() { continue; @@ -1436,13 +1466,13 @@ impl Render for Table> { attributes.push("width", size.x.to_string()); attributes.push("height", size.y.to_string()); - let opacity = row.alpha_blending().opacity(render_params.for_mask); + let opacity = alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending().blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending().blend_mode.render()); + if alpha_blending.blend_mode != BlendMode::default() { + attributes.push("style", alpha_blending.blend_mode.render()); } }, |render| { @@ -1476,12 +1506,12 @@ impl Render for Table> { attributes.push("transform", matrix); } - let opacity = row.alpha_blending().opacity(render_params.for_mask); + let opacity = alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending().blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending().blend_mode.render()); + if alpha_blending.blend_mode != BlendMode::default() { + attributes.push("style", alpha_blending.blend_mode.render()); } }); } @@ -1490,12 +1520,12 @@ impl Render for Table> { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) { for row in self.iter() { - let image = &row.element; + let image = &row.element(); if image.data.is_empty() { continue; } - let alpha_blending = *row.alpha_blending(); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); let blend_mode = alpha_blending.blend_mode.to_peniko(); let opacity = alpha_blending.opacity(render_params.for_mask); @@ -1511,7 +1541,8 @@ impl Render for Table> { } if let RenderMode::Outline = render_params.render_mode { - let outline_transform = transform * *row.transform(); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let outline_transform: DAffine2 = transform * transform_attribute; draw_raster_outline(scene, &outline_transform, render_params); if layer { @@ -1521,7 +1552,8 @@ impl Render for Table> { continue; } - let image_transform = transform * *row.transform() * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let image_transform = transform * transform_attribute * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); let image_brush = peniko::ImageBrush::new(peniko::ImageData { data: image.to_flat_u8().0.into(), @@ -1548,7 +1580,7 @@ impl Render for Table> { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than one row of the raster table if let Some(raster) = self.iter().next() { - metadata.local_transforms.insert(element_id, *raster.transform()); + metadata.local_transforms.insert(element_id, raster.attribute_cloned_or_default("transform")); } } @@ -1567,7 +1599,7 @@ impl Render for Table> { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { for row in self.iter() { - let alpha_blending = *row.alpha_blending(); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); let blend_mode = match render_params.render_mode { RenderMode::Outline => peniko::Mix::Normal, _ => alpha_blending.blend_mode.to_peniko(), @@ -1585,7 +1617,8 @@ impl Render for Table> { } if let RenderMode::Outline = render_params.render_mode { - let outline_transform = transform * *row.transform(); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let outline_transform = transform * transform_attribute; draw_raster_outline(scene, &outline_transform, render_params); if layer { @@ -1595,8 +1628,8 @@ impl Render for Table> { continue; } - let width = row.element.data().width(); - let height = row.element.data().height(); + let width = row.element().data().width(); + let height = row.element().data().height(); let image = peniko::ImageBrush::new(peniko::ImageData { data: peniko::Blob::new(LAZY_ARC_VEC_ZERO_U8.deref().clone()), format: peniko::ImageFormat::Rgba8, @@ -1605,9 +1638,10 @@ impl Render for Table> { alpha_type: peniko::ImageAlphaType::Alpha, }) .with_extend(peniko::Extend::Repeat); - let image_transform = transform * *row.transform() * DAffine2::from_scale(1. / DVec2::new(width as f64, height as f64)); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let image_transform = transform * transform_attribute * DAffine2::from_scale(1. / DVec2::new(width as f64, height as f64)); scene.draw_image(&image, kurbo::Affine::new(image_transform.to_cols_array())); - context.resource_overrides.push((image, row.element.data().clone())); + context.resource_overrides.push((image, row.element().data().clone())); if layer { scene.pop_layer() @@ -1623,7 +1657,7 @@ impl Render for Table> { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than one row of the raster table if let Some(raster) = self.iter().next() { - metadata.local_transforms.insert(element_id, *raster.transform()); + metadata.local_transforms.insert(element_id, raster.attribute_cloned_or_default("transform")); } } @@ -1642,24 +1676,25 @@ impl Render for Table> { impl Render for Table { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for row in self.iter() { + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); render.leaf_tag("polyline", |attributes| { // Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead let max = u64::MAX; attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}")); - let color = row.element; + let color = row.element(); attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma())); if color.a() < 1. { attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string()); } - let opacity = row.alpha_blending().opacity(render_params.for_mask); + let opacity = alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending().blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending().blend_mode.render()); + if alpha_blending.blend_mode != BlendMode::default() { + attributes.push("style", alpha_blending.blend_mode.render()); } }); } @@ -1669,11 +1704,11 @@ impl Render for Table { use vello::peniko; for row in self.iter() { - let alpha_blending = *row.alpha_blending(); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); let blend_mode = alpha_blending.blend_mode.to_peniko(); let opacity = alpha_blending.opacity(render_params.for_mask); - let color = row.element; + let color = row.element(); let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]); let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)); @@ -1698,13 +1733,15 @@ impl Render for Table { // TODO: Fix infinite gradient rendering fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for row in self.iter() { + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); render.leaf_tag("rect", |attributes| { // Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead let max = u64::MAX; attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}")); let mut stop_string = String::new(); - for (position, color, original_midpoint) in row.element.interpolated_samples() { + for (position, color, original_midpoint) in row.element().interpolated_samples() { let _ = write!(stop_string, r##" { stop_string.push_str(" />"); } - let gradient_transform = render_params.footprint.transform * *row.transform(); + let gradient_transform = render_params.footprint.transform * transform; let gradient_transform_matrix = format_transform_matrix(gradient_transform); let gradient_transform_attribute = if gradient_transform_matrix.is_empty() { String::new() @@ -1748,13 +1785,13 @@ impl Render for Table { attributes.push("fill", format!("url('#{gradient_id}')")); - let opacity = row.alpha_blending().opacity(render_params.for_mask); + let opacity = alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); } - if row.alpha_blending().blend_mode != BlendMode::default() { - attributes.push("style", row.alpha_blending().blend_mode.render()); + if alpha_blending.blend_mode != BlendMode::default() { + attributes.push("style", alpha_blending.blend_mode.render()); } }); } @@ -1765,11 +1802,11 @@ impl Render for Table { use vello::peniko; for row in self.iter() { - let alpha_blending = *row.alpha_blending(); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); let blend_mode = alpha_blending.blend_mode.to_peniko(); let opacity = alpha_blending.opacity(render_params.for_mask); - let color = row.element.color.first().copied().unwrap_or(Color::MAGENTA); + let color = row.element().color.first().copied().unwrap_or(Color::MAGENTA); let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]); let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)); diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index 9809c57929..f8b08feb85 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -1,6 +1,7 @@ //! Contains stylistic options for SVG elements. pub use crate::gradient::*; +use core_types::AlphaBlending; use core_types::Color; use core_types::color::Alpha; use core_types::table::Table; @@ -134,8 +135,8 @@ impl From> for Fill { impl From> for Fill { fn from(color: Table) -> Fill { - let alpha = color.get(0).map(|c| c.alpha_blending().opacity).unwrap_or(1.); - let color: Option = color.into(); + let alpha = color.get(0).map(|row| row.attribute_cloned_or_default::("alpha_blending").opacity).unwrap_or(1.); + let color = color.iter().next().map(|row| row.element()).copied(); Fill::solid_or_none(color.map(|c| c.with_alpha(c.alpha() * alpha))) } } @@ -143,7 +144,7 @@ impl From> for Fill { impl From> for Fill { fn from(gradient: Table) -> Fill { Fill::Gradient(Gradient { - stops: gradient.iter().nth(0).map(|row| row.element.clone()).unwrap_or_default(), + stops: gradient.iter().nth(0).map(|row| row.element().clone()).unwrap_or_default(), ..Default::default() }) } diff --git a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs index b2f8a71a7f..91628c4e4c 100644 --- a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs +++ b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs @@ -172,7 +172,7 @@ impl PerPixelAdjustGraphicsPipeline { let out = textures .iter() .map(|instance| { - let tex_in = &instance.element.texture; + let tex_in = &instance.element().texture; let view_in = tex_in.create_view(&TextureViewDescriptor::default()); let format = tex_in.format(); @@ -236,7 +236,8 @@ impl PerPixelAdjustGraphicsPipeline { rp.set_bind_group(0, Some(&bind_group), &[]); rp.draw(0..3, 0..1); - TableRow::new(Raster::new(GPU { texture: tex_out }), *instance.transform(), *instance.alpha_blending(), *instance.source_node_id()) + let (_, attributes) = instance.into_cloned().into_parts(); + TableRow::from_parts(Raster::new(GPU { texture: tex_out }), attributes) }) .collect::>(); context.queue.submit([cmd.finish()]); diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index bc15db155a..f0cc0920e7 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -152,10 +152,11 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { let table = self .iter() .map(|row| { - let image = row.element; + let image = row.element(); let texture = upload_to_texture(device, queue, image); + let (_, attributes) = row.into_cloned().into_parts(); - TableRow::new(Raster::new_gpu(texture), *row.transform(), *row.alpha_blending(), *row.source_node_id()) + TableRow::from_parts(Raster::new_gpu(texture), attributes) }) .collect(); @@ -199,11 +200,9 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { let mut rows_meta = Vec::new(); for row in self { - let transform = *row.transform(); - let alpha_blending = *row.alpha_blending(); - let source_node_id = *row.source_node_id(); - converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, row.element)); - rows_meta.push(TableRow::new((), transform, alpha_blending, source_node_id)); + let (element, attributes) = row.into_parts(); + converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, element)); + rows_meta.push(TableRow::from_parts((), attributes)); } queue.submit([encoder.finish()]); @@ -221,7 +220,10 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { map_results .into_iter() .zip(rows_meta.into_iter()) - .map(|(element, row)| TableRow::new(element, *row.transform(), *row.alpha_blending(), *row.source_node_id())) + .map(|(element, row)| { + let (_, attributes) = row.into_parts(); + TableRow::from_parts(element, attributes) + }) .collect() } } diff --git a/node-graph/nodes/blending/src/lib.rs b/node-graph/nodes/blending/src/lib.rs index 81a5c78963..47f9392d23 100644 --- a/node-graph/nodes/blending/src/lib.rs +++ b/node-graph/nodes/blending/src/lib.rs @@ -1,3 +1,4 @@ +use core_types::AlphaBlending; use core_types::registry::types::Percentage; use core_types::table::Table; use core_types::{BlendMode, Color, Ctx}; @@ -18,35 +19,35 @@ impl MultiplyAlpha for Color { impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().opacity *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; } } } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().opacity *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; } } } impl MultiplyAlpha for Table> { fn multiply_alpha(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().opacity *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; } } } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().opacity *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; } } } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().opacity *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; } } } @@ -62,35 +63,35 @@ impl MultiplyFill for Color { impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().fill *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; } } } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().fill *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; } } } impl MultiplyFill for Table> { fn multiply_fill(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().fill *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; } } } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().fill *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; } } } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { for mut row in self.iter_mut() { - row.alpha_blending_mut().fill *= factor as f32; + row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; } } } @@ -102,35 +103,35 @@ trait SetBlendMode { impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { for mut row in self.iter_mut() { - row.alpha_blending_mut().blend_mode = blend_mode; + row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; } } } impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { for mut row in self.iter_mut() { - row.alpha_blending_mut().blend_mode = blend_mode; + row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; } } } impl SetBlendMode for Table> { fn set_blend_mode(&mut self, blend_mode: BlendMode) { for mut row in self.iter_mut() { - row.alpha_blending_mut().blend_mode = blend_mode; + row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; } } } impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { for mut row in self.iter_mut() { - row.alpha_blending_mut().blend_mode = blend_mode; + row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; } } } impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { for mut row in self.iter_mut() { - row.alpha_blending_mut().blend_mode = blend_mode; + row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; } } } @@ -142,35 +143,35 @@ trait SetClip { impl SetClip for Table { fn set_clip(&mut self, clip: bool) { for mut row in self.iter_mut() { - row.alpha_blending_mut().clip = clip; + row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; } } } impl SetClip for Table { fn set_clip(&mut self, clip: bool) { for mut row in self.iter_mut() { - row.alpha_blending_mut().clip = clip; + row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; } } } impl SetClip for Table> { fn set_clip(&mut self, clip: bool) { for mut row in self.iter_mut() { - row.alpha_blending_mut().clip = clip; + row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; } } } impl SetClip for Table { fn set_clip(&mut self, clip: bool) { for mut row in self.iter_mut() { - row.alpha_blending_mut().clip = clip; + row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; } } } impl SetClip for Table { fn set_clip(&mut self, clip: bool) { for mut row in self.iter_mut() { - row.alpha_blending_mut().clip = clip; + row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; } } } diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index f01b1d7909..9880954920 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -8,8 +8,9 @@ use core_types::math::bbox::{AxisAlignedBbox, Bbox}; use core_types::registry::FutureWrapperNode; use core_types::table::{Table, TableRow}; use core_types::transform::Transform; +use core_types::uuid::NodeId; use core_types::value::ClonedNode; -use core_types::{Ctx, Node}; +use core_types::{AlphaBlending, Ctx, Node}; use glam::{DAffine2, DVec2}; use raster_nodes::blending_nodes::blend_colors; use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds}; @@ -89,14 +90,15 @@ where return target; } - for table_row in target.iter_mut() { - let target_width = table_row.element.width; - let target_height = table_row.element.height; + for mut table_row in target.iter_mut() { + let target_width = table_row.element().width; + let target_height = table_row.element().height; let target_size = DVec2::new(target_width as f64, target_height as f64); let texture_size = DVec2::new(texture.width as f64, texture.height as f64); - let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * table_row.transform().inverse(); + let transform_attribute: DAffine2 = table_row.attribute_cloned_or_default("transform"); + let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * transform_attribute.inverse(); for position in &positions { let start = document_to_target.transform_point2(*position).round(); @@ -116,12 +118,12 @@ where let max_y = (blit_area_offset.y + blit_area_dimensions.y).saturating_sub(1); let max_x = (blit_area_offset.x + blit_area_dimensions.x).saturating_sub(1); assert!(texture_index(max_x, max_y) < texture.data.len()); - assert!(target_index(max_x, max_y) < table_row.element.data.len()); + assert!(target_index(max_x, max_y) < table_row.element().data.len()); for y in blit_area_offset.y..blit_area_offset.y + blit_area_dimensions.y { for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x { let src_pixel = texture.data[texture_index(x, y)]; - let dst_pixel = &mut table_row.element.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)]; + let dst_pixel = &mut table_row.element_mut().data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)]; *dst_pixel = blend_mode.eval((src_pixel, *dst_pixel)); } } @@ -137,7 +139,7 @@ pub async fn create_brush_texture(brush_style: &BrushStyle) -> Raster { let blank_texture = empty_image((), transform, Table::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default(); let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)); - image.element + image.into_element() } pub fn blend_with_mode(background: TableRow>, foreground: TableRow>, blend_mode: BlendMode, opacity: f64) -> TableRow> { @@ -274,7 +276,10 @@ async fn brush( let has_erase_or_restore_strokes = strokes.iter().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)); if has_erase_or_restore_strokes { let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE); - let mut erase_restore_mask = TableRow::new(Raster::new_cpu(opaque_image), background_bounds, Default::default(), None); + let mut erase_restore_mask = TableRow::new_from_element(Raster::new_cpu(opaque_image)) + .with_attribute("transform", background_bounds) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::); for stroke in strokes { let mut brush_texture = cache.get_cached_brush(&stroke.style); @@ -306,28 +311,30 @@ async fn brush( actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b))); } - let transform = *actual_image.transform(); - let alpha_blending = *actual_image.alpha_blending(); - let source_node_id = *actual_image.source_node_id(); + let transform: DAffine2 = actual_image.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default("alpha_blending"); + let source_node_id: Option = actual_image.attribute_cloned_or_default("source_node_id"); let mut first_row = image.iter_mut().next().unwrap(); - *first_row.element = actual_image.element; - *first_row.transform_mut() = transform; - *first_row.alpha_blending_mut() = alpha_blending; - *first_row.source_node_id_mut() = source_node_id; + *first_row.element_mut() = actual_image.into_element(); + first_row.set_attribute("transform", transform); + first_row.set_attribute("alpha_blending", alpha_blending); + first_row.set_attribute("source_node_id", source_node_id); image } pub fn blend_image_closure(foreground: TableRow>, mut background: TableRow>, map_fn: impl Fn(Color, Color) -> Color) -> TableRow> { - let foreground_size = DVec2::new(foreground.element.width as f64, foreground.element.height as f64); - let background_size = DVec2::new(background.element.width as f64, background.element.height as f64); + let foreground_size = DVec2::new(foreground.element().width as f64, foreground.element().height as f64); + let background_size = DVec2::new(background.element().width as f64, background.element().height as f64); // Transforms a point from the background image to the foreground image - let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground.transform().inverse() * *background.transform() * DAffine2::from_scale(1. / background_size); + let foreground_transform: DAffine2 = foreground.attribute_cloned_or_default("transform"); + let background_transform: DAffine2 = background.attribute_cloned_or_default("transform"); + let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground_transform.inverse() * background_transform * DAffine2::from_scale(1. / background_size); // Footprint of the foreground image (0, 0)..(1, 1) in the background image space - let background_aabb = Bbox::unit().affine_transform(background.transform().inverse() * *foreground.transform()).to_axis_aligned_bbox(); + let background_aabb = Bbox::unit().affine_transform(background_transform.inverse() * foreground_transform).to_axis_aligned_bbox(); // Clamp the foreground image to the background image let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2(); @@ -338,8 +345,10 @@ pub fn blend_image_closure(foreground: TableRow>, mut background: Ta let background_point = DVec2::new(x as f64, y as f64); let foreground_point = background_to_foreground.transform_point2(background_point); - let source_pixel = foreground.element.sample(foreground_point); - let Some(destination_pixel) = background.element.data_mut().get_pixel_mut(x, y) else { continue }; + let source_pixel = foreground.element().sample(foreground_point); + let Some(destination_pixel) = background.element_mut().data_mut().get_pixel_mut(x, y) else { + continue; + }; *destination_pixel = map_fn(source_pixel, *destination_pixel); } @@ -349,13 +358,14 @@ pub fn blend_image_closure(foreground: TableRow>, mut background: Ta } pub fn blend_stamp_closure(foreground: BrushStampGenerator, mut background: TableRow>, map_fn: impl Fn(Color, Color) -> Color) -> TableRow> { - let background_size = DVec2::new(background.element.width as f64, background.element.height as f64); + let background_size = DVec2::new(background.element().width as f64, background.element().height as f64); // Transforms a point from the background image to the foreground image - let background_to_foreground = *background.transform() * DAffine2::from_scale(1. / background_size); + let background_transform: DAffine2 = background.attribute_cloned_or_default("transform"); + let background_to_foreground = background_transform * DAffine2::from_scale(1. / background_size); // Footprint of the foreground image (0, 0)..(1, 1) in the background image space - let background_aabb = Bbox::unit().affine_transform(background.transform().inverse() * foreground.transform()).to_axis_aligned_bbox(); + let background_aabb = Bbox::unit().affine_transform(background_transform.inverse() * foreground.transform()).to_axis_aligned_bbox(); // Clamp the foreground image to the background image let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2(); @@ -368,7 +378,9 @@ pub fn blend_stamp_closure(foreground: BrushStampGenerator, mut backgroun let foreground_point = background_to_foreground.transform_point2(background_point); let Some(source_pixel) = foreground.sample(foreground_point, area) else { continue }; - let Some(destination_pixel) = background.element.data_mut().get_pixel_mut(x, y) else { continue }; + let Some(destination_pixel) = background.element_mut().data_mut().get_pixel_mut(x, y) else { + continue; + }; *destination_pixel = map_fn(source_pixel, *destination_pixel); } @@ -411,6 +423,6 @@ mod test { BrushCache::default(), ) .await; - assert_eq!(image.iter().next().unwrap().element.width, 20); + assert_eq!(image.iter().next().unwrap().element().width, 20); } } diff --git a/node-graph/nodes/brush/src/brush_cache.rs b/node-graph/nodes/brush/src/brush_cache.rs index 2af9bcb087..94b1d255ab 100644 --- a/node-graph/nodes/brush/src/brush_cache.rs +++ b/node-graph/nodes/brush/src/brush_cache.rs @@ -63,7 +63,10 @@ impl BrushCacheImpl { background = std::mem::take(&mut self.blended_image); // Check if the first non-blended stroke is an extension of the last one. - let mut first_stroke_texture = TableRow::new(Raster::::default(), glam::DAffine2::ZERO, Default::default(), None); + let mut first_stroke_texture = TableRow::new_from_element(Raster::::default()) + .with_attribute("transform", glam::DAffine2::ZERO) + .with_attribute("alpha_blending", core_types::AlphaBlending::default()) + .with_attribute("source_node_id", None::); let mut first_stroke_point_skip = 0; let strokes = input[num_blended_strokes..].to_vec(); if !strokes.is_empty() && self.prev_input.len() > num_blended_strokes { diff --git a/node-graph/nodes/graphic/src/artboard.rs b/node-graph/nodes/graphic/src/artboard.rs index 2886c342bd..5bb9f77abe 100644 --- a/node-graph/nodes/graphic/src/artboard.rs +++ b/node-graph/nodes/graphic/src/artboard.rs @@ -50,7 +50,7 @@ pub async fn create_artboard( let dimensions = dimensions.abs(); - let background: Option = background.into(); + let background = background.iter().next().map(|row| row.element()).copied(); let background = background.unwrap_or(Color::WHITE); Table::new_from_element(Artboard { diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 0f4c3379e8..0bdcd84e27 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -2,7 +2,7 @@ use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::registry::types::{Angle, SignedInteger}; use core_types::table::{Table, TableRow}; use core_types::uuid::NodeId; -use core_types::{AnyHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::{AlphaBlending, AnyHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicTable}; use graphic_types::{Artboard, Vector}; @@ -169,7 +169,8 @@ where // Create and add mirrored instance for mut row in content.into_iter() { - *row.transform_mut() = reflected_transform * *row.transform(); + let current_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + row.set_attribute("transform", reflected_transform * current_transform); result_table.push(row); } @@ -200,7 +201,7 @@ pub async fn source_node_id( let mut content = content; for mut row in content.iter_mut() { - *row.source_node_id_mut() = source_node_id; + row.set_attribute("source_node_id", source_node_id); } content @@ -242,7 +243,7 @@ pub async fn legacy_layer_extend( let mut base = base; for mut row in new.into_iter() { - *row.source_node_id_mut() = source_node_id; + row.set_attribute("source_node_id", source_node_id); base.push(row); } @@ -292,10 +293,10 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table, fully_flatten // TODO: Avoid mutable reference, instead return a new Table? fn flatten_table(output_graphic_table: &mut Table, current_graphic_table: Table, fully_flatten: bool, recursion_depth: usize) { for current_row in current_graphic_table.iter() { - let current_element = current_row.element.clone(); - let reference = *current_row.source_node_id(); - let current_transform = *current_row.transform(); - let current_alpha_blending = *current_row.alpha_blending(); + let current_element = current_row.element().clone(); + let reference: Option = current_row.attribute_cloned_or_default("source_node_id"); + let current_transform: DAffine2 = current_row.attribute_cloned_or_default("transform"); + let current_alpha_blending: AlphaBlending = current_row.attribute_cloned_or_default("alpha_blending"); let recurse = fully_flatten || recursion_depth == 0; @@ -304,14 +305,20 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table, fully_flatten Graphic::Graphic(mut current_element) if recurse => { // Apply the parent graphic's transform to all child elements for mut graphic in current_element.iter_mut() { - *graphic.transform_mut() = current_transform * *graphic.transform(); + let graphic_transform: DAffine2 = graphic.attribute_cloned_or_default("transform"); + graphic.set_attribute("transform", current_transform * graphic_transform); } flatten_table(output_graphic_table, current_element, fully_flatten, recursion_depth + 1); } // Push any leaf Graphic elements we encounter, which can be either Graphic table elements beyond the recursion depth, or table elements other than Graphic tables _ => { - output_graphic_table.push(TableRow::new(current_element, current_transform, current_alpha_blending, reference)); + output_graphic_table.push( + TableRow::new_from_element(current_element) + .with_attribute("transform", current_transform) + .with_attribute("alpha_blending", current_alpha_blending) + .with_attribute("source_node_id", reference), + ); } } } @@ -376,12 +383,12 @@ fn colors_to_gradient(_: impl Ctx, #[im GradientStop { position: 0., midpoint: 0.5, - color: *color.element, + color: *color.element(), }, GradientStop { position: 1., midpoint: 0.5, - color: *color.element, + color: *color.element(), }, ])); } @@ -389,7 +396,7 @@ fn colors_to_gradient(_: impl Ctx, #[im let colors = colors.into_iter().enumerate().map(|(index, row)| GradientStop { position: index as f64 / (total_colors - 1) as f64, midpoint: 0.5, - color: row.element, + color: row.into_element(), }); Table::new_from_element(GradientStops::new(colors)) } diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 5fa946b539..95df84b31d 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -120,7 +120,7 @@ fn string_to_bytes(_: impl Ctx, string: String) -> Vec { #[node_macro::node(category("Web Request"), name("Image to Bytes"))] fn image_to_bytes(_: impl Ctx, image: Table>) -> Vec { let Some(image) = image.iter().next() else { return vec![] }; - image.element.data.iter().flat_map(|color| color.to_rgb8_srgb().into_iter()).collect::>() + image.element().data.iter().flat_map(|color| color.to_rgb8_srgb().into_iter()).collect::>() } /// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. @@ -187,6 +187,7 @@ where Table: Render, { use core_types::table::TableRow; + use glam::{DAffine2, DVec2}; if footprint.transform.matrix2.determinant() == 0. { log::trace!("Invalid footprint received for rasterization"); @@ -204,10 +205,11 @@ where }; for mut row in data.iter_mut() { - *row.transform_mut() = glam::DAffine2::from_translation(-aabb.start) * *row.transform(); + let current_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + row.set_attribute("transform", DAffine2::from_translation(-aabb.start) * current_transform); } data.render_svg(&mut render, &render_params); - render.format_svg(glam::DVec2::ZERO, size); + render.format_svg(DVec2::ZERO, size); let svg_string = render.svg.to_svg_string(); canvas.set_resolution(resolution); @@ -228,5 +230,5 @@ where let rasterized = context.get_image_data(0., 0., resolution.x as f64, resolution.y as f64).unwrap(); let image = Image::from_image_data(&rasterized.data().0, resolution.x as u32, resolution.y as u32); - Table::new_from_row(TableRow::new(Raster::new_cpu(image), footprint.transform, Default::default(), None)) + Table::new_from_row(TableRow::new_from_element(Raster::new_cpu(image)).with_attribute("transform", footprint.transform)) } diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 295e46d083..39c958d5c5 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -869,7 +869,7 @@ fn sample_gradient(_: impl Ctx, _primary: (), gradient: Table, po let Some(row) = gradient.get(0) else { return Table::new() }; let position = position.clamp(0., 1.); - let color = row.element.evaluate(position); + let color = row.element().evaluate(position); Table::new_from_element(color) } diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index f83b549045..73e15454dc 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -1,5 +1,6 @@ use core_types::table::{Table, TableRow, TableRowRef}; -use core_types::{Color, Ctx}; +use core_types::uuid::NodeId; +use core_types::{AlphaBlending, Color, Ctx}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath}; use graphic_types::vector_types::vector::PointId; @@ -38,15 +39,16 @@ async fn boolean_operation(vector: impl DoubleEndedIterator("alpha_blending"); + *row.attribute_mut_or_insert_default("source_node_id") = copy_from.attribute_cloned_or_default::>("source_node_id"); + row.element_mut().style = copy_from.element().style.clone(); + row.element_mut().upstream_data = copy_from.element().upstream_data.clone(); } for element in vector { - paths.push(to_bez_path(element.element, *element.transform())); + paths.push(to_bez_path(element.element(), element.attribute_cloned_or_default("transform"))); } let top = match Topology::::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) { @@ -139,7 +141,7 @@ fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator) -> Table { graphic_table .iter() .flat_map(|element| { - match element.element.clone() { + match element.element().clone() { Graphic::Vector(vector) => { // Apply the parent graphic's transform to each element of the vector table - let parent_transform = *element.transform(); + let parent_transform: DAffine2 = element.attribute_cloned_or_default("transform"); vector .into_iter() .map(|mut sub_vector| { - *sub_vector.transform_mut() = parent_transform * *sub_vector.transform(); + let current_transform: DAffine2 = sub_vector.attribute_cloned_or_default("transform"); + *sub_vector.attribute_mut_or_insert_default("transform") = parent_transform * current_transform; sub_vector }) .collect::>() } Graphic::RasterCPU(image) => { - let parent_transform = *element.transform(); + let parent_transform: DAffine2 = element.attribute_cloned_or_default("transform"); let make_row = |transform| { // Convert the image frame into a rectangular subpath with the image's transform let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); @@ -177,10 +180,13 @@ fn flatten_vector(graphic_table: &Table) -> Table { }; // Apply the parent graphic's transform to each raster element - image.iter().map(|row| make_row(parent_transform * *row.transform())).collect::>() + image + .iter() + .map(|row| make_row(parent_transform * row.attribute_cloned_or_default::("transform"))) + .collect::>() } Graphic::RasterGPU(image) => { - let parent_transform = *element.transform(); + let parent_transform: DAffine2 = element.attribute_cloned_or_default("transform"); let make_row = |transform| { // Convert the image frame into a rectangular subpath with the image's transform let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); @@ -194,13 +200,17 @@ fn flatten_vector(graphic_table: &Table) -> Table { }; // Apply the parent graphic's transform to each raster element - image.iter().map(|row| make_row(parent_transform * *row.transform())).collect::>() + image + .iter() + .map(|row| make_row(parent_transform * row.attribute_cloned_or_default::("transform"))) + .collect::>() } Graphic::Graphic(mut graphic) => { - let parent_transform = *element.transform(); + let parent_transform: DAffine2 = element.attribute_cloned_or_default("transform"); // Apply the parent graphic's transform to each element of inner table for mut sub_element in graphic.iter_mut() { - *sub_element.transform_mut() = parent_transform * *sub_element.transform(); + let current_transform: DAffine2 = sub_element.attribute_cloned_or_default("transform"); + *sub_element.attribute_mut_or_insert_default("transform") = parent_transform * current_transform; } // Recursively flatten the inner table into the output vector table @@ -211,32 +221,38 @@ fn flatten_vector(graphic_table: &Table) -> Table { Graphic::Color(color) => color .into_iter() .map(|row| { - let transform = *row.transform(); - let alpha_blending = *row.alpha_blending(); - let source_node_id = *row.source_node_id(); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); let mut element = Vector::default(); - element.style.set_fill(Fill::Solid(row.element)); + element.style.set_fill(Fill::Solid(row.into_element())); element.style.set_stroke_transform(DAffine2::IDENTITY); - TableRow::new(element, transform, alpha_blending, source_node_id) + TableRow::new_from_element(element) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", source_node_id) }) .collect::>(), Graphic::Gradient(gradient) => gradient .into_iter() .map(|row| { - let transform = *row.transform(); - let alpha_blending = *row.alpha_blending(); - let source_node_id = *row.source_node_id(); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); let mut element = Vector::default(); element.style.set_fill(Fill::Gradient(graphic_types::vector_types::gradient::Gradient { - stops: row.element, + stops: row.into_element(), ..Default::default() })); element.style.set_stroke_transform(DAffine2::IDENTITY); - TableRow::new(element, transform, alpha_blending, source_node_id) + TableRow::new_from_element(element) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", source_node_id) }) .collect::>(), } diff --git a/node-graph/nodes/raster/src/adjust.rs b/node-graph/nodes/raster/src/adjust.rs index 4c8865d69c..45c2cbc355 100644 --- a/node-graph/nodes/raster/src/adjust.rs +++ b/node-graph/nodes/raster/src/adjust.rs @@ -18,8 +18,8 @@ mod adjust_std { impl Adjust for Table> { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - for row in self.iter_mut() { - for color in row.element.data_mut().data.iter_mut() { + for mut row in self.iter_mut() { + for color in row.element_mut().data_mut().data.iter_mut() { *color = map_fn(color); } } @@ -27,15 +27,15 @@ mod adjust_std { } impl Adjust for Table { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - for row in self.iter_mut() { - *row.element = map_fn(row.element); + for mut row in self.iter_mut() { + *row.element_mut() = map_fn(row.element()); } } } impl Adjust for Table { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - for row in self.iter_mut() { - row.element.adjust(&map_fn); + for mut row in self.iter_mut() { + row.element_mut().adjust(&map_fn); } } } diff --git a/node-graph/nodes/raster/src/blending_nodes.rs b/node-graph/nodes/raster/src/blending_nodes.rs index 80315a5fe3..76eef88bb5 100644 --- a/node-graph/nodes/raster/src/blending_nodes.rs +++ b/node-graph/nodes/raster/src/blending_nodes.rs @@ -30,13 +30,14 @@ mod blend_std { impl Blend for Table> { fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { let mut result_table = self.clone(); - for (over, under) in result_table.iter_mut().zip(under.iter()) { - let data = over.element.data.iter().zip(under.element.data.iter()).map(|(a, b)| blend_fn(*a, *b)).collect(); + for (mut over, under) in result_table.iter_mut().zip(under.iter()) { + let data = over.element().data.iter().zip(under.element().data.iter()).map(|(a, b)| blend_fn(*a, *b)).collect(); + let (width, height) = (over.element().width, over.element().height); - *over.element = Raster::new_cpu(Image { + *over.element_mut() = Raster::new_cpu(Image { data, - width: over.element.width, - height: over.element.height, + width, + height, base64_string: None, }); } @@ -46,8 +47,9 @@ mod blend_std { impl Blend for Table { fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { let mut result_table = self.clone(); - for (over, under) in result_table.iter_mut().zip(under.iter()) { - *over.element = blend_fn(*over.element, *under.element); + for (mut over, under) in result_table.iter_mut().zip(under.iter()) { + let new_val = blend_fn(*over.element(), *under.element()); + *over.element_mut() = new_val; } result_table } @@ -55,8 +57,9 @@ mod blend_std { impl Blend for Table { fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { let mut result_table = self.clone(); - for (over, under) in result_table.iter_mut().zip(under.iter()) { - *over.element = over.element.blend(under.element, &blend_fn); + for (mut over, under) in result_table.iter_mut().zip(under.iter()) { + let new_val = over.element().blend(under.element(), &blend_fn); + *over.element_mut() = new_val; } result_table } @@ -201,7 +204,7 @@ mod test { let opacity = 100.; let result = super::color_overlay((), Table::new_from_element(Raster::new_cpu(image.clone())), overlay_color, BlendMode::Multiply, opacity); - let result = result.iter().next().unwrap().element; + let result = result.iter().next().unwrap().element().clone(); // The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0) assert_eq!(result.data[0], Color::from_rgbaf32_unchecked(0., image_color.g(), 0., image_color.a())); diff --git a/node-graph/nodes/raster/src/dehaze.rs b/node-graph/nodes/raster/src/dehaze.rs index 493aeab861..31b3a053f5 100644 --- a/node-graph/nodes/raster/src/dehaze.rs +++ b/node-graph/nodes/raster/src/dehaze.rs @@ -12,7 +12,7 @@ async fn dehaze(_: impl Ctx, image_frame: Table>, strength: Percenta image_frame .into_iter() .map(|mut row| { - let image = row.element; + let image = std::mem::replace(row.element_mut(), Raster::new_cpu(Image::default())); // Prepare the image data for processing let image_data = bytemuck::cast_vec(image.data.clone()); let image_buffer = image::Rgba32FImage::from_raw(image.width, image.height, image_data).expect("Failed to convert internal image format into image-rs data type."); @@ -31,7 +31,7 @@ async fn dehaze(_: impl Ctx, image_frame: Table>, strength: Percenta base64_string: None, }; - row.element = Raster::new_cpu(dehazed_image); + *row.element_mut() = Raster::new_cpu(dehazed_image); row }) .collect() diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index eacbd9fb1c..736a8ca9ef 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -24,7 +24,7 @@ async fn blur( image_frame .into_iter() .map(|mut row| { - let image = row.element.clone(); + let image = row.element().clone(); // Run blur algorithm let blurred_image = if radius < 0.1 { @@ -36,7 +36,7 @@ async fn blur( Raster::new_cpu(gaussian_blur_algorithm(image.into_data(), radius, gamma)) }; - row.element = blurred_image; + *row.element_mut() = blurred_image; row }) .collect() @@ -56,7 +56,7 @@ async fn median_filter( image_frame .into_iter() .map(|mut row| { - let image = row.element.clone(); + let image = row.element().clone(); // Apply median filter let filtered_image = if radius < 0.5 { @@ -66,7 +66,7 @@ async fn median_filter( Raster::new_cpu(median_filter_algorithm(image.into_data(), radius as u32)) }; - row.element = filtered_image; + *row.element_mut() = filtered_image; row }) .collect() diff --git a/node-graph/nodes/raster/src/gradient_map.rs b/node-graph/nodes/raster/src/gradient_map.rs index ef9c6e734b..2c5206906f 100644 --- a/node-graph/nodes/raster/src/gradient_map.rs +++ b/node-graph/nodes/raster/src/gradient_map.rs @@ -26,7 +26,7 @@ async fn gradient_map>( image.adjust(|color| { let intensity = color.luminance_srgb(); let intensity = if reverse { 1. - intensity } else { intensity }; - row.element.evaluate(intensity as f64).to_linear_srgb() + row.element().evaluate(intensity as f64).to_linear_srgb() }); image diff --git a/node-graph/nodes/raster/src/image_color_palette.rs b/node-graph/nodes/raster/src/image_color_palette.rs index 8a75d5d915..710793b2b4 100644 --- a/node-graph/nodes/raster/src/image_color_palette.rs +++ b/node-graph/nodes/raster/src/image_color_palette.rs @@ -19,7 +19,7 @@ async fn image_color_palette( let mut color_bins = vec![Vec::new(); (bins + 1.) as usize]; for row in image.iter() { - for pixel in row.element.data.iter() { + for pixel in row.element().data.iter() { let r = pixel.r() * GRID; let g = pixel.g() * GRID; let b = pixel.b() * GRID; diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index fe3f696414..3a02087934 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -34,10 +34,8 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Tabl image_frame .into_iter() .filter_map(|row| { - let image_frame_transform = *row.transform(); - let alpha_blending = *row.alpha_blending(); - let source_node_id = *row.source_node_id(); - let image = row.element; + let image_frame_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let (image, mut attributes) = row.into_parts(); // Resize the image using the image crate let data = bytemuck::cast_vec(image.data.clone()); @@ -88,8 +86,9 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Tabl // we need to adjust the offset if we truncate the offset calculation let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size); + attributes.insert("transform", new_transform); - Some(TableRow::new(Raster::new_cpu(image), new_transform, alpha_blending, source_node_id)) + Some(TableRow::from_parts(Raster::new_cpu(image), attributes)) }) .collect() } @@ -114,23 +113,20 @@ pub fn combine_channels( .zip(alpha) .filter_map(|(((red, green), blue), alpha)| { // Turn any default zero-sized image rows into None - let red = red.filter(|i| i.element.width > 0 && i.element.height > 0); - let green = green.filter(|i| i.element.width > 0 && i.element.height > 0); - let blue = blue.filter(|i| i.element.width > 0 && i.element.height > 0); - let alpha = alpha.filter(|i| i.element.width > 0 && i.element.height > 0); + let red = red.filter(|i| i.element().width > 0 && i.element().height > 0); + let green = green.filter(|i| i.element().width > 0 && i.element().height > 0); + let blue = blue.filter(|i| i.element().width > 0 && i.element().height > 0); + let alpha = alpha.filter(|i| i.element().width > 0 && i.element().height > 0); // Get this row's transform and alpha blending mode from the first non-empty channel - let (transform, alpha_blending, source_node_id) = [&red, &green, &blue, &alpha] - .iter() - .find_map(|i| i.as_ref()) - .map(|i| (*i.transform(), *i.alpha_blending(), *i.source_node_id()))?; + let (_, attributes) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| i.clone().into_parts())?; // Get the common width and height of the channels, which must have equal dimensions let channel_dimensions = [ - red.as_ref().map(|r| (r.element.width, r.element.height)), - green.as_ref().map(|g| (g.element.width, g.element.height)), - blue.as_ref().map(|b| (b.element.width, b.element.height)), - alpha.as_ref().map(|a| (a.element.width, a.element.height)), + red.as_ref().map(|r| (r.element().width, r.element().height)), + green.as_ref().map(|g| (g.element().width, g.element().height)), + blue.as_ref().map(|b| (b.element().width, b.element().height)), + alpha.as_ref().map(|a| (a.element().width, a.element().height)), ]; if channel_dimensions.iter().all(Option::is_none) || channel_dimensions @@ -150,22 +146,22 @@ pub fn combine_channels( for x in 0..image.width() { let image_pixel = image.get_pixel_mut(x, y).unwrap(); - if let Some(r) = red.as_ref().and_then(|r| r.element.get_pixel(x, y)) { + if let Some(r) = red.as_ref().and_then(|r| r.element().get_pixel(x, y)) { image_pixel.set_red(r.l().cast_linear_channel()); } else { image_pixel.set_red(Channel::from_linear(0.)); } - if let Some(g) = green.as_ref().and_then(|g| g.element.get_pixel(x, y)) { + if let Some(g) = green.as_ref().and_then(|g| g.element().get_pixel(x, y)) { image_pixel.set_green(g.l().cast_linear_channel()); } else { image_pixel.set_green(Channel::from_linear(0.)); } - if let Some(b) = blue.as_ref().and_then(|b| b.element.get_pixel(x, y)) { + if let Some(b) = blue.as_ref().and_then(|b| b.element().get_pixel(x, y)) { image_pixel.set_blue(b.l().cast_linear_channel()); } else { image_pixel.set_blue(Channel::from_linear(0.)); } - if let Some(a) = alpha.as_ref().and_then(|a| a.element.get_pixel(x, y)) { + if let Some(a) = alpha.as_ref().and_then(|a| a.element().get_pixel(x, y)) { image_pixel.set_alpha(a.l().cast_linear_channel()); } else { image_pixel.set_alpha(Channel::from_linear(1.)); @@ -173,7 +169,7 @@ pub fn combine_channels( } } - Some(TableRow::new(Raster::new_cpu(image), transform, alpha_blending, source_node_id)) + Some(TableRow::from_parts(Raster::new_cpu(image), attributes)) }) .collect() } @@ -192,32 +188,34 @@ pub fn mask( // No stencil provided so we return the original image return image; }; - let stencil_size = DVec2::new(stencil.element.width as f64, stencil.element.height as f64); + let stencil_size = DVec2::new(stencil.element().width as f64, stencil.element().height as f64); image .into_iter() .filter_map(|mut row| { - let image_size = DVec2::new(row.element.width as f64, row.element.height as f64); - let mask_size = stencil.transform().scale_magnitudes(); + let image_size = DVec2::new(row.element().width as f64, row.element().height as f64); + let stencil_transform: DAffine2 = stencil.attribute_cloned_or_default("transform"); + let mask_size = stencil_transform.scale_magnitudes(); if mask_size == DVec2::ZERO { return None; } // Transforms a point from the background image to the foreground image - let bg_to_fg = *row.transform() * DAffine2::from_scale(1. / image_size); - let stencil_transform_inverse = stencil.transform().inverse(); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size); + let stencil_transform_inverse = stencil_transform.inverse(); - for y in 0..row.element.height { - for x in 0..row.element.width { + for y in 0..row.element().height { + for x in 0..row.element().width { let image_point = DVec2::new(x as f64, y as f64); let mask_point = bg_to_fg.transform_point2(image_point); let local_mask_point = stencil_transform_inverse.transform_point2(mask_point); - let mask_point = stencil.transform().transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE)); - let mask_point = (DAffine2::from_scale(stencil_size) * stencil.transform().inverse()).transform_point2(mask_point); + let mask_point = stencil_transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE)); + let mask_point = (DAffine2::from_scale(stencil_size) * stencil_transform.inverse()).transform_point2(mask_point); - let image_pixel = row.element.data_mut().get_pixel_mut(x, y).unwrap(); - let mask_pixel = stencil.element.sample(mask_point); + let image_pixel = row.element_mut().data_mut().get_pixel_mut(x, y).unwrap(); + let mask_pixel = stencil.element().sample(mask_point); *image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel()); } } @@ -232,20 +230,21 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Table>, bounds: DA image .into_iter() .map(|mut row| { - let image_aabb = Bbox::unit().affine_transform(*row.transform()).to_axis_aligned_bbox(); + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let image_aabb = Bbox::unit().affine_transform(row_transform).to_axis_aligned_bbox(); let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox(); if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) { return row; } - let image_data = &row.element.data; - let (image_width, image_height) = (row.element.width, row.element.height); + let image_data = &row.element().data; + let (image_width, image_height) = (row.element().width, row.element().height); if image_width == 0 || image_height == 0 { return empty_image((), bounds, Table::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap(); } let orig_image_scale = DVec2::new(image_width as f64, image_height as f64); - let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row.transform().inverse(); + let layer_to_image_space = DAffine2::from_scale(orig_image_scale) * row_transform.inverse(); let bounds_in_image_space = Bbox::unit().affine_transform(layer_to_image_space * bounds).to_axis_aligned_bbox(); let new_start = bounds_in_image_space.start.floor().min(DVec2::ZERO); @@ -265,10 +264,10 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: Table>, bounds: DA // Compute new transform. // let layer_to_new_texture_space = (DAffine2::from_scale(1. / new_scale) * DAffine2::from_translation(new_start) * layer_to_image_space).inverse(); - let new_texture_to_layer_space = *row.transform() * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale); + let new_texture_to_layer_space = row_transform * DAffine2::from_scale(1. / orig_image_scale) * DAffine2::from_translation(new_start) * DAffine2::from_scale(new_scale); - row.element = Raster::new_cpu(new_image); - *row.transform_mut() = new_texture_to_layer_space; + *row.element_mut() = Raster::new_cpu(new_image); + row.set_attribute("transform", new_texture_to_layer_space); row }) .collect() @@ -279,13 +278,13 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Table) -> Tab let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32; let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32; - let color: Option = color.into(); + let color = color.iter().next().map(|row| row.element()).copied(); let image = Image::new(width, height, color.unwrap_or(Color::WHITE)); let mut result_table = Table::new_from_element(Raster::new_cpu(image)); let mut row = result_table.get_mut(0).unwrap(); - *row.transform_mut() = transform; - *row.alpha_blending_mut() = AlphaBlending::default(); + row.set_attribute("transform", transform); + row.set_attribute("alpha_blending", AlphaBlending::default()); // Callers of empty_image can safely unwrap on returned table result_table @@ -378,12 +377,12 @@ pub fn noise_pattern( } } - return Table::new_from_row(TableRow::new( - Raster::new_cpu(image), - DAffine2::from_translation(offset) * DAffine2::from_scale(size), - AlphaBlending::default(), - None, - )); + return Table::new_from_row( + TableRow::new_from_element(Raster::new_cpu(image)) + .with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size)) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::), + ); } }; noise.set_noise_type(Some(noise_type)); @@ -441,12 +440,12 @@ pub fn noise_pattern( } } - Table::new_from_row(TableRow::new( - Raster::new_cpu(image), - DAffine2::from_translation(offset) * DAffine2::from_scale(size), - AlphaBlending::default(), - None, - )) + Table::new_from_row( + TableRow::new_from_element(Raster::new_cpu(image)) + .with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size)) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::), + ) } #[node_macro::node(category("Raster: Pattern"))] @@ -484,17 +483,17 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> Table> { } } - Table::new_from_row(TableRow::new( - Raster::new_cpu(Image { + Table::new_from_row( + TableRow::new_from_element(Raster::new_cpu(Image { width, height, data, ..Default::default() - }), - DAffine2::from_translation(offset) * DAffine2::from_scale(size), - AlphaBlending::default(), - None, - )) + })) + .with_attribute("transform", DAffine2::from_translation(offset) * DAffine2::from_scale(size)) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::), + ) } #[inline(always)] diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index fd3d795286..2510db6d9c 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -80,9 +80,10 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( for row in generated_instance.iter() { let mut row = row.into_cloned(); - let local_translation = DAffine2::from_translation(row.transform().translation); - let local_matrix = DAffine2::from_mat2(row.transform().matrix2); - *row.transform_mut() = local_translation * transform * local_matrix; + let local_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let local_translation = DAffine2::from_translation(local_transform.translation); + let local_matrix = DAffine2::from_mat2(local_transform.matrix2); + *row.attribute_mut_or_insert_default("transform") = local_translation * transform * local_matrix; result_table.push(row); } @@ -125,9 +126,10 @@ async fn repeat_radial + Default + Send + Clone + 'static>( for row in generated_instance.iter() { let mut row = row.into_cloned(); - let local_translation = DAffine2::from_translation(row.transform().translation); - let local_matrix = DAffine2::from_mat2(row.transform().matrix2); - *row.transform_mut() = local_translation * transform * local_matrix; + let local_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let local_translation = DAffine2::from_translation(local_transform.translation); + let local_matrix = DAffine2::from_mat2(local_transform.matrix2); + *row.attribute_mut_or_insert_default("transform") = local_translation * transform * local_matrix; result_table.push(row); } @@ -153,8 +155,8 @@ async fn repeat_on_points + Default + Send + Clone + 'static>( let mut result_table = Table::new(); for points_row in points.iter() { - let points = points_row.element; - let transform = points_row.transform(); + let points = points_row.element(); + let transform: DAffine2 = points_row.attribute_cloned_or_default("transform"); let mut iteration = async |index, point| { let transformed_point = transform.transform_point2(point); @@ -163,7 +165,7 @@ async fn repeat_on_points + Default + Send + Clone + 'static>( let generated_instance = instance.eval(new_ctx.into_context()).await; for mut generated_row in generated_instance.into_iter() { - generated_row.transform_mut().translation = transformed_point; + generated_row.attribute_mut_or_insert_default::("transform").translation = transformed_point; result_table.push(generated_row); } }; @@ -232,7 +234,7 @@ mod test { let generated = super::repeat_on_points(context, points, &rect, false).await; assert_eq!(generated.len(), positions.len()); for (position, generated_row) in positions.into_iter().zip(generated.iter()) { - let bounds = generated_row.element.bounding_box_with_transform(*generated_row.transform()).unwrap(); + let bounds = generated_row.element().bounding_box_with_transform(generated_row.attribute_cloned_or_default("transform")).unwrap(); assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10)); assert_eq!((bounds[1] - bounds[0]).x, position.y); } @@ -252,7 +254,7 @@ mod test { ) .await; let vector_table = vector_nodes::flatten_path(Footprint::default(), repeated).await; - let vector = vector_table.iter().next().unwrap().element; + let vector = vector_table.iter().next().unwrap().element(); assert_eq!(vector.region_manipulator_groups().count(), 3); for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() { assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5); @@ -273,7 +275,7 @@ mod test { ) .await; let vector_table = vector_nodes::flatten_path(Footprint::default(), repeated).await; - let vector = vector_table.iter().next().unwrap().element; + let vector = vector_table.iter().next().unwrap().element(); assert_eq!(vector.region_manipulator_groups().count(), 8); for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() { assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5); @@ -285,7 +287,7 @@ mod test { let context = OwnedContextImpl::default().into_context(); let repeated = super::repeat_radial(context, &FutureWrapperNode(vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))), 45., 4., 8).await; let vector_table = vector_nodes::flatten_path(Footprint::default(), repeated).await; - let vector = vector_table.iter().next().unwrap().element; + let vector = vector_table.iter().next().unwrap().element(); assert_eq!(vector.region_manipulator_groups().count(), 8); for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() { diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index 11a667c7ca..c30fa55dde 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -52,16 +52,16 @@ impl PathBuilder { } if per_glyph_instances { - self.vector_table.push(TableRow::new( - Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false), - DAffine2::from_translation(glyph_offset), - AlphaBlending::default(), - None, - )); + self.vector_table.push( + TableRow::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false)) + .with_attribute("transform", DAffine2::from_translation(glyph_offset)) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::), + ); } else { for subpath in self.glyph_subpaths.drain(..) { // Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element - self.vector_table.get_mut(0).unwrap().element.append_subpath(subpath, false); + self.vector_table.get_mut(0).unwrap().element_mut().append_subpath(subpath, false); } } } diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index 0634dae9f1..007661224f 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -69,21 +69,23 @@ fn reset_transform( for mut row in content.iter_mut() { // Translation if reset_translation { - row.transform_mut().translation = DVec2::ZERO; + row.attribute_mut_or_insert_default::("transform").translation = DVec2::ZERO; } // (Rotation, Scale) match (reset_rotation, reset_scale) { (true, true) => { - row.transform_mut().matrix2 = DMat2::IDENTITY; + row.attribute_mut_or_insert_default::("transform").matrix2 = DMat2::IDENTITY; } (true, false) => { - let scale = row.transform().scale_magnitudes(); - row.transform_mut().matrix2 = DMat2::from_diagonal(scale); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let scale = transform_attribute.scale_magnitudes(); + row.attribute_mut_or_insert_default::("transform").matrix2 = DMat2::from_diagonal(scale); } (false, true) => { - let rotation = row.transform().decompose_rotation(); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let rotation = transform_attribute.decompose_rotation(); let rotation_matrix = DMat2::from_angle(rotation); - row.transform_mut().matrix2 = rotation_matrix; + row.attribute_mut_or_insert_default::("transform").matrix2 = rotation_matrix; } (false, false) => {} } @@ -107,7 +109,7 @@ fn replace_transform( transform: DAffine2, ) -> Table { for mut row in content.iter_mut() { - *row.transform_mut() = transform.transform(); + *row.attribute_mut_or_insert_default("transform") = transform.transform(); } content } @@ -127,7 +129,7 @@ async fn extract_transform( )] content: Table, ) -> DAffine2 { - content.iter().next().map(|row| *row.transform()).unwrap_or_default() + content.iter().next().map(|row| row.attribute_cloned_or_default("transform")).unwrap_or_default() } /// Produces the inverse of the input transform, which is the transform that undoes the effect of the original transform. diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 914b928dcf..58c589dbc1 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -400,9 +400,9 @@ mod tests { // Works properly let grid = grid((), (), GridType::Isometric, 10., 5, 5, (30., 30.).into()); - assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5); - assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9); - for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() { + assert_eq!(grid.iter().next().unwrap().element().point_domain.ids().len(), 5 * 5); + assert_eq!(grid.iter().next().unwrap().element().segment_bezier_iter().count(), 4 * 5 + 4 * 9); + for (_, bezier, _, _) in grid.iter().next().unwrap().element().segment_bezier_iter() { assert_eq!(bezier.handles, subpath::BezierHandles::Linear); assert!( ((bezier.start - bezier.end).length() - 10.).abs() < 1e-5, @@ -415,9 +415,9 @@ mod tests { #[test] fn skew_isometric_grid_test() { let grid = grid((), (), GridType::Isometric, 10., 5, 5, (40., 30.).into()); - assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5); - assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9); - for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() { + assert_eq!(grid.iter().next().unwrap().element().point_domain.ids().len(), 5 * 5); + assert_eq!(grid.iter().next().unwrap().element().segment_bezier_iter().count(), 4 * 5 + 4 * 9); + for (_, bezier, _, _) in grid.iter().next().unwrap().element().segment_bezier_iter() { assert_eq!(bezier.handles, subpath::BezierHandles::Linear); let vector = bezier.start - bezier.end; let angle = (vector.angle_to(DVec2::X).to_degrees() + 180.) % 180.; @@ -428,7 +428,7 @@ mod tests { #[test] fn qr_code_test() { let qr = qr_code((), (), "https://graphite.art".to_string(), false, 1., QRCodeErrorCorrectionLevel::Low, true); - assert!(qr.iter().next().unwrap().element.point_domain.ids().len() > 0); - assert!(qr.iter().next().unwrap().element.segment_domain.ids().len() > 0); + assert!(qr.iter().next().unwrap().element().point_domain.ids().len() > 0); + assert!(qr.iter().next().unwrap().element().segment_domain.ids().len() > 0); } } diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index 91a3db772a..4ce37f4a84 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -14,12 +14,12 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table, modification: Bo vector.push(TableRow::default()); } let mut row = vector.get_mut(0).expect("push should give one item"); - modification.apply(row.element); + modification.apply(row.element_mut()); // Update the source node id let this_node_path = node_path.iter().rev().nth(1).copied(); - let existing = *row.source_node_id(); - *row.source_node_id_mut() = existing.or(this_node_path); + let existing: Option = row.attribute_cloned_or_default("source_node_id"); + row.set_attribute("source_node_id", existing.or(this_node_path)); if vector.len() > 1 { warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len()); @@ -31,14 +31,14 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table, modification: Bo #[node_macro::node(category("Vector"))] async fn apply_transform(_ctx: impl Ctx, mut vector: Table) -> Table { for mut row in vector.iter_mut() { - let transform = *row.transform(); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); - for (_, point) in row.element.point_domain.positions_mut() { + for (_, point) in row.element_mut().point_domain.positions_mut() { *point = transform.transform_point2(*point); } - row.element.segment_domain.transform(transform); + row.element_mut().segment_domain.transform(transform); - *row.transform_mut() = DAffine2::IDENTITY; + row.set_attribute("transform", DAffine2::IDENTITY); } vector diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index b2e235eb34..275a70c001 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1,10 +1,12 @@ use core::cmp::Ordering; use core::f64::consts::{PI, TAU}; use core::hash::{Hash, Hasher}; +use core_types::AlphaBlending; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::table::{Table, TableRow, TableRowMut}; use core_types::transform::{Footprint, Transform}; +use core_types::uuid::NodeId; use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; @@ -36,7 +38,9 @@ trait VectorTableIterMut { impl VectorTableIterMut for Table { fn vector_iter_mut(&mut self) -> impl Iterator> { // Grab only the direct children - self.iter_mut().filter_map(|element| element.element.as_vector_mut()).flat_map(move |vector| vector.iter_mut()) + self.iter_mut() + .filter_map(|element| element.into_element_mut().as_vector_mut()) + .flat_map(move |vector| vector.iter_mut()) } } @@ -80,11 +84,12 @@ where let Some(row) = gradient.into_iter().next() else { return content }; let length = content.vector_iter_mut().count(); - let gradient = if reverse { row.element.reversed() } else { row.element }; + let element = row.into_element(); + let gradient = if reverse { element.reversed() } else { element }; let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); - for (i, vector) in content.vector_iter_mut().enumerate() { + for (i, mut vector) in content.vector_iter_mut().enumerate() { let factor = match randomize { true => rng.random::(), false => match repeat_every { @@ -97,10 +102,10 @@ where let color = gradient.evaluate(factor); if fill { - vector.element.style.set_fill(Fill::Solid(color)); + vector.element_mut().style.set_fill(Fill::Solid(color)); } - if stroke && let Some(stroke) = vector.element.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) { - vector.element.style.set_stroke(stroke); + if stroke && let Some(stroke) = vector.element_mut().style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) { + vector.element_mut().style.set_stroke(stroke); } } @@ -140,8 +145,8 @@ async fn fill + 'n + Send, V: VectorTableIterMut + 'n + Send>( _backup_gradient: Gradient, ) -> V { let fill: Fill = fill.into(); - for vector in content.vector_iter_mut() { - vector.element.style.set_fill(fill.clone()); + for mut vector in content.vector_iter_mut() { + vector.element_mut().style.set_fill(fill.clone()); } content @@ -203,7 +208,7 @@ where Table: VectorTableIterMut + 'n + Send, { let stroke = Stroke { - color: color.into(), + color: color.iter().next().map(|row| row.element()).copied(), weight, dash_lengths: dash_lengths.into_vec(), dash_offset, @@ -215,10 +220,10 @@ where paint_order, }; - for vector in content.vector_iter_mut() { + for mut vector in content.vector_iter_mut() { let mut stroke = stroke.clone(); - stroke.transform *= *vector.transform(); - vector.element.style.set_stroke(stroke); + stroke.transform *= vector.attribute_cloned_or_default("transform"); + vector.element_mut().style.set_stroke(stroke); } content @@ -264,8 +269,8 @@ async fn copy_to_points( let do_scale = random_scale_difference.abs() > 1e-6; let do_rotation = random_rotation.abs() > 1e-6; - let points_transform = *row.transform(); - for &point in row.element.point_domain.positions() { + let points_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + for &point in row.element().point_domain.positions() { let translation = points_transform.transform_point2(point); let rotation = if do_rotation { @@ -292,7 +297,8 @@ async fn copy_to_points( let transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation); for mut row in instance.iter().map(|row| row.into_cloned()) { - *row.transform_mut() = transform * *row.transform(); + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + row.set_attribute("transform", transform * row_transform); result_table.push(row); } @@ -324,10 +330,10 @@ async fn round_corners( source .iter() .map(|source| { - let source_transform = *source.transform(); + let source_transform: DAffine2 = source.attribute_cloned_or_default("transform"); let source_transform_inverse = source_transform.inverse(); - let source_node_id = *source.source_node_id(); - let source = source.element; + let source_node_id: Option = source.attribute_cloned_or_default("source_node_id"); + let source = source.element(); let upstream_nested_layers = source.upstream_data.clone(); @@ -416,7 +422,10 @@ async fn round_corners( result.upstream_data = upstream_nested_layers; - TableRow::new(result, source_transform, Default::default(), source_node_id) + TableRow::new_from_element(result) + .with_attribute("transform", source_transform) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", source_node_id) }) .collect() } @@ -434,15 +443,15 @@ pub fn merge_by_distance( MergeByDistanceAlgorithm::Spatial => content .into_iter() .map(|mut row| { - let transform = *row.transform(); - row.element.merge_by_distance_spatial(transform, distance); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + row.element_mut().merge_by_distance_spatial(transform, distance); row }) .collect(), MergeByDistanceAlgorithm::Topological => content .into_iter() .map(|mut row| { - row.element.merge_by_distance_topological(distance); + row.element_mut().merge_by_distance_topological(distance); row }) .collect(), @@ -648,23 +657,26 @@ pub mod extrude_algorithms { #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] async fn extrude(_: impl Ctx, mut source: Table, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> Table { - for TableRowMut { element: source, .. } in source.iter_mut() { - extrude_algorithms::extrude(source, direction, joining_algorithm); + for mut row in source.iter_mut() { + extrude_algorithms::extrude(row.element_mut(), direction, joining_algorithm); } source } #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] async fn box_warp(_: impl Ctx, content: Table, #[expose] rectangle: Table) -> Table { - let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.element, *rect.transform())) else { - return content; - }; + let element_and_transform = rectangle.get(0).map(|rect| { + let element = rect.element().clone(); + let transform: DAffine2 = rect.attribute_cloned_or_default("transform"); + (element, transform) + }); + let Some((target, target_transform)) = element_and_transform else { return content }; content .into_iter() .map(|mut row| { - let transform = *row.transform(); - let vector = row.element; + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let vector = std::mem::take(row.element_mut()); // Get the bounding box of the source vector geometry let source_bbox = vector.bounding_box_with_transform(transform).unwrap_or([DVec2::ZERO, DVec2::ONE]); @@ -722,8 +734,8 @@ async fn box_warp(_: impl Ctx, content: Table, #[expose] rectangle: Tabl result.style.set_stroke_transform(DAffine2::IDENTITY); // Add this to the table and reset the transform since we've applied it directly to the points - row.element = result; - *row.transform_mut() = DAffine2::IDENTITY; + *row.element_mut() = result; + row.set_attribute("transform", DAffine2::IDENTITY); row }) .collect() @@ -832,7 +844,8 @@ where RowsOrColumns::Rows => DVec2::new(strip.along_position, strip.cross_position), RowsOrColumns::Columns => DVec2::new(strip.cross_position, strip.along_position), }; - *row.transform_mut() = DAffine2::from_translation(target_position - top_left) * *row.transform(); + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + row.set_attribute("transform", DAffine2::from_translation(target_position - top_left) * row_transform); strip.along_position += along + separation; } else { @@ -843,7 +856,8 @@ where RowsOrColumns::Rows => DVec2::new(0., new_cross), RowsOrColumns::Columns => DVec2::new(new_cross, 0.), }; - *row.transform_mut() = DAffine2::from_translation(target_position - top_left) * *row.transform(); + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + row.set_attribute("transform", DAffine2::from_translation(target_position - top_left) * row_transform); strips.push(Strip { along_position: along + separation, @@ -875,10 +889,10 @@ async fn auto_tangents( source .iter() .map(|source| { - let transform = *source.transform(); - let alpha_blending = *source.alpha_blending(); - let source_node_id = *source.source_node_id(); - let source = source.element; + let transform: DAffine2 = source.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = source.attribute_cloned_or_default("alpha_blending"); + let source_node_id: Option = source.attribute_cloned_or_default("source_node_id"); + let source = source.element(); let mut result = Vector { style: source.style.clone(), @@ -1010,7 +1024,10 @@ async fn auto_tangents( } } - TableRow::new(result, transform, alpha_blending, source_node_id) + TableRow::new_from_element(result) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", source_node_id) }) .collect() } @@ -1020,7 +1037,7 @@ async fn bounding_box(_: impl Ctx, content: Table) -> Table { content .into_iter() .map(|mut row| { - let vector = row.element; + let vector = std::mem::take(row.element_mut()); let mut result = vector .bounding_box_rect() @@ -1034,7 +1051,7 @@ async fn bounding_box(_: impl Ctx, content: Table) -> Table { result.style = vector.style.clone(); result.style.set_stroke_transform(DAffine2::IDENTITY); - row.element = result; + *row.element_mut() = result; row }) .collect() @@ -1044,7 +1061,7 @@ async fn bounding_box(_: impl Ctx, content: Table) -> Table { async fn dimensions(_: impl Ctx, content: Table) -> DVec2 { content .iter() - .filter_map(|vector| vector.element.bounding_box_with_transform(*vector.transform())) + .filter_map(|vector| vector.element().bounding_box_with_transform(vector.attribute_cloned_or_default("transform"))) .reduce(|[acc_top_left, acc_bottom_right], [top_left, bottom_right]| [acc_top_left.min(top_left), acc_bottom_right.max(bottom_right)]) .map(|[top_left, bottom_right]| bottom_right - top_left) .unwrap_or_default() @@ -1065,11 +1082,11 @@ async fn vec2_to_point(_: impl Ctx, vec2: DVec2) -> Table { /// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist. #[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))] async fn points_to_polyline(_: impl Ctx, mut points: Table, #[default(true)] closed: bool) -> Table { - for row in points.iter_mut() { + for mut row in points.iter_mut() { let mut segment_domain = SegmentDomain::new(); let mut next_id = SegmentId::ZERO; - let points_count = row.element.point_domain.ids().len(); + let points_count = row.element().point_domain.ids().len(); if points_count >= 2 { (0..points_count - 1).for_each(|i| { @@ -1079,13 +1096,13 @@ async fn points_to_polyline(_: impl Ctx, mut points: Table, #[default(tr if closed && points_count != 2 { segment_domain.push(next_id.next_id(), points_count - 1, 0, BezierHandles::Linear, StrokeId::generate()); - row.element + row.element_mut() .region_domain .push(RegionId::generate(), segment_domain.ids()[0]..=*segment_domain.ids().last().unwrap(), FillId::generate()); } } - row.element.segment_domain = segment_domain; + row.element_mut().segment_domain = segment_domain; } points @@ -1096,8 +1113,9 @@ async fn offset_path(_: impl Ctx, content: Table, distance: f64, join: S content .into_iter() .map(|mut row| { - let transform = Affine::new(row.transform().to_cols_array()); - let vector = row.element; + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let transform = Affine::new(transform_attribute.to_cols_array()); + let vector = std::mem::take(row.element_mut()); let bezpaths = vector.stroke_bezpath_iter(); let mut result = Vector { @@ -1128,7 +1146,7 @@ async fn offset_path(_: impl Ctx, content: Table, distance: f64, join: S result.append_bezpath(bezpath_out); } - row.element = result; + *row.element_mut() = result; row }) .collect() @@ -1141,11 +1159,11 @@ async fn solidify_stroke(_: impl Ctx, content: Table) -> Table { content .into_iter() .flat_map(|row| { - let transform = *row.transform(); - let alpha_blending = *row.alpha_blending(); - let source_node_id = *row.source_node_id(); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); - let mut vector = row.element; + let mut vector = row.into_element(); let stroke = vector.style.stroke().clone().unwrap_or_default(); let bezpaths = vector.stroke_bezpath_iter(); @@ -1194,13 +1212,19 @@ async fn solidify_stroke(_: impl Ctx, content: Table) -> Table { solidified_stroke.style.set_fill(Fill::solid_or_none(stroke.color)); } - let stroke_row = TableRow::new(solidified_stroke, transform, alpha_blending, source_node_id); + let stroke_row = TableRow::new_from_element(solidified_stroke) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", source_node_id); // If the original vector has a fill, preserve it as a separate row with the stroke cleared. let has_fill = !vector.style.fill().is_none(); let fill_row = has_fill.then(move || { vector.style.clear_stroke(); - TableRow::new(vector, transform, alpha_blending, source_node_id) + TableRow::new_from_element(vector) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", source_node_id) }); // Ordering based on the paint order. The first row in the table is rendered below the second. @@ -1217,19 +1241,22 @@ async fn separate_subpaths(_: impl Ctx, content: Table) -> Table content .into_iter() .flat_map(|row| { - let style = row.element.style.clone(); - let transform = *row.transform(); - let alpha_blending = *row.alpha_blending(); - let source_node_id = *row.source_node_id(); + let style = row.element().style.clone(); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); - row.element + row.element() .stroke_bezpath_iter() .map(move |bezpath| { let mut vector = Vector::default(); vector.append_bezpath(bezpath); vector.style = style.clone(); - TableRow::new(vector, transform, alpha_blending, source_node_id) + TableRow::new_from_element(vector) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", source_node_id) }) .collect::>>() }) @@ -1247,7 +1274,7 @@ async fn path_is_closed( ) -> bool { content .iter() - .flat_map(|row| row.element.build_stroke_path_iter().map(|(_, closed)| closed)) + .flat_map(|row| row.element().build_stroke_path_iter().map(|(_, closed)| closed)) .nth(index.max(0.) as usize) .unwrap_or(false) } @@ -1257,8 +1284,8 @@ async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: Table(_: impl Ctx, #[implementations(Table, Table)] content: T) -> Table { // Create a table with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to let mut output_table = Table::new_from_element(Vector::default()); - let Some(output) = output_table.iter_mut().next() else { return output_table }; + let Some(mut output) = output_table.iter_mut().next() else { return output_table }; // Concatenate every vector element's subpaths into the single output compound path for (index, row) in content.into_flattened_table().iter().enumerate() { - let node_id = row.source_node_id().map(|node_id| node_id.0).unwrap_or_default(); + let node_id: Option = row.attribute_cloned_or_default("source_node_id"); + let node_id = node_id.map(|node_id| node_id.0).unwrap_or_default(); let mut hasher = DefaultHasher::new(); (index, node_id).hash(&mut hasher); let collision_hash_seed = hasher.finish(); - output.element.concat(row.element, *row.transform(), collision_hash_seed); + output.element_mut().concat(row.element(), row.attribute_cloned_or_default("transform"), collision_hash_seed); // TODO: Make this instead use the first encountered style // Use the last encountered style as the output style - output.element.style = row.element.style.clone(); + output.element_mut().style = row.element().style.clone(); } output_table @@ -1315,16 +1343,16 @@ async fn sample_polyline( segment_domain: Default::default(), region_domain: Default::default(), colinear_manipulators: Default::default(), - style: std::mem::take(&mut row.element.style), - upstream_data: std::mem::take(&mut row.element.upstream_data), + style: std::mem::take(&mut row.element_mut().style), + upstream_data: std::mem::take(&mut row.element_mut().upstream_data), }; // Transfer the stroke transform from the input vector content to the result. - result.style.set_stroke_transform(*row.transform()); + result.style.set_stroke_transform(row.attribute_cloned_or_default("transform")); // Using `stroke_bezpath_iter` so that the `subpath_segment_lengths` is aligned to the segments of each bezpath. // So we can index into `subpath_segment_lengths` to get the length of the segments. // NOTE: `subpath_segment_lengths` has precalulated lengths with transformation applied. - let bezpaths = row.element.stroke_bezpath_iter(); + let bezpaths = row.element().stroke_bezpath_iter(); // Keeps track of the index of the first segment of the next bezpath in order to get lengths of all segments. let mut next_segment_index = 0; @@ -1332,7 +1360,8 @@ async fn sample_polyline( for local_bezpath in bezpaths { // Apply the transform to compute sample locations in world space (for correct distance-based spacing) let mut world_bezpath = local_bezpath.clone(); - world_bezpath.apply_affine(Affine::new(row.transform().to_cols_array())); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + world_bezpath.apply_affine(Affine::new(transform_attribute.to_cols_array())); let segment_count = world_bezpath.segments().count(); @@ -1373,7 +1402,7 @@ async fn sample_polyline( result.append_bezpath(sample_bezpath); } - row.element = result; + *row.element_mut() = result; row }) .collect() @@ -1399,16 +1428,17 @@ async fn simplify( content .into_iter() .map(|mut row| { - let transform = Affine::new(row.transform().to_cols_array()); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let transform = Affine::new(transform_attribute.to_cols_array()); let inverse_transform = transform.inverse(); let mut result = Vector { - style: std::mem::take(&mut row.element.style), - upstream_data: std::mem::take(&mut row.element.upstream_data), + style: std::mem::take(&mut row.element_mut().style), + upstream_data: std::mem::take(&mut row.element_mut().upstream_data), ..Default::default() }; - for mut bezpath in row.element.stroke_bezpath_iter() { + for mut bezpath in row.element().stroke_bezpath_iter() { bezpath.apply_affine(transform); let mut simplified = simplify_bezpath(bezpath, tolerance, &options); @@ -1417,7 +1447,7 @@ async fn simplify( result.append_bezpath(simplified); } - row.element = result; + *row.element_mut() = result; row }) .collect() @@ -1495,16 +1525,17 @@ async fn decimate( content .into_iter() .map(|mut row| { - let transform = Affine::new(row.transform().to_cols_array()); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let transform = Affine::new(transform_attribute.to_cols_array()); let inverse_transform = transform.inverse(); let mut result = Vector { - style: std::mem::take(&mut row.element.style), - upstream_data: std::mem::take(&mut row.element.upstream_data), + style: std::mem::take(&mut row.element_mut().style), + upstream_data: std::mem::take(&mut row.element_mut().upstream_data), ..Default::default() }; - for mut bezpath in row.element.stroke_bezpath_iter() { + for mut bezpath in row.element().stroke_bezpath_iter() { bezpath.apply_affine(transform); let is_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath)); @@ -1543,7 +1574,7 @@ async fn decimate( result.append_bezpath(new_bezpath); } - row.element = result; + *row.element_mut() = result; row }) .collect() @@ -1569,7 +1600,7 @@ async fn cut_path( let bezpaths = content .iter() .enumerate() - .flat_map(|(row_index, vector)| vector.element.stroke_bezpath_iter().map(|bezpath| (row_index, bezpath)).collect::>()) + .flat_map(|(row_index, vector)| vector.element().stroke_bezpath_iter().map(|bezpath| (row_index, bezpath)).collect::>()) .collect::>(); let bezpath_count = bezpaths.len() as f64; @@ -1579,7 +1610,7 @@ async fn cut_path( if let Some((row_index, bezpath)) = bezpaths.get(index).cloned() { let mut result_vector = Vector { - style: content.get(row_index).unwrap().element.style.clone(), + style: content.get(row_index).unwrap().element().style.clone(), ..Default::default() }; @@ -1596,7 +1627,7 @@ async fn cut_path( result_vector.append_bezpath(bezpath); } - *content.get_mut(row_index).unwrap().element = result_vector; + *content.get_mut(row_index).unwrap().element_mut() = result_vector; } content @@ -1606,16 +1637,16 @@ async fn cut_path( #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] async fn cut_segments(_: impl Ctx, mut content: Table) -> Table { // Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy - for row in content.iter_mut() { - let points_count = row.element.point_domain.ids().len(); - let segments_count = row.element.segment_domain.ids().len(); + for mut row in content.iter_mut() { + let points_count = row.element().point_domain.ids().len(); + let segments_count = row.element().segment_domain.ids().len(); let mut point_usages = vec![0_usize; points_count]; // Count how many times each point is used as an endpoint of the segments - let start_points = row.element.segment_domain.start_point().iter(); - let end_points = row.element.segment_domain.end_point().iter(); - for (&start, &end) in start_points.zip(end_points) { + let start_points = row.element().segment_domain.start_point().to_vec(); + let end_points = row.element().segment_domain.end_point().to_vec(); + for (&start, &end) in start_points.iter().zip(end_points.iter()) { point_usages[start] += 1; point_usages[end] += 1; } @@ -1625,7 +1656,7 @@ async fn cut_segments(_: impl Ctx, mut content: Table) -> Table let mut points_with_new_offsets = Vec::with_capacity(points_count); // Build a new point domain with the original points, but with duplications based on their extra usages by the segments - for (index, (point_id, point)) in row.element.point_domain.iter().enumerate() { + for (index, (point_id, point)) in row.element().point_domain.iter().enumerate() { // Ensure at least one usage to preserve free-floating points not connected to any segments let usage_count = point_usages[index].max(1); @@ -1640,10 +1671,10 @@ async fn cut_segments(_: impl Ctx, mut content: Table) -> Table } // Reconcile the segment domain with the new points - row.element.point_domain = new_points; + row.element_mut().point_domain = new_points; for original_segment_index in 0..segments_count { - let original_point_start_index = row.element.segment_domain.start_point()[original_segment_index]; - let original_point_end_index = row.element.segment_domain.end_point()[original_segment_index]; + let original_point_start_index = start_points[original_segment_index]; + let original_point_end_index = end_points[original_segment_index]; point_usages[original_point_start_index] -= 1; point_usages[original_point_end_index] -= 1; @@ -1651,8 +1682,8 @@ async fn cut_segments(_: impl Ctx, mut content: Table) -> Table let start_usage = points_with_new_offsets[original_point_start_index] + point_usages[original_point_start_index]; let end_usage = points_with_new_offsets[original_point_end_index] + point_usages[original_point_end_index]; - row.element.segment_domain.set_start_point(original_segment_index, start_usage); - row.element.segment_domain.set_end_point(original_segment_index, end_usage); + row.element_mut().segment_domain.set_start_point(original_segment_index, start_usage); + row.element_mut().segment_domain.set_end_point(original_segment_index, end_usage); } } @@ -1679,8 +1710,8 @@ async fn position_on_path( let mut bezpaths = content .iter() .flat_map(|vector| { - let transform = *vector.transform(); - vector.element.stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)) + let transform: DAffine2 = vector.attribute_cloned_or_default("transform"); + vector.element().stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)) }) .collect::>(); let bezpath_count = bezpaths.len() as f64; @@ -1720,8 +1751,8 @@ async fn tangent_on_path( let mut bezpaths = content .iter() .flat_map(|vector| { - let transform = *vector.transform(); - vector.element.stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)) + let transform: DAffine2 = vector.attribute_cloned_or_default("transform"); + vector.element().stroke_bezpath_iter().map(move |bezpath| (bezpath, transform)) }) .collect::>(); let bezpath_count = bezpaths.len() as f64; @@ -1768,7 +1799,7 @@ async fn poisson_disk_points( let mut result = Vector::default(); let path_with_bounding_boxes: Vec<_> = row - .element + .element() .stroke_bezpath_iter() .map(|mut bezpath| { // TODO: apply transform to points instead of modifying the paths @@ -1789,10 +1820,10 @@ async fn poisson_disk_points( } // Transfer the style from the input vector content to the result. - result.style = row.element.style.clone(); + result.style = row.element().style.clone(); result.style.set_stroke_transform(DAffine2::IDENTITY); - row.element = result; + *row.element_mut() = result; row }) .collect() @@ -1811,9 +1842,9 @@ async fn subpath_segment_lengths(_: impl Ctx, content: Table) -> Vec) -> Table { .into_iter() .filter_map(|mut row| { // Exit early if there are no points to generate splines from. - if row.element.point_domain.positions().is_empty() { + if row.element().point_domain.positions().is_empty() { return None; } let mut segment_domain = SegmentDomain::default(); let mut next_id = SegmentId::ZERO; - for (manipulator_groups, closed) in row.element.stroke_manipulator_groups() { + for (manipulator_groups, closed) in row.element().stroke_manipulator_groups() { let positions = manipulator_groups.iter().map(|manipulators| manipulators.anchor).collect::>(); let closed = closed && positions.len() > 2; @@ -1853,8 +1884,8 @@ async fn spline(_: impl Ctx, content: Table) -> Table { for i in 0..(positions.len() - if closed { 0 } else { 1 }) { let next_index = (i + 1) % positions.len(); - let start_index = row.element.point_domain.resolve_id(manipulator_groups[i].id).unwrap(); - let end_index = row.element.point_domain.resolve_id(manipulator_groups[next_index].id).unwrap(); + let start_index = row.element().point_domain.resolve_id(manipulator_groups[i].id).unwrap(); + let end_index = row.element().point_domain.resolve_id(manipulator_groups[next_index].id).unwrap(); let handle_start = first_handles[i]; let handle_end = positions[next_index] * 2. - first_handles[next_index]; @@ -1864,7 +1895,7 @@ async fn spline(_: impl Ctx, content: Table) -> Table { } } - row.element.segment_domain = segment_domain; + row.element_mut().segment_domain = segment_domain; Some(row) }) .collect() @@ -1943,12 +1974,13 @@ async fn jitter_points( .into_iter() .map(|mut row| { let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); - let inverse_linear = inverse_linear_or_repair(row.transform().matrix2); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2); - let deltas: Vec<_> = (0..row.element.point_domain.positions().len()) + let deltas: Vec<_> = (0..row.element().point_domain.positions().len()) .map(|point_index| { let normal = if along_normals { - row.element.segment_domain.point_tangent(point_index, row.element.point_domain.positions()).map(|t| -t.perp()) + row.element().segment_domain.point_tangent(point_index, row.element().point_domain.positions()).map(|t| -t.perp()) } else { None }; @@ -1963,8 +1995,8 @@ async fn jitter_points( }) .collect(); - let transform = *row.transform(); - apply_point_deltas(&mut row.element, &deltas, transform); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + apply_point_deltas(row.element_mut(), &deltas, transform); row }) @@ -1986,11 +2018,12 @@ async fn offset_points( content .into_iter() .map(|mut row| { - let inverse_linear = inverse_linear_or_repair(row.transform().matrix2); + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2); - let deltas: Vec<_> = (0..row.element.point_domain.positions().len()) + let deltas: Vec<_> = (0..row.element().point_domain.positions().len()) .map(|point_index| { - let Some(normal) = row.element.segment_domain.point_tangent(point_index, row.element.point_domain.positions()).map(|t| -t.perp()) else { + let Some(normal) = row.element().segment_domain.point_tangent(point_index, row.element().point_domain.positions()).map(|t| -t.perp()) else { return DVec2::ZERO; }; @@ -1998,8 +2031,8 @@ async fn offset_points( }) .collect(); - let transform = *row.transform(); - apply_point_deltas(&mut row.element, &deltas, transform); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + apply_point_deltas(row.element_mut(), &deltas, transform); row }) @@ -2139,7 +2172,8 @@ async fn morph( let default_polyline = || { let mut default_path = BezPath::new(); for (i, row) in content.iter().enumerate() { - let origin = row.transform().translation; + let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); + let origin = transform_attribute.translation; let point = kurbo::Point::new(origin.x, origin.y); if i == 0 { default_path.move_to(point); @@ -2157,8 +2191,8 @@ async fn morph( let paths: Vec = path .iter() .flat_map(|vector| { - let transform = *vector.transform(); - vector.element.stroke_bezpath_iter().map(move |mut bezpath| { + let transform: DAffine2 = vector.attribute_cloned_or_default("transform"); + vector.element().stroke_bezpath_iter().map(move |mut bezpath| { bezpath.apply_affine(Affine::new(transform.to_cols_array())); bezpath }) @@ -2227,8 +2261,10 @@ async fn morph( let (Some(source), Some(target)) = (content.get(source_index), content.get(target_index)) else { return 0.; }; - let (s_angle, s_scale, s_skew) = source.transform().decompose_rotation_scale_skew(); - let (t_angle, t_scale, t_skew) = target.transform().decompose_rotation_scale_skew(); + let source_transform: DAffine2 = source.attribute_cloned_or_default("transform"); + let target_transform: DAffine2 = target.attribute_cloned_or_default("transform"); + let (s_angle, s_scale, s_skew) = source_transform.decompose_rotation_scale_skew(); + let (t_angle, t_scale, t_skew) = target_transform.decompose_rotation_scale_skew(); match distribution { InterpolationDistribution::Angles => { @@ -2298,7 +2334,9 @@ async fn morph( }; // Lerp styles - let vector_alpha_blending = source_row.alpha_blending().lerp(target_row.alpha_blending(), time as f32); + let source_alpha_blending: AlphaBlending = source_row.attribute_cloned_or_default("alpha_blending"); + let target_alpha_blending: AlphaBlending = target_row.attribute_cloned_or_default("alpha_blending"); + let vector_alpha_blending = source_alpha_blending.lerp(&target_alpha_blending, time as f32); // Evaluate the spatial position on the control path for the translation component. // When the segment has zero arc length (e.g., two objects at the same position), inv_arclen @@ -2315,8 +2353,10 @@ async fn morph( // This decomposition must match the one used in Stroke::lerp so the renderer's stroke_transform.inverse() // correctly cancels the element transform, keeping the stroke uniform when Stroke is after Transform. let lerped_transform = { - let (s_angle, s_scale, s_skew) = source_row.transform().decompose_rotation_scale_skew(); - let (t_angle, t_scale, t_skew) = target_row.transform().decompose_rotation_scale_skew(); + let source_transform: DAffine2 = source_row.attribute_cloned_or_default("transform"); + let target_transform: DAffine2 = target_row.attribute_cloned_or_default("transform"); + let (s_angle, s_scale, s_skew) = source_transform.decompose_rotation_scale_skew(); + let (t_angle, t_scale, t_skew) = target_transform.decompose_rotation_scale_skew(); let lerp = |a: f64, b: f64| a + (b - a) * time; @@ -2344,7 +2384,8 @@ async fn morph( if lerped_transform.matrix2.determinant().abs() > f64::EPSILON { let lerped_inverse = lerped_transform.inverse(); for mut row in graphic_table_content.iter_mut() { - *row.transform_mut() = lerped_inverse * *row.transform(); + let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); + row.set_attribute("transform", lerped_inverse * row_transform); } } @@ -2352,29 +2393,34 @@ async fn morph( // instead of extracting manipulator groups, subdividing, interpolating, and rebuilding. if time == 0. || time == 1. { let row = if time == 0. { source_row } else { target_row }; - return Table::new_from_row(TableRow::new( - Vector { + + let transform = lerped_transform; + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + let source_node_id: Option = None; + + return Table::new_from_row( + TableRow::new_from_element(Vector { upstream_data: Some(graphic_table_content), - ..row.element.clone() - }, - lerped_transform, - *row.alpha_blending(), - None, - )); + ..row.element().clone() + }) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", source_node_id), + ); } let mut vector = Vector { upstream_data: Some(graphic_table_content), ..Default::default() }; - vector.style = source_row.element.style.lerp(&target_row.element.style, time); + vector.style = source_row.element().style.lerp(&target_row.element().style, time); // Work directly with manipulator groups, bypassing the BezPath intermediate representation. // This avoids the full Vector → BezPath → interpolate → BezPath → Vector roundtrip each frame. - let mut source_subpaths: Vec<_> = source_row.element.stroke_manipulator_groups().collect(); - let mut target_subpaths: Vec<_> = target_row.element.stroke_manipulator_groups().collect(); + let mut source_subpaths: Vec<_> = source_row.element().stroke_manipulator_groups().collect(); + let mut target_subpaths: Vec<_> = target_row.element().stroke_manipulator_groups().collect(); - // Interpolate geometry in local space (no transform baked in) — the lerped transform handles positioning + // Interpolate geometry in local space (no transform baked in); the lerped transform handles positioning let matched_count = source_subpaths.len().min(target_subpaths.len()); let extra_source = source_subpaths.split_off(matched_count); let extra_target = target_subpaths.split_off(matched_count); @@ -2446,21 +2492,21 @@ async fn morph( let target_in = target_manips[next_index].in_handle; match (source_out, source_in, target_out, target_in) { - // Both linear — no handles needed + // Both linear: no handles needed (None, None, None, None) => {} - // Both cubic — lerp handle pairs directly + // Both cubic: lerp handle pairs directly (Some(s_out), Some(s_in), Some(t_out), Some(t_in)) => { interpolated[segment_index].out_handle = Some(s_out.lerp(t_out, time)); interpolated[next_index].in_handle = Some(s_in.lerp(t_in, time)); } - // Both quadratic with handle in the same position — lerp the single handle + // Both quadratic with handle in the same position: lerp the single handle (Some(s_out), None, Some(t_out), None) => { interpolated[segment_index].out_handle = Some(s_out.lerp(t_out, time)); } (None, Some(s_in), None, Some(t_in)) => { interpolated[next_index].in_handle = Some(s_in.lerp(t_in, time)); } - // Linear vs. quadratic — elevate the linear side to a zero-length quadratic in the matching position + // Linear vs. quadratic: elevate the linear side to a zero-length quadratic in the matching position (None, None, Some(t_out), None) => { interpolated[segment_index].out_handle = Some(source_manips[segment_index].anchor.lerp(t_out, time)); } @@ -2473,7 +2519,7 @@ async fn morph( (None, Some(s_in), None, None) => { interpolated[next_index].in_handle = Some(s_in.lerp(target_manips[next_index].anchor, time)); } - // Mismatched types — promote both to cubic and lerp + // Mismatched types: promote both to cubic and lerp _ => { let (s_h1, s_h2) = promote_handles_to_cubic(source_manips[segment_index].anchor, source_out, source_in, source_manips[next_index].anchor); let (t_h1, t_h2) = promote_handles_to_cubic(target_manips[segment_index].anchor, target_out, target_in, target_manips[next_index].anchor); @@ -2512,7 +2558,12 @@ async fn morph( push_manipulators_to_vector(&mut vector, &manips, closed, &mut point_id, &mut segment_id); } - Table::new_from_row(TableRow::new(vector, lerped_transform, vector_alpha_blending, None)) + Table::new_from_row( + TableRow::new_from_element(vector) + .with_attribute("transform", lerped_transform) + .with_attribute("alpha_blending", vector_alpha_blending) + .with_attribute("source_node_id", None::), + ) } fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Vector { @@ -2790,10 +2841,14 @@ fn bevel(_: impl Ctx, source: Table, #[default(10.)] distance: Length) - source .into_iter() .map(|row| { - let transform = *row.transform(); - let alpha_blending = *row.alpha_blending(); - let source_node_id = *row.source_node_id(); - TableRow::new(bevel_algorithm(row.element, transform, distance), transform, alpha_blending, source_node_id) + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let alpha_blending: AlphaBlending = row.attribute_cloned_or_default("alpha_blending"); + let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); + + TableRow::new_from_element(bevel_algorithm(row.into_element(), transform, distance)) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", alpha_blending) + .with_attribute("source_node_id", source_node_id) }) .collect() } @@ -2803,7 +2858,7 @@ fn close_path(_: impl Ctx, source: Table) -> Table { source .into_iter() .map(|mut row| { - row.element.close_subpaths(); + row.element_mut().close_subpaths(); row }) .collect() @@ -2812,8 +2867,8 @@ fn close_path(_: impl Ctx, source: Table) -> Table { #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] fn point_inside(_: impl Ctx, source: Table, point: DVec2) -> bool { source.into_iter().any(|row| { - let transform = *row.transform(); - row.element.check_point_inside_shape(transform, point) + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + row.element().check_point_inside_shape(transform, point) }) } @@ -2854,7 +2909,7 @@ async fn count_elements( #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] async fn count_points(_: impl Ctx, content: Table) -> f64 { - content.into_iter().map(|row| row.element.point_domain.positions().len() as f64).sum() + content.into_iter().map(|row| row.element().point_domain.positions().len() as f64).sum() } /// Retrieves the vec2 position (in local space) of the anchor point at the specified index in table of vector elements. @@ -2867,7 +2922,7 @@ async fn index_points( /// The index of the points to retrieve, starting from 0 for the first point. Negative indices count backwards from the end, starting from -1 for the last item. index: f64, ) -> DVec2 { - let points_count = content.iter().map(|row| row.element.point_domain.positions().len()).sum::(); + let points_count = content.iter().map(|row| row.element().point_domain.positions().len()).sum::(); if points_count == 0 { return DVec2::ZERO; @@ -2883,9 +2938,9 @@ async fn index_points( // Find the point at the given index across all vector elements let mut accumulated = 0; for row in content.iter() { - let row_point_count = row.element.point_domain.positions().len(); + let row_point_count = row.element().point_domain.positions().len(); if index - accumulated < row_point_count { - return row.element.point_domain.positions()[index - accumulated]; + return row.element().point_domain.positions()[index - accumulated]; } accumulated += row_point_count; } @@ -2898,8 +2953,9 @@ async fn path_length(_: impl Ctx, source: Table) -> f64 { source .into_iter() .map(|row| { - let transform = row.transform(); - row.element + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + + row.element() .stroke_bezpath_iter() .map(|mut bezpath| { bezpath.apply_affine(Affine::new(transform.to_cols_array())); @@ -2918,8 +2974,9 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node() + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let area_scale = transform.matrix2.determinant().abs(); + row.element().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::() }) .sum() } @@ -2939,13 +2996,14 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< let mut sum = 0.; for row in vector.iter() { - for subpath in row.element.stroke_bezier_paths() { + for subpath in row.element().stroke_bezier_paths() { let partial = match centroid_type { CentroidType::Area => subpath.area_centroid_and_area(Some(1e-3), Some(1e-3)).filter(|(_, area)| *area > 0.), CentroidType::Length => subpath.length_centroid_and_length(None, true), }; if let Some((subpath_centroid, area_or_length)) = partial { - let subpath_centroid = row.transform().transform_point2(subpath_centroid); + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + let subpath_centroid = transform.transform_point2(subpath_centroid); sum += area_or_length; centroid += area_or_length * subpath_centroid; @@ -2963,8 +3021,8 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< let summed_positions = vector .iter() .flat_map(|row| { - let transform = *row.transform(); - row.element.point_domain.positions().iter().map(move |&p| transform.transform_point2(p)) + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); + row.element().point_domain.positions().iter().map(move |&p| transform.transform_point2(p)) }) .inspect(|_| count += 1) .sum::(); @@ -3002,13 +3060,16 @@ mod test { fn create_vector_row(bezpath: BezPath, transform: DAffine2) -> TableRow { let mut row = Vector::default(); row.append_bezpath(bezpath); - TableRow::new(row, transform, AlphaBlending::default(), None) + TableRow::new_from_element(row) + .with_attribute("transform", transform) + .with_attribute("alpha_blending", AlphaBlending::default()) + .with_attribute("source_node_id", None::) } #[tokio::test] async fn bounding_box() { let bounding_box = super::bounding_box((), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))).await; - let bounding_box = bounding_box.iter().next().unwrap().element; + let bounding_box = bounding_box.iter().next().unwrap().element(); assert_eq!(bounding_box.region_manipulator_groups().count(), 1); let manipulator_groups_anchors = bounding_box .region_manipulator_groups() @@ -3024,9 +3085,9 @@ mod test { // Test a rectangular path with non-zero rotation let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let mut square = Table::new_from_element(square); - *square.get_mut(0).unwrap().transform_mut() *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4); + *square.get_mut(0).unwrap().attribute_mut_or_insert_default::("transform") *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4); let bounding_box = BoundingBoxNode { content: FutureWrapperNode(square) }.eval(Footprint::default()).await; - let bounding_box = bounding_box.iter().next().unwrap().element; + let bounding_box = bounding_box.iter().next().unwrap().element(); assert_eq!(bounding_box.region_manipulator_groups().count(), 1); let manipulator_groups_anchors = bounding_box .region_manipulator_groups() @@ -3051,7 +3112,7 @@ mod test { let copy_to_points = super::copy_to_points(Footprint::default(), vector_node_from_bezpath(points), vector_node_from_bezpath(element), 1., 1., 0., 0, 0., 0).await; let flatten_path = super::flatten_path(Footprint::default(), copy_to_points).await; - let flattened_copy_to_points = flatten_path.iter().next().unwrap().element; + let flattened_copy_to_points = flatten_path.iter().next().unwrap().element(); assert_eq!(flattened_copy_to_points.region_manipulator_groups().count(), expected_points.len()); @@ -3069,7 +3130,7 @@ mod test { async fn sample_polyline() { let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); let sample_polyline = super::sample_polyline(Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 30., 0, 0., 0., false, vec![100.]).await; - let sample_polyline = sample_polyline.iter().next().unwrap().element; + let sample_polyline = sample_polyline.iter().next().unwrap().element(); assert_eq!(sample_polyline.point_domain.positions().len(), 4); for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); @@ -3079,7 +3140,7 @@ mod test { async fn sample_polyline_adaptive_spacing() { let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]); let sample_polyline = super::sample_polyline(Footprint::default(), vector_node_from_bezpath(path), PointSpacingType::Separation, 18., 0, 45., 10., true, vec![100.]).await; - let sample_polyline = sample_polyline.iter().next().unwrap().element; + let sample_polyline = sample_polyline.iter().next().unwrap().element(); assert_eq!(sample_polyline.point_domain.positions().len(), 4); for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); @@ -3094,7 +3155,7 @@ mod test { 0, ) .await; - let poisson_points = poisson_points.iter().next().unwrap().element; + let poisson_points = poisson_points.iter().next().unwrap().element(); assert!( (20..=40).contains(&poisson_points.point_domain.positions().len()), "actual len {}", @@ -3125,7 +3186,7 @@ mod test { #[tokio::test] async fn spline() { let spline = super::spline(Footprint::default(), vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))).await; - let spline = spline.iter().next().unwrap().element; + let spline = spline.iter().next().unwrap().element(); assert_eq!(spline.stroke_bezpath_iter().count(), 1); assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); } @@ -3133,18 +3194,18 @@ mod test { async fn morph() { let mut rectangles = vector_node_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); let mut second_rectangle = rectangles.get(0).unwrap().into_cloned(); - *second_rectangle.transform_mut() *= DAffine2::from_translation((-100., -100.).into()); + *second_rectangle.attribute_mut_or_insert_default::("transform") *= DAffine2::from_translation((-100., -100.).into()); rectangles.push(second_rectangle); let morphed = super::morph(Footprint::default(), rectangles, 0.5, false, InterpolationDistribution::default(), Table::default()).await; let row = morphed.iter().next().unwrap(); // Geometry stays in local space (original rectangle coordinates) assert_eq!( - &row.element.point_domain.positions()[..4], + &row.element().point_domain.positions()[..4], vec![DVec2::new(0., 0.), DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)] ); // The interpolated transform carries the midpoint translation (approximate due to arc-length parameterization) - assert!((row.transform().translation - DVec2::new(-50., -50.)).length() < 1e-3); + assert!((row.attribute_cloned_or_default::("transform").translation - DVec2::new(-50., -50.)).length() < 1e-3); } #[track_caller] @@ -3166,7 +3227,7 @@ mod test { async fn bevel_rect() { let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY); let beveled = super::bevel(Footprint::default(), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.); - let beveled = beveled.iter().next().unwrap().element; + let beveled = beveled.iter().next().unwrap().element(); assert_eq!(beveled.point_domain.positions().len(), 8); assert_eq!(beveled.segment_domain.ids().len(), 8); @@ -3194,7 +3255,7 @@ mod test { source.push(curve.as_path_el()); let beveled = super::bevel((), vector_node_from_bezpath(source), 2_f64.sqrt() * 10.); - let beveled = beveled.iter().next().unwrap().element; + let beveled = beveled.iter().next().unwrap().element(); assert_eq!(beveled.point_domain.positions().len(), 4); assert_eq!(beveled.segment_domain.ids().len(), 3); @@ -3220,10 +3281,10 @@ mod test { let vector = Vector::from_bezpath(source); let mut vector_table = Table::new_from_element(vector.clone()); - *vector_table.get_mut(0).unwrap().transform_mut() = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); + *vector_table.get_mut(0).unwrap().attribute_mut_or_insert_default("transform") = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); let beveled = super::bevel((), Table::new_from_element(vector), 2_f64.sqrt() * 10.); - let beveled = beveled.iter().next().unwrap().element; + let beveled = beveled.iter().next().unwrap().element(); assert_eq!(beveled.point_domain.positions().len(), 4); assert_eq!(beveled.segment_domain.ids().len(), 3); @@ -3246,7 +3307,7 @@ mod test { source.line_to(Point::new(0., 100.)); let beveled = super::bevel(Footprint::default(), vector_node_from_bezpath(source), 999.); - let beveled = beveled.iter().next().unwrap().element; + let beveled = beveled.iter().next().unwrap().element(); assert_eq!(beveled.point_domain.positions().len(), 6); assert_eq!(beveled.segment_domain.ids().len(), 5); @@ -3270,7 +3331,7 @@ mod test { let subpath = BezPath::from_path_segments([line, point, curve].into_iter()); let beveled_table = super::bevel(Footprint::default(), vector_node_from_bezpath(subpath), 5.); - let beveled = beveled_table.iter().next().unwrap().element; + let beveled = beveled_table.iter().next().unwrap().element(); assert_eq!(beveled.point_domain.positions().len(), 6); assert_eq!(beveled.segment_domain.ids().len(), 5); From ead17a6c5bcad7ab4c29ab647dc6d7546f1c3ed9 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 25 Apr 2026 04:15:34 -0700 Subject: [PATCH 6/7] Fix code review soundness concerns --- .../document/overlays/utility_types_native.rs | 3 +- node-graph/libraries/core-types/src/table.rs | 141 +++++++++--------- .../libraries/graphic-types/src/graphic.rs | 7 +- node-graph/libraries/graphic-types/src/lib.rs | 7 +- .../libraries/raster-types/src/image.rs | 7 +- .../libraries/rendering/src/renderer.rs | 6 +- .../wgpu-executor/src/texture_conversion.rs | 2 +- node-graph/nodes/blending/src/lib.rs | 100 ++++++++----- node-graph/nodes/brush/src/brush.rs | 6 +- node-graph/nodes/graphic/src/graphic.rs | 6 +- .../nodes/gstd/src/platform_application_io.rs | 3 +- node-graph/nodes/path-bool/src/lib.rs | 10 +- node-graph/nodes/raster/src/adjust.rs | 9 +- node-graph/nodes/raster/src/blending_nodes.rs | 18 ++- node-graph/nodes/raster/src/std_nodes.rs | 2 +- .../nodes/transform/src/transform_nodes.rs | 16 +- .../vector/src/vector_modification_nodes.rs | 3 +- node-graph/nodes/vector/src/vector_nodes.rs | 84 ++++++++--- 18 files changed, 259 insertions(+), 171 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index fe87361bee..0fd3fdd76e 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -1170,12 +1170,13 @@ impl OverlayContextInternal { // Use the existing bezier_to_path infrastructure to convert Vector to BezPath let mut path = BezPath::new(); let mut last_point = None; + let transform: DAffine2 = row.attribute_cloned_or_default("transform"); for (_, bezier, start_id, end_id) in row.element().segment_iter() { let move_to = last_point != Some(start_id); last_point = Some(end_id); - self.bezier_to_path(bezier, row.attribute_cloned_or_default("transform"), move_to, &mut path); + self.bezier_to_path(bezier, transform, move_to, &mut path); } // Render the path diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 8ee6882e17..8e9af58389 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -97,6 +97,9 @@ trait AttributeColumn: std::any::Any + Send + Sync { /// Creates a new column of the same type filled with `count` number of default values. fn new_with_defaults(&self, count: usize) -> Box; + /// Returns the number of elements in this column. + fn len(&self) -> usize; + /// Appends all values from another column of the same type. fn extend(&mut self, other: Box); @@ -146,9 +149,12 @@ impl AttributeColumn for Col } /// Pushes a scalar attribute value onto the end of this column, downcasting it to `T`. + /// Falls back to a default value if the type doesn't match, to maintain the column-length invariant. fn push(&mut self, value: Box) { if let Ok(value) = value.into_any().downcast::() { self.0.push(*value); + } else { + self.0.push(T::default()); } } @@ -162,10 +168,19 @@ impl AttributeColumn for Col Box::new(Column(vec![T::default(); count])) } + /// Returns the number of elements in this column. + fn len(&self) -> usize { + self.0.len() + } + /// Appends all values from another column, downcasting it to the same `Column` type. + /// Falls back to padding with defaults if the type doesn't match, to maintain the column-length invariant. fn extend(&mut self, other: Box) { + let other_len = other.len(); if let Ok(other) = (other as Box).downcast::() { self.0.extend(other.0); + } else { + self.0.extend(std::iter::repeat_with(T::default).take(other_len)); } } @@ -552,20 +567,11 @@ impl Table { /// Returns a mutable reference to the row at the given index, or `None` if out of bounds. pub fn get_mut(&mut self, index: usize) -> Option> { - if index >= self.element.len() { - return None; - } - - let element = &mut self.element[index] as *mut T; - let columns = &mut self.attributes as *mut AttributeColumns; - - // SAFETY: `element` points into the `Vec` while `columns` points to the `AttributeColumns`. - // These are distinct fields in `self`, so they do not alias. + let element = self.element.get_mut(index)?; Some(TableRowMut { - element: unsafe { &mut *element }, + element, index, - columns, - _marker: std::marker::PhantomData, + columns: &mut self.attributes, }) } @@ -593,13 +599,15 @@ impl Table { }) } - /// Mutably borrows a [`Table`] and returns an iterator of [`TableRowMut`]s, each containing mutable references to the data of the respective row from the table. + /// Mutably borrows a [`Table`] and returns a lender-style iterator yielding [`TableRowMut`]s. + /// + /// Unlike a standard [`Iterator`], each [`TableRowMut`] borrows the iterator itself + /// (via [`TableRowIterMut::next`]), so only one row can be alive at a time. This prevents + /// constructing simultaneous mutable references into the shared column store. pub fn iter_mut(&mut self) -> TableRowIterMut<'_, T> { - let columns = &mut self.attributes as *mut AttributeColumns; TableRowIterMut { inner: self.element.iter_mut().enumerate(), - columns, - _marker: std::marker::PhantomData, + columns: &mut self.attributes, } } } @@ -702,16 +710,18 @@ impl PartialEq for Table { impl ApplyTransform for Table { /// Right-multiplies the modification into each row's transform attribute. fn apply_transform(&mut self, modification: &DAffine2) { - for mut row in self.iter_mut() { - *row.attribute_mut_or_insert_default::("transform") *= *modification; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("transform", |t: &mut DAffine2| *t *= *modification); } } /// Left-multiplies the modification into each row's transform attribute. fn left_apply_transform(&mut self, modification: &DAffine2) { - for mut row in self.iter_mut() { + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { let current_transform: DAffine2 = row.attribute_cloned_or_default("transform"); - *row.attribute_mut_or_insert_default("transform") = *modification * current_transform; + row.set_attribute("transform", *modification * current_transform); } } } @@ -929,6 +939,11 @@ impl<'a, T> TableRowRef<'a, T> { self.attribute(key).cloned().unwrap_or_default() } + /// Clones this row's attributes into a new owned [`AttributeValues`], without cloning the element. + pub fn clone_attributes(&self) -> AttributeValues { + self.columns.clone_row(self.index) + } + /// Clones both the element and its row attributes into a new owned [`TableRow`]. pub fn into_cloned(self) -> TableRow where @@ -947,14 +962,13 @@ impl<'a, T> TableRowRef<'a, T> { /// A mutable view into a single row of a [`Table`], providing read-write access to the element and its attributes. /// -/// Uses a raw pointer to the column store in order to split the borrow between the element -/// (which lives in the `Vec`) and the attribute columns (a separate field). The `PhantomData` -/// marker ties the pointer's validity to the `'a` lifetime of the originating table borrow. +/// Borrows the element (which lives in the table's `Vec`) and the attribute column store as +/// disjoint mutable references. Yielded by [`TableRowIterMut::next`], where the row's lifetime is +/// tied to the iterator's borrow so only one row can exist at a time. pub struct TableRowMut<'a, T> { element: &'a mut T, index: usize, - columns: *mut AttributeColumns, - _marker: std::marker::PhantomData<&'a mut AttributeColumns>, + columns: &'a mut AttributeColumns, } impl std::fmt::Debug for TableRowMut<'_, T> @@ -984,25 +998,9 @@ impl<'a, T> TableRowMut<'a, T> { self.element } - /// Returns a shared reference to the column store backing this row's attributes. - /// - // SAFETY: The raw pointer `self.columns` is guaranteed valid for the lifetime `'a` by the - // PhantomData marker and by the Table methods that construct TableRowMut. - fn columns(&self) -> &AttributeColumns { - unsafe { &*self.columns } - } - - /// Returns a mutable reference to the column store backing this row's attributes. - /// - // SAFETY: The raw pointer `self.columns` is guaranteed valid for the lifetime `'a` by the - // PhantomData marker and by the Table methods that construct TableRowMut. - fn columns_mut(&mut self) -> &mut AttributeColumns { - unsafe { &mut *self.columns } - } - /// Returns a reference to the attribute value for the given key, if it exists and is of the requested type. pub fn attribute(&self, key: &str) -> Option<&U> { - self.columns().get_cell(key, self.index) + self.columns.get_cell(key, self.index) } /// Returns the attribute value for the given key, or the provided default if absent or of a different type. @@ -1020,22 +1018,30 @@ impl<'a, T> TableRowMut<'a, T> { self.attribute(key).cloned().unwrap_or_default() } - /// Returns a mutable reference to the attribute value for the given key, if it exists and is of the requested type. - pub fn attribute_mut(&mut self, key: &str) -> Option<&mut U> { + /// Runs the given closure on a mutable reference to the attribute value for the given key, + /// returning `Some(closure_result)` if the attribute exists with the requested type, or `None` otherwise. + /// + /// Uses a closure rather than returning `&mut U` so the borrow cannot escape the call, + /// which keeps multi-row mutation sound under the shared column store. + pub fn with_attribute_mut R>(&mut self, key: &str, f: F) -> Option { let index = self.index; - self.columns_mut().get_cell_mut(key, index) + self.columns.get_cell_mut::(key, index).map(f) } - /// Returns a mutable reference to the attribute value for the given key, inserting a default value if absent or of a different type. - pub fn attribute_mut_or_insert_default(&mut self, key: &str) -> &mut U { + /// Runs the given closure on a mutable reference to the attribute value for the given key, + /// inserting a default value if the attribute is absent or of a different type, and returns the closure's result. + /// + /// Uses a closure rather than returning `&mut U` so the borrow cannot escape the call, + /// which keeps multi-row mutation sound under the shared column store. + pub fn with_attribute_mut_or_default R>(&mut self, key: &str, f: F) -> R { let index = self.index; - self.columns_mut().get_or_insert_default_cell(key, index) + f(self.columns.get_or_insert_default_cell::(key, index)) } /// Sets the attribute value for the given key, replacing any existing entry with the same key. pub fn set_attribute(&mut self, key: impl Into, value: U) { let index = self.index; - self.columns_mut().set_cell(key, index, value); + self.columns.set_cell(key, index, value); } } @@ -1076,42 +1082,37 @@ impl DoubleEndedIterator for TableRowIter { // TableRowIterMut // ================== -/// Mutable iterator over table rows. Each yielded [`TableRowMut`] provides mutable access to one -/// element and the shared column store at that row's index. +/// Lender-style mutable iterator over table rows. +/// +/// Does not implement [`Iterator`]: each yielded [`TableRowMut`] borrows the iterator itself for +/// the duration of its existence, so callers must use `while let Some(mut row) = iter.next() { ... }` +/// rather than a `for` loop. This guarantees that only one [`TableRowMut`] is ever alive at a time, +/// which is required for soundness because all rows would otherwise share access to the same +/// underlying column store. pub struct TableRowIterMut<'a, T> { inner: std::iter::Enumerate>, - columns: *mut AttributeColumns, - _marker: std::marker::PhantomData<&'a mut AttributeColumns>, + columns: &'a mut AttributeColumns, } -impl<'a, T> Iterator for TableRowIterMut<'a, T> { - type Item = TableRowMut<'a, T>; - - fn next(&mut self) -> Option { +impl<'a, T> TableRowIterMut<'a, T> { + /// Yields the next [`TableRowMut`], borrowing this iterator until the row is dropped. + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Option> { let (index, element) = self.inner.next()?; Some(TableRowMut { element, index, - columns: self.columns, - _marker: std::marker::PhantomData, + columns: &mut *self.columns, }) } -} -impl DoubleEndedIterator for TableRowIterMut<'_, T> { - fn next_back(&mut self) -> Option { + /// Yields the next [`TableRowMut`] from the back, borrowing this iterator until the row is dropped. + pub fn next_back(&mut self) -> Option> { let (index, element) = self.inner.next_back()?; Some(TableRowMut { element, index, - columns: self.columns, - _marker: std::marker::PhantomData, + columns: &mut *self.columns, }) } } - -// SAFETY: The raw `*mut AttributeColumns` pointer is derived from a `&mut Table` borrow that lives -// for `'a`, and `AttributeColumns` is `Send`. The pointer is only used to split the borrow between -// the element slice and the column store, which are disjoint fields. -unsafe impl Send for TableRowIterMut<'_, T> {} -unsafe impl Send for TableRowMut<'_, T> {} diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index d028f2423c..f48b51ef78 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -148,12 +148,13 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic match current_graphic_row.into_element() { // Recurse into nested graphic tables, composing the parent's transform onto each child Graphic::Graphic(mut sub_table) => { - for mut graphic in sub_table.iter_mut() { + let mut iter = sub_table.iter_mut(); + while let Some(mut graphic) = iter.next() { let child_transform: DAffine2 = graphic.attribute_cloned_or_default("transform"); let child_alpha_blending: AlphaBlending = graphic.attribute_cloned_or_default("alpha_blending"); - *graphic.attribute_mut_or_insert_default("transform") = current_transform * child_transform; - *graphic.attribute_mut_or_insert_default("alpha_blending") = compose_alpha_blending(current_alpha_blending, child_alpha_blending); + graphic.set_attribute("transform", current_transform * child_transform); + graphic.set_attribute("alpha_blending", compose_alpha_blending(current_alpha_blending, child_alpha_blending)); } flatten_recursive(output, sub_table, extract_variant); diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index c0bc9eee2f..42b1a5932b 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -81,9 +81,10 @@ pub mod migrations { region_domain: old.region_domain, upstream_data: old.upstream_graphic_group, }); - let mut row = vector_table.iter_mut().next().unwrap(); - *row.attribute_mut_or_insert_default("transform") = old.transform; - *row.attribute_mut_or_insert_default("alpha_blending") = old.alpha_blending; + let mut iter = vector_table.iter_mut(); + let mut row = iter.next().unwrap(); + row.set_attribute("transform", old.transform); + row.set_attribute("alpha_blending", old.alpha_blending); vector_table } VectorFormat::OlderVectorTable(older_table) => older_table diff --git a/node-graph/libraries/raster-types/src/image.rs b/node-graph/libraries/raster-types/src/image.rs index 4f7da07fbd..bea096bc53 100644 --- a/node-graph/libraries/raster-types/src/image.rs +++ b/node-graph/libraries/raster-types/src/image.rs @@ -361,9 +361,10 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> FormatVersions::Image(image) => Table::new_from_element(Raster::new_cpu(image)), FormatVersions::OldImageFrame(OldImageFrame { image, transform, alpha_blending }) => { let mut image_frame_table = Table::new_from_element(Raster::new_cpu(image)); - let mut row = image_frame_table.iter_mut().next().unwrap(); - *row.attribute_mut_or_insert_default("transform") = transform; - *row.attribute_mut_or_insert_default("alpha_blending") = alpha_blending; + let mut iter = image_frame_table.iter_mut(); + let mut row = iter.next().unwrap(); + row.set_attribute("transform", transform); + row.set_attribute("alpha_blending", alpha_blending); image_frame_table } FormatVersions::OlderImageFrameTable(old_table) => from_image_frame_table(older_table_to_new_table(old_table)), diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index cab0c52b5a..559fc1a493 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -874,7 +874,8 @@ impl Render for Table { } fn new_ids_from_hash(&mut self, _reference: Option) { - for mut row in self.iter_mut() { + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { let source_node_id: Option = row.attribute_cloned_or_default("source_node_id"); row.element_mut().new_ids_from_hash(source_node_id); } @@ -1427,7 +1428,8 @@ impl Render for Table { } fn new_ids_from_hash(&mut self, reference: Option) { - for mut row in self.iter_mut() { + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { row.element_mut().vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default()); } } diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index f0cc0920e7..2b1dd6217c 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -154,7 +154,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for Table> { .map(|row| { let image = row.element(); let texture = upload_to_texture(device, queue, image); - let (_, attributes) = row.into_cloned().into_parts(); + let attributes = row.clone_attributes(); TableRow::from_parts(Raster::new_gpu(texture), attributes) }) diff --git a/node-graph/nodes/blending/src/lib.rs b/node-graph/nodes/blending/src/lib.rs index 47f9392d23..7b4d645a6b 100644 --- a/node-graph/nodes/blending/src/lib.rs +++ b/node-graph/nodes/blending/src/lib.rs @@ -18,36 +18,41 @@ impl MultiplyAlpha for Color { } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.opacity *= factor as f32); } } } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.opacity *= factor as f32); } } } impl MultiplyAlpha for Table> { fn multiply_alpha(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.opacity *= factor as f32); } } } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.opacity *= factor as f32); } } } impl MultiplyAlpha for Table { fn multiply_alpha(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").opacity *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.opacity *= factor as f32); } } } @@ -62,36 +67,41 @@ impl MultiplyFill for Color { } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.fill *= factor as f32); } } } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.fill *= factor as f32); } } } impl MultiplyFill for Table> { fn multiply_fill(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.fill *= factor as f32); } } } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.fill *= factor as f32); } } } impl MultiplyFill for Table { fn multiply_fill(&mut self, factor: f64) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").fill *= factor as f32; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.fill *= factor as f32); } } } @@ -102,36 +112,41 @@ trait SetBlendMode { impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.blend_mode = blend_mode); } } } impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.blend_mode = blend_mode); } } } impl SetBlendMode for Table> { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.blend_mode = blend_mode); } } } impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.blend_mode = blend_mode); } } } impl SetBlendMode for Table { fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").blend_mode = blend_mode; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.blend_mode = blend_mode); } } } @@ -142,36 +157,41 @@ trait SetClip { impl SetClip for Table { fn set_clip(&mut self, clip: bool) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.clip = clip); } } } impl SetClip for Table { fn set_clip(&mut self, clip: bool) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.clip = clip); } } } impl SetClip for Table> { fn set_clip(&mut self, clip: bool) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.clip = clip); } } } impl SetClip for Table { fn set_clip(&mut self, clip: bool) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.clip = clip); } } } impl SetClip for Table { fn set_clip(&mut self, clip: bool) { - for mut row in self.iter_mut() { - row.attribute_mut_or_insert_default::("alpha_blending").clip = clip; + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { + row.with_attribute_mut_or_default("alpha_blending", |a: &mut AlphaBlending| a.clip = clip); } } } diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index 9880954920..852048e744 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -90,7 +90,8 @@ where return target; } - for mut table_row in target.iter_mut() { + let mut iter = target.iter_mut(); + while let Some(mut table_row) = iter.next() { let target_width = table_row.element().width; let target_height = table_row.element().height; let target_size = DVec2::new(target_width as f64, target_height as f64); @@ -315,7 +316,8 @@ async fn brush( let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default("alpha_blending"); let source_node_id: Option = actual_image.attribute_cloned_or_default("source_node_id"); - let mut first_row = image.iter_mut().next().unwrap(); + let mut iter = image.iter_mut(); + let mut first_row = iter.next().unwrap(); *first_row.element_mut() = actual_image.into_element(); first_row.set_attribute("transform", transform); first_row.set_attribute("alpha_blending", alpha_blending); diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 0bdcd84e27..05c2e6aa37 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -200,7 +200,8 @@ pub async fn source_node_id( let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied(); let mut content = content; - for mut row in content.iter_mut() { + let mut iter = content.iter_mut(); + while let Some(mut row) = iter.next() { row.set_attribute("source_node_id", source_node_id); } @@ -304,7 +305,8 @@ pub async fn flatten_graphic(_: impl Ctx, content: Table, fully_flatten // If we're allowed to recurse, flatten any graphics we encounter Graphic::Graphic(mut current_element) if recurse => { // Apply the parent graphic's transform to all child elements - for mut graphic in current_element.iter_mut() { + let mut iter = current_element.iter_mut(); + while let Some(mut graphic) = iter.next() { let graphic_transform: DAffine2 = graphic.attribute_cloned_or_default("transform"); graphic.set_attribute("transform", current_transform * graphic_transform); } diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 95df84b31d..fb086101ce 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -204,7 +204,8 @@ where ..Default::default() }; - for mut row in data.iter_mut() { + let mut iter = data.iter_mut(); + while let Some(mut row) = iter.next() { let current_transform: DAffine2 = row.attribute_cloned_or_default("transform"); row.set_attribute("transform", DAffine2::from_translation(-aabb.start) * current_transform); } diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 73e15454dc..a4cf717a2e 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -38,9 +38,10 @@ async fn boolean_operation) -> Table { Graphic::Graphic(mut graphic) => { let parent_transform: DAffine2 = element.attribute_cloned_or_default("transform"); // Apply the parent graphic's transform to each element of inner table - for mut sub_element in graphic.iter_mut() { + let mut iter = graphic.iter_mut(); + while let Some(mut sub_element) = iter.next() { let current_transform: DAffine2 = sub_element.attribute_cloned_or_default("transform"); - *sub_element.attribute_mut_or_insert_default("transform") = parent_transform * current_transform; + sub_element.set_attribute("transform", parent_transform * current_transform); } // Recursively flatten the inner table into the output vector table diff --git a/node-graph/nodes/raster/src/adjust.rs b/node-graph/nodes/raster/src/adjust.rs index 45c2cbc355..8657597374 100644 --- a/node-graph/nodes/raster/src/adjust.rs +++ b/node-graph/nodes/raster/src/adjust.rs @@ -18,7 +18,8 @@ mod adjust_std { impl Adjust for Table> { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - for mut row in self.iter_mut() { + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { for color in row.element_mut().data_mut().data.iter_mut() { *color = map_fn(color); } @@ -27,14 +28,16 @@ mod adjust_std { } impl Adjust for Table { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - for mut row in self.iter_mut() { + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { *row.element_mut() = map_fn(row.element()); } } } impl Adjust for Table { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - for mut row in self.iter_mut() { + let mut iter = self.iter_mut(); + while let Some(mut row) = iter.next() { row.element_mut().adjust(&map_fn); } } diff --git a/node-graph/nodes/raster/src/blending_nodes.rs b/node-graph/nodes/raster/src/blending_nodes.rs index 76eef88bb5..5c6391836d 100644 --- a/node-graph/nodes/raster/src/blending_nodes.rs +++ b/node-graph/nodes/raster/src/blending_nodes.rs @@ -30,7 +30,11 @@ mod blend_std { impl Blend for Table> { fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { let mut result_table = self.clone(); - for (mut over, under) in result_table.iter_mut().zip(under.iter()) { + let pair_count = result_table.len().min(under.len()); + let mut iter = result_table.iter_mut(); + for under_index in 0..pair_count { + let Some(mut over) = iter.next() else { break }; + let Some(under) = under.get(under_index) else { break }; let data = over.element().data.iter().zip(under.element().data.iter()).map(|(a, b)| blend_fn(*a, *b)).collect(); let (width, height) = (over.element().width, over.element().height); @@ -47,7 +51,11 @@ mod blend_std { impl Blend for Table { fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { let mut result_table = self.clone(); - for (mut over, under) in result_table.iter_mut().zip(under.iter()) { + let pair_count = result_table.len().min(under.len()); + let mut iter = result_table.iter_mut(); + for under_index in 0..pair_count { + let Some(mut over) = iter.next() else { break }; + let Some(under) = under.get(under_index) else { break }; let new_val = blend_fn(*over.element(), *under.element()); *over.element_mut() = new_val; } @@ -57,7 +65,11 @@ mod blend_std { impl Blend for Table { fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { let mut result_table = self.clone(); - for (mut over, under) in result_table.iter_mut().zip(under.iter()) { + let pair_count = result_table.len().min(under.len()); + let mut iter = result_table.iter_mut(); + for under_index in 0..pair_count { + let Some(mut over) = iter.next() else { break }; + let Some(under) = under.get(under_index) else { break }; let new_val = over.element().blend(under.element(), &blend_fn); *over.element_mut() = new_val; } diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index 3a02087934..0a5d5f40ec 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -119,7 +119,7 @@ pub fn combine_channels( let alpha = alpha.filter(|i| i.element().width > 0 && i.element().height > 0); // Get this row's transform and alpha blending mode from the first non-empty channel - let (_, attributes) = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| i.clone().into_parts())?; + let attributes = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| i.attributes().clone())?; // Get the common width and height of the channels, which must have equal dimensions let channel_dimensions = [ diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index 007661224f..e34137bd6c 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -66,26 +66,27 @@ fn reset_transform( reset_rotation: bool, reset_scale: bool, ) -> Table { - for mut row in content.iter_mut() { + let mut iter = content.iter_mut(); + while let Some(mut row) = iter.next() { // Translation if reset_translation { - row.attribute_mut_or_insert_default::("transform").translation = DVec2::ZERO; + row.with_attribute_mut_or_default("transform", |t: &mut DAffine2| t.translation = DVec2::ZERO); } // (Rotation, Scale) match (reset_rotation, reset_scale) { (true, true) => { - row.attribute_mut_or_insert_default::("transform").matrix2 = DMat2::IDENTITY; + row.with_attribute_mut_or_default("transform", |t: &mut DAffine2| t.matrix2 = DMat2::IDENTITY); } (true, false) => { let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); let scale = transform_attribute.scale_magnitudes(); - row.attribute_mut_or_insert_default::("transform").matrix2 = DMat2::from_diagonal(scale); + row.with_attribute_mut_or_default("transform", |t: &mut DAffine2| t.matrix2 = DMat2::from_diagonal(scale)); } (false, true) => { let transform_attribute: DAffine2 = row.attribute_cloned_or_default("transform"); let rotation = transform_attribute.decompose_rotation(); let rotation_matrix = DMat2::from_angle(rotation); - row.attribute_mut_or_insert_default::("transform").matrix2 = rotation_matrix; + row.with_attribute_mut_or_default("transform", |t: &mut DAffine2| t.matrix2 = rotation_matrix); } (false, false) => {} } @@ -108,8 +109,9 @@ fn replace_transform( mut content: Table, transform: DAffine2, ) -> Table { - for mut row in content.iter_mut() { - *row.attribute_mut_or_insert_default("transform") = transform.transform(); + let mut iter = content.iter_mut(); + while let Some(mut row) = iter.next() { + row.set_attribute("transform", transform.transform()); } content } diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index 4ce37f4a84..0f89f9ffad 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -30,7 +30,8 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table, modification: Bo /// Applies the vector path's local transformation to its geometry and resets the transform to the identity. #[node_macro::node(category("Vector"))] async fn apply_transform(_ctx: impl Ctx, mut vector: Table) -> Table { - for mut row in vector.iter_mut() { + let mut iter = vector.iter_mut(); + while let Some(mut row) = iter.next() { let transform: DAffine2 = row.attribute_cloned_or_default("transform"); for (_, point) in row.element_mut().point_domain.positions_mut() { diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 275a70c001..06ecf2056f 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -29,24 +29,45 @@ use vector_types::vector::misc::{ use vector_types::vector::style::{Fill, Gradient, GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; -/// Implemented for types that can be converted to an iterator of vector rows. -/// Used for the fill and stroke node so they can be used on `Table` or `Table`. +/// Implemented for types that contain vector rows reachable via mutable access. +/// Used for the fill and stroke nodes so they can apply to either `Table` or `Table`. +/// +/// Uses a callback rather than returning a borrowing iterator so the [`TableRowMut`] borrow +/// cannot escape the call, keeping multi-row access sound under the lender iterator. trait VectorTableIterMut { - fn vector_iter_mut(&mut self) -> impl Iterator>; + fn for_each_vector_mut(&mut self, f: impl FnMut(&mut TableRowMut<'_, Vector>)); + + fn vector_count(&self) -> usize; } impl VectorTableIterMut for Table { - fn vector_iter_mut(&mut self) -> impl Iterator> { + fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut TableRowMut<'_, Vector>)) { // Grab only the direct children - self.iter_mut() - .filter_map(|element| element.into_element_mut().as_vector_mut()) - .flat_map(move |vector| vector.iter_mut()) + let mut outer = self.iter_mut(); + while let Some(element) = outer.next() { + let Some(vector_table) = element.into_element_mut().as_vector_mut() else { continue }; + let mut inner = vector_table.iter_mut(); + while let Some(mut vector) = inner.next() { + f(&mut vector); + } + } + } + + fn vector_count(&self) -> usize { + self.iter().filter_map(|element| element.element().as_vector()).map(|table| table.len()).sum() } } impl VectorTableIterMut for Table { - fn vector_iter_mut(&mut self) -> impl Iterator> { - self.iter_mut() + fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut TableRowMut<'_, Vector>)) { + let mut iter = self.iter_mut(); + while let Some(mut vector) = iter.next() { + f(&mut vector); + } + } + + fn vector_count(&self) -> usize { + self.len() } } @@ -83,13 +104,14 @@ where { let Some(row) = gradient.into_iter().next() else { return content }; - let length = content.vector_iter_mut().count(); + let length = content.vector_count(); let element = row.into_element(); let gradient = if reverse { element.reversed() } else { element }; let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); - for (i, mut vector) in content.vector_iter_mut().enumerate() { + let mut i: usize = 0; + content.for_each_vector_mut(|vector| { let factor = match randomize { true => rng.random::(), false => match repeat_every { @@ -107,7 +129,9 @@ where if stroke && let Some(stroke) = vector.element_mut().style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) { vector.element_mut().style.set_stroke(stroke); } - } + + i += 1; + }); content } @@ -145,9 +169,9 @@ async fn fill + 'n + Send, V: VectorTableIterMut + 'n + Send>( _backup_gradient: Gradient, ) -> V { let fill: Fill = fill.into(); - for mut vector in content.vector_iter_mut() { + content.for_each_vector_mut(|vector| { vector.element_mut().style.set_fill(fill.clone()); - } + }); content } @@ -220,11 +244,11 @@ where paint_order, }; - for mut vector in content.vector_iter_mut() { + content.for_each_vector_mut(|vector| { let mut stroke = stroke.clone(); stroke.transform *= vector.attribute_cloned_or_default("transform"); vector.element_mut().style.set_stroke(stroke); - } + }); content } @@ -657,7 +681,8 @@ pub mod extrude_algorithms { #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] async fn extrude(_: impl Ctx, mut source: Table, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> Table { - for mut row in source.iter_mut() { + let mut iter = source.iter_mut(); + while let Some(mut row) = iter.next() { extrude_algorithms::extrude(row.element_mut(), direction, joining_algorithm); } source @@ -1082,7 +1107,8 @@ async fn vec2_to_point(_: impl Ctx, vec2: DVec2) -> Table { /// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist. #[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))] async fn points_to_polyline(_: impl Ctx, mut points: Table, #[default(true)] closed: bool) -> Table { - for mut row in points.iter_mut() { + let mut iter = points.iter_mut(); + while let Some(mut row) = iter.next() { let mut segment_domain = SegmentDomain::new(); let mut next_id = SegmentId::ZERO; @@ -1284,7 +1310,8 @@ async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: Table(_: impl Ctx, #[implementations(Table, Table)] content: T) -> Table { // Create a table with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to let mut output_table = Table::new_from_element(Vector::default()); - let Some(mut output) = output_table.iter_mut().next() else { return output_table }; + let mut output_iter = output_table.iter_mut(); + let Some(mut output) = output_iter.next() else { return output_table }; // Concatenate every vector element's subpaths into the single output compound path for (index, row) in content.into_flattened_table().iter().enumerate() { @@ -1637,7 +1665,8 @@ async fn cut_path( #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] async fn cut_segments(_: impl Ctx, mut content: Table) -> Table { // Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy - for mut row in content.iter_mut() { + let mut iter = content.iter_mut(); + while let Some(mut row) = iter.next() { let points_count = row.element().point_domain.ids().len(); let segments_count = row.element().segment_domain.ids().len(); @@ -2383,7 +2412,8 @@ async fn morph( // in which case we skip pre-compensation to avoid propagating NaN through upstream_data transforms. if lerped_transform.matrix2.determinant().abs() > f64::EPSILON { let lerped_inverse = lerped_transform.inverse(); - for mut row in graphic_table_content.iter_mut() { + let mut iter = graphic_table_content.iter_mut(); + while let Some(mut row) = iter.next() { let row_transform: DAffine2 = row.attribute_cloned_or_default("transform"); row.set_attribute("transform", lerped_inverse * row_transform); } @@ -3085,7 +3115,10 @@ mod test { // Test a rectangular path with non-zero rotation let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); let mut square = Table::new_from_element(square); - *square.get_mut(0).unwrap().attribute_mut_or_insert_default::("transform") *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4); + square + .get_mut(0) + .unwrap() + .with_attribute_mut_or_default("transform", |t: &mut DAffine2| *t *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4)); let bounding_box = BoundingBoxNode { content: FutureWrapperNode(square) }.eval(Footprint::default()).await; let bounding_box = bounding_box.iter().next().unwrap().element(); assert_eq!(bounding_box.region_manipulator_groups().count(), 1); @@ -3281,7 +3314,10 @@ mod test { let vector = Vector::from_bezpath(source); let mut vector_table = Table::new_from_element(vector.clone()); - *vector_table.get_mut(0).unwrap().attribute_mut_or_insert_default("transform") = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); + vector_table + .get_mut(0) + .unwrap() + .set_attribute("transform", DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.))); let beveled = super::bevel((), Table::new_from_element(vector), 2_f64.sqrt() * 10.); let beveled = beveled.iter().next().unwrap().element(); From a6f11f2400b3462e573378e25af72a2532eaa09d Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 25 Apr 2026 04:28:33 -0700 Subject: [PATCH 7/7] Add todo work Co-authored-by: Copilot --- node-graph/libraries/core-types/src/table.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 8e9af58389..795ff3b285 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -610,6 +610,12 @@ impl Table { columns: &mut self.attributes, } } + + // TODO: Add an `iter_element_values` and `iter_element_values_mut` + // TODO: Add an `iter_attribute_values(key)` and `iter_attribute_values_mut(key)` + // TODO: Remove `iter` and `iter_mut` + // TODO: Remove `TableRowIterMut` and probably `TableRowMut` + // TODO: Maybe remove `TableRowRef` since it encourages per-row linear search of attributes for their key lookup in the storage Vec } #[cfg(feature = "serde")] @@ -693,6 +699,7 @@ impl Default for Table { } } +// TODO: Replace this with `CacheHash` and make it hash the attributes as well (we need to include the `CacheHash` trait bound in the type erasure) impl Hash for Table { fn hash(&self, state: &mut H) { for element in &self.element {