diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 9abd4ba5b7..e508cbd5bc 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -35,9 +35,14 @@ impl MessageHandler for NewDocumentDialogMessageHa }); responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id, + network_path: Vec::new(), alias: "Background".to_string(), }); - responses.add(NodeGraphMessage::SetLocked { node_id, locked: true }); + responses.add(NodeGraphMessage::SetLocked { + node_id, + network_path: Vec::new(), + locked: true, + }); } else if self.dimensions.x > 0 && self.dimensions.y > 0 { // Finite canvas: create an artboard with the specified dimensions responses.add(GraphOperationMessage::NewArtboard { diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs index 57a93ed2b3..88f42bd78f 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs @@ -10,6 +10,9 @@ pub enum DataPanelMessage { inspect_result: InspectResult, }, ClearLayout, + /// Re-render the existing layout against the latest network interface state. Use this when node metadata + /// (display name, visibility, locked, etc.) changes but the introspected output value hasn't. + Refresh, PushToElementPath { step: PathStep, diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index cf366a1bfe..d3d95cbc66 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -51,6 +51,12 @@ impl MessageHandler> for DataPanel self.active_vector_table_tab = VectorTableTab::default(); self.update_layout(responses, context); } + DataPanelMessage::Refresh => { + // Re-render against the current network_interface without disturbing introspected_data or the breadcrumb path. + // Always re-renders, even when introspected_data is None, since the header still shows the inspected node's + // name/lock/visibility state from the network interface and that state can change independently of the data. + self.update_layout(responses, context); + } DataPanelMessage::PushToElementPath { step } => { self.element_path.push(step); @@ -80,6 +86,8 @@ impl DataPanelMessageHandler { let mut layout_data = LayoutData { current_depth: 0, desired_path: &mut self.element_path, + network_interface: &*network_interface, + node_lookup_network_path: Vec::new(), breadcrumbs: Vec::new(), vector_table_tab: self.active_vector_table_tab, }; @@ -98,6 +106,7 @@ impl DataPanelMessageHandler { if let Some((node_id, parent_path)) = self.introspected_node_path.split_last() { let node_id = *node_id; let is_layer = network_interface.is_layer(&node_id, parent_path); + let parent_path_owned = parent_path.to_vec(); widgets.extend([ if is_layer { @@ -111,6 +120,7 @@ impl DataPanelMessageHandler { .on_update(move |text_input| { NodeGraphMessage::SetDisplayName { node_id, + network_path: parent_path_owned.clone(), alias: text_input.value.clone(), skip_adding_history_step: false, } @@ -144,6 +154,11 @@ impl DataPanelMessageHandler { struct LayoutData<'a> { current_depth: usize, desired_path: &'a mut Vec, + network_interface: &'a NodeNetworkInterface, + /// The `network_path` to use when resolving a `NodeId` cell or leaf page against the network interface. + /// Defaults to root (`&[]`); `Table` rendering temporarily sets it to the path's prefix so nested + /// layers (e.g. inside a Ctrl+M-merged custom subgraph) resolve correctly. + node_lookup_network_path: Vec, breadcrumbs: Vec, vector_table_tab: VectorTableTab, } @@ -161,6 +176,11 @@ macro_rules! generate_layout_downcast { } // TODO: We simply try all these types sequentially. Find a better strategy. fn generate_layout(introspected_data: &Arc, data: &mut LayoutData) -> Option> { + // `Table` is interpreted as a path (e.g. the value produced by `path_of_subgraph`), shown as a + // table where each row's NodeId resolves against the prefix made up of the rows above it. + if let Some(io) = introspected_data.downcast_ref::>>() { + return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data)); + } generate_layout_downcast!(introspected_data, data, [ Table, Table, @@ -170,7 +190,6 @@ fn generate_layout(introspected_data: &Arc, Table, Table, - Table, Table, Table, GradientStops, @@ -203,10 +222,12 @@ trait TableRowLayout { } /// Renders this value as a single inline widget inside a row of a Vec/Table. /// `target` is the [`PathStep`] to push when the cell is clicked to drill into the value. + /// `data` provides shared context (notably `network_interface`) for types whose label or content + /// depends on lookup beyond their own value (e.g. `NodeId` resolving a node's display name). /// The default is a button labeled with `identifier()`. Types whose values are best shown /// inline (colors, transforms, primitives, etc.) override this to ignore `target` and /// return a richer non-navigating widget. - fn cell_widget(&self, target: PathStep) -> WidgetInstance { + fn cell_widget(&self, target: PathStep, _data: &LayoutData) -> WidgetInstance { TextButton::new(self.identifier()) .on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into()) .narrow(true) @@ -258,10 +279,10 @@ impl TableRowLayout for Table { let mut rows = (0..self.len()) .map(|index| { let element = self.element(index).unwrap(); - let mut cells = vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), element.cell_widget(PathStep::Element(index))]; + let mut cells = vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), element.cell_widget(PathStep::Element(index), data)]; for key in &attribute_keys { let target = PathStep::Attribute { row: index, key: key.clone() }; - let widget = self.attribute_any(key, index).and_then(|any| dispatch_cell_widget(any, target)).unwrap_or_else(|| { + let widget = self.attribute_any(key, index).and_then(|any| dispatch_cell_widget(any, target, data)).unwrap_or_else(|| { let text = self.attribute_display_value(key, index, |_| None).unwrap_or_else(|| "-".to_string()); TextLabel::new(text).narrow(true).widget_instance() }); @@ -531,7 +552,7 @@ impl TableRowLayout for Color { fn identifier(&self) -> String { format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb()) } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { ColorInput::new(FillChoice::Solid(*self)) .disabled(true) .menu_direction(Some(MenuDirection::Top)) @@ -539,7 +560,7 @@ impl TableRowLayout for Color { .widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - let widgets = vec![self.cell_widget(PathStep::Element(0))]; + let widgets = vec![self.cell_widget(PathStep::Element(0), _data)]; vec![LayoutGroup::row(widgets)] } } @@ -551,7 +572,7 @@ impl TableRowLayout for GradientStops { fn identifier(&self) -> String { format!("Gradient ({} stops)", self.len()) } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { ColorInput::new(FillChoice::Gradient(self.clone())) .menu_direction(Some(MenuDirection::Top)) .disabled(true) @@ -559,7 +580,7 @@ impl TableRowLayout for GradientStops { .widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - let widgets = vec![self.cell_widget(PathStep::Element(0))]; + let widgets = vec![self.cell_widget(PathStep::Element(0), _data)]; vec![LayoutGroup::row(widgets)] } } @@ -569,13 +590,13 @@ impl TableRowLayout for f64 { "Number (f64)" } fn identifier(&self) -> String { - "Number (f64)".to_string() - } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - NumberInput::new(Some(*self)).disabled(true).max_width(220).display_decimal_places(20).widget_instance() + format!("{self}") } + // Cells fall back to the default drill-in button (labeled with the value via `identifier`); the leaf page shows the rich `NumberInput`. fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![ + NumberInput::new(Some(*self)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(), + ])] } } @@ -586,11 +607,9 @@ impl TableRowLayout for u8 { fn identifier(&self) -> String { format!("{self:02X}") } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - TextLabel::new(self.identifier()).narrow(true).widget_instance() - } + // Cells fall back to the default drill-in button (labeled with the hex value via `identifier`); the leaf page shows the same hex value as a label. fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![TextLabel::new(self.identifier()).widget_instance()])] } } @@ -599,13 +618,13 @@ impl TableRowLayout for u32 { "Number (u32)" } fn identifier(&self) -> String { - "Number (u32)".to_string() - } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance() + format!("{self}") } + // Cells fall back to the default drill-in button (labeled with the value via `identifier`); the leaf page shows the rich `NumberInput`. fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![ + NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(), + ])] } } @@ -614,14 +633,14 @@ impl TableRowLayout for u64 { "Number (u64)" } fn identifier(&self) -> String { - "Number (u64)".to_string() + format!("{self}") } + // Cells fall back to the default drill-in button (labeled with the value via `identifier`); the leaf page shows the rich `NumberInput`. // TODO: Make this robust for large u64 values that don't fit in f64 (above roughly 2^53). Perhaps using a bigint kind of approach through the widget's data flow. - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance() - } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![ + NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(), + ])] } } @@ -632,11 +651,11 @@ impl TableRowLayout for bool { fn identifier(&self) -> String { "Bool".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(self.to_string()).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -653,9 +672,7 @@ impl TableRowLayout for String { format!("\"{}\"", first_line) } } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - TextLabel::new(self.identifier()).narrow(true).widget_instance() - } + // Cells fall back to the default drill-in button (labeled with the truncated quoted preview via `identifier`); the leaf page shows the full multi-line text in a `TextAreaInput`. fn element_page(&self, _data: &mut LayoutData) -> Vec { vec![LayoutGroup::row(vec![TextAreaInput::new(self.to_string()).monospace(true).disabled(true).widget_instance()])] } @@ -668,11 +685,11 @@ impl TableRowLayout for Option { fn identifier(&self) -> String { "Option".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format!("{self:?}")).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -683,11 +700,11 @@ impl TableRowLayout for DVec2 { fn identifier(&self) -> String { "Vec2".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format_dvec2(*self)).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -698,11 +715,11 @@ impl TableRowLayout for Vec2 { fn identifier(&self) -> String { "Vec2".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format_dvec2(DVec2::new(self.x as f64, self.y as f64))).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -713,11 +730,11 @@ impl TableRowLayout for DAffine2 { fn identifier(&self) -> String { "Transform".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format_transform_matrix(*self)).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -728,12 +745,12 @@ impl TableRowLayout for Affine2 { fn identifier(&self) -> String { "Transform".to_string() } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64)); TextLabel::new(format_transform_matrix(matrix)).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] } } @@ -744,11 +761,21 @@ impl TableRowLayout for AlphaBlending { fn identifier(&self) -> String { format_alpha_blending(*self) } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { + fn cell_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance { TextLabel::new(format_alpha_blending(*self)).narrow(true).widget_instance() } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0), _data)])] + } +} + +/// Resolves the cell/breadcrumb label for a `NodeId` against `network_interface` at the given `network_path`, +/// falling back to "Node {id}" if the node isn't present (e.g. an ID that no longer maps to a real node). +fn node_id_display_label(node_id: NodeId, network_interface: &NodeNetworkInterface, network_path: &[NodeId]) -> String { + if network_interface.node_metadata(&node_id, network_path).is_some() { + network_interface.display_name(&node_id, network_path) + } else { + format!("Node {node_id}") } } @@ -759,41 +786,108 @@ impl TableRowLayout for NodeId { fn identifier(&self) -> String { format!("Node {self}") } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - let node_id = *self; - TextButton::new("Go to Node") - .tooltip_description("Click to select the node with this ID in the graph.") - .on_update(move |_| NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into()) - .narrow(true) - .widget_instance() + // Override so the breadcrumb uses the same resolved display name as the cell button, instead of the bare-ID fallback `identifier()` returns. + fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec { + data.breadcrumbs.push(node_id_display_label(*self, data.network_interface, &data.node_lookup_network_path)); + self.element_page(data) } - fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + // Cell label resolves the node's display name via the network interface so the button reads as the name shown + // in the Node Graph / Layers panels. The lookup uses `data.node_lookup_network_path` (set by the enclosing + // `Table` if rendering a path) so the resolution succeeds at any nesting depth. The button's icon + // signals layer-vs-node kind. Falls back to "Node {id}" with no icon if the lookup misses. + fn cell_widget(&self, target: PathStep, data: &LayoutData) -> WidgetInstance { + let label = node_id_display_label(*self, data.network_interface, &data.node_lookup_network_path); + let mut button = TextButton::new(label) + .on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into()) + .narrow(true); + if data.network_interface.node_metadata(self, &data.node_lookup_network_path).is_some() { + let icon = if data.network_interface.is_layer(self, &data.node_lookup_network_path) { "Layer" } else { "Node" }; + button = button.icon(icon); + } + button.widget_instance() } -} + // The leaf page shows the node's kind, name (editable), lock/visibility toggles, and a "Select Layer/Node" action button. + fn element_page(&self, data: &mut LayoutData) -> Vec { + let node_id = *self; + let network_path = data.node_lookup_network_path.clone(); + let known = data.network_interface.node_metadata(&node_id, &network_path).is_some(); + let is_layer = known && data.network_interface.is_layer(&node_id, &network_path); + let name = if known { + data.network_interface.display_name(&node_id, &network_path) + } else { + "(node not found)".to_string() + }; + let kind_widget = if known { + IconLabel::new(if is_layer { "Layer" } else { "Node" }).widget_instance() + } else { + TextLabel::new("-").widget_instance() + }; + let name_widget = if known { + let path_for_rename = network_path.clone(); + TextInput::new(name) + .tooltip_description(if is_layer { "Name of this layer." } else { "Name of this node." }) + .on_update(move |text_input| { + NodeGraphMessage::SetDisplayName { + node_id, + network_path: path_for_rename.clone(), + alias: text_input.value.clone(), + skip_adding_history_step: false, + } + .into() + }) + .max_width(200) + .widget_instance() + } else { + TextLabel::new(name).widget_instance() + }; -impl TableRowLayout for Option { - fn type_name() -> &'static str { - "NodeId" - } - fn identifier(&self) -> String { - match self { - Some(node_id) => format!("Node {}", node_id), - None => "-".to_string(), + let mut header = vec![kind_widget, Separator::new(SeparatorStyle::Related).widget_instance(), name_widget]; + + if known { + let is_locked = data.network_interface.is_locked(&node_id, &network_path); + let is_visible = data.network_interface.is_visible(&node_id, &network_path); + + let path_for_lock = network_path.clone(); + let path_for_visibility = network_path.clone(); + + header.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); + header.push( + IconButton::new(if is_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24) + .hover_icon(if is_locked { "PadlockUnlocked" } else { "PadlockLocked" }) + .tooltip_label(if is_locked { "Unlock" } else { "Lock" }) + .on_update(move |_| { + NodeGraphMessage::ToggleLocked { + node_id, + network_path: path_for_lock.clone(), + } + .into() + }) + .widget_instance(), + ); + header.push( + IconButton::new(if is_visible { "EyeVisible" } else { "EyeHidden" }, 24) + .hover_icon(if is_visible { "EyeHide" } else { "EyeShow" }) + .tooltip_label(if is_visible { "Hide" } else { "Show" }) + .on_update(move |_| { + NodeGraphMessage::ToggleVisibility { + node_id, + network_path: path_for_visibility.clone(), + } + .into() + }) + .widget_instance(), + ); } - } - fn cell_widget(&self, _target: PathStep) -> WidgetInstance { - match *self { - Some(node_id) => TextButton::new("Go to Node") - .tooltip_description("Click to select the node with this ID in the graph.") + + header.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); + header.push( + TextButton::new(if is_layer { "Select Layer" } else { "Select Node" }) + .tooltip_description(if is_layer { "Click to select this layer." } else { "Click to select this node." }) .on_update(move |_| NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into()) - .narrow(true) .widget_instance(), - None => TextLabel::new("-").narrow(true).widget_instance(), - } - } - fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] + ); + + vec![LayoutGroup::row(header)] } } @@ -817,7 +911,6 @@ macro_rules! known_table_row_types { GradientStops, Color, NodeId, - Option, AlphaBlending, DAffine2, DVec2, @@ -843,12 +936,12 @@ macro_rules! known_table_row_types { /// Delegates to [`TableRowLayout::cell_widget`] so the same widget code is shared between /// element-column rendering and attribute-column rendering. Returns `None` for unrecognized types so the /// caller can fall back to a debug-formatted [`TextLabel`]. -fn dispatch_cell_widget(any: &dyn Any, target: PathStep) -> Option { +fn dispatch_cell_widget(any: &dyn Any, target: PathStep, data: &LayoutData) -> Option { macro_rules! check { ( $($ty:ty),* $(,)? ) => { $( if let Some(value) = any.downcast_ref::<$ty>() { - return Some(value.cell_widget(target)); + return Some(value.cell_widget(target, data)); } )* }; @@ -857,10 +950,54 @@ fn dispatch_cell_widget(any: &dyn Any, target: PathStep) -> Option` as a path: the standard table view, but each row's `NodeId` cell is resolved +/// against the network path made up of all preceding rows. So for a path `[outer, middle, leaf]`, row 0 +/// resolves at root, row 1 resolves at `[outer]`, and row 2 resolves at `[outer, middle]` — letting deeply +/// nested layers display each step's correct name. Drilling into a row drops into that node's leaf page +/// using the same prefix as `network_path`. +fn table_node_id_path_layout_with_breadcrumb(path: &Table, data: &mut LayoutData) -> Vec { + data.breadcrumbs.push(path.identifier()); + + if let Some(step) = data.desired_path.get(data.current_depth).cloned() { + if let PathStep::Element(index) = step + && let Some(node_id) = path.element(index) + { + let prefix: Vec = path.iter_element_values().take(index).copied().collect(); + let saved = std::mem::replace(&mut data.node_lookup_network_path, prefix); + data.current_depth += 1; + let result = node_id.layout_with_breadcrumb(data); + data.current_depth -= 1; + data.node_lookup_network_path = saved; + return result; + } + warn!("Desired path truncated"); + data.desired_path.truncate(data.current_depth); + } + + let mut rows = (0..path.len()) + .map(|index| { + let node_id = path.element(index).unwrap(); + let prefix: Vec = path.iter_element_values().take(index).copied().collect(); + let saved = std::mem::replace(&mut data.node_lookup_network_path, prefix); + let widget = node_id.cell_widget(PathStep::Element(index), data); + data.node_lookup_network_path = saved; + vec![TextLabel::new(format!("{index}")).narrow(true).widget_instance(), widget] + }) + .collect::>(); + rows.insert(0, column_headings(&["", "element"])); + + vec![LayoutGroup::table(rows, false)] +} + /// Type-dispatched recursion into an attribute value for the data panel breadcrumb navigation. /// Mirrors [`dispatch_cell_widget`] but routes to [`TableRowLayout::layout_with_breadcrumb`]. /// Returns `None` for unrecognized types. fn drilldown_attribute_layout(any: &dyn Any, data: &mut LayoutData) -> Option> { + // `Table` is interpreted as a path (e.g. the `editor:layer` attribute), so each row's NodeId cell + // resolves against the prefix made up of preceding rows. Handled before the generic `Table` blanket impl. + if let Some(path) = any.downcast_ref::>() { + return Some(table_node_id_path_layout_with_breadcrumb(path, data)); + } macro_rules! check { ( $($ty:ty),* $(,)? ) => { $( diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 7414c7a5ff..dae0f1f48a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -699,6 +699,7 @@ impl MessageHandler> for DocumentMes if let Some(name) = name { responses.add(NodeGraphMessage::SetDisplayName { node_id: layer.to_node(), + network_path: Vec::new(), alias: name, skip_adding_history_step: false, }); @@ -756,6 +757,7 @@ impl MessageHandler> for DocumentMes if let Some(name) = name { responses.add(NodeGraphMessage::SetDisplayName { node_id: layer.to_node(), + network_path: Vec::new(), alias: name, skip_adding_history_step: false, }); diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 65e14a3f70..b77914d341 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -197,10 +197,12 @@ impl MessageHandler> for responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id: id, + network_path: Vec::new(), alias: layer_alias.to_string(), }); responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id: control_path_id, + network_path: Vec::new(), alias: path_alias.to_string(), }); } @@ -245,6 +247,7 @@ impl MessageHandler> for network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id: id, + network_path: Vec::new(), alias: "Boolean Operation".to_string(), }); responses.add(NodeGraphMessage::RunDocumentGraph); @@ -343,6 +346,7 @@ impl MessageHandler> for responses.add(NodeGraphMessage::SetDisplayName { node_id, + network_path: Vec::new(), alias: network_interface.display_name(&artboard.to_node(), &[]), skip_adding_history_step: true, }); diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 92354d1224..f692b98163 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -236,7 +236,7 @@ fn document_node_definitions() -> HashMap HashMap HashMap HashMap, alias: String, skip_adding_history_step: bool, }, SetDisplayNameImpl { node_id: NodeId, + network_path: Vec, alias: String, }, SetToNodeOrLayer { @@ -199,15 +203,22 @@ pub enum NodeGraphMessage { ToggleSelectedLocked, ToggleLocked { node_id: NodeId, + /// The path to the network containing `node_id`. Empty for nodes at the root document network. + /// Lets the toggle target a node at any nesting depth, independent of the current selection network. + network_path: Vec, }, SetLocked { node_id: NodeId, + network_path: Vec, locked: bool, }, ToggleSelectedIsPinned, ToggleSelectedVisibility, ToggleVisibility { node_id: NodeId, + /// The path to the network containing `node_id`. Empty for nodes at the root document network. + /// Lets the toggle target a node at any nesting depth, independent of the current selection network. + network_path: Vec, }, SetPinned { node_id: NodeId, @@ -215,10 +226,12 @@ pub enum NodeGraphMessage { }, SetVisibility { node_id: NodeId, + network_path: Vec, visible: bool, }, SetLockedOrVisibilitySideEffects { node_ids: Vec, + network_path: Vec, }, UpdateEdges, UpdateBoxSelection, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 0d8a4239d1..0f8a51298b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -659,6 +659,7 @@ impl<'a> MessageHandler> for NodeG }); responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id: encapsulating_node_id, + network_path: selection_network_path.to_vec(), alias: "Untitled Node".to_string(), }); @@ -909,13 +910,19 @@ impl<'a> MessageHandler> for NodeG // Toggle visibility of clicked node and return if let Some(clicked_visibility) = network_interface.layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Visibility, selection_network_path) { - responses.add(NodeGraphMessage::ToggleVisibility { node_id: clicked_visibility }); + responses.add(NodeGraphMessage::ToggleVisibility { + node_id: clicked_visibility, + network_path: selection_network_path.to_vec(), + }); return; } // Toggle lock of clicked node and return if let Some(clicked_lock) = network_interface.layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Lock, selection_network_path) { - responses.add(NodeGraphMessage::ToggleLocked { node_id: clicked_lock }); + responses.add(NodeGraphMessage::ToggleLocked { + node_id: clicked_lock, + network_path: selection_network_path.to_vec(), + }); return; } @@ -1816,13 +1823,14 @@ impl<'a> MessageHandler> for NodeG } NodeGraphMessage::SetDisplayName { node_id, + network_path, alias, skip_adding_history_step, } => { if !skip_adding_history_step { responses.add(DocumentMessage::StartTransaction); } - responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id, alias }); + responses.add(NodeGraphMessage::SetDisplayNameImpl { node_id, network_path, alias }); if !skip_adding_history_step { // Does not add a history step if the name was not changed responses.add(DocumentMessage::EndTransaction); @@ -1831,9 +1839,10 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::RenderScrollbars); responses.add(NodeGraphMessage::SendGraph); responses.add(OverlaysMessage::Draw); // Redraw overlays to update artboard names + responses.add(DataPanelMessage::Refresh); } - NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => { - network_interface.set_display_name(&node_id, alias, selection_network_path); + NodeGraphMessage::SetDisplayNameImpl { node_id, network_path, alias } => { + network_interface.set_display_name(&node_id, alias, &network_path); } NodeGraphMessage::SetImportExportName { name, index } => { responses.add(DocumentMessage::StartTransaction); @@ -1872,25 +1881,34 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::AddTransaction); for node_id in &node_ids { - responses.add(NodeGraphMessage::SetLocked { node_id: *node_id, locked }); + responses.add(NodeGraphMessage::SetLocked { + node_id: *node_id, + network_path: selection_network_path.to_vec(), + locked, + }); } - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }) + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids, + network_path: selection_network_path.to_vec(), + }) } - NodeGraphMessage::ToggleLocked { node_id } => { - let Some(node_metadata) = network_interface.document_network_metadata().persistent_metadata.node_metadata.get(&node_id) else { - log::error!("Cannot get node {node_id:?} in NodeGraphMessage::ToggleLocked"); - return; - }; - - let locked = !node_metadata.persistent_metadata.locked; + NodeGraphMessage::ToggleLocked { node_id, network_path } => { + let locked = !network_interface.is_locked(&node_id, &network_path); responses.add(DocumentMessage::AddTransaction); - responses.add(NodeGraphMessage::SetLocked { node_id, locked }); - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids: vec![node_id] }) + responses.add(NodeGraphMessage::SetLocked { + node_id, + network_path: network_path.clone(), + locked, + }); + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids: vec![node_id], + network_path, + }); } - NodeGraphMessage::SetLocked { node_id, locked } => { - network_interface.set_locked(&node_id, selection_network_path, locked); + NodeGraphMessage::SetLocked { node_id, network_path, locked } => { + network_interface.set_locked(&node_id, &network_path, locked); } NodeGraphMessage::ToggleSelectedIsPinned => { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { @@ -1906,7 +1924,10 @@ impl<'a> MessageHandler> for NodeG for node_id in &node_ids { responses.add(NodeGraphMessage::SetPinned { node_id: *node_id, pinned }); } - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }); + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids, + network_path: selection_network_path.to_vec(), + }); } NodeGraphMessage::ToggleSelectedVisibility => { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { @@ -1920,31 +1941,46 @@ impl<'a> MessageHandler> for NodeG responses.add(DocumentMessage::AddTransaction); for node_id in &node_ids { - responses.add(NodeGraphMessage::SetVisibility { node_id: *node_id, visible }); + responses.add(NodeGraphMessage::SetVisibility { + node_id: *node_id, + network_path: selection_network_path.to_vec(), + visible, + }); } - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids }); + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids, + network_path: selection_network_path.to_vec(), + }); } - NodeGraphMessage::ToggleVisibility { node_id } => { - let visible = !network_interface.is_visible(&node_id, selection_network_path); + NodeGraphMessage::ToggleVisibility { node_id, network_path } => { + let visible = !network_interface.is_visible(&node_id, &network_path); responses.add(DocumentMessage::AddTransaction); - responses.add(NodeGraphMessage::SetVisibility { node_id, visible }); - responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids: vec![node_id] }); + responses.add(NodeGraphMessage::SetVisibility { + node_id, + network_path: network_path.clone(), + visible, + }); + responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { + node_ids: vec![node_id], + network_path, + }); } NodeGraphMessage::SetPinned { node_id, pinned } => { network_interface.set_pinned(&node_id, selection_network_path, pinned); } - NodeGraphMessage::SetVisibility { node_id, visible } => { - network_interface.set_visibility(&node_id, selection_network_path, visible); + NodeGraphMessage::SetVisibility { node_id, network_path, visible } => { + network_interface.set_visibility(&node_id, &network_path, visible); } - NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids } => { - if node_ids.iter().any(|node_id| network_interface.connected_to_output(node_id, selection_network_path)) { + NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids, network_path } => { + if node_ids.iter().any(|node_id| network_interface.connected_to_output(node_id, &network_path)) { responses.add(NodeGraphMessage::RunDocumentGraph); } responses.add(NodeGraphMessage::UpdateActionButtons); responses.add(NodeGraphMessage::SendGraph); responses.add(PropertiesPanelMessage::Refresh); + responses.add(DataPanelMessage::Refresh); } NodeGraphMessage::UpdateBoxSelection => { if let Some((box_selection_start, _)) = self.box_selection_start { @@ -2397,6 +2433,7 @@ impl NodeGraphMessageHandler { let mut properties = Vec::new(); if let [node_id] = *nodes.as_slice() { + let network_path = context.selection_network_path.to_vec(); properties.push(LayoutGroup::row(vec![ Separator::new(SeparatorStyle::Related).widget_instance(), IconLabel::new("Node").tooltip_description("Name of the selected node.").widget_instance(), @@ -2406,6 +2443,7 @@ impl NodeGraphMessageHandler { .on_update(move |text_input| { NodeGraphMessage::SetDisplayName { node_id, + network_path: network_path.clone(), alias: text_input.value.clone(), skip_adding_history_step: false, } @@ -2468,6 +2506,7 @@ impl NodeGraphMessageHandler { return Vec::new(); } + let layer_network_path = context.selection_network_path.to_vec(); let mut layer_properties = vec![LayoutGroup::row(vec![ Separator::new(SeparatorStyle::Related).widget_instance(), IconLabel::new("Layer").tooltip_description("Name of the selected layer.").widget_instance(), @@ -2477,6 +2516,7 @@ impl NodeGraphMessageHandler { .on_update(move |text_input| { NodeGraphMessage::SetDisplayName { node_id: layer, + network_path: layer_network_path.clone(), alias: text_input.value.clone(), skip_adding_history_step: false, } diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 9d59b2cfd8..1a8c3bca9b 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1016,7 +1016,7 @@ pub fn document_migration_reset_node_definition(document_serialized_content: &st return true; } - // The `source_node_id` proto node was removed in favor of `parent_layer` + `write_attribute`. + // 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") diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index e9dd0edf6d..5e8b04dacc 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -775,6 +775,7 @@ impl EditorWrapper { let layer = LayerNodeIdentifier::new_unchecked(NodeId(id)); let message = NodeGraphMessage::SetDisplayName { node_id: layer.to_node(), + network_path: Vec::new(), alias: name, skip_adding_history_step: false, }; @@ -912,7 +913,7 @@ impl EditorWrapper { #[wasm_bindgen(js_name = toggleNodeVisibilityLayerPanel)] pub fn toggle_node_visibility_layer(&self, id: u64) { let node_id = NodeId(id); - let message = NodeGraphMessage::ToggleVisibility { node_id }; + let message = NodeGraphMessage::ToggleVisibility { node_id, network_path: Vec::new() }; self.dispatch(message); } @@ -931,7 +932,10 @@ impl EditorWrapper { /// Toggle lock state of a layer from the layer list #[wasm_bindgen(js_name = toggleLayerLock)] pub fn toggle_layer_lock(&self, node_id: u64) { - let message = NodeGraphMessage::ToggleLocked { node_id: NodeId(node_id) }; + let message = NodeGraphMessage::ToggleLocked { + node_id: NodeId(node_id), + network_path: Vec::new(), + }; self.dispatch(message); } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index d2d03335ac..9af0bc21e3 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -95,7 +95,6 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table]), @@ -173,7 +172,6 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Table>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Graphic]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => glam::f32::Vec2]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]), diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 9ad7fb150f..d480603541 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -141,7 +141,7 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic fn flatten_recursive(output: &mut Table, current_graphic_table: Table, extract_variant: fn(Graphic) -> Option>) { for current_graphic_row in current_graphic_table.into_iter() { - let layer: Option = current_graphic_row.attribute_cloned_or_default("editor:layer"); + let layer_path: Table = current_graphic_row.attribute_cloned_or_default("editor:layer"); let current_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default("transform"); let current_alpha_blending: AlphaBlending = current_graphic_row.attribute_cloned_or_default("alpha_blending"); @@ -168,7 +168,7 @@ fn flatten_graphic_table(content: Table, extract_variant: fn(Graphic attributes.insert("transform", current_transform * row_transform); attributes.insert("alpha_blending", compose_alpha_blending(current_alpha_blending, row_alpha_blending)); - attributes.insert("editor:layer", layer); + attributes.insert("editor:layer", layer_path.clone()); output.push(TableRow::from_parts(element, attributes)); } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 861a477baa..c5c7e7b634 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -412,7 +412,8 @@ impl Render for Graphic { metadata.upstream_footprints.insert(element_id, footprint); // TODO: Find a way to handle more than the first row if !table.is_empty() { - let layer: Option = table.attribute_cloned_or_default("editor:layer", 0); + let layer_path: Table = table.attribute_cloned_or_default("editor:layer", 0); + let layer = layer_path.iter_element_values().next_back().copied(); let transform: DAffine2 = table.attribute_cloned_or_default("transform", 0); metadata.first_element_source_id.insert(element_id, layer); @@ -655,7 +656,8 @@ impl Render for Table { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { for index in 0..self.len() { - let layer: Option = self.attribute_cloned_or_default("editor:layer", index); + let layer_path: Table = self.attribute_cloned_or_default("editor:layer", index); + let layer = layer_path.iter_element_values().next_back().copied(); self.element(index).unwrap().collect_metadata(metadata, footprint, layer); } } @@ -805,7 +807,8 @@ impl Render for Table { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { for index in 0..self.len() { let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index); - let layer: Option = self.attribute_cloned_or_default("editor:layer", index); + let layer_path: Table = self.attribute_cloned_or_default("editor:layer", index); + let layer = layer_path.iter_element_values().next_back().copied(); let element = self.element(index).unwrap(); let mut footprint = footprint; @@ -860,9 +863,9 @@ impl Render for Table { } fn new_ids_from_hash(&mut self, _reference: Option) { - let (elements, layers) = self.element_and_attribute_slices_mut::>("editor:layer"); + let (elements, layers) = self.element_and_attribute_slices_mut::>("editor:layer"); for (element, layer) in elements.iter_mut().zip(layers.iter()) { - element.new_ids_from_hash(*layer); + element.new_ids_from_hash(layer.iter_element_values().next_back().copied()); } } } @@ -1327,7 +1330,8 @@ impl Render for Table { for index in 0..self.len() { let Some(vector) = self.element(index) else { continue }; let transform: DAffine2 = self.attribute_cloned_or_default("transform", index); - let layer: Option = self.attribute_cloned_or_default("editor:layer", index); + let layer_path: Table = self.attribute_cloned_or_default("editor:layer", index); + let layer = layer_path.iter_element_values().next_back().copied(); if let Some(element_id) = caller_element_id.or(layer) { // When recovering element_id from the row's editor:layer tag (because the caller diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index 0e18ba94d6..1776a058a3 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -314,7 +314,7 @@ async fn brush( let transform: DAffine2 = actual_image.attribute_cloned_or_default("transform"); let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default("alpha_blending"); - let layer: Option = actual_image.attribute_cloned_or_default("editor:layer"); + let layer: Table = actual_image.attribute_cloned_or_default("editor:layer"); *image.element_mut(0).unwrap() = actual_image.into_element(); image.set_attribute("transform", 0, transform); diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index 8e22154e3f..627525ea5e 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -26,7 +26,6 @@ async fn context_modification( Context -> DAffine2, Context -> Footprint, Context -> DVec2, - Context -> Option, Context -> Table, Context -> Table, Context -> Table, diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 999df04d5b..d3c92b9bf3 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -209,14 +209,16 @@ where result_table } -/// Returns the NodeId of the user-facing parent layer node that encapsulates this sub-network. -/// Used as the value source for stamping the `editor:layer` attribute on each row of a layer's output, -/// which lets editor tools (e.g. selection, click target routing) trace data back to its owning layer. -#[node_macro::node(category(""))] -pub fn parent_layer(_: impl Ctx, node_path: Table) -> Option { - // Get the penultimate element of the node path, or None if the path is too short - let index = node_path.len().wrapping_sub(2); - node_path.element(index).copied() +/// Returns the path identifying the subgraph (network) that contains this proto node — i.e. the input `node_path` +/// with its own trailing entry dropped. The terminating element of the returned path is the document node whose +/// encapsulated network we live in, so the path doubles as a unique reference to that node at any nesting depth. +/// Used as the value source for stamping the `editor:layer` attribute on each row of a layer's output, which lets +/// editor tools (e.g. selection, click target routing) trace data back to its owning layer regardless of whether +/// the layer is at the root document network or nested inside a custom subgraph. +#[node_macro::node(name("Path of Subgraph"), category(""))] +pub fn path_of_subgraph(_: impl Ctx, node_path: Table) -> Table { + let len = node_path.len(); + node_path.into_iter().take(len.saturating_sub(1)).collect() } /// Writes a per-row attribute column on the input table. The value-producing input is evaluated once per row, @@ -241,13 +243,13 @@ async fn write_attribute f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, - Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Option, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, + Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table, Context -> DVec2, Context -> DAffine2, Context -> Table, Context -> Table, Context -> Table, )] value: impl Node<'n, Context<'static>, Output = U>, ) -> Table { diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 4076ed5586..aea0cfdb74 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -197,7 +197,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { (0..image.len()) .map(|i| { let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i); - let layer: Option = image.attribute_cloned_or_default("editor:layer", i); + let layer: Table = image.attribute_cloned_or_default("editor:layer", i); let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i); make_row(parent_transform * row_transform, layer, alpha_blending) }) @@ -223,7 +223,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { (0..image.len()) .map(|i| { let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i); - let layer: Option = image.attribute_cloned_or_default("editor:layer", i); + let layer: Table = image.attribute_cloned_or_default("editor:layer", i); let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i); make_row(parent_transform * row_transform, layer, alpha_blending) }) diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index f305d5066b..69eceda02a 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -15,13 +15,14 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table, modification: Bo } modification.apply(vector.element_mut(0).expect("push should give one item")); - // Update the source node id (penultimate element in the path, identifying the user-facing layer node) - let this_node_path = { - let index = node_path.len().wrapping_sub(2); - node_path.element(index).copied() + // Set the path to the encapsulating subgraph (drop our own trailing entry from `node_path`), + // matching the `path_of_subgraph` proto so editor tools can route data back to the parent layer. + let subgraph_path: Table = { + let len = node_path.len(); + node_path.into_iter().take(len.saturating_sub(1)).collect() }; - let existing: Option = vector.attribute_cloned_or_default("editor:layer", 0); - vector.set_attribute("editor:layer", 0, existing.or(this_node_path)); + let existing: Table = vector.attribute_cloned_or_default("editor:layer", 0); + vector.set_attribute("editor:layer", 0, if existing.is_empty() { subgraph_path } else { existing }); if vector.len() > 1 { warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len()); diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 465a66105e..e311c03316 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1296,8 +1296,8 @@ pub async fn flatten_path(_: impl Ctx, #[implem // 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 node_id: Option = flattened.attribute_cloned_or_default("editor:layer", index); - let node_id = node_id.map(|node_id| node_id.0).unwrap_or_default(); + let layer_path: Table = flattened.attribute_cloned_or_default("editor:layer", index); + let node_id = layer_path.iter_element_values().next_back().map(|node_id| node_id.0).unwrap_or_default(); let mut hasher = DefaultHasher::new(); (index, node_id).hash(&mut hasher); @@ -1318,8 +1318,8 @@ pub async fn flatten_path(_: impl Ctx, #[implem // Adopt the last input row's layer so the editor can also bucket clicks under a contributing child layer if !flattened.is_empty() { let primary = flattened.len() - 1; - let layer: Option = flattened.attribute_cloned_or_default("editor:layer", primary); - output_table.set_attribute("editor:layer", 0, layer); + let layer_path: Table = flattened.attribute_cloned_or_default("editor:layer", primary); + output_table.set_attribute("editor:layer", 0, layer_path); } output_table @@ -2529,13 +2529,13 @@ async fn morph( // 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: Option = content.attribute_cloned_or_default("editor:layer", primary_index); + let layer_path: Table = content.attribute_cloned_or_default("editor:layer", primary_index); Table::new_from_row( TableRow::new_from_element(vector) .with_attribute("transform", lerped_transform) .with_attribute("alpha_blending", vector_alpha_blending) - .with_attribute("editor:layer", layer) + .with_attribute("editor:layer", layer_path) .with_attribute("editor:merged_layers", graphic_table_content), ) }