Merge origin/master into the async record refactor

Scaffolding merge for the reconcile; the final series to master is
authored fresh. Rank plumbing resolves to our axis-IR model, the node
macro and the LaneSource render walk stay ours, master's vector
restructure and gradient vocabulary are adopted, and the paint and
appearance adoption is deliberately deferred behind our fill and stroke
markers.
This commit is contained in:
Dennis Kobert
2026-09-08 15:03:57 +00:00
385 changed files with 34669 additions and 20078 deletions

View File

@@ -5,7 +5,7 @@ use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode};
use core_types::memo::MemoHashGuard;
pub use core_types::uuid::NodeId;
pub use core_types::uuid::generate_uuid;
use core_types::{Context, ContextDependencies, Cow, MemoHash, ProtoNodeIdentifier, Type};
use core_types::{Context, ContextDependencies, Cow, MemoHash, NodeParameter, ProtoNodeIdentifier, Type};
use dyn_any::DynAny;
use glam::IVec2;
use rustc_hash::FxHashMap;
@@ -121,6 +121,33 @@ impl OriginalLocation {
}
}
impl DocumentNode {
/// The input slot named by the given parameter symbol, e.g. `node.input(stroke::WeightInput)`.
pub fn input<P: NodeParameter>(&self, _parameter: P) -> Option<&NodeInput> {
self.inputs.get(P::INDEX)
}
/// Mutable access to the input slot named by the given parameter symbol.
pub fn input_mut<P: NodeParameter>(&mut self, _parameter: P) -> Option<&mut NodeInput> {
self.inputs.get_mut(P::INDEX)
}
/// The stored value of the given parameter, if that input currently holds a value rather than a wire.
pub fn input_value<P: NodeParameter>(&self, parameter: P) -> Option<&TaggedValue> {
self.input(parameter)?.as_value()
}
/// Normalizes this node's stored types (call argument, `Import` input types, `TypeDefault` value payloads, and any nested network) to their structural form.
/// Applied once at ingestion (document migration and clipboard paste) so no name-encoded ranked type enters a live document.
pub fn normalize_stored_types(&mut self) {
self.call_argument = self.call_argument.clone().normalize_rank();
for input in &mut self.inputs {
normalize_input_stored_type(input);
}
if let Some(network) = self.implementation.get_network_mut() {
network.normalize_stored_types();
}
}
/// Locate the input that is a [`NodeInput::Import`] at index `offset` and replace it with a [`NodeInput::Node`].
pub fn populate_first_network_input(&mut self, node_id: NodeId, output_index: usize, offset: usize, source: impl Iterator<Item = Source>, skip: usize) {
let (index, _) = self
@@ -239,10 +266,10 @@ impl NodeInput {
Self::Value { tagged_value, exposed }
}
/// Constructs a `NodeInput::Value` whose tagged value is `TaggedValue::TypeDefault(td)`, recording only the
/// Constructs a `NodeInput::Value` whose tagged value is `TaggedValue::TypeDefault(ty)`, recording only the
/// type so the runtime materializes its default rather than baking a placeholder value into the saved document.
pub fn type_default(td: core_types::TypeDescriptor, exposed: bool) -> Self {
Self::value(TaggedValue::TypeDefault(td), exposed)
pub fn type_default(ty: Type, exposed: bool) -> Self {
Self::value(TaggedValue::TypeDefault(ty.normalize_rank()), exposed)
}
pub const fn import(import_type: Type, import_index: usize) -> Self {
@@ -274,6 +301,7 @@ impl NodeInput {
match self {
NodeInput::Node { .. } => unreachable!("ty() called on NodeInput::Node"),
NodeInput::Value { tagged_value, .. } => tagged_value.ty(),
// Stored import types are normalized to their structural form once at document migration
NodeInput::Import { import_type, .. } => import_type.clone(),
NodeInput::Inline(_) => panic!("ty() called on NodeInput::Inline"),
NodeInput::Scope(_) => panic!("ty() called on NodeInput::Scope"),
@@ -727,7 +755,33 @@ impl ScopeChain<'_> {
}
/// Functions for compiling the network
/// Normalizes the ranked types an input can store: an `Import`'s type or a value's `TypeDefault` payload.
fn normalize_input_stored_type(input: &mut NodeInput) {
match input {
NodeInput::Import { import_type, .. } => *import_type = import_type.clone().normalize_rank(),
NodeInput::Value { tagged_value, .. } => {
if let TaggedValue::TypeDefault(ty) = &**tagged_value {
let normalized = ty.clone().normalize_rank();
if normalized != *ty {
*tagged_value = TaggedValue::TypeDefault(normalized).into();
}
}
}
_ => {}
}
}
impl NodeNetwork {
/// Normalizes every stored type in the network (exports and each node's types) to the structural form, recursively.
pub fn normalize_stored_types(&mut self) {
for export in &mut self.exports {
normalize_input_stored_type(export);
}
for node in self.nodes.values_mut() {
node.normalize_stored_types();
}
}
/// Replace all references in the graph of a node ID with a new node ID defined by the function `f`.
pub fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId + Copy) {
self.exports.iter_mut().for_each(|output| {

View File

@@ -2,7 +2,7 @@ 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 brush_nodes::brush_stroke::Stroke;
use core_types::color::SRGBA8;
use core_types::context::Context;
use core_types::gpoll::GPoll;
@@ -16,8 +16,11 @@ use dyn_any::DynAny;
pub use dyn_any::StaticType;
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphene_application_io::resource::ResourceHash;
use graphene_application_io::resource::ResourceId;
use graphic_types::raster_types::{CPU, Image, Raster};
use graphic_types::vector_types::vector::style::GradientStops;
use graphic_types::vector_types::vector::misc::BoxCorners;
use graphic_types::vector_types::vector::style::DashPattern;
use graphic_types::vector_types::vector::style::{Gradient, GradientRamp};
use graphic_types::vector_types::vector::{self, ReferencePoint};
use graphic_types::{Artboard, Graphic, Vector};
use rendering::RenderMetadata;
@@ -26,20 +29,42 @@ 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.
/// Item-cell element types routed through [`TaggedValue::TypeDefault`] instead of another dedicated variant, stored as the concrete `Item<T>` wire type.
/// 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 {
macro_rules! for_each_item_type_default {
($action:ident) => {
$action!(Vector);
$action!(f64);
$action!(Raster<CPU>);
$action!(Graphic);
$action!(Color);
$action!(Gradient);
$action!(Artboard);
$action!(String);
};
}
/// List element types routed through [`TaggedValue::TypeDefault`], stored as the structural [`Type::List`] form.
/// `List<f64>` is absent because it stores as `TaggedValue::F64Array`.
macro_rules! for_each_list_type_default {
($action:ident) => {
$action!(Graphic);
$action!(Artboard);
$action!(Raster<CPU>);
$action!(Vector);
$action!(String);
$action!(Color);
$action!(Gradient);
};
}
/// Unranked types routed through [`TaggedValue::TypeDefault`], stored as their concrete type.
macro_rules! for_each_bare_type_default {
($action:ident) => {
$action!(List<Graphic>);
$action!(List<Artboard>);
$action!(List<Raster<CPU>>);
$action!(List<Vector>);
$action!(List<String>);
$action!(DocumentNode);
$action!(Resource);
};
@@ -57,25 +82,23 @@ macro_rules! tagged_value {
// ===============
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),
/// Example: `TaggedValue::TypeDefault(concrete!(String))` stores the type `String` but no specific string value.
/// (Old documents stored a bare `TypeDescriptor` payload, routed to this shape by `deserialize_tagged_value_with_legacy_migration`.)
TypeDefault(Type),
/// 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(deserialize_with = "core_types::misc::migrate_to_f64_array")] // TODO: Eventually remove this 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 `GradientStops`, materializes as a single-row `List<GradientStops>` 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_stops")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")]
Gradient(GradientStops),
/// 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>),
/// Stored compactly as a `Vec<f64>` of dash lengths, materializes as an `Item<DashPattern>` at runtime via `to_dynany`/`to_any`.
DashPattern(Vec<f64>),
/// Stored compactly as a `Vec<f64>` of corner values, materializes as an `Item<BoxCorners>` at runtime via `to_dynany`/`to_any`.
BoxCorners(Vec<f64>),
/// Stored as the `GradientRamp` exchange struct (nested `{ stops: { color, position?, midpoint? } }`), materializing as an `Item<Gradient>` at runtime. Aliases recover legacy on-disk shapes.
/// (Old documents stored flat stops, a tuple list, or the ancient full `Gradient` struct under the legacy `"Gradient"` tag, all routed by `deserialize_tagged_value_with_legacy_migration`.)
#[serde(alias = "Gradient", alias = "GradientTable", alias = "GradientPositions", alias = "Gradient")]
GradientRamp(GradientRamp),
Strokes(Vec<Stroke>),
BrushCache(BrushCache),
// =======================
// AUTO-GENERATED VARIANTS
// =======================
@@ -85,10 +108,10 @@ macro_rules! tagged_value {
// =======================
#[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.
/// Path to the consumer of a `NodeInput::Reflection(DocumentNodePath)`. Materializes an `Item<NodeIdPath>` at runtime via `to_dynany`/`to_any` during graph flattening, matching the ranked connectors it feeds.
#[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))`.
NodeIdPath(NodeIdPath),
/// The `DocumentNode` value carried by an `Extract` proto node, populated at flatten time by `resolve_extract_nodes`. The on-disk placeholder uses `TypeDefault(concrete!(DocumentNode))`.
#[serde(skip)]
DocumentNode(DocumentNode),
/// Carried by context nullification proto nodes constructed at proto node compilation time in `insert_context_nullification_nodes`.
@@ -115,13 +138,15 @@ macro_rules! tagged_value {
// =======================
$( 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),
Self::DashPattern(lengths) => lengths.cache_hash(state),
Self::BoxCorners(values) => values.cache_hash(state),
Self::GradientRamp(ramp) => ramp.cache_hash(state),
Self::Strokes(strokes) => strokes.cache_hash(state),
Self::BrushCache(cache) => cache.cache_hash(state),
// =======================
// NON-SERIALIZED VARIANTS
// =======================
Self::NodeIdPath(path) => path.hash(state),
Self::NodeIdPath(path) => path.cache_hash(state),
Self::DocumentNode(node) => node.cache_hash(state),
Self::ContextModification(modification) => modification.cache_hash(state),
Self::RenderOutput(x) => x.cache_hash(state),
@@ -149,26 +174,24 @@ macro_rules! tagged_value {
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::from_type_or_none(&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::<GradientStops>::new_from_element(stops)),
Self::BrushStrokes(strokes) => {
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
Self::DashPattern(lengths) => Box::new(Item::new_from_element(DashPattern::from(lengths))),
Self::BoxCorners(values) => Box::new(Item::new_from_element(BoxCorners::from(values))),
Self::GradientRamp(ramp) => Box::new(Item::<Gradient>::from(ramp)),
Self::Strokes(strokes) => {
let list: List<Stroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
Box::new(list)
}
Self::BrushCache(cache) => Box::new(Item::new_from_element(cache)),
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( Self::$identifier(x) => Box::new(x), )*
$( Self::$identifier(x) => Box::new(Item::new_from_element(x)), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
@@ -177,7 +200,7 @@ macro_rules! tagged_value {
Self::DocumentNode(node) => Box::new(node),
Self::ContextModification(modification) => Box::new(modification),
Self::EditorApi(x) => Box::new(x),
Self::ResourceHash(x) => Box::new(x),
Self::ResourceHash(x) => Box::new(Item::new_from_element(x)),
}
}
@@ -196,26 +219,24 @@ macro_rules! tagged_value {
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::from_type_or_none(&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::<GradientStops>::new_from_element(stops)),
Self::BrushStrokes(strokes) => {
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
Self::DashPattern(lengths) => Arc::new(Item::new_from_element(DashPattern::from(lengths))),
Self::BoxCorners(values) => Arc::new(Item::new_from_element(BoxCorners::from(values))),
Self::GradientRamp(ramp) => Arc::new(Item::<Gradient>::from(ramp)),
Self::Strokes(strokes) => {
let list: List<Stroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
Arc::new(list)
}
Self::BrushCache(cache) => Arc::new(Item::new_from_element(cache)),
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( Self::$identifier(x) => Arc::new(x), )*
$( Self::$identifier(x) => Arc::new(Item::new_from_element(x)), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
@@ -224,13 +245,13 @@ macro_rules! tagged_value {
Self::DocumentNode(node) => Arc::new(node),
Self::ContextModification(modification) => Arc::new(modification),
Self::EditorApi(x) => Arc::new(x),
Self::ResourceHash(x) => Arc::new(x),
Self::ResourceHash(x) => Arc::new(Item::new_from_element(x)),
}
}
/// Creates a core_types::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value
/// Creates the wire [`Type`] of the value inside the tagged value, with ranked types in their structural form.
pub fn ty(&self) -> Type {
match self {
let ty = match self {
// ===============
// MANUAL VARIANTS
// ===============
@@ -248,12 +269,12 @@ macro_rules! tagged_value {
}
Self::F64Array(_) => concrete!(f64),
Self::Color(_) => concrete!(Color),
Self::Gradient(_) => concrete!(GradientStops),
Self::BrushStrokes(_) => concrete!(BrushStroke),
Self::GradientRamp(_) => concrete!(Gradient),
Self::Strokes(_) => concrete!(Stroke),
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( Self::$identifier(_) => concrete!($ty), )*
$( Self::$identifier(_) => item!($ty), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
@@ -297,8 +318,8 @@ macro_rules! tagged_value {
}
Self::F64Array(_) => leveled::<f64>(),
Self::Color(_) => leveled::<Color>(),
Self::Gradient(_) => leveled::<GradientStops>(),
Self::BrushStrokes(_) => leveled::<BrushStroke>(),
Self::GradientRamp(_) => leveled::<Gradient>(),
Self::Strokes(_) => leveled::<Stroke>(),
$( Self::$identifier(_) => scalar::<$ty>(), )*
Self::RenderOutput(_) => scalar::<RenderOutput>(),
Self::NodeIdPath(_) => scalar::<Vec<NodeId>>(),
@@ -339,9 +360,9 @@ macro_rules! tagged_value {
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)),
Self::Color(color) => Ok(record_value_source(color)),
Self::GradientRamp(stops) => Ok(leveled_record_value_source(vec![stops])),
Self::Strokes(strokes) => Ok(leveled_record_value_source(strokes)),
// =======================
// AUTO-GENERATED VARIANTS
// =======================
@@ -394,15 +415,27 @@ macro_rules! tagged_value {
// ===============
// MANUAL VARIANTS
// ===============
// The manual variants convert from both their payload and wire forms, with the newtypes flattening to their stored `Vec<f64>` form
x if x == TypeId::of::<()>() => Ok(TaggedValue::None),
x if x == TypeId::of::<Vec<f64>>() => Ok(TaggedValue::F64Array(*downcast(input).unwrap())),
x if x == TypeId::of::<List<f64>>() => Ok(TaggedValue::F64Array(downcast::<List<f64>>(input).unwrap().iter_element_values().copied().collect())),
x if x == TypeId::of::<DashPattern>() => Ok(TaggedValue::DashPattern(downcast::<DashPattern>(input).unwrap().0.iter_element_values().copied().collect())),
x if x == TypeId::of::<Item<DashPattern>>() => Ok(TaggedValue::DashPattern(downcast::<Item<DashPattern>>(input).unwrap().into_element().0.iter_element_values().copied().collect())),
x if x == TypeId::of::<BoxCorners>() => Ok(TaggedValue::BoxCorners(downcast::<BoxCorners>(input).unwrap().0.iter_element_values().copied().collect())),
x if x == TypeId::of::<Item<BoxCorners>>() => Ok(TaggedValue::BoxCorners(downcast::<Item<BoxCorners>>(input).unwrap().into_element().0.iter_element_values().copied().collect())),
x if x == TypeId::of::<Gradient>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(*downcast::<Gradient>(input).unwrap()))),
x if x == TypeId::of::<Item<Gradient>>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(&*downcast::<Item<Gradient>>(input).unwrap()))),
x if x == TypeId::of::<List<Stroke>>() => Ok(TaggedValue::Strokes(downcast::<List<Stroke>>(input).unwrap().into_iter().map(Item::into_element).collect())),
x if x == TypeId::of::<Item<BrushCache>>() => Ok(TaggedValue::BrushCache(downcast::<Item<BrushCache>>(input).unwrap().into_element())),
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )*
$( x if x == TypeId::of::<Item<$ty>>() => Ok(TaggedValue::$identifier(downcast::<Item<$ty>>(input).unwrap().into_element())), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())),
x if x == TypeId::of::<Item<RenderOutput>>() => Ok(TaggedValue::RenderOutput(downcast::<Item<RenderOutput>>(input).unwrap().into_element())),
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
}
@@ -416,42 +449,53 @@ macro_rules! tagged_value {
// ===============
// MANUAL VARIANTS
// ===============
// The manual variants convert from both their payload and wire forms, with the newtypes flattening to their stored `Vec<f64>` form
x if x == TypeId::of::<()>() => Ok(TaggedValue::None),
x if x == TypeId::of::<Vec<f64>>() => Ok(TaggedValue::F64Array(input.downcast_ref::<Vec<f64>>().unwrap().clone())),
x if x == TypeId::of::<List<f64>>() => Ok(TaggedValue::F64Array(input.downcast_ref::<List<f64>>().unwrap().iter_element_values().copied().collect())),
x if x == TypeId::of::<DashPattern>() => Ok(TaggedValue::DashPattern(input.downcast_ref::<DashPattern>().unwrap().0.iter_element_values().copied().collect())),
x if x == TypeId::of::<Item<DashPattern>>() => Ok(TaggedValue::DashPattern(input.downcast_ref::<Item<DashPattern>>().unwrap().element().0.iter_element_values().copied().collect())),
x if x == TypeId::of::<BoxCorners>() => Ok(TaggedValue::BoxCorners(input.downcast_ref::<BoxCorners>().unwrap().0.iter_element_values().copied().collect())),
x if x == TypeId::of::<Item<BoxCorners>>() => Ok(TaggedValue::BoxCorners(input.downcast_ref::<Item<BoxCorners>>().unwrap().element().0.iter_element_values().copied().collect())),
x if x == TypeId::of::<Gradient>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(input.downcast_ref::<Gradient>().unwrap()))),
x if x == TypeId::of::<Item<Gradient>>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(input.downcast_ref::<Item<Gradient>>().unwrap()))),
x if x == TypeId::of::<List<Stroke>>() => Ok(TaggedValue::Strokes(input.downcast_ref::<List<Stroke>>().unwrap().iter_element_values().cloned().collect())),
x if x == TypeId::of::<Item<BrushCache>>() => Ok(TaggedValue::BrushCache(input.downcast_ref::<Item<BrushCache>>().unwrap().element().clone())),
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(<$ty as Clone>::clone(input.downcast_ref().unwrap()))), )*
$( x if x == TypeId::of::<Item<$ty>>() => Ok(TaggedValue::$identifier(Item::<$ty>::clone(input.downcast_ref().unwrap()).into_element())), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(RenderOutput::clone(input.downcast_ref().unwrap()))),
x if x == TypeId::of::<Item<RenderOutput>>() => Ok(TaggedValue::RenderOutput(Item::<RenderOutput>::clone(input.downcast_ref().unwrap()).into_element())),
_ => 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.
/// Dispatches by name for concrete types and structurally by element for ranked types, where the name
/// field is what round-trips through serde so it works even for types deserialized from disk.
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<GradientStops>>()) { return Some(TaggedValue::Gradient(GradientStops::default())) }
if name == core_types::normalize_type_name(std::any::type_name::<List<Color>>()) { return Some(TaggedValue::Color(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())) }
if name == core_types::normalize_type_name(std::any::type_name::<List<Stroke>>()) { 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::<GradientStops>()) { return Some(TaggedValue::Gradient(GradientStops::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::<Color>()) { return Some(TaggedValue::Color(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::<Stroke>()) { 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>>))) }
@@ -462,11 +506,37 @@ macro_rules! tagged_value {
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);
for_each_bare_type_default!(check_bare);
None
}
Type::Fn(_, output) => TaggedValue::from_type(output),
Type::Future(output) => TaggedValue::from_type(output),
// Element types with a dedicated variant use it directly (the variant's value is a rank-0 cell); the rest store the structural type
Type::Item(element) => TaggedValue::from_type(element).or_else(|| {
macro_rules! check {
($type_default:ty) => {
if **element == concrete!($type_default) { return Some(TaggedValue::TypeDefault(input.clone())); }
};
}
for_each_item_type_default!(check);
None
}),
// Structural lists match by element; `List<f64>` stays the dedicated `F64Array` variant
Type::List(element) => {
if **element == concrete!(f64) {
return Some(TaggedValue::F64Array(Vec::new()));
}
if **element == concrete!(Stroke) {
return Some(TaggedValue::Strokes(Vec::new()));
}
macro_rules! check {
($type_default:ty) => {
if **element == concrete!($type_default) { return Some(TaggedValue::TypeDefault(input.clone())); }
};
}
for_each_list_type_default!(check);
None
}
}
}
@@ -480,11 +550,13 @@ macro_rules! tagged_value {
// MANUAL VARIANTS
// ===============
Self::None => "()".to_string(),
Self::TypeDefault(td) => format!("TypeDefault({})", td.name),
Self::TypeDefault(td) => format!("TypeDefault({td})"),
Self::F64Array(values) => format!("F64Array({values:?})"),
Self::Color(color) => format!("Color({color:?})"),
Self::Gradient(stops) => format!("Gradient({stops:?})"),
Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"),
Self::DashPattern(lengths) => format!("DashPattern({lengths:?})"),
Self::BoxCorners(values) => format!("BoxCorners({values:?})"),
Self::GradientRamp(ramp) => format!("GradientRamp({ramp:?})"),
Self::Strokes(strokes) => format!("Strokes({strokes:?})"),
Self::BrushCache(cache) => format!("{cache:?}"),
// =======================
// AUTO-GENERATED VARIANTS
// =======================
@@ -538,19 +610,24 @@ tagged_value! {
DVec2(DVec2),
#[serde(alias = "Affine2")]
DAffine2(DAffine2),
OptionalDAffine2(Option<DAffine2>),
#[serde(alias = "FillGradient")]
LegacyGradient(graphic_types::migrations::legacy::Gradient),
/// A plain, always-present color. Aliases recover legacy on-disk shapes; a legacy `null` payload (the old "no color")
/// is routed to [`TaggedValue::no_paint`] by `deserialize_tagged_value_with_legacy_migration`.
#[serde(deserialize_with = "core_types::misc::migrate_to_color")] // TODO: Eventually remove this document upgrade code
#[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")]
Color(Color),
Font(Font),
Footprint(Footprint),
VectorModification(Box<VectorModification>),
ImageData(Image<Color>),
Resource(graphene_application_io::resource::ResourceId),
Resource(ResourceId),
// Legacy
#[serde(alias = "OptionalDAffine2")]
LegacyOptionalDAffine2(Option<DAffine2>),
#[serde(alias = "FillGradient")]
LegacyGradient(graphic_types::migrations::legacy::LegacyGradient),
// ==========
// ENUM TYPES
// ==========
#[serde(alias = "Fill")]
LegacyFill(graphic_types::migrations::legacy::Fill),
BlendMode(core_types::blending::BlendMode),
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
@@ -579,14 +656,22 @@ tagged_value! {
#[serde(alias = "LineJoin")]
StrokeJoin(vector::style::StrokeJoin),
StrokeAlign(vector::style::StrokeAlign),
PaintOrder(vector::style::PaintOrder),
GradientType(vector::style::GradientType),
GradientSpreadMethod(vector::style::GradientSpreadMethod),
#[serde(alias = "GradientType")] // TODO: Eventually remove this document upgrade code
GradientForm(vector::style::GradientForm),
#[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code
GradientSpread(vector::style::GradientSpread),
GradientSpace(vector::style::GradientSpace),
GradientHueDirection(vector::style::GradientHueDirection),
GradientInterpolation(vector::style::GradientInterpolation),
ReferencePoint(vector::ReferencePoint),
CentroidType(vector::misc::CentroidType),
BooleanOperation(vector::misc::BooleanOperation),
TextAlign(text_nodes::TextAlign),
ScaleType(core_types::transform::ScaleType),
// Legacy
PaintOrder(vector::style::PaintOrder), // TODO: Eventually remove this document upgrade code
#[serde(alias = "Fill")]
LegacyFill(graphic_types::migrations::legacy::LegacyFill), // TODO: Eventually remove this document upgrade code
}
impl TaggedValue {
@@ -648,32 +733,16 @@ impl TaggedValue {
None
}
fn to_gradient(input: &str) -> Option<GradientStops> {
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(GradientStops::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(GradientStops::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
match stops.len() {
0 => {
log::error!("Invalid default value gradient string: {input}");
None
}
1 => Some(Gradient::from(vec![stops[0], stops[0]])),
_ => Some(Gradient::from(stops)),
}
}
@@ -710,7 +779,6 @@ impl TaggedValue {
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,
@@ -721,19 +789,21 @@ impl TaggedValue {
() 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<GradientStops>>() => to_gradient(string).map(TaggedValue::Gradient)?,
() if ty == TypeId::of::<Color>() => to_color(string).map(TaggedValue::Color)?,
// The Fill/Stroke paint wires carry `Graphic` or `Gradient` elements, so a paint default parses through the element recursion as a color or gradient literal
() if ty == TypeId::of::<Graphic>() => to_color(string).map(TaggedValue::Color)?,
() if ty == TypeId::of::<Gradient>() => to_gradient(string).map(|gradient| TaggedValue::GradientRamp(gradient.into()))?,
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
() if ty == TypeId::of::<DashPattern>() => TaggedValue::DashPattern(core_types::misc::parse_f64_list(string)),
() if ty == TypeId::of::<BoxCorners>() => TaggedValue::BoxCorners(core_types::misc::parse_f64_list(string)),
_ => return None,
};
Some(ty)
}
Type::Fn(_, output) => TaggedValue::from_primitive_string(string, output),
Type::Future(fut) => TaggedValue::from_primitive_string(string, fut),
Type::Item(element) => TaggedValue::from_primitive_string(string, element),
Type::List(element) => TaggedValue::from_primitive_string(string, element),
}
}
@@ -743,24 +813,36 @@ impl TaggedValue {
_ => panic!("Passed value is not of type u32"),
}
}
/// The stored form of a paint input's red-slash "no paint" choice: the `Item<Graphic>` type default, materializing as a `Graphic::None` paint.
pub fn no_paint() -> Self {
TaggedValue::TypeDefault(item!(Graphic))
}
/// Whether this is the `Item<Graphic>` type default created by [`Self::no_paint`] (and by disconnecting a paint wire).
pub fn is_no_paint(&self) -> bool {
matches!(self, TaggedValue::TypeDefault(td) if *td == item!(Graphic))
}
}
/// 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>))`
/// - `Graphic` (or alias `GraphicGroup`/`Group`) → `TaggedValue::TypeDefault(list!(Graphic))`
/// - `Artboard` (or alias `ArtboardGroup`) → `TaggedValue::TypeDefault(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>>))`
/// - empty → `TaggedValue::TypeDefault(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>))`
/// - empty → `TaggedValue::TypeDefault(list!(Vector))`
/// - `FillChoice` → `TaggedValue::Color` (solid), `TaggedValue::GradientRamp` (gradient), or `TaggedValue::no_paint()` (none)
/// - `Gradient` (or alias `GradientTable`/`GradientPositions`/`Gradient`) → `TaggedValue::LegacyGradient` (ancient full struct) or `TaggedValue::GradientRamp` (ramp and legacy stops shapes, unwrapped from the legacy table form)
/// - `TypeDefault` with the old bare-`TypeDescriptor` payload → the same variant wrapping a `Type` (name-encoded `List` normalized to structural)
///
/// 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
// TODO: Eventually remove this 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;
@@ -771,9 +853,8 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
&& 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>)))),
"Graphic" | "GraphicGroup" | "Group" => return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Graphic)))),
"Artboard" | "ArtboardGroup" => return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Artboard)))),
"Raster" | "ImageFrame" | "RasterData" | "Image" => {
let first_element = content
.as_object()
@@ -784,7 +865,7 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
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>>))));
return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Raster<CPU>))));
}
"Vector" | "VectorData" => {
let vector = graphic_types::migrations::migrate_to_optional_vector(content.clone()).map_err(serde::de::Error::custom)?;
@@ -792,13 +873,66 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
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>))));
return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Vector))));
}
// The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `LegacyGradient`), and now carries an `Option<GradientStops>`.
// Disambiguate by payload shape: a Gradient struct has `start`/`end` keys; a `GradientStops` 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::Gradient = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;
return Ok(MemoHash::new(TaggedValue::LegacyGradient(gradient)));
// The `TypeDefault` payload used to be a bare `TypeDescriptor`; it now carries a `Type`
"TypeDefault" if content.as_object().is_some_and(|c| c.contains_key("name")) => {
let descriptor: TypeDescriptor = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;
return Ok(MemoHash::new(TaggedValue::TypeDefault(Type::Concrete(descriptor).normalize_rank())));
}
// The `Color` tag used to carry `Option<Color>`, where a `null` payload (or an empty legacy color table) was the red-slash "no paint" choice
"Color" | "ColorTable" | "OptionalColor" | "ColorNotInTable"
if content.is_null()
|| 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())
.is_some_and(|colors| colors.is_empty()) =>
{
return Ok(MemoHash::new(TaggedValue::no_paint()));
}
// The removed `FillChoice` variant decomposes into the plain paint values
"FillChoice" => {
if let Some(payload) = content.as_object() {
if let Some(solid) = payload.get("Solid") {
let color: Color = serde_json::from_value(solid.clone()).map_err(serde::de::Error::custom)?;
return Ok(MemoHash::new(TaggedValue::Color(color)));
}
if let Some(gradient) = payload.get("Gradient") {
let ramp = graphic_types::migrations::migrate_to_gradient_ramp(gradient.clone()).map_err(serde::de::Error::custom)?;
return Ok(MemoHash::new(TaggedValue::GradientRamp(ramp)));
}
}
return Ok(MemoHash::new(TaggedValue::no_paint()));
}
// The gradient tags carried several shapes over time, disambiguated here: the ancient full struct (`start`/`end` keys) becomes `LegacyGradient`,
// while the current ramp, the flat stops struct, the old tuple list, and the legacy one-element table wrapper all parse as the ramp value directly
"Gradient" | "GradientTable" | "GradientPositions" | "Gradient" => {
let table_element = content
.as_object()
.and_then(|c| c.get("element").or_else(|| c.get("instance")).or_else(|| c.get("instances")))
.and_then(|element| element.as_array());
// An empty legacy table wrapper carries no gradient, degrading to the default (in the era's gamma) rather than failing the document load
if let Some(array) = table_element
&& array.is_empty()
{
let ramp = GradientRamp {
gradient_space: vector::style::GradientSpace::RgbGamma,
..Default::default()
};
return Ok(MemoHash::new(TaggedValue::GradientRamp(ramp)));
}
let payload = table_element.and_then(|array| array.first()).unwrap_or(content);
if payload.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(payload.clone()).map_err(serde::de::Error::custom)?;
return Ok(MemoHash::new(TaggedValue::LegacyGradient(gradient)));
}
let ramp = graphic_types::migrations::migrate_to_gradient_ramp(payload.clone()).map_err(serde::de::Error::custom)?;
return Ok(MemoHash::new(TaggedValue::GradientRamp(ramp)));
}
_ => {}
}
@@ -883,18 +1017,18 @@ impl CacheHash for RenderOutput {
#[cfg(test)]
mod typedefault_dispatch {
use super::*;
use core_types::descriptor;
use core_types::{concrete, item, list};
/// 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.
/// Round-trips every type in the type-default lists through `TaggedValue::TypeDefault → to_dynany / to_any` and asserts the resulting concrete type matches the stored type.
///
/// 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);
($type_default:ty, $stored:expr) => {{
let ty: Type = $stored;
let expected_type_id = std::any::TypeId::of::<$type_default>();
let dyn_value = TaggedValue::TypeDefault(descriptor.clone()).to_dynany();
let dyn_value = TaggedValue::TypeDefault(ty.clone()).to_dynany();
assert_eq!(
DynAny::type_id(&*dyn_value),
expected_type_id,
@@ -902,7 +1036,7 @@ mod typedefault_dispatch {
core_types::normalize_type_name(std::any::type_name::<$type_default>()),
);
let arc_value = TaggedValue::TypeDefault(descriptor).to_any();
let arc_value = TaggedValue::TypeDefault(ty).to_any();
assert_eq!(
(*arc_value).type_id(),
expected_type_id,
@@ -911,7 +1045,160 @@ mod typedefault_dispatch {
);
}};
}
for_each_type_default!(check);
macro_rules! check_item {
($element:ty) => {
check!(Item<$element>, item!($element));
};
}
macro_rules! check_list {
($element:ty) => {
check!(List<$element>, list!($element));
};
}
macro_rules! check_bare {
($type_default:ty) => {
check!($type_default, concrete!($type_default));
};
}
for_each_item_type_default!(check_item);
for_each_list_type_default!(check_list);
for_each_bare_type_default!(check_bare);
}
}
#[cfg(test)]
mod paint_default_parsing {
use super::*;
use core_types::{item, list};
/// A Fill/Stroke paint wire carries `Graphic` elements, so its `Color::BLACK` default must parse through the
/// element recursion into a `Color` for a fresh Fill node's paint to resolve.
#[test]
fn paint_wire_parses_color_default_through_its_element() {
let black = Some(TaggedValue::Color(Color::BLACK));
assert_eq!(
TaggedValue::from_primitive_string("Color::BLACK", &list!(Graphic)),
black,
"a `List<Graphic>` paint wire should resolve its color default"
);
assert_eq!(
TaggedValue::from_primitive_string("Color::BLACK", &item!(Graphic)),
black,
"an `Item<Graphic>` paint wire should resolve its color default"
);
}
/// Table-era documents stored the red-slash "no paint" fill as an empty color table, which must keep
/// deserializing to [`TaggedValue::no_paint`] rather than collapsing to a transparent color.
#[test]
fn empty_legacy_color_table_deserializes_to_no_paint() {
for payload in [r#"{"ColorTable": {"instances": []}}"#, r#"{"ColorTable": {"element": []}}"#, r#"{"Color": null}"#] {
let mut deserializer = serde_json::Deserializer::from_str(payload);
let value = deserialize_tagged_value_with_legacy_migration(&mut deserializer).expect("The legacy payload should deserialize");
assert!(value.is_no_paint(), "The legacy payload `{payload}` should migrate to the no-paint choice");
}
}
}
#[cfg(test)]
mod gradient_shape_migration {
use graphic_types::vector_types::{GradientSpace, GradientSpread};
use super::*;
fn load(payload: serde_json::Value) -> TaggedValue {
deserialize_tagged_value_with_legacy_migration(payload)
.expect("The gradient payload should deserialize")
.into_inner()
.as_ref()
.clone()
}
fn white() -> serde_json::Value {
serde_json::to_value(Color::WHITE).unwrap()
}
#[test]
fn modern_ramp_payload_round_trips() {
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
gradient.set_positions(&[0.2, 0.9]);
let value = TaggedValue::GradientRamp(GradientRamp {
gradient_spread: GradientSpread::Reflect,
..GradientRamp::from(gradient)
});
let json = serde_json::to_value(&value).unwrap();
assert!(json.get("GradientRamp").and_then(|payload| payload.get("stops")).is_some(), "the payload should nest its stops: {json}");
assert_eq!(
json.get("GradientRamp").and_then(|payload| payload.get("gradient_space")),
Some(&serde_json::json!("OkLab")),
"the space should serialize even at its default, marking the ramp as post-legacy: {json}"
);
assert_eq!(load(json), value);
}
// TODO: Eventually remove this document upgrade code
#[test]
fn ramp_without_space_field_reads_as_legacy_gamma() {
let json = serde_json::json!({ "GradientRamp": { "stops": { "color": [white(), white()] } } });
let TaggedValue::GradientRamp(ramp) = load(json) else {
panic!("the ramp payload should become a gradient ramp value")
};
assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "a ramp saved before the field existed should read as gamma");
}
// TODO: Eventually remove this document upgrade code
#[test]
fn legacy_flat_stops_parse_faithfully() {
let json = serde_json::json!({ "Gradient": { "color": [white(), white()], "position": [0., 0.25], "midpoint": [0.5, 0.5] } });
let TaggedValue::GradientRamp(ramp) = load(json) else {
panic!("the flat stops should become a gradient ramp value")
};
assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "the pre-ramp flat form should carry the era's gamma");
let gradient = Gradient::from(ramp);
assert_eq!(gradient.positions(false), vec![0., 0.25]);
assert!(gradient.has_midpoint_attribute(), "the flat form must parse faithfully");
}
// TODO: Eventually remove this document upgrade code
#[test]
fn legacy_tuple_stops_parse_with_defaults_elided() {
let json = serde_json::json!({ "Gradient": [[0., white()], [1., white()]] });
let TaggedValue::GradientRamp(ramp) = load(json) else {
panic!("the tuple stops should become a gradient ramp value")
};
assert_eq!(ramp.gradient_space, GradientSpace::RgbGamma, "the pre-ramp tuple form should carry the era's gamma");
let gradient = Gradient::from(ramp);
assert_eq!(gradient.positions(false), vec![0., 1.]);
assert!(!gradient.has_position_attribute(), "even legacy tuple positions should elide");
}
// TODO: Eventually remove this document upgrade code
#[test]
fn empty_legacy_gradient_table_degrades_to_the_default() {
let json = serde_json::json!({ "GradientTable": { "element": [] } });
let expected = GradientRamp {
gradient_space: GradientSpace::RgbGamma,
..Default::default()
};
assert_eq!(load(json), TaggedValue::GradientRamp(expected));
}
// TODO: Eventually remove this document upgrade code
#[test]
fn ancient_full_struct_routes_to_legacy_gradient() {
let json = serde_json::json!({ "Gradient": { "stops": [[0., white()], [1., white()]], "gradient_type": "Linear", "start": [0., 0.], "end": [1., 0.] } });
let TaggedValue::LegacyGradient(legacy) = load(json) else {
panic!("the ancient full struct should become a legacy gradient value")
};
assert_eq!(
Gradient::from(legacy.stops).positions(false),
vec![0., 1.],
"the nested tuple stops should parse through the field adapter"
);
}
}
@@ -927,7 +1214,7 @@ mod leveled_edges {
assert_eq!(edge.ty(), &record_source_type::<f64>());
assert_eq!(edge.layout().depth, 1);
let edge = TaggedValue::Color(Some(Color::default())).to_edge().unwrap();
let edge = TaggedValue::Color(Color::default()).to_edge().unwrap();
assert_eq!(edge.ty(), &record_source_type::<Color>());
assert_eq!(edge.layout().depth, 1);

View File

@@ -3,7 +3,7 @@ extern crate log;
#[macro_use]
extern crate core_types;
pub use core_types::{ProtoNodeIdentifier, Type, TypeDescriptor, concrete, descriptor, generic};
pub use core_types::{ProtoNodeIdentifier, Type, TypeDescriptor, concrete, descriptor, generic, item, list};
pub mod application_io;
pub mod document;

View File

@@ -839,6 +839,31 @@ pub struct TypingContext {
lookup: Cow<'static, Registry>,
inferred: HashMap<NodeId, NodeIOTypes>,
constructor: HashMap<NodeId, NodeConstructor>,
promotions: HashMap<NodeId, Vec<(usize, Promotion)>>,
}
/// A rank adapter which type resolution marks for insertion between a wire and a connector whose ranks differ,
/// carrying the element type the adapter is registered under.
#[derive(Debug, Clone, PartialEq)]
pub enum Promotion {
/// Raises an `Item<X>` wire onto a `List<X>` connector as a one-element list.
ItemToList(Type),
/// Bundles a whole `List<X>` wire into one opaque `Item<Bundle<X>>` cell.
Bundle(Type),
/// Unbundles an `Item<Bundle<X>>` wire back into the whole `List<X>`.
Unbundle(Type),
}
impl Promotion {
/// The registry identifier of the adapter node monomorphized for this promotion's element type.
pub fn adapter_identifier(&self) -> ProtoNodeIdentifier {
let (adapter_name, element) = match self {
Self::ItemToList(element) => ("graphene_core::ops::ItemToListNode", element),
Self::Bundle(element) => ("graphene_core::ops::BundleNode", element),
Self::Unbundle(element) => ("graphene_core::ops::UnbundleNode", element),
};
ProtoNodeIdentifier::with_owned_string(format!("{adapter_name}<{}>", element.identifier_name()))
}
}
impl TypingContext {
@@ -867,9 +892,20 @@ impl TypingContext {
pub fn remove_inference(&mut self, node_id: NodeId) -> Option<NodeIOTypes> {
self.constructor.remove(&node_id);
self.promotions.remove(&node_id);
self.inferred.remove(&node_id)
}
/// Returns the input positions of a node which type resolution marked for rank promotion, with each position's adapter.
pub fn promotions(&self, node_id: NodeId) -> Option<&Vec<(usize, Promotion)>> {
self.promotions.get(&node_id)
}
/// Looks up the sole constructor registered under an adapter identifier, such as an Item -> List promotion adapter.
pub fn adapter_constructor(&self, identifier: &ProtoNodeIdentifier) -> Option<NodeConstructor> {
self.lookup.get(identifier).and_then(|implementations| implementations.values().next().copied())
}
/// Returns the node constructor for a given node id.
pub fn constructor(&self, node_id: NodeId) -> Option<NodeConstructor> {
self.constructor.get(&node_id).copied()
@@ -1196,7 +1232,7 @@ mod test {
// If this assert fails: These NodeIds seem to be changing when you modify TaggedValue, just update them.
assert_eq!(
ids,
vec![NodeId(12815475172301479638), NodeId(13251389748338817266), NodeId(7166921994790432021), NodeId(15318519137317483318)]
vec![NodeId(12331852515109999872), NodeId(5084548161767585362), NodeId(14635346976242256925), NodeId(16015195863711239715)]
);
}