Remove serialization from Table<T> and make TaggedValue only store tooling/widget node inputs (#4129)

* Add TaggedValue::TypeDefault to avoid baking placeholder Tables into saved documents

* Add TaggedValue::TypeDefault to avoid baking placeholder Tables into saved documents

* Migrate empty Vector/Raster/Graphic/Artboard placeholder values to TypeDefault on load

Documents written before the TypeDefault mechanism existed have empty Table<Vector>/<Raster>/<Graphic>/<Artboard> values baked into every unwired exposed input. Walk each migrated node's inputs and rewrite any such placeholder NodeInput::Value into the equivalent NodeInput::type_default, so re-saved documents shed the placeholder payloads. Marked with a TODO for eventual removal once enough documents have been re-saved.

* Re-save demo artwork

* Remove Graphic and Artboard placeholder containers from TaggedValue

* Remove Raster placeholder TaggedValue variant

* Simplify document migration

* Remove Vector placeholder TaggedValue variant

* Remove NodeIdTable from the TaggedValue

* Remove StringTable from the TaggedValue

* Remove F64Table in place of F64Array in TaggedValue

* Replace TaggedValue::Color(Table<Color>) with ::Color(Option<Color>)

* Replace TaggedValue::GradientTable(Table<GradientStops>) with ::Gradient(GradientStops)

* Replace TaggedValue::BrushStrokeTable(Table<BrushStroke>) with ::BrushStrokes(Vec<BrushStroke>)

* Make TaggedValue::DocumentNode runtime-only with TypeDefault placeholder

* Make TaggedValue::ContextFeatures runtime-only

* Remove Serialize/Deserialize from Table<T>

* Add a widget for TaggedValue::BrushStrokes to visualize strokes and samples

* Define a reusable list of TaggedValue::TypeDefault types for its generated methods

* Re-save demo artwork
This commit is contained in:
Keavon Chambers
2026-05-08 16:11:25 -07:00
committed by GitHub
parent d97fe835b5
commit cb21e5960b
35 changed files with 615 additions and 937 deletions

View File

@@ -178,7 +178,11 @@ pub enum NodeInput {
Node { node_id: NodeId, output_index: usize },
/// A hardcoded value that can't change after the graph is compiled. Gets converted into a value node during graph compilation.
Value { tagged_value: MemoHash<TaggedValue>, exposed: bool },
Value {
#[cfg_attr(feature = "loading", serde(deserialize_with = "crate::document::value::deserialize_tagged_value_with_legacy_migration"))]
tagged_value: MemoHash<TaggedValue>,
exposed: bool,
},
// TODO: Remove import_type and get type from parent node input
/// Input that is provided by the import from the parent network to this document node network.
@@ -231,6 +235,12 @@ impl NodeInput {
Self::Value { tagged_value, exposed }
}
/// Constructs a `NodeInput::Value` whose tagged value is `TaggedValue::TypeDefault(td)`, 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 const fn import(import_type: Type, import_index: usize) -> Self {
Self::Import { import_type, import_index }
}
@@ -930,10 +940,7 @@ impl NodeNetwork {
let (tagged_value, exposed) = match previous_export {
NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed),
NodeInput::Reflection(reflect) => match reflect {
DocumentNodeMetadata::DocumentNodePath => {
let table: core_types::table::Table<NodeId> = path.iter().copied().map(core_types::table::TableRow::new_from_element).collect();
(TaggedValue::NodeIdTable(table).into(), false)
}
DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(path.to_vec()).into(), false),
},
previous_export => {
*export = previous_export;

View File

@@ -5,13 +5,12 @@ use brush_nodes::brush_stroke::BrushStroke;
use core_types::table::Table;
use core_types::transform::Footprint;
use core_types::uuid::NodeId;
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type};
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type, TypeDescriptor};
use dyn_any::DynAny;
pub use dyn_any::StaticType;
use glam::{Affine2, Vec2};
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphic_types::raster_types::{CPU, Image, Raster};
use graphic_types::vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
use graphic_types::vector_types::vector::style::{Fill, Gradient, GradientStops};
use graphic_types::vector_types::vector::{self, ReferencePoint};
use graphic_types::{Artboard, Graphic, Vector};
use rendering::RenderMetadata;
@@ -26,6 +25,19 @@ 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!(Table<Graphic>);
$action!(Table<Artboard>);
$action!(Table<Raster<CPU>>);
$action!(Table<Vector>);
$action!(Table<String>);
$action!(DocumentNode);
};
}
/// Macro to generate the tagged value enum.
macro_rules! tagged_value {
($ ($( #[$meta:meta] )* $identifier:ident ($ty:ty) ),* $(,)?) => {
@@ -33,9 +45,48 @@ macro_rules! tagged_value {
#[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 `Table<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 `Table<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 `Table<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 `FillGradient` 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")]
Gradient(GradientStops),
/// Stored compactly as a `Vec<BrushStroke>`, materializes as `Table<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 `Table<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)]
ContextFeatures(ContextFeatures),
#[serde(skip)]
EditorApi(Arc<PlatformEditorApi>),
}
@@ -44,8 +95,25 @@ macro_rules! tagged_value {
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::ContextFeatures(features) => features.cache_hash(state),
Self::RenderOutput(x) => x.cache_hash(state),
Self::EditorApi(x) => x.cache_hash(state),
}
@@ -56,85 +124,231 @@ macro_rules! tagged_value {
/// 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 == 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 table: Table<f64> = values.into_iter().map(core_types::table::TableRow::new_from_element).collect();
Box::new(table)
}
Self::Color(color) => {
let table: Table<Color> = color.into_iter().map(core_types::table::TableRow::new_from_element).collect();
Box::new(table)
}
Self::Gradient(stops) => Box::new(Table::<GradientStops>::new_from_element(stops)),
Self::BrushStrokes(strokes) => {
let table: Table<BrushStroke> = strokes.into_iter().map(core_types::table::TableRow::new_from_element).collect();
Box::new(table)
}
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( Self::$identifier(x) => Box::new(x), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
Self::RenderOutput(x) => Box::new(x),
Self::NodeIdPath(path) => {
let table: Table<NodeId> = path.into_iter().map(core_types::table::TableRow::new_from_element).collect();
Box::new(table)
}
Self::DocumentNode(node) => Box::new(node),
Self::ContextFeatures(features) => Box::new(features),
Self::EditorApi(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 == 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 table: Table<f64> = values.into_iter().map(core_types::table::TableRow::new_from_element).collect();
Arc::new(table)
}
Self::Color(color) => {
let table: Table<Color> = color.into_iter().map(core_types::table::TableRow::new_from_element).collect();
Arc::new(table)
}
Self::Gradient(stops) => Arc::new(Table::<GradientStops>::new_from_element(stops)),
Self::BrushStrokes(strokes) => {
let table: Table<BrushStroke> = strokes.into_iter().map(core_types::table::TableRow::new_from_element).collect();
Arc::new(table)
}
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( Self::$identifier(x) => Arc::new(x), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
Self::RenderOutput(x) => Arc::new(x),
Self::NodeIdPath(path) => {
let table: Table<NodeId> = path.into_iter().map(core_types::table::TableRow::new_from_element).collect();
Arc::new(table)
}
Self::DocumentNode(node) => Arc::new(node),
Self::ContextFeatures(features) => Arc::new(features),
Self::EditorApi(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!(()),
Self::TypeDefault(td) => Type::Concrete(td.clone()),
Self::F64Array(_) => concrete!(Table<f64>),
Self::Color(_) => concrete!(Table<Color>),
Self::Gradient(_) => concrete!(Table<GradientStops>),
Self::BrushStrokes(_) => concrete!(Table<BrushStroke>),
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( Self::$identifier(_) => concrete!($ty), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
Self::RenderOutput(_) => concrete!(RenderOutput),
Self::NodeIdPath(_) => concrete!(Table<NodeId>),
Self::DocumentNode(_) => concrete!(DocumentNode),
Self::ContextFeatures(_) => concrete!(ContextFeatures),
Self::EditorApi(_) => concrete!(&PlatformEditorApi),
}
}
/// 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()`
/// 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::Concrete(concrete_type) => {
use std::any::TypeId;
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.
Some(match concrete_type.id? {
x if x == TypeId::of::<()>() => TaggedValue::None,
// Table-wrapped types need a single-item default with the element's default, not an empty table
x if x == TypeId::of::<Table<Color>>() => TaggedValue::Color(Table::new_from_element(Color::default())),
x if x == TypeId::of::<Table<GradientStops>>() => TaggedValue::GradientTable(Table::new_from_element(GradientStops::default())),
$( x if x == TypeId::of::<$ty>() => TaggedValue::$identifier(Default::default()), )*
_ => return None,
})
if name == std::any::type_name::<()>() { return Some(TaggedValue::None) }
// Table-wrapped types need a single-item default with the element's default, not an empty table
if name == std::any::type_name::<Table<Color>>() { return Some(TaggedValue::Color(Some(Color::default()))) }
if name == std::any::type_name::<Table<GradientStops>>() { return Some(TaggedValue::Gradient(GradientStops::default())) }
$( if name == std::any::type_name::<$ty>() { return Some(TaggedValue::$identifier(Default::default())) } )*
if name == std::any::type_name::<Table<f64>>() { return Some(TaggedValue::F64Array(Vec::new())) }
if name == std::any::type_name::<Table<BrushStroke>>() { return Some(TaggedValue::BrushStrokes(Vec::new())) }
// 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 == 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)
}
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::ContextFeatures(features) => format!("ContextFeatures({features:?})"),
Self::EditorApi(_) => "PlatformEditorApi".to_string(),
}
}
@@ -163,54 +377,21 @@ macro_rules! tagged_value {
}
tagged_value! {
// ===========
// TABLE TYPES
// ===========
StringTable(Table<String>),
#[serde(deserialize_with = "core_types::misc::migrate_vec_f64_to_table")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "VecF64", alias = "VecF32", alias = "F64Array4")]
F64Table(Table<f64>),
NodeIdTable(Table<NodeId>),
#[serde(deserialize_with = "graphic_types::migrations::migrate_vector")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "VectorData")]
Vector(Table<Vector>),
#[serde(deserialize_with = "graphic_types::raster_types::image::migrate_image_frame")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ImageFrame", alias = "RasterData", alias = "Image")]
Raster(Table<Raster<CPU>>),
#[serde(deserialize_with = "graphic_types::graphic::migrate_graphic")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "GraphicGroup", alias = "Group")]
Graphic(Table<Graphic>),
#[serde(deserialize_with = "graphic_types::artboard::migrate_artboard")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ArtboardGroup")]
Artboard(Table<Artboard>),
#[serde(deserialize_with = "core_types::misc::migrate_color")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")]
Color(Table<Color>),
#[serde(deserialize_with = "graphic_types::vector_types::gradient::migrate_gradient_stops")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "GradientPositions", alias = "GradientStops")]
GradientTable(Table<GradientStops>),
#[serde(deserialize_with = "brush_nodes::migrations::migrate_brush_strokes_to_table")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "BrushStrokes")]
BrushStrokeTable(Table<BrushStroke>),
// ============
// SCALAR TYPES
// ============
// ===============
// PRIMITIVE TYPES
// ===============
F32(f32),
F64(f64),
U32(u32),
U64(u64),
Bool(bool),
String(String),
FVec2(Vec2),
FAffine2(Affine2),
#[serde(alias = "IVec2", alias = "UVec2")]
#[serde(alias = "IVec2", alias = "UVec2", alias = "Vec2")]
DVec2(DVec2),
#[serde(alias = "Affine2")]
DAffine2(DAffine2),
Stroke(Stroke),
Gradient(Gradient),
FillGradient(Gradient),
Font(Font),
DocumentNode(DocumentNode),
ContextFeatures(ContextFeatures),
Footprint(Footprint),
VectorModification(Box<VectorModification>),
ImageData(Image<Color>),
@@ -247,7 +428,6 @@ tagged_value! {
StrokeJoin(vector::style::StrokeJoin),
StrokeAlign(vector::style::StrokeAlign),
PaintOrder(vector::style::PaintOrder),
FillType(vector::style::FillType),
GradientType(vector::style::GradientType),
GradientSpreadMethod(vector::style::GradientSpreadMethod),
ReferencePoint(vector::ReferencePoint),
@@ -388,9 +568,9 @@ impl TaggedValue {
() 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 table) 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(Table::new_from_element(color)))?,
() if ty == TypeId::of::<Table<Color>>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?,
() if ty == TypeId::of::<Table<GradientStops>>() => to_gradient(string).map(|color| TaggedValue::GradientTable(Table::new_from_element(color)))?,
() if ty == TypeId::of::<Color>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
() if ty == TypeId::of::<Table<Color>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
() if ty == TypeId::of::<Table<GradientStops>>() => to_gradient(string).map(TaggedValue::Gradient)?,
() if ty == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
_ => return None,
@@ -408,35 +588,65 @@ impl TaggedValue {
_ => panic!("Passed value is not of type u32"),
}
}
}
/// Walks a JSON document tree and replaces any externally-tagged `TaggedValue` whose discriminant is in `REMOVED_VARIANTS` with the unit variant `"None"`.
/// Lets documents written before a variant was removed continue to deserialize. The document migration step then removes any orphan node inputs that result.
#[cfg(feature = "loading")]
pub fn scrub_removed_variants_from_json(value: &mut serde_json::Value) {
// Names of `TaggedValue` variants that have been removed since being released. Any object of the form `{"<name>": <payload>}` is rewritten to `"None"` on load.
const REMOVED_VARIANTS: &[&str] = &["BrushCache"];
/// 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!(Table<Graphic>))`
/// - `Artboard` (or alias `ArtboardGroup`) → `TaggedValue::TypeDefault(descriptor!(Table<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!(Table<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!(Table<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)?;
match value {
serde_json::Value::Object(map) => {
if map.len() == 1
&& let Some(key) = map.keys().next()
&& REMOVED_VARIANTS.contains(&key.as_str())
{
*value = serde_json::Value::String("None".to_string());
return;
}
for child in map.values_mut() {
Self::scrub_removed_variants_from_json(child);
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!(Table<Graphic>)))),
"Artboard" | "ArtboardGroup" => return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(Table<Artboard>)))),
"Raster" | "ImageFrame" | "RasterData" | "Image" => {
let first_element = content.as_object().and_then(|c| c.get("element")).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!(Table<Raster<CPU>>))));
}
serde_json::Value::Array(array) => {
for child in array {
Self::scrub_removed_variants_from_json(child);
"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!(Table<Vector>))));
}
// The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `FillGradient`), 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: Gradient = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;
return Ok(MemoHash::new(TaggedValue::FillGradient(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 {
@@ -544,34 +754,37 @@ impl CacheHash for RenderOutput {
}
}
#[cfg(all(test, feature = "loading"))]
mod tests {
#[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 scrub_replaces_removed_variant_with_none_unit() {
let mut value = serde_json::json!({
"Value": {
"tagged_value": { "BrushCache": { "unique_id": 1, "prev_input": [] } },
"exposed": false
}
});
TaggedValue::scrub_removed_variants_from_json(&mut value);
assert_eq!(value, serde_json::json!({ "Value": { "tagged_value": "None", "exposed": false } }));
}
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.",
std::any::type_name::<$type_default>(),
);
#[test]
fn scrub_leaves_live_variants_unchanged() {
let mut value = serde_json::json!({ "Value": { "tagged_value": { "F64": 1.5 }, "exposed": false } });
let original = value.clone();
TaggedValue::scrub_removed_variants_from_json(&mut value);
assert_eq!(value, original);
}
#[test]
fn scrub_recurses_through_arrays_and_nested_objects() {
let mut value = serde_json::json!([{ "BrushCache": { "any": "payload" } }, { "F32": 0.5 }]);
TaggedValue::scrub_removed_variants_from_json(&mut value);
assert_eq!(value, serde_json::json!(["None", { "F32": 0.5 }]));
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.",
std::any::type_name::<$type_default>(),
);
}};
}
for_each_type_default!(check);
}
}

