Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .branding
Original file line number Diff line number Diff line change
@@ -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
172 changes: 155 additions & 17 deletions editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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: &[
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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();
Comment thread
Keavon marked this conversation as resolved.
Resolved
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);
Expand Down Expand Up @@ -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<NodeInput>| 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}");
}
}
}
6 changes: 0 additions & 6 deletions frontend/src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 },
Expand Down
73 changes: 4 additions & 69 deletions node-graph/nodes/raster/src/std_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,70 +30,6 @@ impl From<std::io::Error> for Error {
}
}

#[node_macro::node(category("Debug"), icon("NodeSampleImage"))]
fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item<Raster<CPU>>) -> Item<Raster<CPU>> {
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,
Expand Down Expand Up @@ -209,7 +145,7 @@ fn extend_image_to_bounds(_: impl Ctx, image: Item<Raster<CPU>>, bounds: Item<DA
let (image, mut attributes) = image.into_parts();
let (image_width, image_height) = (image.width, image.height);
if image_width == 0 || image_height == 0 {
return empty_image((), Item::new_from_element(bounds), Item::new_from_element(Color::TRANSPARENT));
return empty_image(bounds, Color::TRANSPARENT);
}

let orig_image_scale = DVec2::new(image_width as f64, image_height as f64);
Expand Down Expand Up @@ -239,13 +175,12 @@ fn extend_image_to_bounds(_: impl Ctx, image: Item<Raster<CPU>>, bounds: Item<DA
Item::from_parts(Raster::new_cpu(new_image), attributes)
}

#[node_macro::node(category("Debug"), icon("NodeEmptyImage"))]
fn empty_image(_: impl Ctx, transform: Item<DAffine2>, color: Item<Color>) -> Item<Raster<CPU>> {
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<Raster<CPU>> {
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)
}
Expand Down
Loading
Loading