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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 615 additions and 937 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -29,6 +29,7 @@ use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType;
use crate::node_graph_executor::NodeGraphExecutor;
use glam::{DAffine2, DVec2};
use graph_craft::descriptor;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_std::math::quad::Quad;
@ -1760,12 +1761,7 @@ impl DocumentMessageHandler {
}
pub fn deserialize_document(serialized_content: &str) -> Result<Self, EditorError> {
// Walk the document JSON and rewrite any `TaggedValue` variants that have been removed since being released as `"None"` so the document still deserializes.
// `migrate_node` then drops the resulting orphan node inputs so the value never reaches graph execution.
let mut json_value: serde_json::Value = serde_json::from_str(serialized_content).map_err(|e| EditorError::DocumentDeserialization(e.to_string()))?;
graph_craft::document::value::TaggedValue::scrub_removed_variants_from_json(&mut json_value);
let document_message_handler = serde_json::from_value::<DocumentMessageHandler>(json_value.clone())
let document_message_handler = serde_json::from_str::<DocumentMessageHandler>(serialized_content)
.or_else(|e| {
log::warn!("Failed to directly load document with the following error: {e}. Trying old DocumentMessageHandler.");
// TODO: Eventually remove this document upgrade code
@ -1804,7 +1800,7 @@ impl DocumentMessageHandler {
pub snapping_state: SnappingState,
}
serde_json::from_value::<OldDocumentMessageHandler>(json_value).map(|old_message_handler| DocumentMessageHandler {
serde_json::from_str::<OldDocumentMessageHandler>(serialized_content).map(|old_message_handler| DocumentMessageHandler {
network_interface: NodeNetworkInterface::from_old_network(old_message_handler.network),
collapsed: old_message_handler.collapsed,
commit_hash: old_message_handler.commit_hash,
@ -3349,7 +3345,7 @@ impl DocumentMessageHandler {
/// Create a network interface with a single export
fn default_document_network_interface() -> NodeNetworkInterface {
let mut network_interface = NodeNetworkInterface::default();
network_interface.add_export(TaggedValue::Artboard(Default::default()), -1, "", &[]);
network_interface.add_export(TaggedValue::TypeDefault(descriptor!(graphene_std::table::Table<graphene_std::Artboard>)), -1, "", &[]);
network_interface
}

View File

@ -8,14 +8,14 @@ use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::descriptor;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::Color;
use graphene_std::renderer::Quad;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{Artboard, Color};
#[derive(ExtractField)]
pub struct GraphOperationMessageContext<'a> {
@ -170,7 +170,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
}
// Set the bottom input of the artboard back to artboard
let bottom_input = NodeInput::value(TaggedValue::Artboard(Table::new()), true);
let bottom_input = NodeInput::type_default(descriptor!(Table<Artboard>), true);
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
} else {
// We have some non layers (e.g. just a rectangle node). We disconnect the bottom input and connect it to the left input.
@ -178,7 +178,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 1), primary_input, &[]);
// Set the bottom input of the artboard back to artboard
let bottom_input = NodeInput::value(TaggedValue::Artboard(Table::new()), true);
let bottom_input = NodeInput::type_default(descriptor!(Table<Artboard>), true);
network_interface.set_input(&InputConnector::node(artboard_layer.to_node(), 0), bottom_input, &[]);
}
}

View File

@ -6,7 +6,7 @@ use crate::messages::prelude::*;
use glam::{DAffine2, DVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graph_craft::{ProtoNodeIdentifier, concrete};
use graph_craft::{ProtoNodeIdentifier, concrete, descriptor};
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
@ -15,7 +15,7 @@ use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke};
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Color, Graphic, NodeInputDecleration};
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum TransformIn {
@ -132,11 +132,11 @@ impl<'a> ModifyInputsContext<'a> {
/// Creates an artboard as the primary export for the document network.
pub fn create_artboard(&mut self, new_id: NodeId, location: DVec2, dimensions: DVec2, background: Color, clip: bool) -> LayerNodeIdentifier {
let artboard_node_template = resolve_network_node_type("Artboard").expect("Node").node_template_input_override([
Some(NodeInput::value(TaggedValue::Artboard(Default::default()), true)),
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::type_default(descriptor!(Table<Artboard>), true)),
Some(NodeInput::type_default(descriptor!(Table<Graphic>), true)),
Some(NodeInput::value(TaggedValue::DVec2(location), false)),
Some(NodeInput::value(TaggedValue::DVec2(dimensions), false)),
Some(NodeInput::value(TaggedValue::Color(Table::new_from_element(background)), false)),
Some(NodeInput::value(TaggedValue::Color(Some(background)), false)),
Some(NodeInput::value(TaggedValue::Bool(clip), false)),
]);
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
@ -147,7 +147,7 @@ impl<'a> ModifyInputsContext<'a> {
let boolean = resolve_proto_node_type(graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER)
.expect("Boolean node does not exist")
.node_template_input_override([
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::type_default(descriptor!(Table<Graphic>), true)),
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
]);
@ -157,10 +157,9 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn insert_blend_data(&mut self, layer: LayerNodeIdentifier, count: f64) -> NodeId {
let blend = resolve_network_node_type("Blend").expect("Blend node does not exist").node_template_input_override([
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::F64(count), false)),
]);
let blend = resolve_network_node_type("Blend")
.expect("Blend node does not exist")
.node_template_input_override([Some(NodeInput::type_default(descriptor!(Table<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(count), false))]);
let blend_id = NodeId::new();
self.network_interface.insert_node(blend_id, blend, &[]);
@ -172,10 +171,7 @@ impl<'a> ModifyInputsContext<'a> {
pub fn insert_morph_data(&mut self, layer: LayerNodeIdentifier) -> NodeId {
let morph = resolve_proto_node_type(graphene_std::vector::morph::IDENTIFIER)
.expect("Morph node does not exist")
.node_template_input_override([
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
]);
.node_template_input_override([Some(NodeInput::type_default(descriptor!(Table<Graphic>), true)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]);
let morph_id = NodeId::new();
self.network_interface.insert_node(morph_id, morph, &[]);
@ -298,10 +294,7 @@ impl<'a> ModifyInputsContext<'a> {
pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier) {
let color_value = resolve_proto_node_type(graphene_std::math_nodes::color_value::IDENTIFIER)
.expect("Color Value node does not exist")
.node_template_input_override([
Some(NodeInput::value(TaggedValue::None, false)),
Some(NodeInput::value(TaggedValue::Color(Table::new_from_element(color)), false)),
]);
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Color(Some(color)), false))]);
let color_value_id = NodeId::new();
self.network_interface.insert_node(color_value_id, color_value, &[]);
@ -427,15 +420,15 @@ impl<'a> ModifyInputsContext<'a> {
match &fill {
Fill::None => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new()), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), true);
}
Fill::Solid(color) => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new_from_element(*color)), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true);
}
Fill::Gradient(gradient) => {
let input_connector = InputConnector::node(fill_node_id, backup_gradient_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(gradient.clone()), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::FillGradient(gradient.clone()), false), true);
}
}
let input_connector = InputConnector::node(fill_node_id, fill_index);
@ -502,8 +495,7 @@ impl<'a> ModifyInputsContext<'a> {
return;
};
let input_connector = InputConnector::node(gradient_node_id, graphene_std::math_nodes::gradient_value::GradientInput::INDEX);
let stops_table = Table::new_from_element(stops);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientTable(stops_table), false), false);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false);
}
/// Update the gradient line so its endpoints are at `new_start` and `new_end`.
@ -606,10 +598,8 @@ impl<'a> ModifyInputsContext<'a> {
return;
};
let stroke_color = if let Some(color) = stroke.color { Table::new_from_element(color) } else { Table::new() };
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_color), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke.color), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
@ -623,8 +613,7 @@ impl<'a> ModifyInputsContext<'a> {
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::<graphene_std::table::Table<f64>>::INDEX);
let dash_lengths_table = stroke.dash_lengths.into_iter().map(graphene_std::table::TableRow::new_from_element).collect();
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Table(dash_lengths_table), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64Array(stroke.dash_lengths), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
}
@ -726,8 +715,7 @@ impl<'a> ModifyInputsContext<'a> {
let Some(brush_node_id) = self.existing_proto_node_id(graphene_std::brush::brush::brush::IDENTIFIER, true) else {
return;
};
let strokes_table = strokes.into_iter().map(graphene_std::table::TableRow::new_from_element).collect();
self.set_input_with_refresh(InputConnector::node(brush_node_id, 1), NodeInput::value(TaggedValue::BrushStrokeTable(strokes_table), false), false);
self.set_input_with_refresh(InputConnector::node(brush_node_id, 1), NodeInput::value(TaggedValue::BrushStrokes(strokes), false), false);
}
pub fn resize_artboard(&mut self, location: DVec2, dimensions: DVec2) {

View File

@ -13,9 +13,9 @@ use crate::messages::prelude::Message;
use crate::node_graph_executor::NodeGraphExecutor;
use glam::DVec2;
use graph_craft::ProtoNodeIdentifier;
use graph_craft::concrete;
use graph_craft::document::value::*;
use graph_craft::document::*;
use graph_craft::{concrete, descriptor};
use graphene_std::extract_xy::XY;
use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha};
use graphene_std::raster_types::{CPU, Raster};
@ -205,10 +205,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
.collect(),
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
],
inputs: vec![NodeInput::type_default(descriptor!(Table<Graphic>), true), NodeInput::type_default(descriptor!(Table<Graphic>), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
@ -342,11 +339,11 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::Artboard(Default::default()), true),
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
NodeInput::type_default(descriptor!(Table<Artboard>), true),
NodeInput::type_default(descriptor!(Table<Graphic>), true),
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
NodeInput::value(TaggedValue::DVec2(DVec2::new(1920., 1080.)), false),
NodeInput::value(TaggedValue::Color(Table::new_from_element(Color::WHITE)), false),
NodeInput::value(TaggedValue::Color(Some(Color::WHITE)), false),
NodeInput::value(TaggedValue::Bool(true), false),
],
..Default::default()
@ -576,11 +573,11 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::Vector(Default::default()), true),
NodeInput::type_default(descriptor!(Table<Vector>), true),
NodeInput::value(TaggedValue::F64(10.), false),
NodeInput::value(TaggedValue::Bool(Default::default()), false),
NodeInput::value(TaggedValue::InterpolationDistribution(Default::default()), false),
NodeInput::value(TaggedValue::Vector(Default::default()), false),
NodeInput::type_default(descriptor!(Table<Vector>), false),
],
..Default::default()
},
@ -827,7 +824,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
.collect(),
..Default::default()
}),
inputs: vec![NodeInput::value(TaggedValue::Vector(Default::default()), true)],
inputs: vec![NodeInput::type_default(descriptor!(Table<Vector>), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
@ -1012,7 +1009,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::Vector(Default::default()), true),
NodeInput::type_default(descriptor!(Table<Vector>), true),
NodeInput::value(
TaggedValue::Footprint(Footprint {
transform: DAffine2::from_scale_angle_translation(DVec2::new(1000., 1000.), 0., DVec2::new(0., 0.)),
@ -1123,7 +1120,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
.collect(),
..Default::default()
}),
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
inputs: vec![NodeInput::type_default(descriptor!(Table<Raster<CPU>>), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
@ -1278,7 +1275,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
.collect(),
..Default::default()
}),
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
inputs: vec![NodeInput::type_default(descriptor!(Table<Raster<CPU>>), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
@ -1328,7 +1325,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Extract,
inputs: vec![NodeInput::value(TaggedValue::DocumentNode(DocumentNode::default()), true)],
inputs: vec![NodeInput::type_default(descriptor!(DocumentNode), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
@ -1480,7 +1477,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::Vector(Default::default()), true),
NodeInput::type_default(descriptor!(Table<Vector>), true),
NodeInput::value(TaggedValue::VectorModification(Default::default()), false),
],
..Default::default()

View File

@ -15,22 +15,22 @@ use glam::{DAffine2, DVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::{Type, concrete};
use graphene_std::ATTR_TRANSFORM;
use graphene_std::NodeInputDecleration;
use graphene_std::animation::RealTimeMode;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::extract_xy::XY;
use graphene_std::raster::{
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice,
};
use graphene_std::raster_types::Image;
use graphene_std::table::{Table, TableRow};
use graphene_std::table::Table;
use graphene_std::text::{Font, TextAlign};
use graphene_std::text_nodes::StringCapitalization;
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
use graphene_std::vector::misc::BooleanOperation;
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{Fill, FillChoice, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
@ -218,6 +218,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<Table<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
Some(x) if x == TypeId::of::<Table<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
Some(x) if x == TypeId::of::<Table<GradientStops>>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<Table<BrushStroke>>() => brush_strokes_widget(default_info).into(),
// ============
// STRUCT TYPES
// ============
@ -233,7 +234,6 @@ pub(crate) fn property_from_type(
// =========================
// AUTO-GENERATED ENUM TYPES
// =========================
Some(x) if x == TypeId::of::<FillType>() => enum_choice::<FillType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<GradientType>() => enum_choice::<GradientType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<GradientSpreadMethod>() => enum_choice::<GradientSpreadMethod>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<RealTimeMode>() => enum_choice::<RealTimeMode>().for_socket(default_info).property_row(),
@ -416,6 +416,33 @@ pub fn vector_modification_widget(parameter_widgets_info: ParameterWidgetsInfo)
widgets
}
pub fn brush_strokes_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetInstance> {
let ParameterWidgetsInfo { document_node, node_id: _, index, .. } = parameter_widgets_info;
let mut widgets = start_widgets(parameter_widgets_info);
let Some(document_node) = document_node else { return widgets };
let Some(input) = document_node.inputs.get(index) else { return widgets };
if let Some(TaggedValue::BrushStrokes(strokes)) = input.as_non_exposed_value() {
let stroke_count = strokes.len();
let sample_count: usize = strokes.iter().map(|s| s.trace.len()).sum();
let label = if stroke_count == 0 {
"Empty".to_string()
} else {
format!(
"{stroke_count} {} / {sample_count} {}",
if stroke_count == 1 { "Stroke" } else { "Strokes" },
if sample_count == 1 { "Sample" } else { "Samples" }
)
};
widgets.extend_from_slice(&[Separator::new(SeparatorStyle::Unrelated).widget_instance(), TextLabel::new(label).widget_instance()]);
}
widgets
}
pub fn image_data_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetInstance> {
let ParameterWidgetsInfo { document_node, node_id: _, index, .. } = parameter_widgets_info;
@ -774,7 +801,7 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
.map(str::parse::<f64>)
.collect::<Result<Vec<_>, _>>()
.ok()
.map(|values| TaggedValue::F64Table(values.into_iter().map(graphene_std::table::TableRow::new_from_element).collect()))
.map(TaggedValue::F64Array)
};
let Some(document_node) = document_node else { return Vec::new() };
@ -782,11 +809,11 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(TaggedValue::F64Table(table)) = &input.as_non_exposed_value() {
if let Some(TaggedValue::F64Array(values)) = &input.as_non_exposed_value() {
widgets.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
text_input
.value(table.iter_element_values().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
.value(values.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, index))
.widget_instance(),
])
@ -1077,15 +1104,6 @@ pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props:
.on_commit(commit_value)
.widget_instance(),
]),
Some(&TaggedValue::FVec2(vec2)) => widgets.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
number_props
// We use an arbitrary `y` instead of an arbitrary `x` here because the "Grid" node's "Spacing" value's height should be used from rectangular mode when transferred to "Y Spacing" in isometric mode
.value(Some(vec2.y as f64))
.on_update(update_value(move |x: &NumberInput| TaggedValue::F32(x.value.unwrap() as f32), node_id, index))
.on_commit(commit_value)
.widget_instance(),
]),
_ => {}
}
@ -1144,47 +1162,27 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
// Add the color input
match &**tagged_value {
TaggedValue::Color(color_table) => widgets.push(
TaggedValue::Color(color) => widgets.push(
color_button
.value(match color_table.element(0) {
.value(match color {
Some(color) => FillChoice::Solid(*color),
None => FillChoice::None,
})
.on_update(update_value(|input: &ColorInput| TaggedValue::Color(input.value.as_solid()), node_id, index))
.on_commit(commit_value)
.widget_instance(),
),
TaggedValue::Gradient(stops) => widgets.push(
color_button
.value(FillChoice::Gradient(stops.clone()))
.on_update(update_value(
|input: &ColorInput| TaggedValue::Color(input.value.as_solid().iter().map(|&color| TableRow::new_from_element(color)).collect()),
|input: &ColorInput| TaggedValue::Gradient(input.value.as_gradient().cloned().unwrap_or_default()),
node_id,
index,
))
.on_commit(commit_value)
.widget_instance(),
),
TaggedValue::GradientTable(gradient_table) => {
let existing_transform: DAffine2 = gradient_table.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
widgets.push(
color_button
.value(match gradient_table.element(0) {
Some(gradient) => FillChoice::Gradient(gradient.clone()),
None => FillChoice::Gradient(GradientStops::default()),
})
.on_update(update_value(
move |input: &ColorInput| {
TaggedValue::GradientTable(
input
.value
.as_gradient()
.iter()
.map(|&gradient| TableRow::new_from_element(gradient.clone()).with_attribute(ATTR_TRANSFORM, existing_transform))
.collect(),
)
},
node_id,
index,
))
.on_commit(commit_value)
.widget_instance(),
)
}
x => warn!("Color {x:?}"),
}
@ -2212,12 +2210,12 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
};
let uniform_val = match input.as_non_exposed_value() {
Some(TaggedValue::F64(x)) => *x,
Some(TaggedValue::F64Table(table)) => table.iter_element_values().copied().next().unwrap_or(0.),
Some(TaggedValue::F64Array(values)) => values.first().copied().unwrap_or(0.),
_ => 0.,
};
let individual_val = match input.as_non_exposed_value() {
Some(&TaggedValue::F64(x)) => vec![x; 4],
Some(TaggedValue::F64Table(table)) => table.iter_element_values().copied().collect(),
Some(TaggedValue::F64Array(values)) => values.clone(),
_ => vec![0.; 4],
};
@ -2255,7 +2253,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
NodeGraphMessage::SetInputValue {
node_id,
input_index: CornerRadiusInput::<f64>::INDEX,
value: TaggedValue::F64Table(individual_val_for_switch.iter().copied().map(graphene_std::table::TableRow::new_from_element).collect()),
value: TaggedValue::F64Array(individual_val_for_switch.clone()),
}
.into(),
]),
@ -2273,7 +2271,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
.map(str::parse::<f64>)
.collect::<Result<Vec<f64>, _>>()
.ok()
.map(|values| TaggedValue::F64Table(values.into_iter().take(4).map(graphene_std::table::TableRow::new_from_element).collect()))
.map(|values| TaggedValue::F64Array(values.into_iter().take(4).collect()))
};
TextInput::default()
.value(individual_val.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
@ -2444,7 +2442,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}
};
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), Some(TaggedValue::Color(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = (
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), Some(TaggedValue::Color(backup_color)), Some(TaggedValue::FillGradient(backup_gradient))) = (
&document_node.inputs[FillInput::<Color>::INDEX].as_value(),
&document_node.inputs[BackupColorInput::INDEX].as_value(),
&document_node.inputs[BackupGradientInput::INDEX].as_value(),
@ -2454,7 +2452,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
return vec![LayoutGroup::row(widgets_first_row)];
};
let fill2 = fill.clone();
let backup_color_fill: Fill = backup_color.clone().into();
let backup_color_fill: Fill = (*backup_color).into();
let backup_gradient_fill: Fill = backup_gradient.clone().into();
match fill {
@ -2491,19 +2489,19 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Fill::None => NodeGraphMessage::SetInputValue {
node_id,
input_index: BackupColorInput::INDEX,
value: TaggedValue::Color(Table::new()),
value: TaggedValue::Color(None),
}
.into(),
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
node_id,
input_index: BackupColorInput::INDEX,
value: TaggedValue::Color(Table::new_from_element(*color)),
value: TaggedValue::Color(Some(*color)),
}
.into(),
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
node_id,
input_index: BackupGradientInput::INDEX,
value: TaggedValue::Gradient(gradient.clone()),
value: TaggedValue::FillGradient(gradient.clone()),
}
.into(),
},
@ -2635,7 +2633,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
move |_: &()| {
let mut new_gradient = gradient_for_backup.clone();
new_gradient.spread_method = spread_method;
TaggedValue::Gradient(new_gradient)
TaggedValue::FillGradient(new_gradient)
},
node_id,
BackupGradientInput::INDEX,
@ -2684,7 +2682,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
};
let has_dash_lengths = match &document_node.inputs[DashLengthsInput::<Table<f64>>::INDEX].as_value() {
Some(TaggedValue::F64Table(table)) => table.is_empty(),
Some(TaggedValue::F64Array(values)) => values.is_empty(),
_ => true,
};
let miter_limit_disabled = join_value != &StrokeJoin::Miter;

