// TODO: Eventually remove this document upgrade code // This file contains lots of hacky code for upgrading old documents to the new format use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; use crate::messages::prelude::DocumentMessageHandler; use glam::{DVec2, IVec2}; use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId}; use graph_craft::descriptor; use graph_craft::document::DocumentNode; use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue}; use graphene_std::Color; use graphene_std::NodeInputDecleration; use graphene_std::ProtoNodeIdentifier; use graphene_std::text::{TextAlign, TypesettingConfig}; use graphene_std::transform::ScaleType; use graphene_std::uuid::NodeId; use graphene_std::vector::graphic_types; use graphene_std::vector::style::{PaintOrder, StrokeAlign}; use std::collections::HashMap; use std::f64::consts::PI; use std::ops::Range; const TEXT_REPLACEMENTS: &[(&str, &str)] = &[ ("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePolylineNode"), ("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"), ("\"manual_composition\":null", "\"manual_composition\":{\"Generic\":\"T\"}"), ( "core::option::Option>", "core::option::Option>", ), ("graphene_core::transform::Footprint", "graphene_core::transform::Footprint"), ("\"OptionalF64\":", "\"F64\":"), ("\"path_bool_nodes::BooleanOperation\"", "\"vector_types::vector::misc::BooleanOperation\""), ("\"core_types::table::Table<", "\"core_types::list::List<"), // The `GradientStops` type was renamed to `Gradient`; stale stored output names are cleared so the display falls back to the live type name ("\"output_names\":[\"GradientStops\"]", "\"output_names\":[\"\"]"), ("vector_types::gradient::GradientStops", "vector_types::gradient::Gradient"), ]; pub struct NodeReplacement<'a> { node: ProtoNodeIdentifier, aliases: &'a [&'a str], } const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ // ================================ // blending // ================================ // The legacy combined "Blending" node was split into separate Blend Mode, Opacity (now also covers fill), and Clip nodes. // Old Blending references are remapped here to `blend_mode::IDENTIFIER` so the per-node migration in `migrate_node` can // detect a 5-input Blend Mode node and rewrite it as a chain (skipping any sub-node whose value is at the default and // not exposed/wired up). NodeReplacement { node: graphene_std::blending_nodes::blend_mode::IDENTIFIER, aliases: &[ "graphene_core::raster::BlendModeNode", "graphene_core::blending_nodes::BlendModeNode", "graphene_core::raster::BlendingNode", "graphene_core::blending_nodes::BlendingNode", "blending_nodes::BlendingNode", ], }, NodeReplacement { node: graphene_std::blending_nodes::opacity::IDENTIFIER, aliases: &["graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"], }, // ================================ // brush // ================================ NodeReplacement { node: graphene_std::brush::brush::blit::IDENTIFIER, aliases: &["graphene_brush::BlitNode", "graphene_std::brush::BlitNode", "graphene_brush::brush::BlitNode"], }, NodeReplacement { node: graphene_std::brush::brush::brush::IDENTIFIER, aliases: &["graphene_brush::BrushNode", "graphene_std::brush::BrushNode", "graphene_brush::brush::BrushNode"], }, NodeReplacement { node: graphene_std::brush::brush::brush_stamp_generator::IDENTIFIER, aliases: &[ "graphene_brush::BrushStampGeneratorNode", "graphene_std::brush::BrushStampGeneratorNode", "graphene_brush::brush::BrushStampGeneratorNode", ], }, // ================================ // gcore // ================================ NodeReplacement { node: graphene_std::animation::animation_time::IDENTIFIER, aliases: &["graphene_core::animation::AnimationTimeNode"], }, NodeReplacement { node: graphene_std::debug::clone::IDENTIFIER, aliases: &["graphene_core::ops::CloneNode"], }, NodeReplacement { node: graphene_std::extract_xy::extract_xy::IDENTIFIER, aliases: &["graphene_core::ops::ExtractXyNode"], }, NodeReplacement { node: graphene_std::ops::passthrough::IDENTIFIER, aliases: &[ "graphene_core::ops::IdentityNode", "graphene_core::transform::CullNode", "graphene_core::transform::BoundlessFootprintNode", "graphene_core::transform::FreezeRealTimeNode", "graphene_core::transform_nodes::BoundlessFootprintNode", "graphene_core::transform_nodes::FreezeRealTimeNode", "graphene_core::vector::SubpathSegmentLengthsNode", "core_types::vector::SubpathSegmentLengthsNode", ], }, NodeReplacement { node: graphene_std::memo::monitor::IDENTIFIER, aliases: &["graphene_core::memo::MonitorNode"], }, NodeReplacement { node: graphene_std::memo::memoize::IDENTIFIER, aliases: &["graphene_core::memo::MemoNode", "graphene_core::memo::ImpureMemoNode"], }, NodeReplacement { node: graphene_std::animation::real_time::IDENTIFIER, aliases: &["graphene_core::animation::RealTimeNode"], }, NodeReplacement { node: graphene_std::debug::size_of::IDENTIFIER, aliases: &["graphene_core::ops::SizeOfNode"], }, NodeReplacement { node: graphene_std::debug::some::IDENTIFIER, aliases: &["graphene_core::ops::SomeNode"], }, NodeReplacement { node: graphene_std::debug::unwrap_option::IDENTIFIER, aliases: &["graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"], }, // ================================ // graphic // ================================ NodeReplacement { node: graphene_std::artboard::create_artboard::IDENTIFIER, aliases: &[ "graphene_core::artboard::CreateArtboardNode", "graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode", "graphene_core::artboard::ToArtboardNode", ], }, NodeReplacement { node: graphene_std::graphic::extend::IDENTIFIER, aliases: &["graphene_core::graphic::graphic::ExtendNode", "graphene_core::graphic::ExtendNode"], }, NodeReplacement { node: graphene_std::graphic::flatten_graphic::IDENTIFIER, aliases: &[ "graphene_core::graphic::FlattenGraphicNode", "graphene_core::graphic_element::FlattenGroupNode", "graphene_core::graphic_types::FlattenGroupNode", ], }, NodeReplacement { node: graphene_std::graphic::flatten_vector::IDENTIFIER, aliases: &["graphene_core::graphic::FlattenVectorNode", "graphene_core::graphic_element::FlattenVectorNode"], }, NodeReplacement { node: graphene_std::graphic::index_elements::IDENTIFIER, aliases: &[ "graphene_core::graphic_element::IndexNode", "graphene_core::graphic::IndexNode", "graphene_core::graphic::IndexElementsNode", ], }, NodeReplacement { node: graphene_std::graphic::legacy_layer_extend::IDENTIFIER, aliases: &[ "graphene_core::graphic_element::LayerNode", "graphene_core::graphic_types::LayerNode", // Converted from "Append Artboard" "graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode", "graphene_core::graphic_types::AppendArtboardNode", "graphene_core::artboard::AppendArtboardNode", "graphene_core::graphic::LegacyLayerExtendNode", ], }, NodeReplacement { node: graphene_std::graphic::to_graphic::IDENTIFIER, aliases: &[ "graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode", "graphene_core::graphic_types::ToGroupNode", "graphene_core::graphic::ToGraphicNode", ], }, NodeReplacement { node: graphene_std::graphic::wrap_graphic::IDENTIFIER, aliases: &[ // Converted from "To Element" "graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode", "graphene_core::graphic_types::ToElementNode", "graphene_core::graphic::WrapGraphicNode", ], }, // ================================ // math // ================================ NodeReplacement { node: graphene_std::math_nodes::absolute_value::IDENTIFIER, aliases: &["graphene_math_nodes::AbsoluteValueNode", "graphene_core::ops::AbsoluteValueNode"], }, NodeReplacement { node: graphene_std::math_nodes::add::IDENTIFIER, aliases: &["graphene_math_nodes::AddNode", "graphene_core::ops::AddNode"], }, NodeReplacement { node: graphene_std::math_nodes::bool_value::IDENTIFIER, aliases: &["graphene_math_nodes::BoolValueNode", "graphene_core::ops::BoolValueNode"], }, NodeReplacement { node: graphene_std::math_nodes::ceiling::IDENTIFIER, aliases: &["graphene_math_nodes::CeilingNode", "graphene_core::ops::CeilingNode"], }, NodeReplacement { node: graphene_std::math_nodes::clamp::IDENTIFIER, aliases: &["graphene_math_nodes::ClampNode", "graphene_core::ops::ClampNode"], }, NodeReplacement { node: graphene_std::math_nodes::color_value::IDENTIFIER, aliases: &["graphene_math_nodes::ColorValueNode", "graphene_core::ops::ColorValueNode"], }, NodeReplacement { node: graphene_std::math_nodes::cosine::IDENTIFIER, aliases: &["graphene_math_nodes::CosineNode", "graphene_core::ops::CosineNode"], }, NodeReplacement { node: graphene_std::math_nodes::cosine_inverse::IDENTIFIER, aliases: &["graphene_math_nodes::CosineInverseNode", "graphene_core::ops::CosineInverseNode"], }, NodeReplacement { node: graphene_std::math_nodes::divide::IDENTIFIER, aliases: &["graphene_math_nodes::DivideNode", "graphene_core::ops::DivideNode"], }, NodeReplacement { node: graphene_std::math_nodes::dot_product::IDENTIFIER, aliases: &["graphene_math_nodes::DotProductNode", "graphene_core::ops::DotProductNode"], }, NodeReplacement { node: graphene_std::math_nodes::equals::IDENTIFIER, aliases: &["graphene_math_nodes::EqualsNode", "graphene_core::ops::EqualsNode"], }, NodeReplacement { node: graphene_std::math_nodes::exponent::IDENTIFIER, aliases: &["graphene_math_nodes::ExponentNode", "graphene_core::ops::ExponentNode"], }, NodeReplacement { node: graphene_std::math_nodes::floor::IDENTIFIER, aliases: &["graphene_math_nodes::FloorNode", "graphene_core::ops::FloorNode"], }, NodeReplacement { node: graphene_std::math_nodes::footprint_value::IDENTIFIER, aliases: &["graphene_math_nodes::FootprintValueNode", "graphene_core::ops::FootprintValueNode"], }, NodeReplacement { node: graphene_std::math_nodes::gradient_value::IDENTIFIER, aliases: &[ "graphene_math_nodes::GradientValueNode", "graphene_core::ops::GradientValueNode", "graphene_math_nodes::GradientTableValueNode", "graphene_core::ops::GradientTableValueNode", "math_nodes::GradientTableValueNode", ], }, NodeReplacement { node: graphene_std::math_nodes::greater_than::IDENTIFIER, aliases: &["graphene_math_nodes::GreaterThanNode", "graphene_core::ops::GreaterThanNode"], }, NodeReplacement { node: graphene_std::math_nodes::greatest_common_divisor::IDENTIFIER, aliases: &[ "graphene_math_nodes::GreatestCommonDivisor", "graphene_core::ops::GreatestCommonDivisor", "graphene_math_nodes::GreatestCommonDivisorNode", ], }, NodeReplacement { node: graphene_std::math_nodes::least_common_multiple::IDENTIFIER, aliases: &[ "graphene_math_nodes::LeastCommonMultiple", "graphene_core::ops::LeastCommonMultiple", "graphene_math_nodes::LeastCommonMultipleNode", ], }, NodeReplacement { node: graphene_std::math_nodes::length::IDENTIFIER, aliases: &["graphene_math_nodes::LengthNode", "graphene_core::ops::LenghtNode"], }, NodeReplacement { node: graphene_std::math_nodes::less_than::IDENTIFIER, aliases: &["graphene_math_nodes::LessThanNode", "graphene_core::ops::LessThanNode"], }, NodeReplacement { node: graphene_std::math_nodes::logarithm::IDENTIFIER, aliases: &["graphene_math_nodes::LogarithmNode", "graphene_core::ops::LogarithmNode"], }, NodeReplacement { node: graphene_std::math_nodes::logical_and::IDENTIFIER, aliases: &[ "graphene_core::ops::LogicalAndNode", "graphene_core::ops::LogicNotNode", "graphene_core::logic::LogicNotNode", "graphene_math_nodes::LogicalAndNode", ], }, NodeReplacement { node: graphene_std::math_nodes::logical_not::IDENTIFIER, aliases: &[ "graphene_core::ops::LogicalNotNode", "graphene_core::ops::LogicOrNode", "graphene_core::logic::LogicOrNode", "graphene_math_nodes::LogicalNotNode", ], }, NodeReplacement { node: graphene_std::math_nodes::logical_or::IDENTIFIER, aliases: &[ "graphene_core::ops::LogicalOrNode", "graphene_core::ops::LogicAndNode", "graphene_core::logic::LogicAndNode", "graphene_math_nodes::LogicalOrNode", ], }, NodeReplacement { node: graphene_std::math_nodes::math::IDENTIFIER, aliases: &["graphene_math_nodes::MathNode", "graphene_core::ops::MathNode"], }, NodeReplacement { node: graphene_std::math_nodes::max::IDENTIFIER, aliases: &["graphene_math_nodes::MaxNode", "graphene_core::ops::MaxNode"], }, NodeReplacement { node: graphene_std::math_nodes::min::IDENTIFIER, aliases: &["graphene_math_nodes::MinNode", "graphene_core::ops::MinNode"], }, NodeReplacement { node: graphene_std::math_nodes::modulo::IDENTIFIER, aliases: &["graphene_math_nodes::ModuloNode", "graphene_core::ops::ModuloNode"], }, NodeReplacement { node: graphene_std::math_nodes::multiply::IDENTIFIER, aliases: &["graphene_math_nodes::MultiplyNode", "graphene_core::ops::MultiplyNode"], }, NodeReplacement { node: graphene_std::math_nodes::normalize::IDENTIFIER, aliases: &["graphene_math_nodes::NormalizeNode"], }, NodeReplacement { node: graphene_std::math_nodes::not_equals::IDENTIFIER, aliases: &["graphene_math_nodes::NotEqualsNode", "graphene_core::ops::NotEqualsNode"], }, NodeReplacement { node: graphene_std::math_nodes::number_value::IDENTIFIER, aliases: &["graphene_math_nodes::NumberValueNode", "graphene_core::ops::NumberValueNode"], }, NodeReplacement { node: graphene_std::math_nodes::percentage_value::IDENTIFIER, aliases: &["graphene_math_nodes::PercentageValueNode", "graphene_core::ops::PercentageValueNode"], }, NodeReplacement { node: graphene_std::math_nodes::random::IDENTIFIER, aliases: &["graphene_math_nodes::RandomNode", "graphene_core::ops::RandomNode"], }, NodeReplacement { node: graphene_std::math_nodes::remap::IDENTIFIER, aliases: &["graphene_math_nodes::RemapNode", "graphene_core::ops::RemapNode"], }, NodeReplacement { node: graphene_std::math_nodes::root::IDENTIFIER, aliases: &["graphene_math_nodes::RootNode", "graphene_core::ops::RootNode"], }, NodeReplacement { node: graphene_std::math_nodes::round::IDENTIFIER, aliases: &["graphene_math_nodes::RoundNode", "graphene_core::ops::RoundNode"], }, NodeReplacement { node: graphene_std::math_nodes::sample_gradient::IDENTIFIER, aliases: &["graphene_math_nodes::SampleGradientNode", "graphene_core::ops::SampleGradientNode"], }, NodeReplacement { node: graphene_std::math_nodes::sine::IDENTIFIER, aliases: &["graphene_math_nodes::SineNode", "graphene_core::ops::SineNode"], }, NodeReplacement { node: graphene_std::math_nodes::sine_inverse::IDENTIFIER, aliases: &["graphene_math_nodes::SineInverseNode", "graphene_core::ops::SineInverseNode"], }, NodeReplacement { node: graphene_std::math_nodes::subtract::IDENTIFIER, aliases: &["graphene_math_nodes::SubtractNode", "graphene_core::ops::SubtractNode"], }, NodeReplacement { node: graphene_std::math_nodes::tangent::IDENTIFIER, aliases: &["graphene_math_nodes::TangentNode", "graphene_core::ops::TangentNode"], }, NodeReplacement { node: graphene_std::math_nodes::tangent_inverse::IDENTIFIER, aliases: &["graphene_math_nodes::TangentInverseNode", "graphene_core::ops::TangentInverseNode"], }, NodeReplacement { node: graphene_std::math_nodes::as_f_64::IDENTIFIER, aliases: &["graphene_math_nodes::ToF64Node", "graphene_core::ops::ToF64Node", "math_nodes::ToF64Node"], }, NodeReplacement { node: graphene_std::math_nodes::as_u_32::IDENTIFIER, aliases: &["graphene_math_nodes::ToU32Node", "graphene_core::ops::ToU32Node", "math_nodes::ToU32Node"], }, NodeReplacement { node: graphene_std::math_nodes::as_u_64::IDENTIFIER, aliases: &["graphene_math_nodes::ToU64Node", "graphene_core::ops::ToU64Node", "math_nodes::ToU64Node"], }, NodeReplacement { node: graphene_std::math_nodes::vec_2_value::IDENTIFIER, aliases: &[ "graphene_math_nodes::Vec2ValueNode", "graphene_core::ops::ConstructVector2", "graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode", "graphene_math_nodes::CoordinateValueNode", ], }, // ================================ // path bool // ================================ NodeReplacement { node: graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER, aliases: &["graphene_path_bool::BooleanOperationNode", "graphene_std::vector::BooleanOperationNode"], }, // ================================ // raster // ================================ NodeReplacement { node: graphene_std::raster_nodes::adjustments::black_and_white::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode", "graphene_core::raster::BlackAndWhiteNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::blending_nodes::mix::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::BlendNode", "raster_nodes::adjustments::BlendNode", "graphene_core::raster::adjustments::BlendNode", "graphene_core::raster::BlendNode", "graphene_raster_nodes::blending_nodes::BlendNode", "raster_nodes::blending_nodes::BlendNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::filter::blur::IDENTIFIER, aliases: &["graphene_raster_nodes::filter::BlurNode", "graphene_std::filter::BlurNode"], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::brightness_contrast::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::BrightnessContrastNode", "graphene_core::raster::adjustments::BrightnessContrastNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::brightness_contrast_classic::IDENTIFIER, aliases: &["graphene_raster_nodes::adjustments::BrightnessContrastClassicNode"], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::channel_mixer::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode", "graphene_core::raster::ChannelMixerNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::blending_nodes::color_overlay::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::ColorOverlayNode", "graphene_raster_nodes::generate_curves::ColorOverlayNode", "raster_nodes::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode", "raster_nodes::generate_curves::ColorOverlayNode", "graphene_raster_nodes::blending_nodes::ColorOverlayNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::std_nodes::combine_channels::IDENTIFIER, aliases: &["graphene_raster_nodes::std_nodes::CombineChannelsNode", "graphene_std::raster::CombineChannelsNode"], }, NodeReplacement { node: graphene_std::raster_nodes::dehaze::dehaze::IDENTIFIER, aliases: &["graphene_raster_nodes::dehaze::DehazeNode", "graphene_std::dehaze::DehazeNode"], }, NodeReplacement { node: graphene_std::raster_nodes::std_nodes::empty_image::IDENTIFIER, aliases: &["graphene_raster_nodes::std_nodes::EmptyImageNode", "graphene_std::raster::EmptyImageNode"], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::exposure::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::ExposureNode", "graphene_core::raster::adjustments::ExposureNode", "graphene_core::raster::ExposureNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::std_nodes::extend_image_to_bounds::IDENTIFIER, aliases: &["graphene_raster_nodes::std_nodes::ExtendImageToBoundsNode", "graphene_std::raster::ExtendImageToBoundsNode"], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::extract_channel::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::ExtractChannelNode", "graphene_core::raster::adjustments::ExtractChannelNode", "graphene_core::raster::ExtractChannelNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::gamma_correction::IDENTIFIER, aliases: &["graphene_raster_nodes::adjustments::GammaCorrectionNode", "graphene_core::raster::adjustments::GammaCorrectionNode"], }, NodeReplacement { node: graphene_std::raster_nodes::gradient_map::gradient_map::IDENTIFIER, aliases: &[ "graphene_raster_nodes::gradient_map::GradientMapNode", "graphene_raster_nodes::adjustments::GradientMapNode", "raster_nodes::gradient_map::GradientMapNode", "raster_nodes::adjustments::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode", "graphene_core::raster::GradientMapNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::hue_saturation::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::HueSaturationNode", "graphene_core::raster::adjustments::HueSaturationNode", "graphene_core::raster::HueSaturationNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::image_color_palette::image_color_palette::IDENTIFIER, aliases: &[ "graphene_raster_nodes::image_color_palette::ImageColorPaletteNode", "graphene_std::image_color_palette::ImageColorPaletteNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::std_nodes::image::IDENTIFIER, aliases: &[ "raster_nodes::std_nodes::ImageValueNode", "graphene_raster_nodes::std_nodes::ImageValueNode", "graphene_std::raster::ImageValueNode", "graphene_std::raster::ImageNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::invert::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::InvertNode", "graphene_core::raster::adjustments::InvertNode", "graphene_core::raster::InvertNode", "graphene_core::raster::InvertRGBNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::levels::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::LevelsNode", "graphene_core::raster::adjustments::LevelsNode", "graphene_core::raster::LevelsNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::luminance::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::LuminanceNode", "graphene_core::raster::adjustments::LuminanceNode", "graphene_core::raster::LuminanceNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::make_opaque::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::MakeOpaqueNode", "graphene_core::raster::adjustments::MakeOpaqueNode", "graphene_core::raster::ExtractOpaqueNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::std_nodes::mandelbrot::IDENTIFIER, aliases: &["graphene_raster_nodes::std_nodes::MandelbrotNode", "graphene_std::raster::MandelbrotNode"], }, NodeReplacement { node: graphene_std::raster_nodes::std_nodes::mask::IDENTIFIER, aliases: &["graphene_raster_nodes::std_nodes::MaskNode", "graphene_std::raster::MaskNode", "graphene_std::raster::MaskImageNode"], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::posterize::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::PosterizeNode", "graphene_core::raster::adjustments::PosterizeNode", "graphene_core::raster::PosterizeNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::std_nodes::noise_pattern::IDENTIFIER, aliases: &["graphene_raster_nodes::std_nodes::NoisePatternNode", "graphene_std::raster::NoisePatternNode"], }, NodeReplacement { node: graphene_std::raster_nodes::std_nodes::sample_image::IDENTIFIER, aliases: &[ "graphene_raster_nodes::std_nodes::SampleImageNode", "graphene_std::raster::SampleImageNode", "graphene_std::raster::SampleNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::selective_color::IDENTIFIER, aliases: &["graphene_raster_nodes::adjustments::SelectiveColorNode", "graphene_core::raster::adjustments::SelectiveColorNode"], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::threshold::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode", "graphene_core::raster::ThresholdNode", ], }, NodeReplacement { node: graphene_std::raster_nodes::adjustments::vibrance::IDENTIFIER, aliases: &[ "graphene_raster_nodes::adjustments::VibranceNode", "graphene_core::raster::adjustments::VibranceNode", "graphene_core::raster::VibranceNode", ], }, // ================================ // text // ================================ NodeReplacement { node: ProtoNodeIdentifier::new("graphene_std::text::TextNode"), aliases: &["graphene_core::text::text::TextNode", "graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"], }, NodeReplacement { node: graphene_std::text_nodes::string_value::IDENTIFIER, aliases: &["graphene_math_nodes::StringValueNode", "graphene_core::ops::StringValueNode", "math_nodes::StringValueNode"], }, NodeReplacement { node: graphene_std::text_nodes::string_concatenate::IDENTIFIER, aliases: &["graphene_core::logic::StringConcatenateNode"], }, NodeReplacement { node: graphene_std::text_nodes::string_length::IDENTIFIER, aliases: &["graphene_core::logic::StringLengthNode"], }, NodeReplacement { node: graphene_std::text_nodes::string_replace::IDENTIFIER, aliases: &["graphene_core::logic::StringReplaceNode"], }, NodeReplacement { node: graphene_std::text_nodes::string_slice::IDENTIFIER, aliases: &["graphene_core::logic::StringSliceNode"], }, NodeReplacement { node: graphene_std::text_nodes::string_split::IDENTIFIER, aliases: &["graphene_core::logic::StringSplitNode"], }, NodeReplacement { node: graphene_std::math_nodes::switch::IDENTIFIER, aliases: &["graphene_core::logic::SwitchNode"], }, NodeReplacement { node: graphene_std::text_nodes::as_string::IDENTIFIER, aliases: &["graphene_core::logic::ToStringNode", "text_nodes::ToStringNode"], }, NodeReplacement { node: graphene_std::text_nodes::json::query_json::IDENTIFIER, aliases: &["graphene_core::logic::JsonGetNode", "graphene_std::text_nodes::JsonGetNode"], }, NodeReplacement { node: graphene_std::text_nodes::serialize::IDENTIFIER, aliases: &["graphene_core::logic::SerializeNode"], }, // ================================ // transform // ================================ NodeReplacement { node: graphene_std::transform_nodes::decompose_rotation::IDENTIFIER, aliases: &[ "graphene_core::transform_nodes::RotationScaleNode", "graphene_core::transform::RotationScaleNode", "graphene_core::transform_nodes::DecomposeRotationNode", ], }, NodeReplacement { node: graphene_std::transform_nodes::decompose_scale::IDENTIFIER, aliases: &["graphene_core::transform_nodes::DecomposeScaleNode", "graphene_core::transform::DecomposeScaleNode"], }, NodeReplacement { node: graphene_std::transform_nodes::decompose_translation::IDENTIFIER, aliases: &["graphene_core::transform_nodes::DecomposeTranslationNode", "graphene_core::transform::DecomposeTranslationNode"], }, NodeReplacement { node: graphene_std::transform_nodes::extract_transform::IDENTIFIER, aliases: &[ "graphene_core::transform_nodes::ExtractTransformNode", "graphene_core::transform::ExtractTransformNode", "graphene_core::vector::ExtractTransformNode", ], }, NodeReplacement { node: graphene_std::transform_nodes::invert_transform::IDENTIFIER, aliases: &["graphene_core::transform_nodes::InvertTransformNode", "graphene_core::transform::InvertTransformNode"], }, NodeReplacement { node: graphene_std::transform_nodes::replace_transform::IDENTIFIER, aliases: &[ "graphene_core::transform_nodes::ReplaceTransformNode", "graphene_core::transform::SetTransformNode", "graphene_core::transform::ReplaceTransformNode", ], }, NodeReplacement { node: graphene_std::transform_nodes::transform::IDENTIFIER, aliases: &["graphene_core::transform_nodes::TransformNode", "graphene_core::transform::TransformNode"], }, // ================================ // vector // ================================ NodeReplacement { node: graphene_std::vector::apply_transform::IDENTIFIER, aliases: &["graphene_core::vector::ApplyTransformNode", "graphene_core::vector::vector_modification::ApplyTransformNode"], }, NodeReplacement { node: graphene_std::vector::area::IDENTIFIER, aliases: &["graphene_core::vector::AreaNode"], }, NodeReplacement { node: graphene_std::vector::assign_colors::IDENTIFIER, aliases: &["graphene_core::vector::AssignColorsNode"], }, NodeReplacement { node: graphene_std::vector::auto_tangents::IDENTIFIER, aliases: &["graphene_core::vector::vector_nodes::AutoTangentsNode", "graphene_core::vector::AutoTangentsNode"], }, NodeReplacement { node: graphene_std::vector::bevel::IDENTIFIER, aliases: &["graphene_core::vector::BevelNode"], }, NodeReplacement { node: graphene_std::vector::bounding_box::IDENTIFIER, aliases: &["graphene_core::vector::BoundingBoxNode"], }, NodeReplacement { node: graphene_std::vector::box_warp::IDENTIFIER, aliases: &["graphene_core::vector::BoxWarpNode"], }, NodeReplacement { node: graphene_std::vector::centroid::IDENTIFIER, aliases: &["graphene_core::vector::CentroidNode"], }, NodeReplacement { node: graphene_std::vector::close_path::IDENTIFIER, aliases: &["graphene_core::vector::ClosePathNode"], }, NodeReplacement { node: graphene_std::vector::count_elements::IDENTIFIER, aliases: &["graphene_core::vector::CountElementsNode"], }, NodeReplacement { node: graphene_std::vector::cut_path::IDENTIFIER, aliases: &["graphene_core::vector::vector_nodes::SplitPathNode", "graphene_core::vector::SplitPathNode"], }, NodeReplacement { node: graphene_std::vector::cut_segments::IDENTIFIER, aliases: &[ "graphene_core::vector::vector_nodes::SplitSegmentsNode", "graphene_core::vector::SplitSegmentsNode", "graphene_core::vector::CutSegmentsNode", ], }, NodeReplacement { node: graphene_std::vector::dimensions::IDENTIFIER, aliases: &["graphene_core::vector::DimensionsNode"], }, NodeReplacement { node: graphene_std::vector_nodes::fill::IDENTIFIER, aliases: &["graphene_core::vector::vector_nodes::FillNode", "graphene_core::vector::FillNode"], }, NodeReplacement { node: graphene_std::vector::flatten_path::IDENTIFIER, aliases: &[ "graphene_core::vector::vector_nodes::FlattenPathNode", "graphene_core::vector::FlattenVectorElementsNode", "graphene_core::vector::FlattenPathNode", ], }, NodeReplacement { node: graphene_std::vector::generator_nodes::arc::IDENTIFIER, aliases: &["graphene_core::vector::generator_nodes::ArcNode"], }, NodeReplacement { node: graphene_std::vector::generator_nodes::circle::IDENTIFIER, aliases: &["graphene_core::vector::generator_nodes::CircleNode"], }, NodeReplacement { node: graphene_std::vector::generator_nodes::ellipse::IDENTIFIER, aliases: &["graphene_core::vector::generator_nodes::EllipseNode"], }, NodeReplacement { node: graphene_std::vector::generator_nodes::grid::IDENTIFIER, aliases: &["graphene_core::vector::generator_nodes::GridNode"], }, NodeReplacement { node: graphene_std::vector::generator_nodes::line::IDENTIFIER, aliases: &["graphene_core::vector::generator_nodes::LineNode"], }, NodeReplacement { node: graphene_std::vector::generator_nodes::rectangle::IDENTIFIER, aliases: &["graphene_core::vector::generator_nodes::RectangleNode"], }, NodeReplacement { node: graphene_std::vector::generator_nodes::regular_polygon::IDENTIFIER, aliases: &["graphene_core::vector::generator_nodes::RegularPolygonNode"], }, NodeReplacement { node: graphene_std::vector::generator_nodes::spiral::IDENTIFIER, aliases: &["graphene_core::vector::generator_nodes::SpiralNode"], }, NodeReplacement { node: graphene_std::vector::generator_nodes::star::IDENTIFIER, aliases: &["graphene_core::vector::generator_nodes::StarNode"], }, NodeReplacement { node: graphene_std::context::read_index::IDENTIFIER, aliases: &["graphene_core::vector::InstanceIndexNode", "core_types::vector::InstanceIndexNode"], }, NodeReplacement { node: graphene_std::graphic::map::IDENTIFIER, aliases: &["graphene_core::vector::InstanceMapNode"], }, NodeReplacement { node: graphene_std::context::read_position::IDENTIFIER, aliases: &["graphene_core::vector::InstancePositionNode", "core_types::vector::InstancePositionNode"], }, NodeReplacement { node: graphene_std::context::read_vector::IDENTIFIER, aliases: &["graphene_core::vector::InstanceVectorNode"], }, NodeReplacement { node: graphene_std::repeat::repeat::IDENTIFIER, aliases: &["graphene_core::vector::InstanceRepeatNode", "core_types::vector::InstanceRepeatNode"], }, NodeReplacement { node: graphene_std::repeat::repeat_array::IDENTIFIER, aliases: &["graphene_core::vector::RepeatNode", "core_types::vector::RepeatNode"], }, NodeReplacement { node: graphene_std::repeat::repeat_radial::IDENTIFIER, aliases: &["graphene_core::vector::CircularRepeatNode", "core_types::vector::CircularRepeatNode"], }, NodeReplacement { node: graphene_std::repeat::repeat_on_points::IDENTIFIER, aliases: &["graphene_core::vector::InstanceOnPointsNode", "core_types::vector::InstanceOnPointsNode"], }, NodeReplacement { node: graphene_std::vector::copy_to_points::IDENTIFIER, aliases: &["graphene_core::vector::CopyToPointsNode", "core_types::vector::CopyToPointsNode"], }, NodeReplacement { node: graphene_std::vector::jitter_points::IDENTIFIER, aliases: &["graphene_core::vector::JitterPointsNode"], }, NodeReplacement { node: graphene_std::vector::merge_by_distance::IDENTIFIER, aliases: &["graphene_core::vector::MergeByDistanceNode"], }, NodeReplacement { node: graphene_std::graphic::mirror::IDENTIFIER, aliases: &["graphene_core::vector::MirrorNode", "core_types::vector::MirrorNode"], }, NodeReplacement { node: graphene_std::vector::morph::IDENTIFIER, aliases: &["graphene_core::vector::MorphNode"], }, NodeReplacement { node: graphene_std::vector::offset_path::IDENTIFIER, aliases: &["graphene_core::vector::OffsetPathNode"], }, NodeReplacement { node: graphene_std::vector::path_length::IDENTIFIER, aliases: &["graphene_core::vector::PathLengthNode"], }, NodeReplacement { node: graphene_std::vector::path_modify::IDENTIFIER, aliases: &[ "graphene_core::vector::vector_modification_nodes::PathModifyNode", "graphene_core::vector::vector_data::modification::PathModifyNode", "graphene_core::vector::vector_modification::PathModifyNode", ], }, NodeReplacement { node: graphene_std::vector::point_inside::IDENTIFIER, aliases: &["graphene_core::vector::PointInsideNode"], }, NodeReplacement { node: graphene_std::vector::points_to_polyline::IDENTIFIER, aliases: &["graphene_core::vector::PointsToPolylineNode"], }, NodeReplacement { node: graphene_std::vector::scatter_points::IDENTIFIER, aliases: &["graphene_core::vector::PoissonDiskPointsNode", "core_types::vector::PoissonDiskPointsNode"], }, NodeReplacement { node: graphene_std::vector::position_on_path::IDENTIFIER, aliases: &["graphene_core::vector::PositionOnPathNode"], }, NodeReplacement { node: graphene_std::vector::round_corners::IDENTIFIER, aliases: &["graphene_core::vector::RoundCornersNode"], }, NodeReplacement { node: graphene_std::vector::sample_polyline::IDENTIFIER, aliases: &[ "graphene_core::vector::SamplePolylineNode", "graphene_core::vector::SamplePointsNode", "graphene_core::vector::vector_nodes::SamplePointsNode", ], }, NodeReplacement { node: graphene_std::vector::separate_subpaths::IDENTIFIER, aliases: &["graphene_core::vector::SeparateSubpathsNode"], }, NodeReplacement { node: graphene_std::vector::solidify_stroke::IDENTIFIER, aliases: &["graphene_core::vector::SolidifyStrokeNode"], }, NodeReplacement { node: graphene_std::vector::spline::IDENTIFIER, aliases: &[ "graphene_core::vector::vector_nodes::SplineNode", "graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode", ], }, NodeReplacement { node: graphene_std::vector::stroke::IDENTIFIER, aliases: &["graphene_core::vector::StrokeNode"], }, NodeReplacement { node: graphene_std::vector::tangent_on_path::IDENTIFIER, aliases: &["graphene_core::vector::TangentOnPathNode"], }, NodeReplacement { node: graphene_std::vector::as_vector::IDENTIFIER, aliases: &[ "graphene_core::vector::vector_nodes::PositionToPointNode", "graphene_core::vector::PositionToPointNode", "graphene_core::vector::Vec2ToPointNode", "core_types::vector::Vec2ToPointNode", ], }, ]; const REPLACEMENTS: &[(&str, &str)] = &[]; pub fn document_migration_string_preprocessing(document_serialized_content: String) -> String { let document_serialized_content = replace_optional_f64_null(&document_serialized_content); TEXT_REPLACEMENTS .iter() .fold(document_serialized_content, |document_serialized_content, (old, new)| document_serialized_content.replace(old, new)) } /// Rebuilds the old 13-input "Text" node template from the current `text` template plus the trailing `separate_glyphs` input it dropped, /// so the staged input-count migrations can still upgrade old text nodes before the split. fn legacy_text_node_template() -> Option { let mut template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?.default_node_template(); template.document_node.implementation = DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")); template.document_node.inputs.push(NodeInput::value(TaggedValue::Bool(false), false)); template.persistent_node_metadata.input_metadata.push(Default::default()); Some(template) } fn replace_optional_f64_null(input: &str) -> String { let mut result = String::new(); let mut last_end = 0; let key = "\"OptionalF64\":"; for (start, _) in input.match_indices(key) { let search_start = start + key.len(); if search_start >= input.len() { continue; } let mut after_key_start = search_start; for (i, c) in input[search_start..].char_indices() { if !c.is_whitespace() { after_key_start = search_start + i; break; } // If we reach the end and it's all whitespace, update after_key_start if search_start + i + c.len_utf8() == input.len() { after_key_start = input.len(); } } if input[after_key_start..].starts_with("null") { result.push_str(&input[last_end..start]); result.push_str(key); result.push_str("0.0"); last_end = after_key_start + "null".len(); } } result.push_str(&input[last_end..]); result } pub fn document_migration_reset_node_definition(document_serialized_content: &str) -> bool { // Upgrade a document being opened to use fresh copies of all nodes if document_serialized_content.contains("node_output_index") { return true; } // Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946 (see also `fn fix_nodes()` in `main.rs` of Graphene CLI) if document_serialized_content.contains("graphene_core::ConstructLayerNode") || document_serialized_content.contains("graphene_core::AddArtboardNode") { return true; } // The `source_node_id` proto node was removed in favor of `path_of_subgraph` + `write_attribute`. // Documents that still reference it inside their Merge or Artboard layer networks need those layer definitions // reset to the current default so the new internal plumbing replaces the obsolete node. if document_serialized_content.contains("graphic_nodes::graphic::SourceNodeIdNode") || document_serialized_content.contains("graphene_core::graphic::graphic::SourceNodeIdNode") || document_serialized_content.contains("graphene_core::graphic::SourceNodeIdNode") { return true; } false } pub fn document_migration_replace_resources_referenced_by_hash(document_serialized_content: String) -> (String, HashMap) { fn collect_resources_referenced_by_hash(s: &str) -> HashMap>> { let mut out: HashMap>> = HashMap::new(); let mut offset = 0; while let Some(i) = s[offset..].find("\"Resource\":") { let abs = offset + i + 11; // pos after `"Resource":` offset = abs; if let Some(j) = s[offset..].find('}') { let chunk_start = offset; let chunk = &s[chunk_start..chunk_start + j]; let quotes: Vec<_> = chunk.match_indices('"').collect(); if quotes.len() == 2 { let q0 = chunk_start + quotes[0].0; let q1 = chunk_start + quotes[1].0; let hash = &s[q0 + 1..q1]; if hash.len() == 64 && hash.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) && let Ok(hash) = hash.parse::() { out.entry(hash).or_default().push(q0..q1 + 1); } } offset = chunk_start + j + 1; } else { break; } } out } let resources_by_hash = collect_resources_referenced_by_hash(&document_serialized_content); // Require each referenced hash to also appear as an embedded resource map key (more than ranges.len() occurrences). if resources_by_hash.is_empty() || !resources_by_hash .iter() .all(|(hash, ranges)| document_serialized_content.matches(&hash.to_string()).count() > ranges.len()) { return (document_serialized_content, HashMap::new()); } // Assign a new ResourceId for each hash let mut hash_to_id: HashMap = HashMap::new(); for hash in resources_by_hash.keys() { #[allow(clippy::unwrap_or_default)] hash_to_id.entry(*hash).or_insert_with(ResourceId::new); } // Each range is 66 bytes (64 hex + 2 quotes) and a ResourceId serializes to at most 20 ASCII digits, so the ID always fits. // Overwrite each hash in place with its ID and pad the leftover bytes with spaces, which JSON deserialization discards. let mut bytes = document_serialized_content.into_bytes(); for (hash, ranges) in &resources_by_hash { let id_str = format!("{}", hash_to_id[hash]); let id_bytes = id_str.as_bytes(); for range in ranges { bytes[range.start..range.start + id_bytes.len()].copy_from_slice(id_bytes); bytes[range.start + id_bytes.len()..range.end].fill(b' '); } } let out = String::from_utf8(bytes).expect("in-place hash-to-ID rewrite produced invalid UTF-8"); (out, hash_to_id) } pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) { document.network_interface.migrate_path_modify_node(); let network = document.network_interface.document_network().clone(); // Apply string and node replacements to each node let mut replacements = HashMap::<&str, ProtoNodeIdentifier>::new(); Iterator::chain( NODE_REPLACEMENTS.iter().flat_map(|NodeReplacement { node, aliases }| aliases.iter().map(|old| (*old, node.clone()))), REPLACEMENTS.iter().map(|(old, new)| (*old, ProtoNodeIdentifier::new(new))), ) .for_each(|(old, new)| { if replacements.insert(old, new).is_some() { panic!("Duplicate old name `{old}`"); } }); for (node_id, node, network_path) in network.recursive_nodes() { if let DocumentNodeImplementation::ProtoNode(protonode_id) = &node.implementation { let node_path_without_type_args = protonode_id.as_str().split('<').next(); if let Some(new) = node_path_without_type_args.and_then(|node_path| replacements.get(node_path)) { let mut default_template = NodeTemplate::default(); default_template.document_node.implementation = DocumentNodeImplementation::ProtoNode(new.clone()); document.network_interface.replace_implementation(node_id, &network_path, &mut default_template); document.network_interface.set_call_argument(node_id, &network_path, default_template.document_node.call_argument); } } } // The "Brush" wrapper network was replaced with the `brush` proto node directly. Convert old `Network("Brush")` instances to the proto node, forwarding all 3 inputs (Background, Trace, Cache) one-to-one. // This must run as a pre-pass before the recursive iteration below: replacing the outer Brush's network impl orphans its child paths, and the recursive iteration would log errors for those stale paths. let brush_layers: Vec<(NodeId, Vec)> = document .network_interface .document_network() .recursive_nodes() .filter_map(|(node_id, _, path)| (document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::Network("Brush".into()))).then_some((*node_id, path))) .collect(); for (node_id, network_path) in &brush_layers { // Pre-load `outward_wires` so the chain-break check inside `set_input` resolves the original upstream→node wire from cache // rather than triggering a fresh rebuild from the (already-mutated) post-`replace_inputs` state, which would orphan wires. let _ = document.network_interface.outward_wires(network_path); let new_reference = DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER); let Some(definition) = resolve_document_node_type(&new_reference) else { continue }; let mut node_template = definition.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let Some(old_inputs) = document.network_interface.replace_inputs(node_id, network_path, &mut node_template) else { continue; }; for (index, input) in old_inputs.iter().take(3).enumerate() { document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path); } } // The "Transform" wrapper network was replaced with the `transform` proto node directly. Convert old `Network("Transform")` instances to the proto node, forwarding the 5 user-facing inputs (Value, Translation, Rotation, Scale, Skew) and dropping the legacy migration sentinels (Origin Offset, Scale Appearance) at indices 5 and 6 if present. // Pre-pass for the same reason as the Brush migration above: replacing the outer Transform's network impl orphans its child paths. let transform_layers: Vec<(NodeId, Vec, usize)> = document .network_interface .document_network() .recursive_nodes() .filter_map(|(node_id, node, path)| { (document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::Network("Transform".into()))).then_some((*node_id, path, node.inputs.len())) }) .collect(); for (node_id, network_path, old_inputs_count) in &transform_layers { // Pre-load `outward_wires` so the chain-break check inside `set_input` resolves the original upstream→node wire from cache // rather than triggering a fresh rebuild from the (already-mutated) post-`replace_inputs` state, which would orphan wires. let _ = document.network_interface.outward_wires(network_path); let new_reference = DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::transform::IDENTIFIER); let Some(definition) = resolve_document_node_type(&new_reference) else { continue }; let mut node_template = definition.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let Some(old_inputs) = document.network_interface.replace_inputs(node_id, network_path, &mut node_template) else { continue; }; // Forward the first 5 inputs (Value, Translation, Rotation, Scale, Skew); drop indices 5 and 6 if present. for (index, input) in old_inputs.iter().take(5).enumerate() { document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path); } // Pre-2024 documents stored Transform with 6 inputs and used radians for Rotation and `tan(radians)` for Skew. Detect that legacy // shape (no input at index 6) and convert the units to degrees so the values match what the new Properties panel widgets expect. if *old_inputs_count == 6 { match old_inputs.get(2) { Some(NodeInput::Value { tagged_value, exposed }) => { if let TaggedValue::F64(radians) = *tagged_value.clone().into_inner() { let degrees = NodeInput::value(TaggedValue::F64(radians.to_degrees()), *exposed); document.network_interface.set_input(&InputConnector::node(*node_id, 2), degrees, network_path); } } Some(NodeInput::Node { .. }) => { // Wired upstream: splice in a Multiply node by 180/π that converts radians to degrees so the upstream value // (which represented radians in the legacy format) reaches the now-degrees Rotation input correctly. if let Some(multiply_node) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::multiply::IDENTIFIER)) { let mut multiply_template = multiply_node.default_node_template(); multiply_template.document_node.inputs[1] = NodeInput::value(TaggedValue::F64(180. / PI), false); let multiply_node_id = NodeId::new(); if let Some(transform_position) = document.network_interface.position_from_downstream_node(node_id, network_path) { let multiply_position = transform_position + IVec2::new(-7, 1); document.network_interface.insert_node(multiply_node_id, multiply_template, network_path); document.network_interface.shift_absolute_node_position(&multiply_node_id, multiply_position, network_path); document.network_interface.insert_node_between(&multiply_node_id, &InputConnector::node(*node_id, 2), 0, network_path); } } } _ => {} } if let Some(NodeInput::Value { tagged_value, exposed }) = old_inputs.get(4) && let TaggedValue::DVec2(old_value) = *tagged_value.clone().into_inner() { // The previous skew value stored `tan(radians)`, now it stores degrees directly. let new_value = DVec2::new(old_value.x.atan().to_degrees(), old_value.y.atan().to_degrees()); let new_input = NodeInput::value(TaggedValue::DVec2(new_value), *exposed); document.network_interface.set_input(&InputConnector::node(*node_id, 4), new_input, network_path); } } } // The "Image" wrapper network was replaced with the `image` proto node directly. Convert old `Network("Image")` instances to the proto node, forwarding the embedded image data so the `image` pass in `migrate_node` below turns it into a resource. // Pre-pass for the same reason as the Brush and Transform migrations above: replacing the outer Image's network impl orphans its child paths. let image_layers: Vec<(NodeId, Vec)> = document .network_interface .document_network() .recursive_nodes() .filter_map(|(node_id, _, path)| (document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::Network("Image".into()))).then_some((*node_id, path))) .collect(); for (node_id, network_path) in &image_layers { let _ = document.network_interface.outward_wires(network_path); let new_reference = DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER); let Some(definition) = resolve_document_node_type(&new_reference) else { continue }; let mut node_template = definition.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let Some(old_inputs) = document.network_interface.replace_inputs(node_id, network_path, &mut node_template) else { continue; }; // Forward the embedded image data into input 0, where the `migrate_node` `image` pass finds it and converts it to a resource. if let Some(image_input) = old_inputs.into_iter().find(|input| matches!(input.as_value(), Some(TaggedValue::ImageData(_)))) { document.network_interface.set_input(&InputConnector::node(*node_id, 0), image_input, network_path); } } // Record which old text nodes are chain-positioned now, before `migrate_node`'s staged input-count migrations run, since those set // the upstream chain to absolute; the split below re-chains exactly the nodes that were originally part of a layer chain. let text_nodes_in_chain: std::collections::HashSet = document .network_interface .document_network() .recursive_nodes() .filter_map(|(node_id, _, path)| { (document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode"))) && document.network_interface.is_chain(node_id, &path)) .then_some(*node_id) }) .collect(); // Apply upgrades to each unmodified node. let nodes = document .network_interface .document_network() .recursive_nodes() .map(|(node_id, node, path)| (*node_id, node.clone(), path)) .collect::)>>(); for (node_id, node, network_path) in &nodes { migrate_node(node_id, node, network_path, document, reset_node_definitions_on_open); } // The old geometry-producing "Text" node was split into the current "Text" (`String[]`) -> "Text to Vector" pair, which reuses the same // proto identifier. Runs after `migrate_node` normalizes old text nodes to the legacy 13-input layout, distinguished from the current // 12-input node by the trailing `separate_glyphs` input (index 12): forward inputs 0..=11 onto the new node and move it onto `text_to_vector`. let old_text_nodes: Vec<(NodeId, Vec)> = document .network_interface .document_network() .recursive_nodes() .filter_map(|(node_id, node, path)| { // `separate_glyphs` is a `Bool` value or a wire feeding one; only a different value type there means a newer input, not the old node let has_legacy_separate_glyphs = node.inputs.len() == 13 && node.inputs.get(12).is_some_and(|input| matches!(input.as_value(), None | Some(TaggedValue::Bool(_)))); (has_legacy_separate_glyphs && document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")))) .then_some((*node_id, path)) }) .collect(); for (node_id, network_path) in &old_text_nodes { // Pre-load `outward_wires` so the splice below resolves the original downstream wiring from cache rather than a mutated state. let _ = document.network_interface.outward_wires(network_path); // Convert the old node in place to the current `text` node (12 inputs), capturing its old inputs. let Some(text_definition) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) else { continue; }; let mut text_template = text_definition.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut text_template); let Some(old_inputs) = document.network_interface.replace_inputs(node_id, network_path, &mut text_template) else { continue; }; // The current `text` node reorders the legacy inputs (Letter Tilt moved up to sit right after Letter Spacing), so map each new // input index to the legacy 13-input index it sources from. Legacy order: // [primary, text, font, size, line_height, letter_spacing, has_max_width, max_width, has_max_height, max_height, letter_tilt, align, separate_glyphs]. const LEGACY_INPUT_FOR_NEW: [usize; 12] = [0, 1, 2, 3, 4, 5, 10, 6, 7, 8, 9, 11]; for (new_index, &legacy_index) in LEGACY_INPUT_FOR_NEW.iter().enumerate() { if let Some(input) = old_inputs.get(legacy_index) { document.network_interface.set_input(&InputConnector::node(*node_id, new_index), input.clone(), network_path); } } let separate_glyphs = old_inputs.get(12).cloned(); // Collect the inputs reading the old text node's output before any rewiring so the new node can be spliced onto those wires. let downstream_consumers: Vec = document .network_interface .outward_wires(network_path) .and_then(|wires| wires.get(&OutputConnector::node(*node_id, 0))) .cloned() .unwrap_or_default(); let text_was_in_chain = text_nodes_in_chain.contains(node_id); // Insert the `text_to_vector` node that converts the `text` `String[]` output back into vector geometry. let Some(text_to_vector_definition) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_to_vector::IDENTIFIER)) else { continue; }; let text_to_vector_id = NodeId::new(); document .network_interface .insert_node(text_to_vector_id, text_to_vector_definition.default_node_template(), network_path); // Splice `text_to_vector` onto the wire(s) leaving `text` (`insert_node_between` is the pure wire-splice the editor uses for // dropping a node on a wire), then carry the old `separate_glyphs` value onto its second input. if let Some((first_consumer, remaining_consumers)) = downstream_consumers.split_first() { document.network_interface.insert_node_between(&text_to_vector_id, first_consumer, 0, network_path); for consumer in remaining_consumers { document.network_interface.set_input(consumer, NodeInput::node(text_to_vector_id, 0), network_path); } } else { document .network_interface .set_input(&InputConnector::node(text_to_vector_id, 0), NodeInput::node(*node_id, 0), network_path); } if let Some(separate_glyphs) = separate_glyphs { document.network_interface.set_input(&InputConnector::node(text_to_vector_id, 1), separate_glyphs, network_path); } // If `text` was in a layer chain, re-chain `text_to_vector` and its upstream so both lay out by distance from the layer (the splice // broke the chain, like `move_node_to_chain_start`). Otherwise `text` is absolute, so place `text_to_vector` beside it instead of // leaving it at the origin. if text_was_in_chain { document.network_interface.force_set_upstream_to_chain(&text_to_vector_id, network_path); } else if let Some(text_position) = document.network_interface.position(node_id, network_path) { document .network_interface .shift_absolute_node_position(&text_to_vector_id, text_position + IVec2::new(7, 0), network_path); } } } fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) -> Option<()> { // Must run before the reset block below: a node referencing a removed catalog entry would otherwise abort // `migrate_node` via the `?` on `resolve_document_node_type`, preventing subsequent migration blocks from running. migrate_removed_catalog_definitions(node_id, node, network_path, document); if reset_node_definitions_on_open && let Some(reference) = document.network_interface.reference(node_id, network_path) { let node_definition = resolve_document_node_type(&reference)?; document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template()); // The leveled-records flip moved the Copy to Points content wire ahead of the points wire. if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::copy_to_points::IDENTIFIER) { let mut node_template = node_definition.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[0].clone(), network_path); for (index, input) in old_inputs.into_iter().enumerate().skip(2) { document.network_interface.set_input(&InputConnector::node(*node_id, index), input, network_path); } } // The leveled-records flip moved the Repeat on Points content wire ahead of the points wire. if reference == DefinitionIdentifier::ProtoNode(graphene_std::repeat::repeat_on_points::IDENTIFIER) { let mut node_template = node_definition.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[0].clone(), network_path); for (index, input) in old_inputs.into_iter().enumerate().skip(2) { document.network_interface.set_input(&InputConnector::node(*node_id, index), input, network_path); } } // The leveled-records flip gave Mandelbrot a unit primary input. if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::mandelbrot::IDENTIFIER) { let mut node_template = node_definition.default_node_template(); document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; } } // Rebuild stale Merge/Artboard subgraphs that still use the removed LegacyLayerExtendNode internally if let DocumentNodeImplementation::Network(inner) = &node.implementation && inner .nodes .values() .any(|n| matches!(&n.implementation, DocumentNodeImplementation::ProtoNode(id) if id.as_str().contains("LegacyLayerExtend") || id.as_str().contains("legacy_layer_extend"))) && let Some(reference) = document.network_interface.reference(node_id, network_path) && let Some(node_definition) = resolve_document_node_type(&reference) { document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template()); } // Rebuild stale Rasterize subgraphs from before the async-source conversion gave the inner proto node a unit primary input (5 inputs, now 6). if let DocumentNodeImplementation::Network(inner) = &node.implementation && inner .nodes .values() .any(|n| n.inputs.len() == 5 && matches!(&n.implementation, DocumentNodeImplementation::ProtoNode(id) if id.as_str().contains("rasterize"))) && document.network_interface.reference(node_id, network_path) == Some(DefinitionIdentifier::Network("Rasterize".into())) && let Some(node_definition) = resolve_document_node_type(&DefinitionIdentifier::Network("Rasterize".into())) { document.network_interface.replace_implementation(node_id, network_path, &mut node_definition.default_node_template()); } // Upgrade old nodes to use `Context` instead of `()` or `Footprint` as their call argument if node.call_argument == graph_craft::concrete!(()) || node.call_argument == graph_craft::concrete!(graphene_std::transform::Footprint) { document.network_interface.set_call_argument(node_id, network_path, graph_craft::concrete!(graphene_std::Context)); } // Only nodes that have not been modified and still refer to a definition can be updated let reference = document.network_interface.reference(node_id, network_path)?; let mut inputs_count = node.inputs.len(); // Split the legacy combined "Blending" node into a chain of separate Blend Mode, Opacity (now also covers fill), and Clip nodes. // `NODE_REPLACEMENTS` rewrites the old `Blending` proto identifier (and its older aliases) to `blend_mode::IDENTIFIER`, so a leftover // 5-input shape on a Blend Mode node identifies an old Blending node that still needs structural splitting. Sub-nodes whose values // were at the default and weren't exposed/wired up are skipped, and the existing node ID is reused for the first kept sub-node so // any references elsewhere in the document survive. If everything would be skipped, a no-op default Blend Mode node is left behind // to preserve the chain structure (the user can delete it manually). if reference == DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::blend_mode::IDENTIFIER) && inputs_count == 5 { use graphene_std::blending::BlendMode; // Snapshot the old inputs before any rewriting let old_inputs = node.inputs.clone(); let content_input = old_inputs[0].clone(); let blend_mode_input = old_inputs[1].clone(); let opacity_input = old_inputs[2].clone(); let fill_input = old_inputs[3].clone(); let clip_input = old_inputs[4].clone(); // A sub-node is kept if its input is exposed/wired (so the user can drive it from the graph) OR if its value differs from the default let keep_blend_mode = blend_mode_input.is_exposed() || !matches!(blend_mode_input.as_value(), Some(TaggedValue::BlendMode(BlendMode::Normal))); let keep_opacity = opacity_input.is_exposed() || !matches!(opacity_input.as_value(), Some(TaggedValue::F64(v)) if (*v - 100.).abs() < f64::EPSILON); let keep_fill = fill_input.is_exposed() || !matches!(fill_input.as_value(), Some(TaggedValue::F64(v)) if (*v - 100.).abs() < f64::EPSILON); let keep_clip = clip_input.is_exposed() || !matches!(clip_input.as_value(), Some(TaggedValue::Bool(false))); // Find the downstream connection so we can chain new nodes between this node and downstream let downstream = document .network_interface .outward_wires(network_path) .and_then(|wires| wires.get(&OutputConnector::node(*node_id, 0))) .and_then(|connections| connections.first().cloned()); // The position of the existing node; subsequent chain nodes are placed to the right let base_position = document.network_interface.position(node_id, network_path).unwrap_or_default(); // Decide which sub-node the existing ID becomes (the first one in the chain order: Blend Mode, Opacity, Clip) #[derive(Clone, Copy, PartialEq)] enum SubNode { BlendMode, Opacity, Clip, } let first_kind = if keep_blend_mode { SubNode::BlendMode } else if keep_opacity || keep_fill { SubNode::Opacity } else if keep_clip { SubNode::Clip } else { // Everything was at default and not exposed: leave a no-op Blend Mode node so the chain structure is preserved SubNode::BlendMode }; let identifier_for = |kind: SubNode| match kind { SubNode::BlendMode => graphene_std::blending_nodes::blend_mode::IDENTIFIER, SubNode::Opacity => graphene_std::blending_nodes::opacity::IDENTIFIER, SubNode::Clip => graphene_std::blending_nodes::clipping_mask::IDENTIFIER, }; // Replace the existing node's implementation with the first kept sub-node's template let first_reference = DefinitionIdentifier::ProtoNode(identifier_for(first_kind)); let mut first_template = resolve_document_node_type(&first_reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut first_template); let _ = document.network_interface.replace_inputs(node_id, network_path, &mut first_template); // Wire the existing node's inputs based on its new role (input 0 is always `content`) document.network_interface.set_input(&InputConnector::node(*node_id, 0), content_input, network_path); match first_kind { SubNode::BlendMode => { document.network_interface.set_input(&InputConnector::node(*node_id, 1), blend_mode_input.clone(), network_path); } SubNode::Opacity => { document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(keep_opacity), false), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), opacity_input.clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(keep_fill), false), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 4), fill_input.clone(), network_path); } SubNode::Clip => { document.network_interface.set_input(&InputConnector::node(*node_id, 1), clip_input.clone(), network_path); } } // Build the list of remaining sub-nodes to insert (after the first one), in chain order. Blend Mode is never // reinserted because if it was kept it would already be the `first_kind`. let mut remaining: Vec = Vec::new(); if first_kind != SubNode::Opacity && (keep_opacity || keep_fill) { remaining.push(SubNode::Opacity); } if first_kind != SubNode::Clip && keep_clip { remaining.push(SubNode::Clip); } // Insert each remaining sub-node into the chain between the previous node and the original downstream let mut chain_x_offset = 7; for sub in remaining { let identifier = identifier_for(sub); let new_id = NodeId::new(); let definition = match resolve_document_node_type(&DefinitionIdentifier::ProtoNode(identifier.clone())) { Some(definition) => definition, None => { log::error!("Could not resolve `{identifier:?}` while migrating Blending node"); continue; } }; let template = definition.default_node_template(); document.network_interface.insert_node(new_id, template, network_path); document .network_interface .shift_absolute_node_position(&new_id, base_position + IVec2::new(chain_x_offset, 0), network_path); chain_x_offset += 7; // Splice this node in just before the original downstream input (so each iteration appends to the chain end) if let Some(downstream_input) = &downstream { document.network_interface.insert_node_between(&new_id, downstream_input, 0, network_path); } // Wire the parameter inputs for the inserted node match sub { SubNode::BlendMode => { document.network_interface.set_input(&InputConnector::node(new_id, 1), blend_mode_input.clone(), network_path); } SubNode::Opacity => { document .network_interface .set_input(&InputConnector::node(new_id, 1), NodeInput::value(TaggedValue::Bool(keep_opacity), false), network_path); document.network_interface.set_input(&InputConnector::node(new_id, 2), opacity_input.clone(), network_path); document .network_interface .set_input(&InputConnector::node(new_id, 3), NodeInput::value(TaggedValue::Bool(keep_fill), false), network_path); document.network_interface.set_input(&InputConnector::node(new_id, 4), fill_input.clone(), network_path); } SubNode::Clip => { document.network_interface.set_input(&InputConnector::node(new_id, 1), clip_input.clone(), network_path); } } } inputs_count = match first_kind { SubNode::BlendMode => 2, SubNode::Opacity => 5, SubNode::Clip => 2, }; } // Upgrade old single-purpose Opacity node (content, opacity) to the new shape that also covers fill: // (content, has_opacity, opacity, has_fill, fill). The original opacity input is preserved with `has_opacity` enabled, // while `has_fill` is left disabled and the fill value defaults to 100% (no change to fill behavior). if reference == DefinitionIdentifier::ProtoNode(graphene_std::blending_nodes::opacity::IDENTIFIER) && inputs_count == 2 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(false), false), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::F64(100.), false), network_path); inputs_count = 5; } // Upgrade the legacy 4-input Fill node (content, fill: Fill, _backup_color, _backup_gradient: Gradient) to the // value-model 7-input shape (content, fill: generic paint list, _backup_color, _backup_gradient, _gradient_type, _spread_method, _transform). if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER) && inputs_count == 4 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; // Content: no change document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); // Fill: a literal Fill value is decomposed, and a wired input (`List / List`) is kept as-is match old_inputs[1].as_value() { Some(TaggedValue::LegacyFill(old_fill)) => { let exposed = old_inputs[1].is_exposed(); let fill_value = match old_fill { graphic_types::migrations::legacy::LegacyFill::None => TaggedValue::no_paint(), graphic_types::migrations::legacy::LegacyFill::Solid(color) => TaggedValue::Color(*color), graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()), }; document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(fill_value, exposed), network_path); // Gradient metadata (4, 5, 6): applies only to a literal gradient, solids/none keep the template defaults if let graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) = old_fill { document.network_interface.set_input( &InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false), network_path, ); document.network_interface.set_input( &InputConnector::node(*node_id, 5), NodeInput::value(TaggedValue::GradientSpreadMethod(gradient.spread_method), false), network_path, ); let transform = if gradient.absolute { Some(gradient.transform * gradient.to_transform()) } else { // Baking a legacy bounding-box-relative gradient is deferred until the measurement pre-pass can supply the paint target's bounds document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, gradient.clone())); None }; document .network_interface .set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::OptionalDAffine2(transform), false), network_path); } } // Wired/exposed fill keeps the connection. // The generic paint connector accepts the existing `List`/`List` paint sources directly. _ => { document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); } } // Color backup: no change document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); // Gradient backup: extract stops if let Some(TaggedValue::LegacyGradient(g)) = old_inputs[3].as_value() { document .network_interface .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(g.stops.clone()), false), network_path); // A solid/no-fill node leaves the gradient metadata inputs unused, so seed them from the backup gradient for a later Solid -> Gradient toggle to restore if matches!( old_inputs[1].as_value(), Some(TaggedValue::LegacyFill( graphic_types::migrations::legacy::LegacyFill::None | graphic_types::migrations::legacy::LegacyFill::Solid(_) )) ) { document .network_interface .set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::GradientType(g.gradient_type), false), network_path); document.network_interface.set_input( &InputConnector::node(*node_id, 5), NodeInput::value(TaggedValue::GradientSpreadMethod(g.spread_method), false), network_path, ); let transform = if g.absolute { Some(g.transform * g.to_transform()) } else { document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, g.clone())); None }; document .network_interface .set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::OptionalDAffine2(transform), false), network_path); } } inputs_count = 7; } // Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644) if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false); let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false); document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 3), align_input, network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path); } // TODO: Eventually remove this migration document upgrade code // A legacy "no color" on a plain color connector (`TaggedValue::no_paint()` restored by the deserializer) becomes a color, // since only paint connectors keep the no-paint choice { let migrate_color_input = |input: &NodeInput, fallback: Color| -> Option { let NodeInput::Value { tagged_value, exposed } = input else { return None }; if !tagged_value.is_no_paint() { return None; } Some(NodeInput::value(TaggedValue::Color(fallback), *exposed)) }; let conversions: &[(ProtoNodeIdentifier, usize, Color)] = &[ (graphene_std::vector::fill::IDENTIFIER, graphene_std::vector::fill::BackupColorInput::INDEX, Color::BLACK), ( graphene_std::artboard::create_artboard::IDENTIFIER, graphene_std::artboard::create_artboard::BackgroundInput::INDEX, Color::WHITE, ), ( graphene_std::math_nodes::color_value::IDENTIFIER, graphene_std::math_nodes::color_value::ColorInput::INDEX, Color::TRANSPARENT, ), ( graphene_std::raster_nodes::adjustments::black_and_white::IDENTIFIER, graphene_std::raster_nodes::adjustments::black_and_white::TintInput::INDEX, Color::BLACK, ), ( graphene_std::raster_nodes::blending_nodes::color_overlay::IDENTIFIER, graphene_std::raster_nodes::blending_nodes::color_overlay::ColorInput::INDEX, Color::BLACK, ), ( graphene_std::raster_nodes::std_nodes::empty_image::IDENTIFIER, graphene_std::raster_nodes::std_nodes::empty_image::ColorInput::INDEX, Color::WHITE, ), ]; for &(ref identifier, index, fallback) in conversions { if reference != DefinitionIdentifier::ProtoNode(identifier.clone()) { continue; } let Some(input) = node.inputs.get(index) else { continue }; if let Some(migrated) = migrate_color_input(input, fallback) { document.network_interface.set_input(&InputConnector::node(*node_id, index), migrated, network_path); } } } // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 8 { let mut template: NodeTemplate = legacy_text_node_template()?; document.network_interface.replace_implementation(node_id, network_path, &mut template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path); document.network_interface.set_input( &InputConnector::node(*node_id, 4), if inputs_count == 6 { old_inputs[4].clone() } else { NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false) }, network_path, ); document.network_interface.set_input( &InputConnector::node(*node_id, 5), if inputs_count == 6 { old_inputs[5].clone() } else { NodeInput::value(TaggedValue::F64(TypesettingConfig::default().letter_spacing), false) }, network_path, ); document.network_interface.set_input( &InputConnector::node(*node_id, 6), if inputs_count >= 7 { old_inputs[6].clone() } else { NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_width.unwrap_or_default()), false) }, network_path, ); document.network_interface.set_input( &InputConnector::node(*node_id, 7), if inputs_count >= 8 { old_inputs[7].clone() } else { NodeInput::value(TaggedValue::F64(TypesettingConfig::default().max_width.unwrap_or_default()), false) }, network_path, ); document.network_interface.set_input( &InputConnector::node(*node_id, 8), if inputs_count >= 9 { old_inputs[8].clone() } else { NodeInput::value(TaggedValue::F64(TypesettingConfig::default().letter_tilt), false) }, network_path, ); document.network_interface.set_input( &InputConnector::node(*node_id, 9), if inputs_count >= 10 { old_inputs[9].clone() } else { NodeInput::value(TaggedValue::TextAlign(TextAlign::default()), false) }, network_path, ); document.network_interface.set_input( &InputConnector::node(*node_id, 10), if inputs_count >= 11 { old_inputs[10].clone() } else { NodeInput::value(TaggedValue::Bool(false), false) }, network_path, ); inputs_count = 11 } // Insert bool parameters for `has_max_width` and `has_max_height`: // https://github.com/GraphiteEditor/Graphite/pull/3643 if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 11 { let mut template: NodeTemplate = legacy_text_node_template()?; document.network_interface.replace_implementation(node_id, network_path, &mut template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; // Copy over old inputs #[allow(clippy::needless_range_loop)] for i in 0..=5 { document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i].clone(), network_path); } // Max Width let Some(&TaggedValue::F64(old_max_width)) = old_inputs[6].as_value() else { return None }; document .network_interface .set_input(&InputConnector::node(*node_id, 6), NodeInput::value(TaggedValue::Bool(old_max_width != 0.), false), network_path); document.network_interface.set_input( &InputConnector::node(*node_id, 7), NodeInput::value(TaggedValue::F64(if old_max_width == 0. { 100. } else { old_max_width }), false), network_path, ); // Max Height let Some(&TaggedValue::F64(old_max_height)) = old_inputs[7].as_value() else { return None }; document .network_interface .set_input(&InputConnector::node(*node_id, 8), NodeInput::value(TaggedValue::Bool(old_max_height != 0.), false), network_path); document.network_interface.set_input( &InputConnector::node(*node_id, 9), NodeInput::value(TaggedValue::F64(if old_max_height == 0. { 100. } else { old_max_height }), false), network_path, ); // Copy over old inputs #[allow(clippy::needless_range_loop)] for i in 10..=12 { document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i - 2].clone(), network_path); } inputs_count = 13; } // Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default if inputs_count == 1 && (reference == DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::sine::IDENTIFIER) || reference == DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::cosine::IDENTIFIER) || reference == DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::tangent::IDENTIFIER)) { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path); } // Upgrade the 'Tangent on Path' node to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::tangent_on_path::IDENTIFIER) && inputs_count == 4 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::Bool(true), false), network_path); } // Upgrade the Modulo node to include a boolean input for whether the output should be always positive, which was previously not an option if reference == DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::modulo::IDENTIFIER) && inputs_count == 2 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path); } // Upgrade the Mirror node to add the `keep_original` boolean input if reference == DefinitionIdentifier::ProtoNode(graphene_std::graphic::mirror::IDENTIFIER) && inputs_count == 3 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), network_path); } // Upgrade the Mirror node to add the `reference_point` input and change `offset` from `DVec2` to `f64` if reference == DefinitionIdentifier::ProtoNode(graphene_std::graphic::mirror::IDENTIFIER) && inputs_count == 4 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; let Some(&TaggedValue::DVec2(old_offset)) = old_inputs[1].as_value() else { return None }; let old_offset = if old_offset.x.abs() > old_offset.y.abs() { old_offset.x } else { old_offset.y }; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input( &InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::ReferencePoint(graphene_std::transform::ReferencePoint::Center), false), network_path, ); document .network_interface .set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(old_offset), false), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[2].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path); } // Upgrade `image` nodes that stored image data as `Image`` to `image` nodes that store a reference to the image data as a resource. // Encodes the image data as PNG and stores it in the document's embedded resources and rewires the node to reference that resource. if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER) { let image = node.inputs.iter().find_map(|input| match input.as_value()? { TaggedValue::ImageData(image) => Some(image.clone()), _ => None, }); if let Some(image) = image { let hash = document.resources.embedded.store(Resource::new(image.to_png())); let resource_id = ResourceId::new(); document.resources.registry.push_source_back(&resource_id, DataSource::Embedded); document.resources.registry.resolve(&resource_id, hash); let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let _ = document.network_interface.replace_inputs(node_id, network_path, &mut node_template); document .network_interface .set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::Resource(resource_id), false), network_path); } } // Convert text nodes from the old `editor-api` scope + `Font` input to a single font `Resource` input. // The chosen typeface is recorded as a `DataSource::Font` in the document's resource registry and loaded on open. if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_std::text::TextNode")) && inputs_count == 13 && matches!(node.inputs.first(), Some(NodeInput::Scope(_))) { document .network_interface .set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path); if let Some(TaggedValue::Font(font)) = node.inputs.get(2).and_then(|input| input.as_value()) { let resource_id = ResourceId::new(); document.resources.registry.push_source_back(&resource_id, DataSource::Embedded); document.resources.registry.push_source_back( &resource_id, DataSource::Font { family: font.font_family.clone(), style: Some(font.font_style.clone()), }, ); document .network_interface .set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Resource(resource_id), false), network_path); } } if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::noise_pattern::IDENTIFIER) && inputs_count == 15 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document .network_interface .set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path); for (i, input) in old_inputs.iter().enumerate() { document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), input.clone(), network_path); } } if reference == DefinitionIdentifier::ProtoNode(graphene_std::repeat::repeat_on_points::IDENTIFIER) && inputs_count == 2 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); } // Old shape: [background, bounds, trace, cache]. Both "bounds" (input 1) and "cache" (input 3) are dropped, and "cache" is now stored as // internal node state via `#[data]` on the brush node, so it is not a node input at all in the new shape. if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && inputs_count == 4 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path); } // Old shape: [background, trace, cache]. The "cache" input is dropped because the brush node now stores its cache as internal `#[data]` state. if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && inputs_count == 3 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); } if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::RemoveHandlesNode")) { let mut node_template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::auto_tangents::IDENTIFIER))?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::F64(0.), false), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path); } if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::GenerateHandlesNode")) { let mut node_template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::auto_tangents::IDENTIFIER))?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(true), false), network_path); } if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::merge_by_distance::IDENTIFIER) && inputs_count == 2 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document.network_interface.set_input( &InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Topological), false), network_path, ); } if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SpatialMergeByDistanceNode")) { let mut node_template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::merge_by_distance::IDENTIFIER))?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document.network_interface.set_input( &InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Spatial), false), network_path, ); } if reference == DefinitionIdentifier::Network("Sample Points".into()) && inputs_count == 5 { let mut node_template = resolve_network_node_type("Sample Polyline")?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; let new_spacing_value = NodeInput::value(TaggedValue::PointSpacingType(graphene_std::vector::misc::PointSpacingType::Separation), false); let new_quantity_value = NodeInput::value(TaggedValue::U32(100), false); document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), new_spacing_value, network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 3), new_quantity_value, network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[2].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[4].clone(), network_path); } // Make the "Quantity" parameter a u32 instead of f64 if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::sample_polyline::IDENTIFIER) { // Get the inputs, obtain the quantity value, and put the inputs back let quantity_value = document .network_interface .input_from_connector(&InputConnector::Node { node_id: *node_id, input_index: 3 }, network_path)?; if let NodeInput::Value { tagged_value, exposed } = quantity_value && let TaggedValue::F64(value) = **tagged_value { let new_quantity_value = NodeInput::value(TaggedValue::U32(value as u32), *exposed); document.network_interface.set_input(&InputConnector::node(*node_id, 3), new_quantity_value, network_path); } } // Make the "Grid" node, if its input of index 3 is a DVec2 for "angles" instead of a u32 for the "columns" input that now succeeds "angles", move the angle to index 5 (after "columns" and "rows") if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::grid::IDENTIFIER) && inputs_count == 6 { let node_definition = resolve_document_node_type(&reference)?; let mut new_node_template = node_definition.default_node_template(); let mut current_node_template = document.network_interface.create_node_template(node_id, network_path)?; let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut new_node_template)?; let index_3_value = old_inputs.get(3).cloned(); let mut upgraded = false; if let Some(NodeInput::Value { tagged_value, exposed: _ }) = index_3_value && matches!(*tagged_value, TaggedValue::DVec2(_)) { // Move index 3 to the end document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[4].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path); upgraded = true; } if !upgraded { let _ = document.network_interface.replace_inputs(node_id, network_path, &mut current_node_template); } } // Add the "Depth" parameter to the "Read Index" node if reference == DefinitionIdentifier::ProtoNode(graphene_std::context::read_index::IDENTIFIER) && inputs_count == 0 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); document.network_interface.set_display_name(node_id, "Read Index".to_string(), network_path); let mut node_path = network_path.to_vec(); node_path.push(*node_id); document.network_interface.add_import(TaggedValue::None, false, 0, "Primary", "", &node_path); document.network_interface.add_import(TaggedValue::U32(0), false, 1, "Loop Level", "TODO", &node_path); } // Upgrade the "Animation" node to add the "Rate" input if reference == DefinitionIdentifier::ProtoNode(graphene_std::animation::animation_time::IDENTIFIER) && inputs_count < 2 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let _ = document.network_interface.replace_inputs(node_id, network_path, &mut node_template); document .network_interface .set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::F64(1.), false), network_path); } // Upgrade the "Read Position" node to add the "Loop Level" input if reference == DefinitionIdentifier::ProtoNode(graphene_std::context::read_position::IDENTIFIER) && inputs_count < 2 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let _ = document.network_interface.replace_inputs(node_id, network_path, &mut node_template); document .network_interface .set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::U32(0), false), network_path); } // Migrate from the old source/target v1 "Morph" node to the new `List`-based v2 "Morph" node. // This doesn't produce exactly equivalent results in cases involving input `List` values with multiple items. // The old version would zip the source and target items, interpolating each pair together. // The migrated version will instead deeply flatten both merged `List`s and morph sequentially between all source vectors and all target vector elements. // This migration assumes most usages didn't involve multiple parallel vector elements, and instead morphed from a single source to a single target vector element. if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::morph::IDENTIFIER) && (inputs_count == 3 || inputs_count == 4) { // 3 inputs - old signature (#3405): // async fn morph(_: impl Ctx, source: List, #[expose] target: List, #[default(0.5)] time: Fraction) -> List { ... } // // 4 inputs - even older signature (commit 80b8df8d4298b6669f124b929ce61bfabfc44e41): // async fn morph(_: impl Ctx, source: List, #[expose] target: List, #[default(0.5)] time: Fraction, start_index: u32) -> List { ... } // // v2 signature: // async fn morph(_: impl Ctx, #[implementations(List, List)] content: I, progression: Progression) -> List { ... } let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; // Create a new Merge node let Some(merge_node_type) = resolve_network_node_type("Merge") else { log::error!("Could not get merge node from definition when upgrading morph"); return None; }; let merge_template = merge_node_type.default_node_template(); let merge_node_id = NodeId::new(); // Decide on the placement position of the new Merge node let Some(morph_position) = document.network_interface.position_from_downstream_node(node_id, network_path) else { log::error!("Could not get position for morph node {node_id}"); return None; }; let merge_position = morph_position + IVec2::new(-7, 0); // Insert the new Merge node into the network document.network_interface.insert_node(merge_node_id, merge_template, network_path); document.network_interface.set_to_node_or_layer(&merge_node_id, network_path, false); document.network_interface.shift_absolute_node_position(&merge_node_id, merge_position, network_path); // Connect the old 'source' and 'target' inputs to the new Merge node document.network_interface.set_input(&InputConnector::node(merge_node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(merge_node_id, 1), old_inputs[1].clone(), network_path); // Connect the new Merge node to the 'content' input of the Morph node document .network_interface .set_input(&InputConnector::node(*node_id, 0), NodeInput::node(merge_node_id, 0), network_path); // Connect the old 'progression' input to the new 'progression' input of the Morph node document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path); inputs_count = 2; } // Migrate from the v2 "Morph" node (2 inputs: content, progression) to the v3 "Morph" node (5 inputs: content, progression, reverse, distribution, path). // The old progression used integer part for pair selection (range 0..N-1 where N is the number of content objects). // The new progression uses fractional 0..1 for euclidean traversal through all objects. // We insert Count Elements → Subtract 1 → Divide to remap: new_progression = old_progression / (N - 1). // For the common 2-object case (N=2), this divides by 1 which is a no-op, preserving identical behavior. if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::morph::IDENTIFIER) && inputs_count == 2 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; // Reconnect content (input 0) and leave path (input 4) as default document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); let Some(morph_position) = document.network_interface.position_from_downstream_node(node_id, network_path) else { log::error!("Could not get position for morph node {node_id}"); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); return None; }; // Create Count Elements node: counts content `List` items → N let Some(count_elements_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::count_elements::IDENTIFIER)) else { log::error!("Could not get count_elements node from definition when upgrading morph"); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); return None; }; let count_elements_template = count_elements_def.default_node_template(); let count_elements_id = NodeId::new(); // Create Subtract node: N → N-1 let Some(subtract_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::subtract::IDENTIFIER)) else { log::error!("Could not get subtract node from definition when upgrading morph"); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); return None; }; let mut subtract_template = subtract_def.default_node_template(); subtract_template.document_node.inputs[1] = NodeInput::value(TaggedValue::F64(1.), false); let subtract_id = NodeId::new(); // Create Divide node: old_progression / (N-1) → new progression let Some(divide_def) = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::divide::IDENTIFIER)) else { log::error!("Could not get divide node from definition when upgrading morph"); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); return None; }; let divide_template = divide_def.default_node_template(); let divide_id = NodeId::new(); // Insert and position nodes document.network_interface.insert_node(count_elements_id, count_elements_template, network_path); document .network_interface .shift_absolute_node_position(&count_elements_id, morph_position + IVec2::new(-21, 2), network_path); document.network_interface.insert_node(subtract_id, subtract_template, network_path); document.network_interface.shift_absolute_node_position(&subtract_id, morph_position + IVec2::new(-14, 2), network_path); document.network_interface.insert_node(divide_id, divide_template, network_path); document.network_interface.shift_absolute_node_position(÷_id, morph_position + IVec2::new(-7, 1), network_path); // Wire: content source → Count Elements input 0 document.network_interface.set_input(&InputConnector::node(count_elements_id, 0), old_inputs[0].clone(), network_path); // Wire: Count Elements output → Subtract input 0 (minuend) document .network_interface .set_input(&InputConnector::node(subtract_id, 0), NodeInput::node(count_elements_id, 0), network_path); // Wire: old progression → Divide input 0 (numerator) document.network_interface.set_input(&InputConnector::node(divide_id, 0), old_inputs[1].clone(), network_path); // Wire: Subtract output → Divide input 1 (denominator) document.network_interface.set_input(&InputConnector::node(divide_id, 1), NodeInput::node(subtract_id, 0), network_path); // Wire: Divide output → Morph progression input document.network_interface.set_input(&InputConnector::node(*node_id, 1), NodeInput::node(divide_id, 0), network_path); } // Migrate old Arrow node from (start, end, shaft_width, head_width, head_length) to (arrow_to, shaft_width, head_width, head_length) with a Transform node for positioning if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::arrow::IDENTIFIER) && inputs_count == 6 { // Read old start and end values let start = match node.inputs.get(1)? { NodeInput::Value { tagged_value, .. } => { if let TaggedValue::DVec2(v) = *tagged_value.clone().into_inner() { v } else { DVec2::ZERO } } _ => DVec2::ZERO, }; let end = match node.inputs.get(2)? { NodeInput::Value { tagged_value, .. } => { if let TaggedValue::DVec2(v) = *tagged_value.clone().into_inner() { v } else { DVec2::new(100., 0.) } } _ => DVec2::new(100., 0.), }; // Replace inputs with the new node definition (primary + arrow_to + shaft_width + head_width + head_length) let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; // Preserve primary input connection document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); // Set arrow_to = end - start document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::DVec2(end - start), false), network_path); // Preserve shaft_width, head_width, head_length (shifted from indices 3,4,5 to 2,3,4) document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[3].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[4].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path); // Find downstream connection to insert Transform node let downstream = document .network_interface .outward_wires(network_path) .and_then(|wires| wires.get(&OutputConnector::node(*node_id, 0))) .and_then(|connections| connections.first().cloned()); if let Some(downstream_input) = downstream { // Create a Transform node with translation = start let Some(transform_node_type) = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER) else { log::error!("Transform node definition not found during Arrow migration"); return None; }; let mut transform_template = transform_node_type.default_node_template(); transform_template.document_node.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false); let transform_id = NodeId::new(); // Position the Transform node to the right of the Arrow node let arrow_position = document.network_interface.position(node_id, network_path).unwrap_or_default(); document.network_interface.insert_node(transform_id, transform_template, network_path); document.network_interface.shift_absolute_node_position(&transform_id, arrow_position + IVec2::new(7, 0), network_path); document.network_interface.insert_node_between(&transform_id, &downstream_input, 0, network_path); } } // Migrate old Line node from (start, end) to (line_to) with a Transform node for positioning if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::line::IDENTIFIER) && inputs_count == 3 { // Read old start and end values let start = match node.inputs.get(1)? { NodeInput::Value { tagged_value, .. } => { if let TaggedValue::DVec2(v) = *tagged_value.clone().into_inner() { v } else { DVec2::ZERO } } _ => DVec2::ZERO, }; let end = match node.inputs.get(2)? { NodeInput::Value { tagged_value, .. } => { if let TaggedValue::DVec2(v) = *tagged_value.clone().into_inner() { v } else { DVec2::new(100., 100.) } } _ => DVec2::new(100., 100.), }; // Replace inputs with the new node definition (primary + line_to) let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; // Preserve primary input connection document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); // Set line_to = end - start document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::DVec2(end - start), false), network_path); // Find downstream connection to insert Transform node let downstream = document .network_interface .outward_wires(network_path) .and_then(|wires| wires.get(&OutputConnector::node(*node_id, 0))) .and_then(|connections| connections.first().cloned()); if let Some(downstream_input) = downstream { // Create a Transform node with translation = start let Some(transform_node_type) = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER) else { log::error!("Transform node definition not found during Line migration"); return None; }; let mut transform_template = transform_node_type.default_node_template(); transform_template.document_node.inputs[1] = NodeInput::value(TaggedValue::DVec2(start), false); let transform_id = NodeId::new(); // Position the Transform node to the right of the Line node let line_position = document.network_interface.position(node_id, network_path).unwrap_or_default(); document.network_interface.insert_node(transform_id, transform_template, network_path); document.network_interface.shift_absolute_node_position(&transform_id, line_position + IVec2::new(7, 0), network_path); document.network_interface.insert_node_between(&transform_id, &downstream_input, 0, network_path); } } // Add the "Scale Type" parameter to the "Decompose Scale" node if reference == DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::decompose_scale::IDENTIFIER) && inputs_count == 1 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::ScaleType(ScaleType::Magnitude), false), network_path); } // Add the "Along Normals" parameter to the "Jitter Points" node if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::jitter_points::IDENTIFIER) && inputs_count == 3 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path); document .network_interface .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(false), false), network_path); } // 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 && let TaggedValue::VectorModification(modification) = &**tagged_value { let modification = modification.clone(); let was_exposed = *exposed; document.network_interface.set_input( &InputConnector::node(*node_id, 0), NodeInput::type_default(descriptor!(graphene_std::list::List), true), network_path, ); if !was_exposed { document .network_interface .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::VectorModification(modification), false), network_path); } } } // `load_resource` no longer takes the `editor-api` if reference == DefinitionIdentifier::ProtoNode(graphene_std::platform_application_io::load_resource::IDENTIFIER) && inputs_count == 3 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path); } // `wgpu-executor` scope was removed, change to the auto injected scope node `graphene_std::platform_application_io::wgpu_executor` for (i, input) in node.inputs.iter().enumerate() { if let NodeInput::Scope(name) = input && *name == "wgpu-executor" { document.network_interface.set_input( &InputConnector::node(*node_id, i), NodeInput::Scope("graphene_std::platform_application_io::WgpuExecutorNode".into()), network_path, ); } } // ================================== // PUT ALL MIGRATIONS ABOVE THIS LINE // ================================== // Ensure layers are positioned as stacks if they are upstream siblings of another layer document.network_interface.load_structure(); let all_layers = LayerNodeIdentifier::ROOT_PARENT.descendants(document.network_interface.document_metadata()).collect::>(); for layer in all_layers { let (downstream_node, input_index) = document .network_interface .outward_wires(&[]) .and_then(|outward_wires| outward_wires.get(&OutputConnector::node(layer.to_node(), 0))) .and_then(|outward_wires| outward_wires.first()) .and_then(|input_connector| input_connector.node_id().map(|node_id| (node_id, input_connector.input_index())))?; // If the downstream node is a layer and the input is the first input and the current layer is not in a stack if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && !document.network_interface.is_stack(&layer.to_node(), &[]) { // Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files let (Some(layer_position), Some(downstream_position)) = (document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[])) else { log::error!("Could not get position for layer {:?} or downstream node {} when opening file", layer.to_node(), downstream_node); return None; }; if layer_position.x == downstream_position.x { document.network_interface.set_stack_position_calculated_offset(&layer.to_node(), &downstream_node, &[]); } } } Some(()) } /// Migrates document nodes whose catalog definitions have been removed. /// /// This is called from `migrate_node` BEFORE its standard reset/migration logic, since that logic aborts when it can't /// resolve the node's `reference` against the current catalog. Each block here detects a specific decommissioned /// definition by its old reference name, swaps it to a still-supported implementation, and preserves the user's inputs. /// After this runs, the node's reference resolves cleanly so the rest of `migrate_node` proceeds normally. fn migrate_removed_catalog_definitions(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler) -> Option<()> { // Collapse the legacy "Sample Polyline" wrapper network into the standalone `sample_polyline` proto node. // The proto node now computes per-bezpath segment lengths inline, so the wrapper's separate `subpath_segment_lengths` // and `Memoize` nodes are no longer needed. The 7 user-facing inputs are positionally identical between the // old wrapper and the new proto node. if let Some(DefinitionIdentifier::Network(name)) = document.network_interface.reference(node_id, network_path) && name == "Sample Polyline" && node.inputs.len() == 7 { let mut node_template = resolve_proto_node_type(graphene_std::vector::sample_polyline::IDENTIFIER)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; for (index, input) in old_inputs.iter().take(7).enumerate() { document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path); } } // Collapse the legacy "Scatter Points" wrapper network into the standalone `scatter_points` proto node. // The wrapper's trailing `Memoize` node is now produced automatically by the `memoize` attribute on the // proto node, so the wrapper itself is redundant. The 3 user-facing inputs are positionally identical // between the old wrapper and the new proto node. if let Some(DefinitionIdentifier::Network(name)) = document.network_interface.reference(node_id, network_path) && name == "Scatter Points" && node.inputs.len() == 3 { let mut node_template = resolve_proto_node_type(graphene_std::vector::scatter_points::IDENTIFIER)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; for (index, input) in old_inputs.iter().take(3).enumerate() { document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path); } } // Collapse the legacy "Boolean Operation" wrapper network into the standalone `boolean_operation` proto node. // The wrapper's trailing `Memoize` node is now produced automatically by the `memoize` attribute on the // proto node, so the wrapper itself is redundant. The 2 user-facing inputs are positionally identical // between the old wrapper and the new proto node. if let Some(DefinitionIdentifier::Network(name)) = document.network_interface.reference(node_id, network_path) && name == "Boolean Operation" && node.inputs.len() == 2 { let mut node_template = resolve_proto_node_type(graphene_std::path_bool_nodes::boolean_operation::IDENTIFIER)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; for (index, input) in old_inputs.iter().take(2).enumerate() { document.network_interface.set_input(&InputConnector::node(*node_id, index), input.clone(), network_path); } } Some(()) } #[cfg(test)] mod tests { use super::*; #[test] fn a_written_attribute_no_longer_resets_the_layer_definitions() { // The node resolves natively again, so a document carrying it keeps // its own layer internals instead of rebuilding them. assert!(!document_migration_reset_node_definition( r#"{"implementation":{"ProtoNode":{"name":"graphic_nodes::graphic::WriteAttributeNode"}}}"# )); } #[test] fn test_no_duplicate_node_replacements() { let mut hashmap = HashMap::::new(); NODE_REPLACEMENTS.iter().for_each(|node| { *hashmap.entry(node.node.clone()).or_default() += 1; }); let duplicates = hashmap.iter().filter(|(_, count)| **count > 1).map(|(node, _)| node.as_str()).collect::>(); if !duplicates.is_empty() { panic!("Duplicate entries in `NODE_REPLACEMENTS`: {duplicates:?}"); } } }