View File

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

View File

@@ -951,7 +951,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(12189222519765806511), NodeId(15012204941197567462), NodeId(15525229164021892418), NodeId(1252248957706694248)]
vec![NodeId(12815475172301479638), NodeId(13251389748338817266), NodeId(7166921994790432021), NodeId(15318519137317483318)]
);
}

View File

@@ -179,7 +179,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::FillType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
@@ -273,7 +272,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::FillType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),

View File

@@ -61,47 +61,44 @@ impl Clampable for DVec2 {
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
struct LegacyTable<T> {
#[serde(alias = "instances", alias = "instance")]
element: Vec<T>,
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_color<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<crate::table::Table<no_std_types::color::Color>, D::Error> {
use crate::table::Table;
pub fn migrate_to_optional_color<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<no_std_types::color::Color>, D::Error> {
use no_std_types::color::Color;
use serde::Deserialize;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum ColorFormat {
Color(Color),
OptionalColor(Option<Color>),
ColorTable(Table<Color>),
Table(LegacyTable<Color>),
}
Ok(match ColorFormat::deserialize(deserializer)? {
ColorFormat::Color(color) => Table::new_from_element(color),
ColorFormat::OptionalColor(color) => {
if let Some(color) = color {
Table::new_from_element(color)
} else {
Table::new()
}
}
ColorFormat::ColorTable(color_table) => color_table,
ColorFormat::OptionalColor(color) => color,
ColorFormat::Table(table) => table.element.into_iter().next(),
})
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_vec_f64_to_table<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<crate::table::Table<f64>, D::Error> {
use crate::table::{Table, TableRow};
pub fn migrate_to_f64_array<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Vec<f64>, D::Error> {
use serde::Deserialize;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum F64TableFormat {
VecF64(Vec<f64>),
F64Table(Table<f64>),
enum F64ArrayFormat {
Array(Vec<f64>),
Table(LegacyTable<f64>),
}
Ok(match F64TableFormat::deserialize(deserializer)? {
F64TableFormat::VecF64(values) => values.into_iter().map(TableRow::new_from_element).collect(),
F64TableFormat::F64Table(table) => table,
Ok(match F64ArrayFormat::deserialize(deserializer)? {
F64ArrayFormat::Array(values) => values,
F64ArrayFormat::Table(table) => table.element,
})
}

View File

@@ -1063,39 +1063,6 @@ impl<T> Table<T> {
}
}
#[cfg(feature = "serde")]
impl<T: serde::Serialize> serde::Serialize for Table<T> {
/// Serializes only the element vec, omitting type-erased attributes which are not serializable.
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(serde::Serialize)]
struct TableHelper<'a, T: serde::Serialize> {
element: &'a Vec<T>,
}
TableHelper { element: &self.element }.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Table<T> {
/// Deserializes the element vec and initializes an empty attribute column store with the matching row count.
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
struct TableHelper<T> {
#[serde(alias = "instances", alias = "instance")]
element: Vec<T>,
}
let helper = TableHelper::deserialize(deserializer)?;
let len = helper.element.len();
Ok(Table {
element: helper.element,
attributes: AttributeColumns::with_len(len),
})
}
}
impl<T: BoundingBox> BoundingBox for Table<T> {
/// Computes the combined bounding box of all items, composing each item's transform attribute with the given transform.
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
@@ -1254,34 +1221,6 @@ impl<T: PartialEq> PartialEq for TableRow<T> {
}
}
#[cfg(feature = "serde")]
impl<T: serde::Serialize> serde::Serialize for TableRow<T> {
/// Serializes only the element, omitting type-erased attributes which are not serializable.
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(serde::Serialize)]
struct TableRowHelper<'a, T: serde::Serialize> {
element: &'a T,
}
TableRowHelper { element: &self.element }.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for TableRow<T> {
/// Deserializes the element and initializes an empty set of attributes.
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
struct TableRowHelper<T> {
#[serde(alias = "instance")]
element: T,
}
let helper = TableRowHelper::deserialize(deserializer)?;
Ok(TableRow::new_from_element(helper.element))
}
}
impl<T> TableRow<T> {
/// Constructs a row from a pre-built element and attributes pair.
pub fn from_parts(element: T, attributes: AttributeValues) -> Self {

View File

@@ -6,22 +6,32 @@ use std::fmt::{Display, Formatter};
#[macro_export]
macro_rules! concrete {
($type:ty) => {
$crate::Type::Concrete($crate::TypeDescriptor {
$crate::Type::Concrete($crate::descriptor!($type))
};
($type:ty, $name:ty) => {
$crate::Type::Concrete($crate::descriptor!($type, $name))
};
}
#[macro_export]
macro_rules! descriptor {
($type:ty) => {
$crate::TypeDescriptor {
id: Some(std::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
alias: None,
size: std::mem::size_of::<$type>(),
align: std::mem::align_of::<$type>(),
})
}
};
($type:ty, $name:ty) => {
$crate::Type::Concrete($crate::TypeDescriptor {
$crate::TypeDescriptor {
id: Some(std::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
alias: Some($crate::Cow::Borrowed(stringify!($name))),
size: std::mem::size_of::<$type>(),
align: std::mem::align_of::<$type>(),
})
}
};
}
@@ -160,7 +170,7 @@ pub struct TypeDescriptor {
#[cfg_attr(feature = "serde", serde(skip))]
pub id: Option<TypeId>,
pub name: Cow<'static, str>,
#[cfg_attr(feature = "serde", serde(default))]
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
pub alias: Option<Cow<'static, str>>,
#[cfg_attr(feature = "serde", serde(skip))]
pub size: usize,

View File

@@ -1,13 +1,10 @@
use crate::graphic::Graphic;
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::render_complexity::RenderComplexity;
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, Color};
use core_types::table::Table;
use dyn_any::DynAny;
use glam::{DAffine2, IVec2};
use glam::DAffine2;
/// Nominal wrapper around `Table<Graphic>` representing a single artboard's content.
///
@@ -15,7 +12,6 @@ use glam::{DAffine2, IVec2};
/// enclosing `Table<Artboard>`, not as fields here. This keeps `Artboard` a pure type-system boundary
/// that prevents arbitrary `Table<Table<...<Graphic>>>` nesting.
#[derive(Clone, Debug, Default, CacheHash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Artboard(Table<Graphic>);
impl Artboard {
@@ -63,73 +59,3 @@ impl RenderComplexity for Artboard {
self.0.render_complexity()
}
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Artboard>, D::Error> {
use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
/// Legacy artboard struct shape, kept for deserializing old documents into `Table<Artboard>`.
#[derive(Clone, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LegacyArtboard {
pub content: Table<Graphic>,
pub label: String,
pub location: IVec2,
pub dimensions: IVec2,
pub background: Color,
pub clip: bool,
}
#[derive(Clone, Default, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LegacyArtboardGroup {
pub artboards: Vec<(LegacyArtboard, Option<NodeId>)>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldTable<T> {
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<LegacyAlphaBlending>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum ArtboardFormat {
ArtboardGroup(LegacyArtboardGroup),
OldArtboardTable(OldTable<LegacyArtboard>),
LegacyArtboardTable(Table<LegacyArtboard>),
// NOTE: Must come last so older tagged formats above are tried first.
// Also covers the intermediate `Table<Table<Graphic>>` shape since `Artboard` deserializes transparently.
ArtboardTable(Table<Artboard>),
}
fn legacy_to_row(legacy: LegacyArtboard) -> TableRow<Artboard> {
// Legacy `label` field is dropped (the artboard's name comes from its parent layer's display name)
TableRow::new_from_element(Artboard::new(legacy.content))
.with_attribute(ATTR_LOCATION, legacy.location.as_dvec2())
.with_attribute(ATTR_DIMENSIONS, legacy.dimensions.as_dvec2())
.with_attribute(ATTR_BACKGROUND, legacy.background)
.with_attribute(ATTR_CLIP, legacy.clip)
}
Ok(match ArtboardFormat::deserialize(deserializer)? {
ArtboardFormat::ArtboardGroup(group) => group.artboards.into_iter().map(|(artboard, _)| legacy_to_row(artboard)).collect(),
ArtboardFormat::OldArtboardTable(old_table) => old_table.element.into_iter().map(legacy_to_row).collect(),
ArtboardFormat::LegacyArtboardTable(legacy_table) => legacy_table.into_iter().map(|row| legacy_to_row(row.into_element())).collect(),
ArtboardFormat::ArtboardTable(artboard_table) => artboard_table,
})
}

View File

@@ -1,9 +1,8 @@
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash;
use core_types::ops::TableConvert;
use core_types::render_complexity::RenderComplexity;
use core_types::table::{Table, TableRow};
use core_types::table::Table;
use core_types::uuid::NodeId;
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use dyn_any::DynAny;
@@ -16,7 +15,6 @@ pub use vector_types::Vector;
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Graphic {
Graphic(Table<Graphic>),
Vector(Table<Vector>),
@@ -486,97 +484,3 @@ impl<T: Clone> OmitIndex for Table<T> {
self.omit_index(self.len() - index)
}
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Graphic>, D::Error> {
use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldGraphicGroup {
elements: Vec<(Graphic, Option<NodeId>)>,
transform: DAffine2,
alpha_blending: LegacyAlphaBlending,
}
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GraphicGroup {
elements: Vec<(Graphic, Option<NodeId>)>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OlderTable<T> {
id: Vec<u64>,
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldTable<T> {
id: Vec<u64>,
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<LegacyAlphaBlending>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum GraphicFormat {
OldGraphicGroup(OldGraphicGroup),
OlderTableOldGraphicGroup(OlderTable<OldGraphicGroup>),
OldTableOldGraphicGroup(OldTable<OldGraphicGroup>),
OldTableGraphicGroup(OldTable<GraphicGroup>),
Table(serde_json::Value),
}
// Attributes (transform, alpha_blending, editor:layer_path) are not serialized, so migration only needs
// to recover the elements. Per-item attribute values are populated at runtime by the node graph.
Ok(match GraphicFormat::deserialize(deserializer)? {
GraphicFormat::OldGraphicGroup(old) => old.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)).collect(),
GraphicFormat::OlderTableOldGraphicGroup(old) => old
.element
.into_iter()
.flat_map(|element| element.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)))
.collect(),
GraphicFormat::OldTableOldGraphicGroup(old) => old
.element
.into_iter()
.flat_map(|element| element.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)))
.collect(),
GraphicFormat::OldTableGraphicGroup(old) => old
.element
.into_iter()
.flat_map(|element| element.elements.into_iter().map(|(graphic, _)| TableRow::new_from_element(graphic)))
.collect(),
GraphicFormat::Table(value) => {
// Try to deserialize as either `Table` format
if let Ok(old_table) = serde_json::from_value::<Table<GraphicGroup>>(value.clone()) {
let mut graphic_table = Table::new();
for index in 0..old_table.len() {
for (graphic, _) in &old_table.element(index).unwrap().elements {
graphic_table.push(TableRow::new_from_element(graphic.clone()));
}
}
graphic_table
} else if let Ok(new_table) = serde_json::from_value::<Table<Graphic>>(value) {
new_table
} else {
return Err(serde::de::Error::custom("Failed to deserialize Table<Graphic>"));
}
}
})
}

View File

@@ -11,88 +11,50 @@ pub use artboard::Artboard;
pub use graphic::{Graphic, IntoGraphicTable, TryFromGraphic, Vector};
pub mod migrations {
use core_types::blending::BlendMode;
use core_types::table::{Table, TableRow};
use dyn_any::DynAny;
use glam::DAffine2;
use vector_types::vector::{PathStyle, PointDomain, RegionDomain, SegmentDomain, misc::HandleId};
use crate::{Graphic, Vector};
use crate::Vector;
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Vector>, D::Error> {
/// Returns the first `Vector` recovered from any of the legacy on-disk shapes (a single `Vector`, the old `OldVectorData` flat struct, or any of the historical `Table<Vector>` variants).
pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Vector>, D::Error> {
use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
/// Old documents stored a `Vector` flattened with table attributes (`transform`, `alpha_blending`, `upstream_graphic_group`); only the geometry fields are recovered.
#[derive(serde::Deserialize)]
struct OldVectorData {
style: PathStyle,
colinear_manipulators: Vec<[HandleId; 2]>,
point_domain: PointDomain,
segment_domain: SegmentDomain,
region_domain: RegionDomain,
}
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldVectorData {
pub transform: DAffine2,
pub alpha_blending: LegacyAlphaBlending,
pub style: PathStyle,
pub colinear_manipulators: Vec<[HandleId; 2]>,
pub point_domain: PointDomain,
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
pub upstream_graphic_group: Option<Table<Graphic>>,
#[derive(serde::Deserialize)]
struct LegacyTable {
#[serde(alias = "instances", alias = "instance")]
element: Vec<Vector>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldTable<T> {
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<LegacyAlphaBlending>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OlderTable<T> {
id: Vec<u64>,
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
#[derive(serde::Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum VectorFormat {
Vector(Vector),
OldVectorData(OldVectorData),
OldVectorTable(OldTable<Vector>),
OlderVectorTable(OlderTable<Vector>),
VectorTable(Table<Vector>),
Table(LegacyTable),
}
Ok(match VectorFormat::deserialize(deserializer)? {
VectorFormat::Vector(vector) => Table::new_from_element(vector),
// Attributes (transform, alpha_blending, editor:layer_path) are not serialized, so migration only needs
// to recover the elements. Per-item attribute values are populated at runtime by the node graph.
VectorFormat::OldVectorData(old) => Table::new_from_element(Vector {
VectorFormat::Vector(vector) => Some(vector),
VectorFormat::OldVectorData(old) => Some(Vector {
style: old.style,
colinear_manipulators: old.colinear_manipulators,
point_domain: old.point_domain,
segment_domain: old.segment_domain,
region_domain: old.region_domain,
}),
VectorFormat::OlderVectorTable(older_table) => older_table.element.into_iter().map(TableRow::new_from_element).collect(),
VectorFormat::OldVectorTable(old_table) => old_table.element.into_iter().map(TableRow::new_from_element).collect(),
VectorFormat::VectorTable(vector_table) => vector_table,
VectorFormat::Table(table) => table.element.into_iter().next(),
})
}
}

View File

@@ -1,12 +1,9 @@
use crate::raster_types::{CPU, Raster};
use crate::{Bitmap, BitmapMut};
use core_types::blending::BlendMode;
use core_types::Color;
use core_types::color::float_to_srgb_u8;
use core_types::table::{Table, TableRow};
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
// use crate::vector::Vector; // TODO: Check if Vector is actually used, if so handle differently
use core_types::color::*;
use dyn_any::{DynAny, StaticType};
use dyn_any::StaticType;
use glam::{DAffine2, DVec2};
use std::vec::Vec;
@@ -15,7 +12,7 @@ mod base64_serde {
use base64::Engine;
use core_types::color::*;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::{Deserializer, Serialize, Serializer};
pub fn as_base64<S: Serializer, P: Pixel>(key: &[P], serializer: S) -> Result<S::Ok, S::Error> {
let u8_data = bytemuck::cast_slice(key);
@@ -25,16 +22,29 @@ mod base64_serde {
pub fn from_base64<'a, D: Deserializer<'a>, P: Pixel>(deserializer: D) -> Result<Vec<P>, D::Error> {
use serde::de::Error;
<(u64, &[u8])>::deserialize(deserializer)
.and_then(|(len, str)| {
// Use a small visitor that accepts both borrowed bytes (from a streaming JSON deserializer) and owned strings (from an intermediate like `serde_json::Value`, which can't preserve the borrow).
// The migration loader takes the second path, so without this allowance documents containing image base64 data fail with `expected a borrowed byte array`.
struct LenAndBase64Visitor<P: Pixel>(std::marker::PhantomData<P>);
impl<'de, P: Pixel> serde::de::Visitor<'de> for LenAndBase64Visitor<P> {
type Value = Vec<P>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a tuple of (length, base64-encoded data)")
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let len: u64 = seq.next_element()?.ok_or_else(|| A::Error::missing_field("length"))?;
let base64_string: std::borrow::Cow<'de, str> = seq.next_element()?.ok_or_else(|| A::Error::missing_field("base64 data"))?;
let mut output: Vec<P> = vec![P::zeroed(); len as usize];
base64::engine::general_purpose::STANDARD
.decode_slice(str, bytemuck::cast_slice_mut(output.as_mut_slice()))
.map_err(|err| Error::custom(err.to_string()))?;
.decode_slice(base64_string.as_bytes(), bytemuck::cast_slice_mut(output.as_mut_slice()))
.map_err(|err| A::Error::custom(err.to_string()))?;
Ok(output)
})
.map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_tuple(2, LenAndBase64Visitor::<P>(std::marker::PhantomData))
}
}
@@ -219,253 +229,6 @@ impl<P: Pixel> IntoIterator for Image<P> {
}
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Raster<CPU>>, D::Error> {
use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
#[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny)]
enum RasterFrame {
ImageFrame(Table<Image<Color>>),
}
impl<'de> serde::Deserialize<'de> for RasterFrame {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(RasterFrame::ImageFrame(Table::new_from_element(Image::deserialize(deserializer)?)))
}
}
impl serde::Serialize for RasterFrame {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
RasterFrame::ImageFrame(table) => table.serialize(serializer),
}
}
}
#[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GraphicElement {
GraphicGroup(Table<GraphicElement>),
RasterFrame(RasterFrame),
}
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageFrame<P: Pixel> {
pub image: Image<P>,
}
impl From<ImageFrame<Color>> for GraphicElement {
fn from(image_frame: ImageFrame<Color>) -> Self {
GraphicElement::RasterFrame(RasterFrame::ImageFrame(Table::new_from_element(image_frame.image)))
}
}
impl From<GraphicElement> for ImageFrame<Color> {
fn from(element: GraphicElement) -> Self {
match element {
GraphicElement::RasterFrame(RasterFrame::ImageFrame(image)) => Self {
image: image.element(0).unwrap().clone(),
},
_ => panic!("Expected Image, found {element:?}"),
}
}
}
unsafe impl<P> StaticType for ImageFrame<P>
where
P: dyn_any::StaticTypeSized + Pixel,
P::Static: Pixel,
{
type Static = ImageFrame<P::Static>;
}
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldImageFrame<P: Pixel> {
image: Image<P>,
transform: DAffine2,
alpha_blending: LegacyAlphaBlending,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum FormatVersions {
Image(Image<Color>),
OldImageFrame(OldImageFrame<Color>),
OlderImageFrameTable(OlderTable<ImageFrame<Color>>),
OldImageFrameTable(OldTable<ImageFrame<Color>>),
OldImageTable(OldTable<Image<Color>>),
OldRasterTable(OldTable<Raster<CPU>>),
ImageFrameTable(Table<ImageFrame<Color>>),
ImageTable(Table<Image<Color>>),
RasterTable(Table<Raster<CPU>>),
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldTable<T> {
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<LegacyAlphaBlending>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OlderTable<T> {
id: Vec<u64>,
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
}
fn from_image_table(table: Table<Image<Color>>) -> Table<Raster<CPU>> {
Table::new_from_element(Raster::new_cpu(table.element(0).unwrap().clone()))
}
// Attributes (transform, alpha_blending, editor:layer_path) are not serialized, so migration only needs
// to recover the elements. Per-item attribute values are populated at runtime by the node graph.
fn old_table_to_new_table<T>(old_table: OldTable<T>) -> Table<T> {
old_table.element.into_iter().map(TableRow::new_from_element).collect()
}
fn older_table_to_new_table<T>(old_table: OlderTable<T>) -> Table<T> {
old_table.element.into_iter().map(TableRow::new_from_element).collect()
}
fn from_image_frame_table(image_frame: Table<ImageFrame<Color>>) -> Table<Raster<CPU>> {
let default = ImageFrame::default();
let element = image_frame.element(0).unwrap_or(&default);
Table::new_from_element(Raster::new_cpu(element.image.clone()))
}
Ok(match FormatVersions::deserialize(deserializer)? {
FormatVersions::Image(image) => Table::new_from_element(Raster::new_cpu(image)),
FormatVersions::OldImageFrame(OldImageFrame { image, transform, alpha_blending }) => {
let mut image_frame_table = Table::new_from_element(Raster::new_cpu(image));
image_frame_table.set_attribute(ATTR_TRANSFORM, 0, transform);
image_frame_table.set_attribute(ATTR_BLEND_MODE, 0, alpha_blending.blend_mode);
image_frame_table.set_attribute(ATTR_OPACITY, 0, alpha_blending.opacity as f64);
image_frame_table.set_attribute(ATTR_OPACITY_FILL, 0, alpha_blending.fill as f64);
image_frame_table.set_attribute(ATTR_CLIPPING_MASK, 0, alpha_blending.clip);
image_frame_table
}
FormatVersions::OlderImageFrameTable(old_table) => from_image_frame_table(older_table_to_new_table(old_table)),
FormatVersions::OldImageFrameTable(old_table) => from_image_frame_table(old_table_to_new_table(old_table)),
FormatVersions::OldImageTable(old_table) => from_image_table(old_table_to_new_table(old_table)),
FormatVersions::OldRasterTable(old_table) => old_table_to_new_table(old_table),
FormatVersions::ImageFrameTable(image_frame) => from_image_frame_table(image_frame),
FormatVersions::ImageTable(table) => from_image_table(table),
FormatVersions::RasterTable(table) => table,
})
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<TableRow<Raster<CPU>>, D::Error> {
use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
#[derive(Clone, Debug, PartialEq, DynAny)]
enum RasterFrame {
/// A CPU-based bitmap image with a finite position and extent, equivalent to the SVG <image> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
ImageFrame(Table<Image<Color>>),
}
impl<'de> serde::Deserialize<'de> for RasterFrame {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(RasterFrame::ImageFrame(Table::new_from_element(Image::deserialize(deserializer)?)))
}
}
impl serde::Serialize for RasterFrame {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
RasterFrame::ImageFrame(table) => table.serialize(serializer),
}
}
}
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GraphicElement {
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(Table<GraphicElement>),
RasterFrame(RasterFrame),
}
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageFrame<P: Pixel> {
pub image: Image<P>,
}
impl From<ImageFrame<Color>> for GraphicElement {
fn from(image_frame: ImageFrame<Color>) -> Self {
GraphicElement::RasterFrame(RasterFrame::ImageFrame(Table::new_from_element(image_frame.image)))
}
}
impl From<GraphicElement> for ImageFrame<Color> {
fn from(element: GraphicElement) -> Self {
match element {
GraphicElement::RasterFrame(RasterFrame::ImageFrame(image)) => Self {
image: image.element(0).unwrap().clone(),
},
_ => panic!("Expected Image, found {element:?}"),
}
}
}
unsafe impl<P> StaticType for ImageFrame<P>
where
P: dyn_any::StaticTypeSized + Pixel,
P::Static: Pixel,
{
type Static = ImageFrame<P::Static>;
}
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldImageFrame<P: Pixel> {
image: Image<P>,
transform: DAffine2,
alpha_blending: LegacyAlphaBlending,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
enum FormatVersions {
Image(Image<Color>),
OldImageFrame(OldImageFrame<Color>),
ImageFrameTable(Table<ImageFrame<Color>>),
RasterTable(Table<Raster<CPU>>),
RasterTableRow(TableRow<Raster<CPU>>),
}
// Attributes (transform, alpha_blending, editor:layer_path) are not serialized, so migration only needs
// to recover the element. Per-item attribute values are populated at runtime by the node graph.
Ok(match FormatVersions::deserialize(deserializer)? {
FormatVersions::Image(image) => TableRow::new_from_element(Raster::new_cpu(image)),
FormatVersions::OldImageFrame(old) => TableRow::new_from_element(Raster::new_cpu(old.image)),
FormatVersions::ImageFrameTable(image_frame) => TableRow::new_from_element(Raster::new_cpu(image_frame.element(0).unwrap().image.clone())),
FormatVersions::RasterTable(image_frame_table) => image_frame_table.into_iter().next().unwrap_or_default(),
FormatVersions::RasterTableRow(image_table_row) => image_table_row,
})
}
impl<P: std::fmt::Debug + Copy + Pixel> Sample for Image<P> {
type Pixel = P;

View File

@@ -526,20 +526,25 @@ impl Gradient {
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_gradient_stops<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<core_types::table::Table<GradientStops>, D::Error> {
use core_types::table::Table;
pub fn migrate_to_gradient_stops<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GradientStops, D::Error> {
use serde::Deserialize;
#[derive(serde::Deserialize)]
struct LegacyTable {
#[serde(alias = "instances", alias = "instance")]
element: Vec<GradientStops>,
}
#[derive(serde::Deserialize)]
#[cfg_attr(feature = "serde", serde(untagged))]
enum GradientStopsFormat {
GradientStops(GradientStops),
GradientTable(Table<GradientStops>),
Stops(GradientStops),
Table(LegacyTable),
}
Ok(match GradientStopsFormat::deserialize(deserializer)? {
GradientStopsFormat::GradientStops(stops) => Table::new_from_element(stops),
GradientStopsFormat::GradientTable(table) => table,
GradientStopsFormat::Stops(stops) => stops,
GradientStopsFormat::Table(table) => table.element.into_iter().next().unwrap_or_default(),
})
}

View File

@@ -222,17 +222,6 @@ impl From<Fill> for FillChoice {
}
}
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, DynAny, Hash, graphene_hash::CacheHash, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
pub enum FillType {
#[default]
Solid,
Gradient,
}
/// The stroke (outline) style of an SVG element.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]

View File

@@ -4,22 +4,27 @@ pub mod brush_stroke;
pub mod migrations {
use crate::brush_stroke::BrushStroke;
use core_types::table::{Table, TableRow};
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_brush_strokes_to_table<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<BrushStroke>, D::Error> {
pub fn migrate_to_brush_strokes<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Vec<BrushStroke>, D::Error> {
use serde::Deserialize;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
enum BrushStrokeTableFormat {
BrushStrokes(Vec<BrushStroke>),
BrushStrokeTable(Table<BrushStroke>),
#[derive(serde::Deserialize)]
struct LegacyTable {
#[serde(alias = "instances", alias = "instance")]
element: Vec<BrushStroke>,
}
Ok(match BrushStrokeTableFormat::deserialize(deserializer)? {
BrushStrokeTableFormat::BrushStrokes(strokes) => strokes.into_iter().map(TableRow::new_from_element).collect(),
BrushStrokeTableFormat::BrushStrokeTable(table) => table,
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum BrushStrokesFormat {
Strokes(Vec<BrushStroke>),
Table(LegacyTable),
}
Ok(match BrushStrokesFormat::deserialize(deserializer)? {
BrushStrokesFormat::Strokes(strokes) => strokes,
BrushStrokesFormat::Table(table) => table.element,
})
}
}

View File

@@ -6,14 +6,12 @@ mod text_context;
mod to_path;
use convert_case::{Boundary, Converter, pattern};
use core_types::Color;
use core_types::graphene_hash::CacheHash;
use core_types::registry::types::{SignedInteger, TextArea};
use core_types::table::{Table, TableRow};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use raster_types::{CPU, Raster};
use unicode_segmentation::UnicodeSegmentation;
// Re-export for convenience
@@ -797,24 +795,6 @@ fn read_string(ctx: impl Ctx + ExtractVarArgs) -> String {
/// Converts a value to a JSON string representation.
#[node_macro::node(category("Debug"))]
fn serialize<T: serde::Serialize>(
_: impl Ctx,
#[implementations(
String,
bool,
f64,
u32,
u64,
DVec2,
DAffine2,
// Table<Artboard>,
// Table<Graphic>,
// Table<Vector>,
Table<Raster<CPU>>,
Table<Color>,
// Table<GradientStops>,
)]
value: T,
) -> String {
fn serialize<T: serde::Serialize>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, DAffine2)] value: T) -> String {
serde_json::to_string(&value).unwrap_or_else(|_| "Serialization Error".to_string())
}