View File

@ -1,6 +1,10 @@
use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue;
use graphene_std::Type;
use graphene_std::raster_types::{CPU, Raster};
use graphene_std::table::Table;
use graphene_std::vector::Vector;
use graphene_std::{Artboard, Graphic};
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
@ -21,14 +25,19 @@ pub enum FrontendGraphDataType {
impl FrontendGraphDataType {
pub fn from_type(input: &Type) -> Self {
match TaggedValue::from_type_or_none(input) {
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Table(_) | TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::Artboard(_) => Self::Artboard,
TaggedValue::Graphic(_) => Self::Graphic,
TaggedValue::Raster(_) => Self::Raster,
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Array(_) | TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::Color(_) => Self::Color,
TaggedValue::Gradient(_) | TaggedValue::GradientTable(_) => Self::Gradient,
TaggedValue::String(_) | TaggedValue::StringTable(_) => Self::Typography,
TaggedValue::FillGradient(_) | TaggedValue::Gradient(_) => Self::Gradient,
TaggedValue::String(_) => Self::Typography,
// Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name.
TaggedValue::TypeDefault(td) => match td.name.as_ref() {
n if n == std::any::type_name::<Table<Graphic>>() => Self::Graphic,
n if n == std::any::type_name::<Table<Artboard>>() => Self::Artboard,
n if n == std::any::type_name::<Table<Raster<CPU>>>() => Self::Raster,
n if n == std::any::type_name::<Table<Vector>>() => Self::Vector,
n if n == std::any::type_name::<Table<String>>() => Self::Typography,
_ => Self::General,
},
_ => Self::General,
}
}

View File

@ -4,7 +4,11 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNodeImplementation, InlineRust, NodeInput};
use graph_craft::proto::{GraphErrorType, GraphErrors};
use graph_craft::{Type, concrete};
use graphene_std::raster_types::{CPU, Raster};
use graphene_std::table::Table;
use graphene_std::uuid::NodeId;
use graphene_std::vector::Vector;
use graphene_std::{Artboard, Graphic};
use interpreted_executor::dynamic_executor::{NodeTypes, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::node_registry::NODE_REGISTRY;
@ -53,16 +57,21 @@ impl TypeSource {
};
match self.compiled_nested_type() {
Some(nested_type) => match TaggedValue::from_type_or_none(nested_type) {
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Table(_) | TaggedValue::DAffine2(_) => {
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Array(_) | TaggedValue::DAffine2(_) => {
FrontendGraphDataType::Number
}
TaggedValue::Artboard(_) => FrontendGraphDataType::Artboard,
TaggedValue::Graphic(_) => FrontendGraphDataType::Graphic,
TaggedValue::Raster(_) => FrontendGraphDataType::Raster,
TaggedValue::Vector(_) => FrontendGraphDataType::Vector,
TaggedValue::Color(_) => FrontendGraphDataType::Color,
TaggedValue::Gradient(_) | TaggedValue::GradientTable(_) => FrontendGraphDataType::Gradient,
TaggedValue::FillGradient(_) | TaggedValue::Gradient(_) => FrontendGraphDataType::Gradient,
TaggedValue::String(_) => FrontendGraphDataType::Typography,
// Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name.
TaggedValue::TypeDefault(td) => match td.name.as_ref() {
n if n == std::any::type_name::<Table<Graphic>>() => FrontendGraphDataType::Graphic,
n if n == std::any::type_name::<Table<Artboard>>() => FrontendGraphDataType::Artboard,
n if n == std::any::type_name::<Table<Raster<CPU>>>() => FrontendGraphDataType::Raster,
n if n == std::any::type_name::<Table<Vector>>() => FrontendGraphDataType::Vector,
n if n == std::any::type_name::<Table<String>>() => FrontendGraphDataType::Typography,
_ => FrontendGraphDataType::General,
},
_ => FrontendGraphDataType::General,
},
None => FrontendGraphDataType::General,

View File

@ -6,6 +6,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
use crate::messages::prelude::DocumentMessageHandler;
use glam::{DVec2, IVec2};
use graph_craft::descriptor;
use graph_craft::document::DocumentNode;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
use graphene_std::ProtoNodeIdentifier;
@ -2089,42 +2090,30 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(false), false), network_path);
}
// Migrate Path nodes that stored geometry directly in input 0 (as a Table<Vector>) to instead use a VectorModification in input 1
// SVG-import legacy Path nodes baked their geometry at non-exposed input 0; move it to input 1 (the modern slot for VectorModification).
// For exposed input 0, any baked value is unused at runtime, so just reset it.
if reference == DefinitionIdentifier::Network("Path".into()) {
let input_0 = node.inputs.first()?;
if let NodeInput::Value { tagged_value, exposed } = input_0
&& !exposed
&& let TaggedValue::Vector(vector_table) = &**tagged_value
&& !vector_table.is_empty()
&& let TaggedValue::VectorModification(modification) = &**tagged_value
{
let vector = vector_table.element(0)?;
let modification = Box::new(graphene_std::vector::VectorModification::create_from_vector(vector));
let modification = modification.clone();
let was_exposed = *exposed;
// Reset input 0 to the default exposed state
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::Vector(Default::default()), true), network_path);
document.network_interface.set_input(
&InputConnector::node(*node_id, 0),
NodeInput::type_default(descriptor!(graphene_std::table::Table<graphene_std::vector::Vector>), true),
network_path,
);
// Store the converted VectorModification in input 1
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::VectorModification(modification), false), network_path);
if !was_exposed {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::VectorModification(modification), false), network_path);
}
}
}
// Migrate Image nodes that stored a Table<Raster<CPU>> in input 1 to instead use bare Image<Color> via TaggedValue::ImageData
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER)
&& let Some(NodeInput::Value { tagged_value, .. }) = node.inputs.get(1)
&& let TaggedValue::Raster(raster_table) = &**tagged_value
&& let Some(element) = raster_table.element(0)
{
let image = element.data().clone();
document
.network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::ImageData(image), false), network_path);
}
// ==================================
// PUT ALL MIGRATIONS ABOVE THIS LINE
// ==================================

