From 48451355e353e8baac43226d299522b3362460f1 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 23 Oct 2026 21:40:17 -0700 Subject: [PATCH] Remove the 'Serialize', 'Empty Image', and 'Sample Image' nodes --- .branding | 4 +- .../messages/portfolio/document_migration.rs | 172 ++++++++++++++++-- frontend/src/icons.ts | 6 - node-graph/nodes/raster/src/std_nodes.rs | 73 +------- node-graph/nodes/text/src/lib.rs | 12 +- 5 files changed, 162 insertions(+), 105 deletions(-) diff --git a/.branding b/.branding index a268632f7be..0f60d4f660e 100644 --- a/.branding +++ b/.branding @@ -1,2 +1,2 @@ -https://github.com/Keavon/graphite-branded-assets/archive/df284651aeb9370927500ab7f336cfb63ca8069a.tar.gz -d88f21500aba0de491efd03f2553dfc457288409923b7f52386d2a81e0cdaf5d +https://github.com/Keavon/graphite-branded-assets/archive/f6a8460b0e85d05bb93a9536dc3d6ed6cc331178.tar.gz +6e8ea35b8fe9f7a371c5c2570259c0e0be8a52cbdfa747551874724b0c58a576 diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index c2065b4e94d..b7f944d648a 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -110,6 +110,11 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ "graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode", "graphene_core::debug::UnwrapOptionNode", + // The deleted Sample Image degrades to a passthrough of its image + "graphene_raster_nodes::std_nodes::SampleImageNode", + "graphene_std::raster::SampleImageNode", + "graphene_std::raster::SampleNode", + "raster_nodes::std_nodes::SampleImageNode", ], }, NodeReplacement { @@ -549,10 +554,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ 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: &[ @@ -658,14 +659,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ 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"], @@ -729,10 +722,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[ 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"], - }, NodeReplacement { node: graphene_std::text_nodes::string_map::IDENTIFIER, aliases: &["text_nodes::MapStringNode"], @@ -2336,7 +2325,6 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], (graphene_std::math_nodes::color_value::ColorInput.into(), Color::TRANSPARENT), (graphene_std::raster_nodes::adjustments::black_and_white::TintInput.into(), Color::BLACK), (graphene_std::raster_nodes::blending_nodes::color_overlay::ColorInput.into(), Color::BLACK), - (graphene_std::raster_nodes::std_nodes::empty_image::ColorInput.into(), Color::WHITE), ]; for (parameter, fallback) in conversions { if reference != DefinitionIdentifier::ProtoNode(parameter.node_identifier.clone()) { @@ -2782,6 +2770,57 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[1].clone(), network_path); } + // Sample Image nodes, now passthroughs, drop their old title + if reference == DefinitionIdentifier::ProtoNode(graphene_std::ops::passthrough::IDENTIFIER) { + reset_default_display_name(document, node_id, network_path, "Sample Image"); + } + + // Serialize became As String, whose input converts a wired value to text, while a stored value keeps the JSON text Serialize made of it + let serialize_identifiers = ["text_nodes::SerializeNode", "graphene_core::logic::SerializeNode"]; + if serialize_identifiers + .iter() + .any(|&identifier| reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new(identifier))) + { + let mut node_template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::text_nodes::as_string::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)?; + + let value_input = match old_inputs.first() { + Some(NodeInput::Value { tagged_value, exposed }) => { + let json = match &**tagged_value { + TaggedValue::String(value) => serde_json::to_string(value), + TaggedValue::Bool(value) => serde_json::to_string(value), + TaggedValue::Number(value) => serde_json::to_string(value), + TaggedValue::Integer(value) => serde_json::to_string(value), + TaggedValue::DVec2(value) => serde_json::to_string(value), + TaggedValue::DAffine2(value) => serde_json::to_string(value), + _ => Ok(String::new()), + }; + NodeInput::value(TaggedValue::String(json.unwrap_or_default()), *exposed) + } + Some(wired) => wired.clone(), + None => return None, + }; + document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), value_input, network_path); + reset_default_display_name(document, node_id, network_path, "Serialize"); + } + + // Empty Image has no equivalent, so it becomes an Image node with no file, which still feeds an image to the nodes after it + let empty_image_identifiers = [ + "raster_nodes::std_nodes::EmptyImageNode", + "graphene_raster_nodes::std_nodes::EmptyImageNode", + "graphene_std::raster::EmptyImageNode", + ]; + if empty_image_identifiers + .iter() + .any(|&identifier| reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new(identifier))) + { + let mut node_template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER))?.default_node_template(); + document.network_interface.replace_implementation(node_id, network_path, &mut node_template); + document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; + reset_default_display_name(document, node_id, network_path, "Empty Image"); + } + if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::RemoveHandlesNode")) { let mut node_template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::smooth_anchors::IDENTIFIER))?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); @@ -4706,4 +4745,103 @@ mod tests { assert_eq!(opened.network_interface.display_name(&node_id, &[]), *title); } } + + // Serialize, Sample Image, and Empty Image nodes open as As String, Passthrough, and Image nodes, keeping their wires and Serialize's stored text + #[test] + fn removed_nodes_open_as_their_stand_ins() { + use crate::messages::portfolio::document_storage_io::load_legacy_document; + use crate::test_utils::capture_logged_errors; + use glam::DAffine2; + use graph_craft::application_io::resource::HashMapResourceStorage; + + let legacy_node = |identifier: &'static str, display_name: &str, inputs: Vec| NodeTemplate { + implementation: NodeTemplateImplementation::ProtoNode(ProtoNodeIdentifier::new(identifier)), + input_metadata: inputs.iter().map(|_| ("Value", "").into()).collect(), + inputs, + display_name: display_name.to_string(), + ..Default::default() + }; + let default_template = |identifier| { + resolve_document_node_type(&DefinitionIdentifier::ProtoNode(identifier)) + .expect("the definition should exist") + .default_node_template() + }; + + let (number, image) = (NodeId(1), NodeId(2)); + let (serialize_stored_number, serialize_stored_string, serialize_wired) = (NodeId(3), NodeId(4), NodeId(5)); + let (sample_image, empty_image, oldest_empty_image) = (NodeId(6), NodeId(7), NodeId(8)); + let empty_image_inputs = || { + vec![ + NodeInput::value(TaggedValue::DAffine2(DAffine2::from_scale(DVec2::splat(10.))), false), + NodeInput::value(TaggedValue::Color(Color::WHITE), false), + ] + }; + + let mut document = DocumentMessageHandler::default(); + let interface = &mut document.network_interface; + interface.insert_node(number, default_template(graphene_std::math_nodes::number_value::IDENTIFIER), &[]); + interface.insert_node(image, default_template(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER), &[]); + interface.insert_node( + serialize_stored_number, + legacy_node("text_nodes::SerializeNode", "Serialize", vec![NodeInput::value(TaggedValue::Number(2.5), false)]), + &[], + ); + interface.insert_node( + serialize_stored_string, + legacy_node("graphene_core::logic::SerializeNode", "Serialize", vec![NodeInput::value(TaggedValue::String("Hi".into()), false)]), + &[], + ); + interface.insert_node( + serialize_wired, + legacy_node("text_nodes::SerializeNode", "Serialize", vec![NodeInput::value(TaggedValue::None, false)]), + &[], + ); + interface.insert_node( + sample_image, + legacy_node("raster_nodes::std_nodes::SampleImageNode", "Sample Image", vec![NodeInput::value(TaggedValue::None, false)]), + &[], + ); + interface.insert_node(empty_image, legacy_node("raster_nodes::std_nodes::EmptyImageNode", "Empty Image", empty_image_inputs()), &[]); + interface.insert_node(oldest_empty_image, legacy_node("graphene_std::raster::EmptyImageNode", "Empty Image", empty_image_inputs()), &[]); + interface.set_input(&InputConnector::node_at_index(serialize_wired, 0), NodeInput::node(number, 0), &[]); + interface.set_input(&InputConnector::node_at_index(sample_image, 0), NodeInput::node(image, 0), &[]); + + let serialized = document.serialize_document(); + let (opened, errors) = capture_logged_errors(|| load_legacy_document(serialized, &HashMapResourceStorage::new(), false)); + let opened = opened.expect("the document should open"); + assert!(errors.is_empty(), "opening the document logged errors: {errors:?}"); + + let interface = &opened.network_interface; + let node = |node_id| interface.document_node(&node_id, &[]).expect("the node should still exist"); + let as_string = DocumentNodeImplementation::ProtoNode(graphene_std::text_nodes::as_string::IDENTIFIER); + + // Serialize's stored values keep the JSON text it made of them, and its wire is kept for As String to convert + for (node_id, expected) in [(serialize_stored_number, "2.5"), (serialize_stored_string, "\"Hi\"")] { + assert_eq!(node(node_id).implementation, as_string, "node {node_id}"); + assert_eq!(node(node_id).inputs[0].as_value(), Some(&TaggedValue::String(expected.to_string())), "node {node_id}"); + assert_eq!(interface.display_name(&node_id, &[]), "As String", "node {node_id}"); + } + assert_eq!(node(serialize_wired).implementation, as_string); + assert!(matches!(node(serialize_wired).inputs[0], NodeInput::Node { node_id, output_index: 0, .. } if node_id == number)); + + // Sample Image passes its image through + assert_eq!(node(sample_image).implementation, DocumentNodeImplementation::ProtoNode(graphene_std::ops::passthrough::IDENTIFIER)); + assert!(matches!(node(sample_image).inputs[0], NodeInput::Node { node_id, output_index: 0, .. } if node_id == image)); + assert_ne!(interface.display_name(&sample_image, &[]), "Sample Image"); + + // Empty Image becomes an Image node with no file + for node_id in [empty_image, oldest_empty_image] { + assert_eq!( + node(node_id).implementation, + DocumentNodeImplementation::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER), + "node {node_id}" + ); + assert_eq!( + node(node_id).inputs, + default_template(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER).inputs, + "node {node_id}" + ); + assert_eq!(interface.display_name(&node_id, &[]), "Image", "node {node_id}"); + } + } } diff --git a/frontend/src/icons.ts b/frontend/src/icons.ts index 4d2f323b891..b1ee565dabd 100644 --- a/frontend/src/icons.ts +++ b/frontend/src/icons.ts @@ -177,7 +177,6 @@ import NodeDistance from "/../branding/assets/16x16-nodes/node-distance.svg"; import NodeDivide from "/../branding/assets/16x16-nodes/node-divide.svg"; import NodeDotProduct from "/../branding/assets/16x16-nodes/node-dot-product.svg"; import NodeEllipse from "/../branding/assets/16x16-nodes/node-ellipse.svg"; -import NodeEmptyImage from "/../branding/assets/16x16-nodes/node-empty-image.svg"; import NodeEquals from "/../branding/assets/16x16-nodes/node-equals.svg"; import NodeEvaluateGradient from "/../branding/assets/16x16-nodes/node-evaluate-gradient.svg"; import NodeEvaluatePath from "/../branding/assets/16x16-nodes/node-evaluate-path.svg"; @@ -321,12 +320,10 @@ import NodeRgbaToColor from "/../branding/assets/16x16-nodes/node-rgba-to-color. import NodeRoot from "/../branding/assets/16x16-nodes/node-root.svg"; import NodeRoundCorners from "/../branding/assets/16x16-nodes/node-round-corners.svg"; import NodeRound from "/../branding/assets/16x16-nodes/node-round.svg"; -import NodeSampleImage from "/../branding/assets/16x16-nodes/node-sample-image.svg"; import NodeSamplePolyline from "/../branding/assets/16x16-nodes/node-sample-polyline.svg"; import NodeScatterPoints from "/../branding/assets/16x16-nodes/node-scatter-points.svg"; import NodeSelectiveColor from "/../branding/assets/16x16-nodes/node-selective-color.svg"; import NodeSeparatePaths from "/../branding/assets/16x16-nodes/node-separate-paths.svg"; -import NodeSerialize from "/../branding/assets/16x16-nodes/node-serialize.svg"; import NodeShift from "/../branding/assets/16x16-nodes/node-shift.svg"; import NodeShuffle from "/../branding/assets/16x16-nodes/node-shuffle.svg"; import NodeSign from "/../branding/assets/16x16-nodes/node-sign.svg"; @@ -459,7 +456,6 @@ export const ICON_16X16_NODES = { NodeDivide: { svg: NodeDivide, size: 16 }, NodeDotProduct: { svg: NodeDotProduct, size: 16 }, NodeEllipse: { svg: NodeEllipse, size: 16 }, - NodeEmptyImage: { svg: NodeEmptyImage, size: 16 }, NodeEquals: { svg: NodeEquals, size: 16 }, NodeEvaluateGradient: { svg: NodeEvaluateGradient, size: 16 }, NodeEvaluatePath: { svg: NodeEvaluatePath, size: 16 }, @@ -603,12 +599,10 @@ export const ICON_16X16_NODES = { NodeRoot: { svg: NodeRoot, size: 16 }, NodeRound: { svg: NodeRound, size: 16 }, NodeRoundCorners: { svg: NodeRoundCorners, size: 16 }, - NodeSampleImage: { svg: NodeSampleImage, size: 16 }, NodeSamplePolyline: { svg: NodeSamplePolyline, size: 16 }, NodeScatterPoints: { svg: NodeScatterPoints, size: 16 }, NodeSelectiveColor: { svg: NodeSelectiveColor, size: 16 }, NodeSeparatePaths: { svg: NodeSeparatePaths, size: 16 }, - NodeSerialize: { svg: NodeSerialize, size: 16 }, NodeShift: { svg: NodeShift, size: 16 }, NodeShuffle: { svg: NodeShuffle, size: 16 }, NodeSign: { svg: NodeSign, size: 16 }, diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index 2dfdeff73a6..9124cfb8fb8 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -30,70 +30,6 @@ impl From for Error { } } -#[node_macro::node(category("Debug"), icon("NodeSampleImage"))] -fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item>) -> Item> { - let image_frame_transform: DAffine2 = image_frame.attribute_cloned_or_default(ATTR_TRANSFORM); - - let footprint = ctx.footprint(); - let viewport_bounds = footprint.viewport_bounds_in_local_space(); - let image_bounds = Bbox::from_transform(image_frame_transform).to_axis_aligned_bbox(); - let intersection = viewport_bounds.intersect(&image_bounds); - let size = intersection.size(); - - // If the image would not be visible, return it unchanged - if size.x <= 0. || size.y <= 0. { - return image_frame; - } - - let (image, mut attributes) = image_frame.into_parts(); - let (width, height) = (image.width, image.height); - - // Resize the image using the image crate, which filters each channel independently, so premultiply to keep transparent texels from bleeding into edges - let mut image = image.into_data(); - image.map_pixels(|px| px.to_associated_alpha()); - let data = bytemuck::cast_vec(image.data); - let image_size = DAffine2::from_scale(DVec2::new(width as f64, height as f64)); - let size_px = image_size.transform_vector2(size).as_uvec2(); - - let image_buffer = ::image::Rgba32FImage::from_raw(width, height, data).expect("Failed to convert internal image format into image-rs data type."); - - let dynamic_image: ::image::DynamicImage = image_buffer.into(); - let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO); - let offset_px = image_size.transform_vector2(offset).as_uvec2(); - let cropped = dynamic_image.crop_imm(offset_px.x, offset_px.y, size_px.x, size_px.y); - - let viewport_resolution_x = footprint.transform.transform_vector2(DVec2::X * size.x).length(); - let viewport_resolution_y = footprint.transform.transform_vector2(DVec2::Y * size.y).length(); - let mut new_width = size_px.x; - let mut new_height = size_px.y; - - // Only downscale the image for now - let resized = if new_width < width || new_height < height { - new_width = viewport_resolution_x as u32; - new_height = viewport_resolution_y as u32; - // TODO: choose filter based on quality requirements - cropped.resize_exact(new_width, new_height, ::image::imageops::Triangle) - } else { - cropped - }; - let buffer = resized.to_rgba32f(); - let buffer = buffer.into_raw(); - let vec = bytemuck::cast_vec(buffer); - let mut image = Image { - width: new_width, - height: new_height, - data: vec, - base64_string: None, - }; - image.map_pixels(|px: Color| px.to_unassociated_alpha()); - // we need to adjust the offset if we truncate the offset calculation - - let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size); - attributes.insert(ATTR_TRANSFORM, new_transform); - - Item::from_parts(Raster::new_cpu(image), attributes) -} - #[node_macro::node(category("Raster: Channels"), icon("NodeChannelsToImage"), name("Channels to Image"))] fn channels_to_image( _: impl Ctx, @@ -209,7 +145,7 @@ fn extend_image_to_bounds(_: impl Ctx, image: Item>, bounds: Item>, bounds: Item, color: Item) -> Item> { - let transform = transform.into_element(); +/// Creates an image filled with one color, sized in pixels by the transform's scale and placed by the transform. +fn empty_image(transform: DAffine2, color: Color) -> Item> { let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32; let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32; - let image = Image::new(width, height, color.into_element()); + let image = Image::new(width, height, color); Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform) } diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 84afa444fb2..89250e74a48 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -14,7 +14,7 @@ use core_types::math::float_noise::round_away_float_noise; use core_types::misc::{format_f64, parse_f64}; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl}; use dyn_any::DynAny; -use glam::{DAffine2, DVec2}; +use glam::DVec2; use rand::{Rng, SeedableRng}; use unicode_segmentation::UnicodeSegmentation; @@ -978,16 +978,6 @@ fn read_string(ctx: impl Ctx + ExtractVarArgs) -> Item { var_arg.downcast_ref::>().cloned().unwrap_or_default() } -/// Converts a value to a JSON string representation. -#[node_macro::node(category("Debug"), icon("NodeSerialize"))] -fn serialize(_: impl Ctx, #[implementations(String, bool, f64, i64, DVec2, DAffine2)] value: Item) -> Item { - let (value, attributes) = value.into_parts(); - - let result = serde_json::to_string(&value).unwrap_or_else(|_| "Serialization Error".to_string()); - - Item::from_parts(result, attributes) -} - #[cfg(test)] mod tests { use super::*;