use super::DocumentNode; use crate::application_io::PlatformEditorApi; use crate::application_io::resource::Resource; use crate::proto::Any as DAny; use brush_nodes::brush_stroke::BrushStroke; use core_types::color::SRGBA8; use core_types::context::Context; use core_types::gpoll::GPoll; use core_types::list::List; use core_types::registry::SourceHandle; use core_types::transform::Footprint; use core_types::uuid::NodeId; use core_types::value::{leveled_record_value_source, record_value_source}; use core_types::{CacheHash, Color, ContextModification, MemoHash, Type, TypeDescriptor}; use dyn_any::DynAny; pub use dyn_any::StaticType; pub use glam::{DAffine2, DVec2, IVec2, UVec2}; use graphene_application_io::resource::ResourceHash; use graphic_types::raster_types::{CPU, Image, Raster}; use graphic_types::vector_types::vector::style::Gradient; use graphic_types::vector_types::vector::{self, ReferencePoint}; use graphic_types::{Artboard, Graphic, Vector}; use rendering::RenderMetadata; use std::fmt::Display; use std::hash::Hash; use std::str::FromStr; pub use std::sync::Arc; use text_nodes::Font; use text_nodes::vector_types::GradientStop; use vector::VectorModification; pub struct TaggedValueTypeError; /// List of types routed through [`TaggedValue::TypeDefault`] instead of another dedicated variant. /// Consumed by [`TaggedValue::from_type`] (which creates `TypeDefault` values) and [`TaggedValue::to_dynany`]/[`TaggedValue::to_any`] (which unwrap them into real default values). macro_rules! for_each_type_default { ($action:ident) => { $action!(List); $action!(List); $action!(List>); $action!(List); $action!(List); $action!(DocumentNode); $action!(Resource); }; } /// Macro to generate the tagged value enum. macro_rules! tagged_value { ($ ($( #[$meta:meta] )* $identifier:ident ($ty:ty) ),* $(,)?) => { /// A type that is known, allowing serialization (serde::Deserialize is not object safe) #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[allow(clippy::large_enum_variant)] // TODO(TrueDoctor): Properly solve this disparity between the size of the largest and next largest variants pub enum TaggedValue { // =============== // MANUAL VARIANTS // =============== None, /// Stores a type, from which its `Default::default()` value can be obtained, rather than storing an actual type's value. /// Example: `TaggedValue::TypeDefault(descriptor!(String))` stores the type `String` but no specific string value. TypeDefault(TypeDescriptor), /// Stored compactly as a `Vec`, materializes as `List` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes. #[serde(deserialize_with = "core_types::misc::migrate_to_f64_array")] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "F64Table", alias = "VecF64", alias = "VecF32", alias = "F64Array4")] F64Array(Vec), /// Stored compactly as an `Option`, materializes as `List` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes. #[serde(deserialize_with = "core_types::misc::migrate_to_optional_color")] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")] Color(Option), /// Stored compactly as a `Gradient`, materializes as a single-row `List` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes. /// (Old documents that stored a full `Gradient` struct under this same `"Gradient"` tag are routed to `LegacyGradient` by `deserialize_tagged_value_with_legacy_migration`.) #[serde(deserialize_with = "graphic_types::vector_types::gradient::migrate_to_gradient")] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")] Gradient(Gradient), /// Stored compactly as a `Vec`, materializes as `List` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes. #[serde(deserialize_with = "brush_nodes::migrations::migrate_to_brush_strokes")] // TODO: Eventually remove this migration document upgrade code #[serde(alias = "BrushStrokeTable")] BrushStrokes(Vec), // ======================= // AUTO-GENERATED VARIANTS // ======================= $( $(#[$meta] ) *$identifier( $ty ), )* // ======================= // NON-SERIALIZED VARIANTS // ======================= #[serde(skip)] RenderOutput(RenderOutput), /// Path to the consumer of a `NodeInput::Reflection(DocumentNodePath)`. Materializes a `List` at runtime via `to_dynany`/`to_any` during graph flattening. #[serde(skip)] NodeIdPath(Vec), /// The `DocumentNode` value carried by an `Extract` proto node, populated at flatten time by `resolve_extract_nodes`. The on-disk placeholder uses `TypeDefault(descriptor!(DocumentNode))`. #[serde(skip)] DocumentNode(DocumentNode), /// Carried by context nullification proto nodes constructed at proto node compilation time in `insert_context_nullification_nodes`. #[serde(skip)] ContextModification(ContextModification), #[serde(skip)] EditorApi(Arc), /// Only used by the `resource` node, should never be serialized #[serde(skip)] ResourceHash(ResourceHash), } impl CacheHash for TaggedValue { fn cache_hash(&self, state: &mut H) { core::mem::discriminant(self).hash(state); match self { // =============== // MANUAL VARIANTS // =============== Self::None => {} Self::TypeDefault(td) => td.cache_hash(state), // ======================= // AUTO-GENERATED VARIANTS // ======================= $( Self::$identifier(x) => { x.cache_hash(state) }),* Self::F64Array(values) => values.cache_hash(state), Self::Color(color) => color.cache_hash(state), Self::Gradient(stops) => stops.cache_hash(state), Self::BrushStrokes(strokes) => strokes.cache_hash(state), // ======================= // NON-SERIALIZED VARIANTS // ======================= Self::NodeIdPath(path) => path.hash(state), Self::DocumentNode(node) => node.cache_hash(state), Self::ContextModification(modification) => modification.cache_hash(state), Self::RenderOutput(x) => x.cache_hash(state), Self::EditorApi(x) => x.cache_hash(state), Self::ResourceHash(x) => x.cache_hash(state), } } } impl<'a> TaggedValue { /// Converts to a Box pub fn to_dynany(self) -> DAny<'a> { match self { // =============== // MANUAL VARIANTS // =============== Self::None => Box::new(()), Self::TypeDefault(td) => { // Construct the actual default for types without a `TaggedValue` variant directly. // Recursion through `from_type_or_none` below is safe only because `for_each_type_default!` // exhaustively handles every type that `from_type` would route back to `TypeDefault`. let name = td.name.as_ref(); macro_rules! check { ($type_default:ty) => { if name == core_types::normalize_type_name(std::any::type_name::<$type_default>()) { return Box::new(<$type_default>::default()); } }; } for_each_type_default!(check); Self::from_type_or_none(&Type::Concrete(td)).to_dynany() } Self::F64Array(values) => { let list: List = values.into_iter().map(core_types::list::Item::new_from_element).collect(); Box::new(list) } Self::Color(color) => { let list: List = color.into_iter().map(core_types::list::Item::new_from_element).collect(); Box::new(list) } Self::Gradient(stops) => Box::new(List::::new_from_element(stops)), Self::BrushStrokes(strokes) => { let list: List = strokes.into_iter().map(core_types::list::Item::new_from_element).collect(); Box::new(list) } // ======================= // AUTO-GENERATED VARIANTS // ======================= $( Self::$identifier(x) => Box::new(x), )* // ======================= // NON-SERIALIZED VARIANTS // ======================= Self::RenderOutput(x) => Box::new(x), Self::NodeIdPath(path) => Box::new(path), Self::DocumentNode(node) => Box::new(node), Self::ContextModification(modification) => Box::new(modification), Self::EditorApi(x) => Box::new(x), Self::ResourceHash(x) => Box::new(x), } } /// Converts to a Arc pub fn to_any(self) -> Arc { match self { // =============== // MANUAL VARIANTS // =============== Self::None => Arc::new(()), Self::TypeDefault(td) => { // Same direct-construction path as `to_dynany` for the same reason as in `to_dynany`. let name = td.name.as_ref(); macro_rules! check { ($type_default:ty) => { if name == core_types::normalize_type_name(std::any::type_name::<$type_default>()) { return Arc::new(<$type_default>::default()); } }; } for_each_type_default!(check); Self::from_type_or_none(&Type::Concrete(td)).to_any() } Self::F64Array(values) => { let list: List = values.into_iter().map(core_types::list::Item::new_from_element).collect(); Arc::new(list) } Self::Color(color) => { let list: List = color.into_iter().map(core_types::list::Item::new_from_element).collect(); Arc::new(list) } Self::Gradient(stops) => Arc::new(List::::new_from_element(stops)), Self::BrushStrokes(strokes) => { let list: List = strokes.into_iter().map(core_types::list::Item::new_from_element).collect(); Arc::new(list) } // ======================= // AUTO-GENERATED VARIANTS // ======================= $( Self::$identifier(x) => Arc::new(x), )* // ======================= // NON-SERIALIZED VARIANTS // ======================= Self::RenderOutput(x) => Arc::new(x), Self::NodeIdPath(path) => Arc::new(path), Self::DocumentNode(node) => Arc::new(node), Self::ContextModification(modification) => Arc::new(modification), Self::EditorApi(x) => Arc::new(x), Self::ResourceHash(x) => Arc::new(x), } } /// Creates a core_types::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value pub fn ty(&self) -> Type { match self { // =============== // MANUAL VARIANTS // =============== Self::None => concrete!(()), // The list-typed defaults type by their element: depth is not a // `Type` axis, it rides the layout proven at wiring. Self::TypeDefault(td) => { let name = td.name.as_ref(); if name == core_types::normalize_type_name(std::any::type_name::>()) { return concrete!(Graphic); } if name == core_types::normalize_type_name(std::any::type_name::>()) { return concrete!(Artboard); } if name == core_types::normalize_type_name(std::any::type_name::>>()) { return concrete!(Raster); } if name == core_types::normalize_type_name(std::any::type_name::>()) { return concrete!(Vector); } if name == core_types::normalize_type_name(std::any::type_name::>()) { return concrete!(String); } Type::Concrete(td.clone()) } Self::F64Array(_) => concrete!(f64), Self::Color(_) => concrete!(Color), Self::Gradient(_) => concrete!(Gradient), Self::BrushStrokes(_) => concrete!(BrushStroke), // ======================= // AUTO-GENERATED VARIANTS // ======================= $( Self::$identifier(_) => concrete!($ty), )* // ======================= // NON-SERIALIZED VARIANTS // ======================= Self::RenderOutput(_) => concrete!(RenderOutput), Self::NodeIdPath(_) => concrete!(Vec), Self::DocumentNode(_) => concrete!(DocumentNode), Self::ContextModification(_) => concrete!(ContextModification), Self::EditorApi(_) => concrete!(Arc), Self::ResourceHash(_) => concrete!(ResourceHash), } } /// The record layout of this value's source: leveled for the list-carrying /// variants, element-only at rank 0 otherwise. `None` for a /// [`Self::TypeDefault`] whose named type is outside `for_each_type_default!`. pub fn value_layout(&self) -> Option { fn leveled() -> Option where T::Static: Clone + Send + Sync, { Some(core_types::record::Layout::default().with_writes(1, core_types::record::element_write_hashed::(), &[])) } fn scalar() -> Option where T::Static: Clone + Send + Sync, { Some(core_types::record::Layout::default().with_writes(0, core_types::record::element_write::(), &[])) } match self { Self::None => scalar::<()>(), Self::TypeDefault(td) => { let name = td.name.as_ref(); if name == core_types::normalize_type_name(std::any::type_name::>()) { return leveled::(); } if name == core_types::normalize_type_name(std::any::type_name::>()) { return leveled::(); } if name == core_types::normalize_type_name(std::any::type_name::>>()) { return leveled::>(); } if name == core_types::normalize_type_name(std::any::type_name::>()) { return leveled::(); } if name == core_types::normalize_type_name(std::any::type_name::>()) { return leveled::(); } if name == core_types::normalize_type_name(std::any::type_name::()) { return scalar::(); } if name == core_types::normalize_type_name(std::any::type_name::()) { return scalar::(); } None } Self::F64Array(_) => leveled::(), Self::Color(_) => leveled::(), Self::Gradient(_) => leveled::(), Self::BrushStrokes(_) => leveled::(), $( Self::$identifier(_) => scalar::<$ty>(), )* Self::RenderOutput(_) => scalar::(), Self::NodeIdPath(_) => scalar::>(), Self::DocumentNode(_) => scalar::(), Self::ContextModification(_) => scalar::(), Self::EditorApi(_) => scalar::>(), Self::ResourceHash(_) => scalar::(), } } /// Materializes the value as [`Self::to_dynany`] does, wrapped in a value source typed by [`Self::ty`]. pub fn to_edge(self) -> Result { match self { // =============== // MANUAL VARIANTS // =============== Self::None => Ok(record_value_source(())), Self::TypeDefault(td) => { let name = td.name.as_ref(); // The list-typed defaults serve an empty level; the rest construct // their default directly, mirroring `to_dynany`'s recursion guard. macro_rules! check_level { ($list:ty, $element:ty) => { if name == core_types::normalize_type_name(std::any::type_name::<$list>()) { return Ok(leveled_record_value_source(Vec::<$element>::new())); } }; } check_level!(List, Graphic); check_level!(List, Artboard); check_level!(List>, Raster); // One default lane rather than an empty level: an unwired path input // starts from a blank vector, as the legacy path modify synthesized itself. if name == core_types::normalize_type_name(std::any::type_name::>()) { return Ok(leveled_record_value_source(vec![Vector::default()])); } check_level!(List, String); if name == core_types::normalize_type_name(std::any::type_name::()) { return Ok(record_value_source(DocumentNode::default())); } if name == core_types::normalize_type_name(std::any::type_name::()) { return Ok(record_value_source(Resource::default())); } Self::from_type_or_none(&Type::Concrete(td)).to_edge() } Self::F64Array(values) => Ok(leveled_record_value_source(values)), Self::Color(color) => Ok(leveled_record_value_source(color.into_iter().collect::>())), Self::Gradient(stops) => Ok(leveled_record_value_source(vec![stops])), Self::BrushStrokes(strokes) => Ok(leveled_record_value_source(strokes)), // ======================= // AUTO-GENERATED VARIANTS // ======================= $( Self::$identifier(x) => Ok(record_value_source(x)), )* // ======================= // NON-SERIALIZED VARIANTS // ======================= Self::RenderOutput(x) => Ok(record_value_source(x)), Self::NodeIdPath(path) => Ok(record_value_source(path)), Self::DocumentNode(node) => Ok(record_value_source(node)), Self::ContextModification(modification) => Ok(record_value_source(modification)), Self::EditorApi(x) => Ok(record_value_source(x)), Self::ResourceHash(x) => Ok(record_value_source(x)), } } /// Evaluates a typed source and converts the landed value into a tagged value, with the coverage of [`Self::try_from_any`]. pub fn from_edge<'f>(handle: SourceHandle, ctx: &Context<'f>, frames: &core_types::record::Frames<'f>) -> Result, String> { let ty = handle.ty().clone(); // ======================= // RECORD WIRES, WHICH LAND AS THEIR ELEMENT // ======================= if ty == core_types::registry::record_source_type::<()>() { let edge = handle.downcast_record::<()>().map_err(|e| format!("{e:?}"))?; return Ok(core_types::record::serve_input(&edge, ctx, frames).map(|_| TaggedValue::None)); } $( if ty == core_types::registry::record_source_type::<$ty>() { let layout = handle.layout().clone(); let edge = handle.downcast_record::<$ty>().map_err(|e| format!("{e:?}"))?; return Ok(core_types::record::serve_input(&edge, ctx, frames) .map(|value| TaggedValue::$identifier(unsafe { core_types::record::read_element::<$ty>(layout.rec(&value)) }))); } )* if ty == core_types::registry::record_source_type::() { let layout = handle.layout().clone(); let edge = handle.downcast_record::().map_err(|e| format!("{e:?}"))?; return Ok(core_types::record::serve_input(&edge, ctx, frames) .map(|value| TaggedValue::RenderOutput(unsafe { core_types::record::read_element::(layout.rec(&value)) }))); } Err(format!("Cannot convert edge of type {ty} to TaggedValue")) } /// Attempts to downcast the dynamic type to a tagged value pub fn try_from_any(input: Box + 'a>) -> Result { use dyn_any::downcast; use std::any::TypeId; match DynAny::type_id(input.as_ref()) { // =============== // MANUAL VARIANTS // =============== x if x == TypeId::of::<()>() => Ok(TaggedValue::None), // ======================= // AUTO-GENERATED VARIANTS // ======================= $( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )* // ======================= // NON-SERIALIZED VARIANTS // ======================= x if x == TypeId::of::() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())), _ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))), } } /// Attempts to downcast the dynamic type to a tagged value pub fn try_from_std_any_ref(input: &dyn std::any::Any) -> Result { use std::any::TypeId; match input.type_id() { // =============== // MANUAL VARIANTS // =============== x if x == TypeId::of::<()>() => Ok(TaggedValue::None), // ======================= // AUTO-GENERATED VARIANTS // ======================= $( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(<$ty as Clone>::clone(input.downcast_ref().unwrap()))), )* // ======================= // NON-SERIALIZED VARIANTS // ======================= x if x == TypeId::of::() => Ok(TaggedValue::RenderOutput(RenderOutput::clone(input.downcast_ref().unwrap()))), _ => Err(format!("Cannot convert {:?} to TaggedValue", std::any::type_name_of_val(input))), } } /// Returns a TaggedValue from the type, where that value is its type's `Default::default()`. /// Dispatches by the type's name (the field that round-trips through serde) so it works for both /// freshly constructed types and types deserialized from disk where the runtime `TypeId` is unavailable. pub fn from_type(input: &Type) -> Option { match input { Type::Generic(_) => None, Type::Record(inner) => Self::from_type(inner), Type::Concrete(concrete_type) => { let name = concrete_type.name.as_ref(); // TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types // Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned. if name == core_types::normalize_type_name(std::any::type_name::<()>()) { return Some(TaggedValue::None) } // List-wrapped types need a single-item default with the element's default, not an empty list if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::Color(Some(Color::default()))) } if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::Gradient(Gradient::default())) } $( if name == core_types::normalize_type_name(std::any::type_name::<$ty>()) { return Some(TaggedValue::$identifier(Default::default())) } )* if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::F64Array(Vec::new())) } if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::BrushStrokes(Vec::new())) } // Leveled inputs type by their element; each element name maps to the // same tagged default as its legacy list form. if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::Color(Some(Color::default()))) } if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::Gradient(Gradient::default())) } if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::BrushStrokes(Vec::new())) } if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List))) } if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List))) } if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List>))) } if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List))) } // Types whose `TaggedValue` variant has been removed. They route through `TypeDefault` instead, with `to_dynany`/`to_any` constructing the actual default at execution time. macro_rules! check { ($type_default:ty) => { if name == core_types::normalize_type_name(std::any::type_name::<$type_default>()) { return Some(TaggedValue::TypeDefault(concrete_type.clone())); } }; } for_each_type_default!(check); None } Type::Fn(_, output) => TaggedValue::from_type(output), Type::Future(output) => TaggedValue::from_type(output), } } pub fn from_type_or_none(input: &Type) -> Self { Self::from_type(input).unwrap_or(TaggedValue::None) } pub fn to_debug_string(&self) -> String { match self { // =============== // MANUAL VARIANTS // =============== Self::None => "()".to_string(), Self::TypeDefault(td) => format!("TypeDefault({})", td.name), Self::F64Array(values) => format!("F64Array({values:?})"), Self::Color(color) => format!("Color({color:?})"), Self::Gradient(stops) => format!("Gradient({stops:?})"), Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"), // ======================= // AUTO-GENERATED VARIANTS // ======================= $( Self::$identifier(x) => format!("{:?}", x), )* // ======================= // NON-SERIALIZED VARIANTS // ======================= Self::RenderOutput(_) => "RenderOutput".to_string(), Self::NodeIdPath(path) => format!("NodeIdPath({path:?})"), Self::DocumentNode(node) => format!("DocumentNode({node:?})"), Self::ContextModification(modification) => format!("ContextModification({modification:?})"), Self::EditorApi(_) => "PlatformEditorApi".to_string(), Self::ResourceHash(hash) => format!("ResourceHash({hash:?})"), } } } $( impl From<$ty> for TaggedValue { fn from(value: $ty) -> Self { Self::$identifier(value) } } )* $( impl<'a> TryFrom<&'a TaggedValue> for &'a $ty { type Error = TaggedValueTypeError; fn try_from(value: &'a TaggedValue) -> Result { match value{ TaggedValue::$identifier(value) => Ok(value), _ => Err(TaggedValueTypeError), } } } )* }; } tagged_value! { // =============== // PRIMITIVE TYPES // =============== F32(f32), F64(f64), U32(u32), U64(u64), Bool(bool), String(String), #[serde(alias = "IVec2", alias = "UVec2", alias = "Vec2")] DVec2(DVec2), #[serde(alias = "Affine2")] DAffine2(DAffine2), OptionalDAffine2(Option), #[serde(alias = "FillGradient")] LegacyGradient(graphic_types::migrations::legacy::LegacyGradient), Font(Font), Footprint(Footprint), VectorModification(Box), ImageData(Image), Resource(graphene_application_io::resource::ResourceId), // ========== // ENUM TYPES // ========== #[serde(alias = "Fill")] LegacyFill(graphic_types::migrations::legacy::LegacyFill), BlendMode(core_types::blending::BlendMode), LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation), QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel), XY(graphene_core::extract_xy::XY), StringCapitalization(text_nodes::StringCapitalization), RedGreenBlue(raster_nodes::adjustments::RedGreenBlue), RedGreenBlueAlpha(raster_nodes::adjustments::RedGreenBlueAlpha), RealTimeMode(graphene_core::animation::RealTimeMode), NoiseType(raster_nodes::adjustments::NoiseType), FractalType(raster_nodes::adjustments::FractalType), CellularDistanceFunction(raster_nodes::adjustments::CellularDistanceFunction), CellularReturnType(raster_nodes::adjustments::CellularReturnType), DomainWarpType(raster_nodes::adjustments::DomainWarpType), RelativeAbsolute(raster_nodes::adjustments::RelativeAbsolute), SelectiveColorChoice(raster_nodes::adjustments::SelectiveColorChoice), GridType(vector::misc::GridType), ArcType(vector::misc::ArcType), RowsOrColumns(vector::misc::RowsOrColumns), MergeByDistanceAlgorithm(vector::misc::MergeByDistanceAlgorithm), ExtrudeJoiningAlgorithm(vector::misc::ExtrudeJoiningAlgorithm), PointSpacingType(vector::misc::PointSpacingType), SpiralType(vector::misc::SpiralType), InterpolationDistribution(vector::misc::InterpolationDistribution), #[serde(alias = "LineCap")] StrokeCap(vector::style::StrokeCap), #[serde(alias = "LineJoin")] StrokeJoin(vector::style::StrokeJoin), StrokeAlign(vector::style::StrokeAlign), PaintOrder(vector::style::PaintOrder), GradientType(vector::style::GradientType), GradientSpreadMethod(vector::style::GradientSpreadMethod), ReferencePoint(vector::ReferencePoint), CentroidType(vector::misc::CentroidType), BooleanOperation(vector::misc::BooleanOperation), TextAlign(text_nodes::TextAlign), ScaleType(core_types::transform::ScaleType), } impl TaggedValue { pub fn to_primitive_string(&self) -> String { match self { TaggedValue::None => "()".to_string(), TaggedValue::String(x) => format!("\"{x}\""), TaggedValue::U32(x) => x.to_string() + "_u32", TaggedValue::U64(x) => x.to_string() + "_u64", TaggedValue::F32(x) => x.to_string() + "_f32", TaggedValue::F64(x) => x.to_string() + "_f64", TaggedValue::Bool(x) => x.to_string(), TaggedValue::BlendMode(x) => "BlendMode::".to_string() + &x.to_string(), _ => panic!("Cannot convert to primitive string"), } } pub fn from_primitive_string(string: &str, ty: &Type) -> Option { fn to_dvec2(input: &str) -> Option { let mut split = input.split(','); let x = split.next()?.trim().parse().ok()?; let y = split.next()?.trim().parse().ok()?; Some(DVec2::new(x, y)) } fn to_color(input: &str) -> Option { // String syntax (e.g. "000000ff") if input.starts_with('"') && input.ends_with('"') { let hex = input.trim().trim_matches('"').trim().trim_start_matches('#'); let color = SRGBA8::from_hex_str(hex).map(Color::from); if color.is_none() { log::error!("Invalid default value color string: {input}"); } return color; } // Color constant syntax (e.g. Color::BLACK) let mut choices = input.split("::"); let (first, second) = (choices.next()?.trim(), choices.next()?.trim()); if first == "Color" { return Some(match second { "BLACK" => Color::BLACK, "WHITE" => Color::WHITE, "RED" => Color::RED, "GREEN" => Color::GREEN, "BLUE" => Color::BLUE, "YELLOW" => Color::YELLOW, "CYAN" => Color::CYAN, "MAGENTA" => Color::MAGENTA, "TRANSPARENT" => Color::TRANSPARENT, _ => { log::error!("Invalid default value color constant: {input}"); return None; } }); } log::error!("Invalid default value color: {input}"); None } fn to_gradient(input: &str) -> Option { // String syntax: (e.g. "000000ff, ff0000ff") let stops = input.split(',').filter_map(|s| to_color(s.trim())).collect::>(); if stops.len() == 1 { Some(Gradient::new(vec![ GradientStop { position: 0., midpoint: 0.5, color: stops[0], }, GradientStop { position: 1., midpoint: 0.5, color: stops[0], }, ])) } else if stops.len() >= 2 { let step = 1. / (stops.len() - 1) as f64; Some(Gradient::new(stops.into_iter().enumerate().map(|(i, color)| GradientStop { position: i as f64 * step, midpoint: 0.5, color, }))) } else { log::error!("Invalid default value gradient string: {input}"); None } } fn to_reference_point(input: &str) -> Option { let mut choices = input.split("::"); let (first, second) = (choices.next()?.trim(), choices.next()?.trim()); if first == "ReferencePoint" { return Some(match second { "None" => ReferencePoint::None, "TopLeft" => ReferencePoint::TopLeft, "TopCenter" => ReferencePoint::TopCenter, "TopRight" => ReferencePoint::TopRight, "CenterLeft" => ReferencePoint::CenterLeft, "Center" => ReferencePoint::Center, "CenterRight" => ReferencePoint::CenterRight, "BottomLeft" => ReferencePoint::BottomLeft, "BottomCenter" => ReferencePoint::BottomCenter, "BottomRight" => ReferencePoint::BottomRight, _ => { log::error!("Invalid ReferencePoint default type variant: {input}"); return None; } }); } log::error!("Invalid ReferencePoint default type: {input}"); None } match ty { Type::Generic(_) => None, // A leveled input's default is its element's default, as in `from_type`. Type::Record(inner) => TaggedValue::from_primitive_string(string, inner), Type::Concrete(concrete_type) => { let ty = concrete_type.id?; use std::any::TypeId; // TODO: Add default implementations for types such as TaggedValue::Subpaths, and use the defaults here and in document_node_types // Tries using the default for the tagged value type. If it not implemented, then uses the default used in document_node_types. If it is not used there, then TaggedValue::None is returned. let ty = match () { () if ty == TypeId::of::<()>() => TaggedValue::None, () if ty == TypeId::of::() => TaggedValue::String(string.into()), () if ty == TypeId::of::() => FromStr::from_str(string).map(TaggedValue::F64).ok()?, () if ty == TypeId::of::() => FromStr::from_str(string).map(TaggedValue::F32).ok()?, () if ty == TypeId::of::() => FromStr::from_str(string).map(TaggedValue::U64).ok()?, () if ty == TypeId::of::() => FromStr::from_str(string).map(TaggedValue::U32).ok()?, () if ty == TypeId::of::() => to_dvec2(string).map(TaggedValue::DVec2)?, () if ty == TypeId::of::() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?, // `Color` (not in a `List`) is still currently needed by `BlackAndWhiteNode` and `ColorOverlayNode` GPU `shader_node(PerPixelAdjust)` variants () if ty == TypeId::of::() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?, () if ty == TypeId::of::>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?, // The Fill and Stroke nodes' paint connectors default to `List`, their first registered implementation row () if ty == TypeId::of::>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?, () if ty == TypeId::of::>() => to_gradient(string).map(TaggedValue::Gradient)?, () if ty == TypeId::of::() => to_reference_point(string).map(TaggedValue::ReferencePoint)?, _ => return None, }; Some(ty) } Type::Fn(_, output) => TaggedValue::from_primitive_string(string, output), Type::Future(fut) => TaggedValue::from_primitive_string(string, fut), } } pub fn to_u32(&self) -> u32 { match self { TaggedValue::U32(x) => *x, _ => panic!("Passed value is not of type u32"), } } } /// Custom deserializer hooked onto `NodeInput::Value::tagged_value` that intercepts removed-variant tags before delegating to `TaggedValue`'s standard derive. /// /// Routes legacy variant names into modern variants, in typed Rust. Each legacy name is also matched against the historical `#[serde(alias = "...")]` spellings the deleted variant accepted, so old-shape inner payloads are caught: /// /// - `BrushCache` → `TaggedValue::None` (purely runtime cache; no payload to preserve) /// - `Graphic` (or alias `GraphicGroup`/`Group`) → `TaggedValue::TypeDefault(descriptor!(List))` /// - `Artboard` (or alias `ArtboardGroup`) → `TaggedValue::TypeDefault(descriptor!(List))` /// - `Raster` (or alias `ImageFrame`/`RasterData`/`Image`): /// - non-empty (the legacy `image` proto's input 1, where the inner `Raster` serializes as the embedded `Image`) → `TaggedValue::ImageData(>)` /// - empty → `TaggedValue::TypeDefault(descriptor!(List>))` /// - `Vector` (or alias `VectorData`): /// - non-empty → `TaggedValue::VectorModification()` (the document_migration's Path pass disambiguates this between SVG-import legacy and a discardable modern baked value via the input's `exposed` flag) /// - empty → `TaggedValue::TypeDefault(descriptor!(List))` /// /// All other tags (including ones with the modern shape) fall through to the standard derived `Deserialize` for `TaggedValue`. // TODO: Eventually remove this migration document upgrade code #[cfg(feature = "loading")] pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { use serde::Deserialize; let value = serde_json::Value::deserialize(deserializer)?; if let Some(map) = value.as_object() && map.len() == 1 && let Some((tag, content)) = map.iter().next() { match tag.as_str() { "BrushCache" => return Ok(MemoHash::new(TaggedValue::None)), "Graphic" | "GraphicGroup" | "Group" => return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List)))), "Artboard" | "ArtboardGroup" => return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List)))), "Raster" | "ImageFrame" | "RasterData" | "Image" => { let first_element = content .as_object() .and_then(|c| c.get("element").or_else(|| c.get("instance")).or_else(|| c.get("instances"))) .and_then(|e| e.as_array()) .and_then(|arr| arr.first()); if let Some(image_value) = first_element { let image: Image = serde_json::from_value(image_value.clone()).map_err(serde::de::Error::custom)?; return Ok(MemoHash::new(TaggedValue::ImageData(image))); } return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List>)))); } "Vector" | "VectorData" => { let vector = graphic_types::migrations::migrate_to_optional_vector(content.clone()).map_err(serde::de::Error::custom)?; if let Some(vector) = vector { let modification = Box::new(VectorModification::create_from_vector(&vector)); return Ok(MemoHash::new(TaggedValue::VectorModification(modification))); } return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List)))); } // The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `LegacyGradient`), and now carries an `Option`. // Disambiguate by payload shape: a Gradient struct has `start`/`end` keys; a `Gradient` has none of those (it has `position`/`midpoint`/`color`). "Gradient" if content.as_object().is_some_and(|c| c.contains_key("start") && c.contains_key("end")) => { let gradient: graphic_types::migrations::legacy::LegacyGradient = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?; return Ok(MemoHash::new(TaggedValue::LegacyGradient(gradient))); } _ => {} } } let tagged_value: TaggedValue = serde_json::from_value(value).map_err(serde::de::Error::custom)?; Ok(MemoHash::new(tagged_value)) } impl Display for TaggedValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TaggedValue::String(x) => f.write_str(x), TaggedValue::U32(x) => f.write_fmt(format_args!("{x}")), TaggedValue::U64(x) => f.write_fmt(format_args!("{x}")), TaggedValue::F32(x) => f.write_fmt(format_args!("{x}")), TaggedValue::F64(x) => f.write_fmt(format_args!("{x}")), TaggedValue::Bool(x) => f.write_fmt(format_args!("{x}")), _ => panic!("Cannot convert to string"), } } } #[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub struct RenderOutput { pub data: RenderOutputType, pub metadata: RenderMetadata, } #[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub enum RenderOutputType { #[serde(skip)] Texture(graphene_application_io::Texture), #[serde(skip)] Buffer { data: Vec, width: u32, height: u32, }, Svg { svg: String, image_data: Vec<(u64, Image)>, }, #[cfg(target_family = "wasm")] CanvasFrame { canvas_id: u64, resolution: DVec2, }, } impl CacheHash for RenderOutputType { fn cache_hash(&self, state: &mut H) { core::mem::discriminant(self).hash(state); match self { Self::Texture(texture) => texture.hash(state), Self::Buffer { data, width, height } => { data.cache_hash(state); width.cache_hash(state); height.cache_hash(state); } Self::Svg { svg, image_data } => { svg.cache_hash(state); image_data.cache_hash(state); } #[cfg(target_family = "wasm")] Self::CanvasFrame { canvas_id, resolution } => { canvas_id.cache_hash(state); resolution.cache_hash(state); } } } } // Metadata is excluded because it's editor-side auxiliary data (click targets, transforms) // that shouldn't affect render cache invalidation, and it contains HashMaps with non-deterministic iteration order impl CacheHash for RenderOutput { fn cache_hash(&self, state: &mut H) { self.data.cache_hash(state); } } #[cfg(test)] mod typedefault_dispatch { use super::*; use core_types::descriptor; /// Round-trips every type listed in [`for_each_type_default`] through `TaggedValue::TypeDefault → to_dynany / to_any` and asserts the resulting concrete type matches the descriptor. /// /// This guards against the only way to break the recursion invariant in the unwrap functions: someone hand-rolling a `TypeDefault`-yielding case in `from_type` (or the macro's expansion in one of the unwrap sites silently failing to match a name). If it fails, the message points at the specific type and the structural reason. #[test] fn typedefault_dispatch_terminates() { macro_rules! check { ($type_default:ty) => {{ let descriptor = descriptor!($type_default); let expected_type_id = std::any::TypeId::of::<$type_default>(); let dyn_value = TaggedValue::TypeDefault(descriptor.clone()).to_dynany(); assert_eq!( DynAny::type_id(&*dyn_value), expected_type_id, "`to_dynany(TypeDefault({0}))` did not produce a `{0}` — `for_each_type_default!` lists this type but the unwrap site doesn't handle it. Without a match, `to_dynany` falls back to `from_type_or_none`, which returns `TypeDefault({0})` again and recurses forever.", core_types::normalize_type_name(std::any::type_name::<$type_default>()), ); let arc_value = TaggedValue::TypeDefault(descriptor).to_any(); assert_eq!( (*arc_value).type_id(), expected_type_id, "`to_any(TypeDefault({0}))` did not produce a `{0}` — same recursion hazard as above for the `to_any` path.", core_types::normalize_type_name(std::any::type_name::<$type_default>()), ); }}; } for_each_type_default!(check); } } #[cfg(test)] mod leveled_edges { use super::*; use core_types::descriptor; use core_types::registry::record_source_type; #[test] fn list_variants_produce_leveled_edges_typed_by_element() { let edge = TaggedValue::F64Array(vec![1., 2., 3.]).to_edge().unwrap(); assert_eq!(edge.ty(), &record_source_type::()); assert_eq!(edge.layout().depth, 1); let edge = TaggedValue::Color(Some(Color::default())).to_edge().unwrap(); assert_eq!(edge.ty(), &record_source_type::()); assert_eq!(edge.layout().depth, 1); let edge = TaggedValue::TypeDefault(descriptor!(List)).to_edge().unwrap(); assert_eq!(edge.ty(), &record_source_type::()); assert_eq!(edge.layout().depth, 1); } #[test] fn scalar_variants_keep_their_rank_zero_edges() { let edge = TaggedValue::Bool(true).to_edge().unwrap(); assert_eq!(edge.ty(), &record_source_type::()); assert_eq!(edge.layout().depth, 0); } #[test] fn the_value_layout_matches_the_edge_layout() { for value in [ TaggedValue::F64Array(vec![1.]), TaggedValue::Bool(true), TaggedValue::TypeDefault(descriptor!(List)), TaggedValue::Gradient(Default::default()), ] { let layout = value.value_layout().unwrap(); let edge = value.to_edge().unwrap(); assert_eq!(&layout, edge.layout()); } } } #[cfg(test)] mod record_defaults { use super::*; use core_types::registry::record_source_type; // The registry can present a record row first (wasm registration order), // so primitive defaults must parse through the record wrapping. #[test] fn primitive_defaults_parse_through_record_wires() { let leveled = core_types::registry::record_type::(); assert_eq!(TaggedValue::from_primitive_string("2.", &leveled), Some(TaggedValue::F64(2.))); assert_eq!(TaggedValue::from_primitive_string("5", &record_source_type::()), Some(TaggedValue::U32(5))); assert_eq!(TaggedValue::from_primitive_string("true", &record_source_type::()), Some(TaggedValue::Bool(true))); } }