From e4adf16ebae00d91dc399a8994f72e73ccdb696c Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sat, 22 Aug 2026 10:23:02 +0000 Subject: [PATCH] Move the paint and merged-layer markers to graphic-types as optional references --- .../libraries/core-types/src/attribute.rs | 37 ++++++++++++++- node-graph/libraries/core-types/src/lib.rs | 4 +- node-graph/libraries/core-types/src/list.rs | 20 +++------ .../libraries/graphic-types/src/graphic.rs | 3 +- node-graph/libraries/graphic-types/src/lib.rs | 2 + .../libraries/graphic-types/src/markers.rs | 45 +++++++++++++++++++ .../libraries/rendering/src/renderer.rs | 8 ++-- .../libraries/vector-types/src/markers.rs | 36 +++------------ node-graph/nodes/graphic/src/graphic.rs | 4 +- .../nodes/gstd/src/platform_application_io.rs | 4 +- node-graph/nodes/path-bool/src/lib.rs | 6 +-- node-graph/nodes/vector/src/vector_nodes.rs | 6 +-- 12 files changed, 112 insertions(+), 63 deletions(-) create mode 100644 node-graph/libraries/graphic-types/src/markers.rs diff --git a/node-graph/libraries/core-types/src/attribute.rs b/node-graph/libraries/core-types/src/attribute.rs index 977fa4d6c7..0e8e36c21a 100644 --- a/node-graph/libraries/core-types/src/attribute.rs +++ b/node-graph/libraries/core-types/src/attribute.rs @@ -164,10 +164,45 @@ pub fn default_value(name: &str) -> Option> { /// /// The trailing `= expr` is the name-specific default; without it the value /// type's `Default` applies. A `&T` value carries the eval lifetime, so its -/// default must be `'static` data. +/// default must be `'static` data. An `Option<&T>` value is an optional +/// parked reference whose default is `None`, for attributes whose absence +/// means something a present value cannot. #[macro_export] macro_rules! attribute { () => {}; + ($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): Option<&$value:ty>; $($rest:tt)*) => { + $(#[$meta])* + $vis struct $marker; + + impl $crate::attribute::Attribute for $marker { + const NAME: &'static str = $name; + type Value<'e> = ::core::option::Option<&'e $value>; + + unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box { + ::std::boxed::Box::new(unsafe { ptr.cast::<::core::option::Option<&$value>>().read() }.map(|value| <$value as ::std::borrow::ToOwned>::to_owned(value))) + } + + const REPARK: ::core::option::Option ::core::option::Option<()>> = { + unsafe fn repark(value: &dyn $crate::list::AnyAttributeValue, dst: *mut u8, arena: &$crate::arena::Arena) -> ::core::option::Option<()> { + let owned: &::core::option::Option<<$value as ::std::borrow::ToOwned>::Owned> = + value.as_any().downcast_ref().expect("an optional reference attribute replays its owned clone"); + let parked = match owned { + ::core::option::Option::Some(owned) => { + let (parked, _) = arena.alloc(<$value as ::std::borrow::ToOwned>::to_owned(::std::borrow::Borrow::borrow(owned)))?; + ::core::option::Option::Some(::std::borrow::Borrow::borrow(parked)) + } + ::core::option::Option::None => ::core::option::Option::None, + }; + unsafe { dst.cast::<::core::option::Option<&$value>>().write(parked) }; + ::core::option::Option::Some(()) + } + ::core::option::Option::Some(repark) + }; + } + + $crate::attribute!(@register $marker); + $crate::attribute!($($rest)*); + }; ($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): &$value:ty $(= $default:expr)?; $($rest:tt)*) => { $(#[$meta])* $vis struct $marker; diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index b7ef0e5179..93de249f31 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -31,8 +31,8 @@ pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync}; pub use graphene_hash; pub use graphene_hash::CacheHash; pub use list::{ - ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END, ATTR_FONT_SIZE, - ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE, + ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_TEXT_FRAME, ATTR_END, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, + ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE, }; pub use memo::MemoHash; pub use no_std_types::AsU32; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index cb0c30868c..c32cd11c0a 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -10,9 +10,11 @@ use std::fmt::Debug; // ================================================= // Standard attribute keys used across the data flow // ================================================= -// Each name is declared once by its marker in [`crate::attribute`], which -// documents the attribute and fixes its value type. The constants re-export -// the markers' names for the string-keyed legacy readers and writers. +// Each name is declared once by its marker, which documents the attribute and +// fixes its value type. The constants re-export the markers' names for the +// string-keyed legacy readers and writers. Markers whose value types live +// below core-types are declared in their types' crates (vector-types, the +// text nodes, graphic-types), each with its own name constants. pub const ATTR_TRANSFORM: &str = crate::attribute::Transform::NAME; pub const ATTR_BLEND_MODE: &str = crate::attribute::BlendMode::NAME; @@ -36,18 +38,6 @@ pub const ATTR_MAX_WIDTH: &str = crate::attribute::MaxWidth::NAME; pub const ATTR_MAX_HEIGHT: &str = crate::attribute::MaxHeight::NAME; pub const ATTR_LETTER_TILT: &str = crate::attribute::LetterTilt::NAME; -// The remaining names' value types live in graphic-types. Their markers and -// constants move there with the marker wave. - -/// `List` snapshot of the upstream content that fed into a destructive merge -/// (Boolean Operation, Rasterize, etc.), so the editor can still surface click targets for -/// the original child layers after their content has been collapsed. -pub const ATTR_EDITOR_MERGED_LAYERS: &str = "editor:merged_layers"; -/// Vector graphics object's filled area paint, of type List where T is any graphic type. -pub const ATTR_FILL: &str = "fill"; -/// Vector graphics object's stroke paint, of type List where T is any graphic type. -pub const ATTR_STROKE: &str = "stroke"; - // =========================== // Implicit attribute defaults // =========================== diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index ad6daa38e0..c1ed51aef6 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -1,6 +1,7 @@ use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::graphene_hash::CacheHash; -use core_types::list::{ATTR_FILL, ATTR_STROKE, AttributeValueDyn, Item, ItemAttributeValues, List}; +use crate::markers::{ATTR_FILL, ATTR_STROKE}; +use core_types::list::{AttributeValueDyn, Item, ItemAttributeValues, List}; use core_types::ops::{FromAnchorPosition, ListConvert}; use core_types::render_complexity::RenderComplexity; use core_types::uuid::NodeId; diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 24f40b88e4..126e21d0bb 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -1,5 +1,6 @@ pub mod artboard; pub mod graphic; +pub mod markers; // Re-export all transitive dependencies so downstream crates only need to depend on graphic-types pub use core_types; @@ -9,6 +10,7 @@ pub use vector_types; // Re-export commonly used types at the crate root pub use artboard::Artboard; pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector}; +pub use markers::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE}; pub mod migrations { use crate::Vector; diff --git a/node-graph/libraries/graphic-types/src/markers.rs b/node-graph/libraries/graphic-types/src/markers.rs new file mode 100644 index 0000000000..1682af6c43 --- /dev/null +++ b/node-graph/libraries/graphic-types/src/markers.rs @@ -0,0 +1,45 @@ +//! Attribute markers whose value types live in this crate, with their name +//! constants for the string-keyed legacy readers and writers. +//! +//! The list-valued markers deep-copy by cloning the list, so their values must +//! stay free of arena-borrowing content such as [`Graphic::Group`]. + +use crate::Graphic; +use core_types::attribute::Attribute; +use core_types::list::List; + +core_types::attribute! { + /// Vector graphics object's filled area paint, a graphic list in the canonical paint form. + /// An absent value means no fill. + pub Fill("fill"): Option<&List>; + /// Vector graphics object's stroke paint, a graphic list in the canonical paint form. + /// An absent value means no stroke paint. + pub Stroke("stroke"): Option<&List>; + /// Snapshot of the upstream content that fed into a destructive merge (Boolean Operation, + /// Rasterize, etc.), so the editor can still surface click targets for the original child + /// layers after their content has been collapsed. + pub EditorMergedLayers("editor:merged_layers"): Option<&List>; +} + +pub const ATTR_FILL: &str = Fill::NAME; +pub const ATTR_STROKE: &str = Stroke::NAME; +pub const ATTR_EDITOR_MERGED_LAYERS: &str = EditorMergedLayers::NAME; + +#[cfg(test)] +mod tests { + use super::*; + use core_types::attribute::info; + use std::any::TypeId; + + #[test] + fn the_census_carries_this_crates_names() { + for name in ["fill", "stroke", "editor:merged_layers"] { + assert_eq!(info(name).unwrap().value_type, TypeId::of::>>()); + } + } + + #[test] + fn an_absent_paint_defaults_to_none() { + assert_eq!(::default(), None); + } +} diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index b23b5c01db..7368140bbd 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -7,14 +7,14 @@ use core_types::bounds::RenderBoundingBox; use core_types::color::Color; use core_types::color::SRGBA8; use core_types::consts::DEFAULT_FONT_SIZE; -use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List}; +use core_types::list::{Item, List}; use core_types::math::quad::Quad; use core_types::render_complexity::RenderComplexity; use core_types::transform::Footprint; use core_types::uuid::{NodeId, generate_uuid}; use core_types::{ - ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, - ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, + ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, + ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, }; use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; @@ -26,7 +26,7 @@ use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; -use graphic_types::{Artboard, Graphic, Vector}; +use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE, Artboard, Graphic, Vector}; use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts}; use num_traits::Zero; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; diff --git a/node-graph/libraries/vector-types/src/markers.rs b/node-graph/libraries/vector-types/src/markers.rs index 47a114fce7..951bbeccce 100644 --- a/node-graph/libraries/vector-types/src/markers.rs +++ b/node-graph/libraries/vector-types/src/markers.rs @@ -2,45 +2,19 @@ //! constants for the string-keyed legacy readers and writers. use core_types::attribute::Attribute; -use core_types::list::AnyAttributeValue; core_types::attribute! { /// Gradient's spread behavior past its endpoints (`Pad`, `Reflect`, or `Repeat`). pub SpreadMethod("spread_method"): crate::gradient::GradientSpreadMethod; /// Gradient's shape (`Linear` or `Radial`). pub GradientType("gradient_type"): crate::gradient::GradientType; + /// Optional `Vector` that overrides the item's own geometry for click-target generation. + /// Used by the 'Text' node for per-glyph bounding-box rectangles so glyphs are selectable + /// by clicking anywhere within their bounds, not just the filled letterform. An absent + /// value means the item's own geometry is the click target. + pub EditorClickTarget("editor:click_target"): Option<&crate::Vector>; } -/// Optional `Vector` that overrides the item's own geometry for click-target generation. -/// Used by the 'Text' node for per-glyph bounding-box rectangles so glyphs are selectable -/// by clicking anywhere within their bounds, not just the filled letterform. An absent -/// value means the item's own geometry is the click target. -pub struct EditorClickTarget; - -impl Attribute for EditorClickTarget { - const NAME: &'static str = "editor:click_target"; - type Value<'e> = Option<&'e crate::Vector>; - - unsafe fn read_erased(ptr: *const u8) -> Box { - Box::new(unsafe { ptr.cast::>().read() }.cloned()) - } - - const REPARK: Option Option<()>> = { - unsafe fn repark(value: &dyn AnyAttributeValue, dst: *mut u8, arena: &core_types::arena::Arena) -> Option<()> { - let owned: &Option = value.as_any().downcast_ref().expect("an optional vector attribute replays its owned clone"); - let parked = match owned { - Some(vector) => Some(arena.alloc(vector.clone())?.0), - None => None, - }; - unsafe { dst.cast::>().write(parked) }; - Some(()) - } - Some(repark) - }; -} - -core_types::attribute!(@register EditorClickTarget); - pub const ATTR_SPREAD_METHOD: &str = SpreadMethod::NAME; pub const ATTR_GRADIENT_TYPE: &str = GradientType::NAME; pub const ATTR_EDITOR_CLICK_TARGET: &str = EditorClickTarget::NAME; diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 67ae51f400..08fd1cfa99 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -3,10 +3,10 @@ use core_types::gpoll::Interrupt; use core_types::list::{AttributeDyn, AttributeValueDyn, Item, List, ListDyn}; use core_types::registry::types::{Angle, SignedInteger}; use core_types::uuid::NodeId; -use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, Color, Context, Ctx, DeriveCtx}; +use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, Color, Context, Ctx, DeriveCtx}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList}; -use graphic_types::{Artboard, Vector}; +use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, Artboard, Vector}; use raster_types::{CPU, GPU, Raster}; use vector_types::gradient::{GradientSpreadMethod, GradientType}; use vector_types::{GradientStop, GradientStops, ReferencePoint}; diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 871add45b6..f679e719e4 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -11,8 +11,10 @@ use core_types::runtime::SourceFuture; #[cfg(target_family = "wasm")] use core_types::transform::Footprint; #[cfg(target_family = "wasm")] -use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, WasmNotSend}; +use core_types::{ATTR_TRANSFORM, WasmNotSend}; use core_types::{Color, Ctx}; +#[cfg(target_family = "wasm")] +use graphic_types::ATTR_EDITOR_MERGED_LAYERS; pub use graph_craft::application_io::resource::{Resource, ResourceHash}; pub use graph_craft::application_io::*; pub use graph_craft::document::value::RenderOutputType; diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 18fa295b82..103d588dd4 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -1,6 +1,6 @@ -use core_types::list::{ATTR_FILL, Item, List}; +use core_types::list::{Item, List}; use core_types::uuid::NodeId; -use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, BlendMode, Color, Ctx}; +use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, BlendMode, Color, Ctx}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{bake_paint_transforms, set_paint_attribute}; use graphic_types::vector_types::gradient::{GradientSpreadMethod, GradientType}; @@ -8,7 +8,7 @@ use graphic_types::vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD}; use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath}; use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; -use graphic_types::{Graphic, Vector}; +use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, Graphic, Vector}; use linesweeper::topology::Topology; use linesweeper::{BinaryOp, FillRule, binary_op}; use smallvec::SmallVec; diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index adaa2ec5d5..a6f9457ae3 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -4,16 +4,16 @@ use core::hash::{Hash, Hasher}; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::gpoll::Interrupt; -use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn}; +use core_types::list::{Item, ItemAttributeValues, List, ListDyn}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; -use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, DeriveCtx}; +use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, DeriveCtx}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at}; use graphic_types::raster_types::{CPU, GPU, Raster}; -use graphic_types::{Graphic, IntoGraphicList}; +use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList}; use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape}; use rand::{Rng, SeedableRng};