View File

@ -282,13 +282,13 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
Some(gradient.clone())
}
/// Get the gradient table of a layer.
pub fn get_gradient_table(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Table<GradientStops>> {
/// Get the gradient stops of a layer, if any.
pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<GradientStops> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER))?;
let TaggedValue::GradientTable(gradient_table) = inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else {
let TaggedValue::Gradient(stops) = inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else {
return None;
};
Some(gradient_table.clone())
Some(stops.clone())
}
/// Compute the transform from a gradient's local space to viewport space for the given layer. For a `Table<GradientStops>`

View File

@ -321,8 +321,8 @@ impl BrushToolData {
if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && node_id != layer.to_node() {
let points_input = node.inputs.get(1)?;
let Some(TaggedValue::BrushStrokeTable(strokes)) = points_input.as_value() else { continue };
self.strokes = strokes.iter_element_values().cloned().collect();
let Some(TaggedValue::BrushStrokes(strokes)) = points_input.as_value() else { continue };
self.strokes = strokes.clone();
return Some(layer);
}

View File

@ -8,7 +8,7 @@ use crate::messages::portfolio::document::overlays::utility_types::{GizmoEmphasi
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_table};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_stops};
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use graph_craft::document::value::TaggedValue;
use graphene_std::raster::color::Color;
@ -336,10 +336,9 @@ fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessa
graph_modification_utils::gradient_space_transform(layer, &document.network_interface)
}
// TODO: Remove this whole function once all gradients are `Table<GradientStops>`
// TODO: Remove this whole function once all gradients are stored via the modern `Gradient(GradientStops)` slot
fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
if let Some(stops_table) = get_gradient_table(layer, network_interface) {
let stops = stops_table.element(0).cloned().unwrap_or_default();
if let Some(stops) = get_gradient_stops(layer, network_interface) {
let GradientChainState {
transform,
gradient_type,
@ -505,7 +504,7 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: D
impl SelectedGradient {
pub fn new(gradient: Gradient, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self {
let transform = gradient_space_transform(layer, document);
let is_gradient_table = get_gradient_table(layer, &document.network_interface).is_some();
let is_gradient_table = get_gradient_stops(layer, &document.network_interface).is_some();
Self {
layer: Some(layer),
transform,
@ -1243,7 +1242,7 @@ impl Fsm for GradientToolFsmState {
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue };
let transform = gradient_space_transform(layer, document);
let is_gradient_table = get_gradient_table(layer, &document.network_interface).is_some();
let is_gradient_table = get_gradient_stops(layer, &document.network_interface).is_some();
// Check for dragging a midpoint diamond
if drag_hint.is_none() {
@ -1385,7 +1384,7 @@ impl Fsm for GradientToolFsmState {
.network_interface
.selected_nodes()
.selected_visible_layers(&document.network_interface)
.find(|&layer| get_gradient_table(layer, &document.network_interface).is_some())
.find(|&layer| get_gradient_stops(layer, &document.network_interface).is_some())
});
// Apply the gradient to the selected layer
@ -1750,7 +1749,7 @@ fn apply_gradient_update(
// Only check for the gradient table once we know we'll write back, since this is a graph traversal per layer
// TODO: Drop the `Fill::Gradient` branch when all gradients become `Table<GradientStops>`
if get_gradient_table(layer, &context.document.network_interface).is_some() {
if get_gradient_stops(layer, &context.document.network_interface).is_some() {
dispatch_gradient_writes(layer, &gradient, responses);
} else {
responses.add(GraphOperationMessage::FillSet {
@ -1791,7 +1790,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
continue;
}
if get_gradient_table(layer, &context.document.network_interface).is_some() {
if get_gradient_stops(layer, &context.document.network_interface).is_some() {
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: stops.clone() });
} else if let Some(mut gradient) = get_gradient(layer, &context.document.network_interface) {
gradient.stops = stops.clone();
@ -1887,8 +1886,6 @@ mod test_gradient {
pub use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graph_craft::document::value::TaggedValue;
use graphene_std::ATTR_TRANSFORM;
use graphene_std::table::{Table, TableRow};
use graphene_std::vector::style::{Fill, Gradient};
use graphene_std::vector::{GradientStop, GradientStops, fill};
@ -1953,21 +1950,18 @@ mod test_gradient {
.handle_message(NodeGraphMessage::SetInputValue {
node_id: gradient_node_id,
input_index: 1,
value: TaggedValue::GradientTable(Table::new_from_row(
TableRow::new_from_element(GradientStops::new([
GradientStop {
position: 0.,
midpoint: 0.5,
color: Color::RED,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: Color::BLUE,
},
]))
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY),
)),
value: TaggedValue::Gradient(GradientStops::new([
GradientStop {
position: 0.,
midpoint: 0.5,
color: Color::RED,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: Color::BLUE,
},
])),
})
.await;

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())
}