From ed6922d7eb3d6a05d92d2cac191237f216b9daba Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Aug 2026 10:26:24 +0000 Subject: [PATCH] Read the layer path in its owned form and retarget the introspection tests --- .../graph_modification_utils.rs | 7 +++--- .../tool/tool_messages/artboard_tool.rs | 15 ++++++++---- .../messages/tool/tool_messages/fill_tool.rs | 19 ++++++++------- editor/src/node_graph_executor.rs | 14 +++++++++++ .../libraries/graphic-types/src/graphic.rs | 8 +++---- .../libraries/rendering/src/renderer.rs | 24 +++++++++---------- node-graph/nodes/graphic/src/graphic.rs | 5 +--- node-graph/nodes/path-bool/src/lib.rs | 9 +++---- node-graph/nodes/text/src/to_path.rs | 4 ++-- node-graph/nodes/vector/src/vector_nodes.rs | 17 ++++--------- 10 files changed, 66 insertions(+), 56 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index ed779bd779..c1813bb6e3 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -7,8 +7,8 @@ use glam::{DAffine2, DVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::{ProtoNodeIdentifier, concrete}; +use graphene_std::Color; use graphene_std::NodeInputDecleration; -use graphene_std::list::List; use graphene_std::raster::BlendMode; use graphene_std::raster_types::{CPU, GPU, Image, Raster}; use graphene_std::subpath::Subpath; @@ -16,7 +16,6 @@ use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box}; use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, PointId, SegmentId, VectorModificationType}; -use graphene_std::Color; use std::collections::VecDeque; /// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists. @@ -977,6 +976,8 @@ impl<'a> NodeGraphLayer<'a> { pub fn is_raster_layer(layer: LayerNodeIdentifier, network_interface: &mut NodeNetworkInterface) -> bool { let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]); - layer_input_type.compiled_nested_type() == Some(&concrete!(List>)) || layer_input_type.compiled_nested_type() == Some(&concrete!(List>)) + // A leveled wire is typed by its element; depth rides the layout. + let compiled = layer_input_type.compiled_nested_type(); + compiled == Some(&concrete!(Raster)) || compiled == Some(&concrete!(Raster)) } } diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 70f34be928..6eefe47e64 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -574,15 +574,22 @@ mod test_artboard { use graphene_std::Artboard; use graphene_std::list::List; + /// A leveled wire introspects as its whole legacy list, so each `extend` + /// occurrence yields one list rather than one element. async fn get_artboards(editor: &mut EditorTestUtils) -> List { let instrumented = match editor.eval_graph().await { Ok(instrumented) => instrumented, Err(e) => panic!("Failed to evaluate graph: {e}"), }; - instrumented - .grab_all_input::>(&editor.runtime) - .map(graphene_std::list::Item::new_from_element) - .collect() + let mut artboards = List::new(); + for list in instrumented.grab_all_input_level::, Artboard>(&editor.runtime) { + for index in 0..list.len() { + if let Some(item) = list.clone_item(index) { + artboards.push(item); + } + } + } + artboards } #[derive(Debug, PartialEq)] diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index f2083b2c49..7094ddb176 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -205,17 +205,22 @@ impl Fsm for FillToolFsmState { #[cfg(test)] mod test_fill { pub use crate::test_utils::test_prelude::*; + use graphene_std::Graphic; use graphene_std::color::SRGBA8; use graphene_std::vector::fill; - use graphene_std::Graphic; - async fn get_fills(editor: &mut EditorTestUtils) -> Vec { + /// Paint inputs are single-typed now, so the monitored wire carries the + /// colors themselves and the `Graphic` conversion sits downstream of it. + async fn get_fills(editor: &mut EditorTestUtils) -> Vec { let instrumented = match editor.eval_graph().await { Ok(instrumented) => instrumented, Err(e) => panic!("Failed to evaluate graph: {e}"), }; - instrumented.grab_all_input::(&editor.runtime).collect() + instrumented + .grab_all_input_level::(&editor.runtime) + .flat_map(|list| list.iter_element_values().cloned().collect::>()) + .collect() } #[tokio::test] @@ -245,9 +250,7 @@ mod test_fill { editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await; let fills = get_fills(&mut editor).await; assert_eq!(fills.len(), 1); - let Some(Graphic::Color(color_list)) = fills.first() else { panic!("the fill paint holds a color") }; - let color = color_list.element(0).expect("Color is stored in the list"); - assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::GREEN)); + assert_eq!(SRGBA8::from(fills[0]), SRGBA8::from(Color::GREEN)); } #[tokio::test] @@ -259,8 +262,6 @@ mod test_fill { editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await; let fills = get_fills(&mut editor).await; assert_eq!(fills.len(), 1); - let Some(Graphic::Color(color_list)) = fills.first() else { panic!("the fill paint holds a color") }; - let color = color_list.element(0).expect("Color is stored in the list"); - assert_eq!(SRGBA8::from(*color), SRGBA8::from(Color::YELLOW)); + assert_eq!(SRGBA8::from(fills[0]), SRGBA8::from(Color::YELLOW)); } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 59babbc62a..b41db554ac 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -999,6 +999,20 @@ mod test { .filter_map(Instrumented::downcast::) // Some might not resolve (e.g. generics that don't work properly) } + /// Grab all of the values of a LEVELED input, which introspects as its + /// whole legacy list rather than as one element. `T` is the introspected + /// element type, which differs from the declared one where a conversion + /// sits downstream of the monitor. + pub fn grab_all_input_level<'a, Input: NodeInputDecleration + 'a, T: Send + Sync + Clone + 'static>(&'a self, runtime: &'a NodeRuntime) -> impl Iterator> + 'a { + self.protonodes_by_name + .get(&Input::identifier()) + .map_or([].as_slice(), |x| x.as_slice()) + .iter() + .filter_map(|inputs| inputs.get(Input::INDEX)) + .filter_map(|input_monitor_node| runtime.executor.introspect(input_monitor_node).ok()) + .filter_map(|dynamic| dynamic.downcast_ref::>().cloned()) + } + pub fn grab_protonode_input(&self, path: &Vec, runtime: &NodeRuntime) -> Option where Input::Result: Send + Sync + Clone + 'static, diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 73f77f6627..99dfdbe41c 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -128,9 +128,9 @@ fn flatten_graphic_list(content: List, extract_variant: fn(Graphic) let parent_has_transform = current_graphic_item.attribute::(ATTR_TRANSFORM).is_some(); let parent_has_opacity = current_graphic_item.attribute::(ATTR_OPACITY).is_some(); let parent_has_fill = current_graphic_item.attribute::(ATTR_OPACITY_FILL).is_some(); - let parent_has_layer_path = current_graphic_item.attribute::>(ATTR_EDITOR_LAYER_PATH).is_some(); + let parent_has_layer_path = current_graphic_item.attribute::>(ATTR_EDITOR_LAYER_PATH).is_some(); - let layer_path: List = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH); + let layer_path: Vec = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH); let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM); let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.); let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); @@ -323,7 +323,7 @@ impl IntoGraphicList for List { fn into_graphic_list(self) -> List { // Propagate `editor:layer_path` from item 0 onto the wrapper Graphic item so a subsequent // `flatten_graphic_list` doesn't overwrite the inner Vector's stamp with an empty value - let layer_path: List = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); + let layer_path: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); let mut graphic_list = List::new_from_element(Graphic::Vector(self)); if !layer_path.is_empty() { graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); @@ -358,7 +358,7 @@ impl IntoGraphicList for List { impl IntoGraphicList for List { fn into_graphic_list(self) -> List { - let layer_path: List = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); + let layer_path: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); let mut graphic_list = List::new_from_element(Graphic::Text(self)); if !layer_path.is_empty() { graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 7368140bbd..af2bbfd471 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -584,8 +584,8 @@ impl Render for Graphic { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than the first item if !list.is_empty() { - let layer_path: List = list.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); - let layer = layer_path.iter_element_values().next_back().copied(); + let layer_path: Vec = list.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); + let layer = layer_path.last().copied(); let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); metadata.first_element_source_id.insert(element_id, layer); @@ -803,8 +803,8 @@ impl Render for List { let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue }; let (location, dimensions, _background, clip) = read_artboard_attributes(self, index); - let layer_path: List = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); - let element_id = layer_path.iter_element_values().next_back().copied(); + let layer_path: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); + let element_id = layer_path.last().copied(); if let Some(element_id) = element_id { let subpath = Subpath::new_rectangle(DVec2::ZERO, dimensions); @@ -975,8 +975,8 @@ impl Render for List { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for index in 0..self.len() { let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let layer_path: List = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); - let layer = layer_path.iter_element_values().next_back().copied(); + let layer_path: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); + let layer = layer_path.last().copied(); let element = self.element(index).unwrap(); let mut footprint = footprint; @@ -1057,9 +1057,9 @@ impl Render for List { } fn new_ids_from_hash(&mut self, _reference: Option) { - let (elements, layers) = self.element_and_attribute_slices_mut::>(ATTR_EDITOR_LAYER_PATH); + let (elements, layers) = self.element_and_attribute_slices_mut::>(ATTR_EDITOR_LAYER_PATH); for (element, layer) in elements.iter_mut().zip(layers.iter()) { - element.new_ids_from_hash(layer.iter_element_values().next_back().copied()); + element.new_ids_from_hash(layer.last().copied()); } } } @@ -1576,8 +1576,8 @@ impl Render for List { for index in 0..self.len() { let Some(source) = self.element(index) else { continue }; let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let layer_path: List = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); - let layer = layer_path.iter_element_values().next_back().copied(); + let layer_path: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); + let layer = layer_path.last().copied(); if let Some(element_id) = caller_element_id.or(layer) { // When recovering element_id from the item's editor:layer_path tag (because the caller @@ -2573,8 +2573,8 @@ impl Render for List { let mut accumulated_click_targets: HashMap>> = HashMap::new(); for index in 0..self.len() { - let layer_path: List = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); - let layer = layer_path.iter_element_values().next_back().copied(); + let layer_path: Vec = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); + let layer = layer_path.last().copied(); let Some(element_id) = caller_element_id.or(layer) else { continue }; // When recovering element_id from the item's tag (caller passed None), also store the transform metadata. diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 51c5daaf4b..46e92bbfa8 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -225,10 +225,7 @@ where } let fill = park_paint(legacy.attribute::>(graphic_types::ATTR_FILL, source).cloned())?; let stroke = park_paint(legacy.attribute::>(graphic_types::ATTR_STROKE, source).cloned())?; - let layer_path: Vec = legacy - .attribute::>(ATTR_EDITOR_LAYER_PATH, source) - .map(|path| path.iter_element_values().copied().collect()) - .unwrap_or_default(); + let layer_path: Vec = legacy.attribute::>(ATTR_EDITOR_LAYER_PATH, source).map(|path| path.clone()).unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; Ok(( diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index f356aeb865..fdf484892f 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -74,10 +74,7 @@ fn boolean_core<'e>( let element = result_vector_list.element(0).cloned().unwrap_or_default(); let fill = park_paint(graphic_types::graphic::graphic_list_at(&result_vector_list, 0, graphic_types::ATTR_FILL).map(|paint| paint.into_owned()))?; let stroke = park_paint(graphic_types::graphic::graphic_list_at(&result_vector_list, 0, graphic_types::ATTR_STROKE).map(|paint| paint.into_owned()))?; - let layer_path: Vec = result_vector_list - .attribute::>(ATTR_EDITOR_LAYER_PATH, 0) - .map(|path| path.iter_element_values().copied().collect()) - .unwrap_or_default(); + let layer_path: Vec = result_vector_list.attribute::>(ATTR_EDITOR_LAYER_PATH, 0).map(|path| path.clone()).unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; // Snapshot the input layers so the renderer can recurse into them for // editor click-target preservation. @@ -321,7 +318,7 @@ fn flatten_vector(graphic_list: &List) -> List { (0..image.len()) .map(|i| { let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i); - let layer: List = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i); + let layer: Vec = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i); let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i); let opacity: f64 = image.attribute_cloned_or(ATTR_OPACITY, i, 1.); let fill: f64 = image.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); @@ -354,7 +351,7 @@ fn flatten_vector(graphic_list: &List) -> List { (0..image.len()) .map(|i| { let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i); - let layer: List = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i); + let layer: Vec = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i); let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i); let opacity: f64 = image.attribute_cloned_or(ATTR_OPACITY, i, 1.); let fill: f64 = image.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); diff --git a/node-graph/nodes/text/src/to_path.rs b/node-graph/nodes/text/src/to_path.rs index 944c02ba72..5dd27a6608 100644 --- a/node-graph/nodes/text/src/to_path.rs +++ b/node-graph/nodes/text/src/to_path.rs @@ -1,9 +1,9 @@ use super::TypesettingConfig; use super::text_context::TextContext; +use crate::markers::{ATTR_FONT, ATTR_TEXT_ALIGN}; use core_types::blending::BlendMode; use core_types::list::List; use core_types::uuid::NodeId; -use crate::markers::{ATTR_FONT, ATTR_TEXT_ALIGN}; use core_types::{ ATTR_BLEND_MODE, ATTR_EDITOR_LAYER_PATH, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, }; @@ -54,7 +54,7 @@ pub fn shape_text_list(strings: &List, separate_glyphs: bool) -> List(ATTR_TRANSFORM, index); - let layer_path = strings.attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, index); + let layer_path = strings.attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, index); let blend_mode = strings.attribute::(ATTR_BLEND_MODE, index).copied(); let opacity = strings.attribute::(ATTR_OPACITY, index).copied(); let opacity_fill = strings.attribute::(ATTR_OPACITY_FILL, index).copied(); diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 3d8eb0d9ed..049daa42b2 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1492,10 +1492,7 @@ fn emit_legacy_lane<'e>( let element = output.element(lane).cloned().unwrap_or_default(); let fill = output.attribute::>(ATTR_FILL, lane).map(|paint| park_paint(arena, paint.clone())).transpose()?; let stroke = output.attribute::>(ATTR_STROKE, lane).map(|paint| park_paint(arena, paint.clone())).transpose()?; - let layer_path: Vec = output - .attribute::>(ATTR_EDITOR_LAYER_PATH, lane) - .map(|path| path.iter_element_values().copied().collect()) - .unwrap_or_default(); + let layer_path: Vec = output.attribute::>(ATTR_EDITOR_LAYER_PATH, lane).map(|path| path.clone()).unwrap_or_default(); let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; let merged_layers = output .attribute::>(ATTR_EDITOR_MERGED_LAYERS, lane) @@ -1753,8 +1750,8 @@ fn flatten_path_core<'e>( // Concatenate every vector element's subpaths into the single output compound path for index in 0..flattened.len() { let Some(element) = flattened.element(index) else { continue }; - let layer_path: List = flattened.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); - let node_id = layer_path.iter_element_values().next_back().map(|node_id| node_id.0).unwrap_or_default(); + let layer_path: Vec = flattened.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index); + let node_id = layer_path.last().map(|node_id| node_id.0).unwrap_or_default(); let mut hasher = DefaultHasher::new(); (index, node_id).hash(&mut hasher); @@ -1786,11 +1783,7 @@ fn flatten_path_core<'e>( stroke = carrier.attribute::>(ATTR_STROKE, 0).map(|paint| park_paint(arena, paint.clone())).transpose()?; // Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer - layer_path = flattened - .attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, primary) - .iter_element_values() - .copied() - .collect(); + layer_path = flattened.attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, primary); } let exhausted = || { Interrupt::from(GraphError { @@ -3150,7 +3143,7 @@ fn morph_core(content: List, progression: f64, reverse: bool, distribut // The result is a synthesis of source and target, so adopt whichever endpoint the result is closer to as // the click-target identity (so the editor can route clicks back to one of the contributing layers) let primary_index = if time < 0.5 { source_index } else { target_index }; - let layer_path: List = content.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary_index); + let layer_path: Vec = content.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, primary_index); let mut item = Item::new_from_element(vector) .with_attribute(ATTR_TRANSFORM, lerped_transform)