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

@@ -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))]