mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
976 lines
44 KiB
Rust
976 lines
44 KiB
Rust
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<Graphic>);
|
|
$action!(List<Artboard>);
|
|
$action!(List<Raster<CPU>>);
|
|
$action!(List<Vector>);
|
|
$action!(List<String>);
|
|
$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<f64>`, materializes as `List<f64>` 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<f64>),
|
|
/// Stored compactly as an `Option<Color>`, materializes as `List<Color>` 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<Color>),
|
|
/// Stored compactly as a `Gradient`, materializes as a single-row `List<Gradient>` 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<BrushStroke>`, materializes as `List<BrushStroke>` 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<BrushStroke>),
|
|
// =======================
|
|
// 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<NodeId>` at runtime via `to_dynany`/`to_any` during graph flattening.
|
|
#[serde(skip)]
|
|
NodeIdPath(Vec<NodeId>),
|
|
/// 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<PlatformEditorApi>),
|
|
/// Only used by the `resource` node, should never be serialized
|
|
#[serde(skip)]
|
|
ResourceHash(ResourceHash),
|
|
}
|
|
|
|
impl CacheHash for TaggedValue {
|
|
fn cache_hash<H: core::hash::Hasher>(&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<dyn DynAny>
|
|
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<f64> = values.into_iter().map(core_types::list::Item::new_from_element).collect();
|
|
Box::new(list)
|
|
}
|
|
Self::Color(color) => {
|
|
let list: List<Color> = color.into_iter().map(core_types::list::Item::new_from_element).collect();
|
|
Box::new(list)
|
|
}
|
|
Self::Gradient(stops) => Box::new(List::<Gradient>::new_from_element(stops)),
|
|
Self::BrushStrokes(strokes) => {
|
|
let list: List<BrushStroke> = 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<dyn Any + Send + Sync + 'static>
|
|
pub fn to_any(self) -> Arc<dyn std::any::Any + Send + Sync + 'static> {
|
|
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<f64> = values.into_iter().map(core_types::list::Item::new_from_element).collect();
|
|
Arc::new(list)
|
|
}
|
|
Self::Color(color) => {
|
|
let list: List<Color> = color.into_iter().map(core_types::list::Item::new_from_element).collect();
|
|
Arc::new(list)
|
|
}
|
|
Self::Gradient(stops) => Arc::new(List::<Gradient>::new_from_element(stops)),
|
|
Self::BrushStrokes(strokes) => {
|
|
let list: List<BrushStroke> = 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::<List<Graphic>>()) { return concrete!(Graphic); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<Artboard>>()) { return concrete!(Artboard); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<Raster<CPU>>>()) { return concrete!(Raster<CPU>); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<Vector>>()) { return concrete!(Vector); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<String>>()) { 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<NodeId>),
|
|
Self::DocumentNode(_) => concrete!(DocumentNode),
|
|
Self::ContextModification(_) => concrete!(ContextModification),
|
|
Self::EditorApi(_) => concrete!(Arc<PlatformEditorApi>),
|
|
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<core_types::record::Layout> {
|
|
fn leveled<T: Clone + Send + Sync + CacheHash + PartialEq + dyn_any::StaticTypeSized>() -> Option<core_types::record::Layout>
|
|
where
|
|
T::Static: Clone + Send + Sync,
|
|
{
|
|
Some(core_types::record::Layout::default().with_writes(1, core_types::record::element_write_hashed::<T>(), &[]))
|
|
}
|
|
fn scalar<T: Clone + Send + Sync + dyn_any::StaticTypeSized>() -> Option<core_types::record::Layout>
|
|
where
|
|
T::Static: Clone + Send + Sync,
|
|
{
|
|
Some(core_types::record::Layout::default().with_writes(0, core_types::record::element_write::<T>(), &[]))
|
|
}
|
|
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::<List<Graphic>>()) { return leveled::<Graphic>(); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<Artboard>>()) { return leveled::<Artboard>(); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<Raster<CPU>>>()) { return leveled::<Raster<CPU>>(); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<Vector>>()) { return leveled::<Vector>(); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<String>>()) { return leveled::<String>(); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<DocumentNode>()) { return scalar::<DocumentNode>(); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<Resource>()) { return scalar::<Resource>(); }
|
|
None
|
|
}
|
|
Self::F64Array(_) => leveled::<f64>(),
|
|
Self::Color(_) => leveled::<Color>(),
|
|
Self::Gradient(_) => leveled::<Gradient>(),
|
|
Self::BrushStrokes(_) => leveled::<BrushStroke>(),
|
|
$( Self::$identifier(_) => scalar::<$ty>(), )*
|
|
Self::RenderOutput(_) => scalar::<RenderOutput>(),
|
|
Self::NodeIdPath(_) => scalar::<Vec<NodeId>>(),
|
|
Self::DocumentNode(_) => scalar::<DocumentNode>(),
|
|
Self::ContextModification(_) => scalar::<ContextModification>(),
|
|
Self::EditorApi(_) => scalar::<Arc<PlatformEditorApi>>(),
|
|
Self::ResourceHash(_) => scalar::<ResourceHash>(),
|
|
}
|
|
}
|
|
|
|
/// Materializes the value as [`Self::to_dynany`] does, wrapped in a value source typed by [`Self::ty`].
|
|
pub fn to_edge(self) -> Result<SourceHandle, String> {
|
|
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>, Graphic);
|
|
check_level!(List<Artboard>, Artboard);
|
|
check_level!(List<Raster<CPU>>, Raster<CPU>);
|
|
// 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::<List<Vector>>()) { return Ok(leveled_record_value_source(vec![Vector::default()])); }
|
|
check_level!(List<String>, String);
|
|
if name == core_types::normalize_type_name(std::any::type_name::<DocumentNode>()) { return Ok(record_value_source(DocumentNode::default())); }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<Resource>()) { 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::<Vec<_>>())),
|
|
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<GPoll<Self>, 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::<RenderOutput>() {
|
|
let layout = handle.layout().clone();
|
|
let edge = handle.downcast_record::<RenderOutput>().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::<RenderOutput>(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<dyn DynAny<'a> + 'a>) -> Result<Self, String> {
|
|
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::<RenderOutput>() => 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<Self, String> {
|
|
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::<RenderOutput>() => 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<Self> {
|
|
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::<List<Color>>()) { return Some(TaggedValue::Color(Some(Color::default()))) }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<Gradient>>()) { 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::<List<f64>>()) { return Some(TaggedValue::F64Array(Vec::new())) }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<List<BrushStroke>>()) { 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::<Color>()) { return Some(TaggedValue::Color(Some(Color::default()))) }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<Gradient>()) { return Some(TaggedValue::Gradient(Gradient::default())) }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<BrushStroke>()) { return Some(TaggedValue::BrushStrokes(Vec::new())) }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<Graphic>()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List<Graphic>))) }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<Artboard>()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List<Artboard>))) }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<Raster<CPU>>()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List<Raster<CPU>>))) }
|
|
if name == core_types::normalize_type_name(std::any::type_name::<Vector>()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List<Vector>))) }
|
|
// 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<Self, Self::Error> {
|
|
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<DAffine2>),
|
|
#[serde(alias = "FillGradient")]
|
|
LegacyGradient(graphic_types::migrations::legacy::LegacyGradient),
|
|
Font(Font),
|
|
Footprint(Footprint),
|
|
VectorModification(Box<VectorModification>),
|
|
ImageData(Image<Color>),
|
|
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<Self> {
|
|
fn to_dvec2(input: &str) -> Option<DVec2> {
|
|
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<Color> {
|
|
// 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<Gradient> {
|
|
// String syntax: (e.g. "000000ff, ff0000ff")
|
|
let stops = input.split(',').filter_map(|s| to_color(s.trim())).collect::<Vec<_>>();
|
|
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<ReferencePoint> {
|
|
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::<String>() => TaggedValue::String(string.into()),
|
|
() if ty == TypeId::of::<f64>() => FromStr::from_str(string).map(TaggedValue::F64).ok()?,
|
|
() if ty == TypeId::of::<f32>() => FromStr::from_str(string).map(TaggedValue::F32).ok()?,
|
|
() if ty == TypeId::of::<u64>() => FromStr::from_str(string).map(TaggedValue::U64).ok()?,
|
|
() if ty == TypeId::of::<u32>() => FromStr::from_str(string).map(TaggedValue::U32).ok()?,
|
|
() if ty == TypeId::of::<DVec2>() => to_dvec2(string).map(TaggedValue::DVec2)?,
|
|
() if ty == TypeId::of::<bool>() => 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::<Color>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
|
() if ty == TypeId::of::<List<Color>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
|
// The Fill and Stroke nodes' paint connectors default to `List<Graphic>`, their first registered implementation row
|
|
() if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
|
|
() if ty == TypeId::of::<List<Gradient>>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
|
() if ty == TypeId::of::<ReferencePoint>() => 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<Graphic>))`
|
|
/// - `Artboard` (or alias `ArtboardGroup`) → `TaggedValue::TypeDefault(descriptor!(List<Artboard>))`
|
|
/// - `Raster` (or alias `ImageFrame`/`RasterData`/`Image`):
|
|
/// - non-empty (the legacy `image` proto's input 1, where the inner `Raster<CPU>` serializes as the embedded `Image<Color>`) → `TaggedValue::ImageData(<inner Image<Color>>)`
|
|
/// - empty → `TaggedValue::TypeDefault(descriptor!(List<Raster<CPU>>))`
|
|
/// - `Vector` (or alias `VectorData`):
|
|
/// - non-empty → `TaggedValue::VectorModification(<built from first element>)` (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<Vector>))`
|
|
///
|
|
/// 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<MemoHash<TaggedValue>, 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<Graphic>)))),
|
|
"Artboard" | "ArtboardGroup" => return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List<Artboard>)))),
|
|
"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<Color> = 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<Raster<CPU>>))));
|
|
}
|
|
"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<Vector>))));
|
|
}
|
|
// The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `LegacyGradient`), and now carries an `Option<Gradient>`.
|
|
// 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<u8>,
|
|
width: u32,
|
|
height: u32,
|
|
},
|
|
Svg {
|
|
svg: String,
|
|
image_data: Vec<(u64, Image<Color>)>,
|
|
},
|
|
#[cfg(target_family = "wasm")]
|
|
CanvasFrame {
|
|
canvas_id: u64,
|
|
resolution: DVec2,
|
|
},
|
|
}
|
|
|
|
impl CacheHash for RenderOutputType {
|
|
fn cache_hash<H: core::hash::Hasher>(&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<H: core::hash::Hasher>(&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::<f64>());
|
|
assert_eq!(edge.layout().depth, 1);
|
|
|
|
let edge = TaggedValue::Color(Some(Color::default())).to_edge().unwrap();
|
|
assert_eq!(edge.ty(), &record_source_type::<Color>());
|
|
assert_eq!(edge.layout().depth, 1);
|
|
|
|
let edge = TaggedValue::TypeDefault(descriptor!(List<Graphic>)).to_edge().unwrap();
|
|
assert_eq!(edge.ty(), &record_source_type::<Graphic>());
|
|
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::<bool>());
|
|
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<Vector>)),
|
|
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::<f64>();
|
|
assert_eq!(TaggedValue::from_primitive_string("2.", &leveled), Some(TaggedValue::F64(2.)));
|
|
assert_eq!(TaggedValue::from_primitive_string("5", &record_source_type::<u32>()), Some(TaggedValue::U32(5)));
|
|
assert_eq!(TaggedValue::from_primitive_string("true", &record_source_type::<bool>()), Some(TaggedValue::Bool(true)));
|
|
}
|
|
}
|