Improve the Data panel with type-specific detail pages and nested-layer support (#4070)

* Improve the Data panel with more type-specific detail pages

* Add network_path to SetDisplayName so renames target any network depth

* Track nested layers via full editor:layer paths and rename parent_layer to path_of_subgraph

* Polish the data panel NodeId leaf page with an editable name field

* Make lock and visibility toggles work for layers in nested subgraphs

* Fix formatting

* Fix connected_to_output running in the wrong network for nested-layer toggles
This commit is contained in:
Keavon Chambers 2026-04-28 15:37:07 -07:00 committed by GitHub
parent 84fb901b5a
commit 6b11b47753
19 changed files with 367 additions and 155 deletions

View File

@ -35,9 +35,14 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
}); });
responses.add(NodeGraphMessage::SetDisplayNameImpl { responses.add(NodeGraphMessage::SetDisplayNameImpl {
node_id, node_id,
network_path: Vec::new(),
alias: "Background".to_string(), 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 { } else if self.dimensions.x > 0 && self.dimensions.y > 0 {
// Finite canvas: create an artboard with the specified dimensions // Finite canvas: create an artboard with the specified dimensions
responses.add(GraphOperationMessage::NewArtboard { responses.add(GraphOperationMessage::NewArtboard {

View File

@ -10,6 +10,9 @@ pub enum DataPanelMessage {
inspect_result: InspectResult, inspect_result: InspectResult,
}, },
ClearLayout, 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 { PushToElementPath {
step: PathStep, step: PathStep,

View File

@ -51,6 +51,12 @@ impl MessageHandler<DataPanelMessage, DataPanelMessageContext<'_>> for DataPanel
self.active_vector_table_tab = VectorTableTab::default(); self.active_vector_table_tab = VectorTableTab::default();
self.update_layout(responses, context); 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 } => { DataPanelMessage::PushToElementPath { step } => {
self.element_path.push(step); self.element_path.push(step);
@ -80,6 +86,8 @@ impl DataPanelMessageHandler {
let mut layout_data = LayoutData { let mut layout_data = LayoutData {
current_depth: 0, current_depth: 0,
desired_path: &mut self.element_path, desired_path: &mut self.element_path,
network_interface: &*network_interface,
node_lookup_network_path: Vec::new(),
breadcrumbs: Vec::new(), breadcrumbs: Vec::new(),
vector_table_tab: self.active_vector_table_tab, 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() { if let Some((node_id, parent_path)) = self.introspected_node_path.split_last() {
let node_id = *node_id; let node_id = *node_id;
let is_layer = network_interface.is_layer(&node_id, parent_path); let is_layer = network_interface.is_layer(&node_id, parent_path);
let parent_path_owned = parent_path.to_vec();
widgets.extend([ widgets.extend([
if is_layer { if is_layer {
@ -111,6 +120,7 @@ impl DataPanelMessageHandler {
.on_update(move |text_input| { .on_update(move |text_input| {
NodeGraphMessage::SetDisplayName { NodeGraphMessage::SetDisplayName {
node_id, node_id,
network_path: parent_path_owned.clone(),
alias: text_input.value.clone(), alias: text_input.value.clone(),
skip_adding_history_step: false, skip_adding_history_step: false,
} }
@ -144,6 +154,11 @@ impl DataPanelMessageHandler {
struct LayoutData<'a> { struct LayoutData<'a> {
current_depth: usize, current_depth: usize,
desired_path: &'a mut Vec<PathStep>, desired_path: &'a mut Vec<PathStep>,
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<NodeId>` 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<NodeId>,
breadcrumbs: Vec<String>, breadcrumbs: Vec<String>,
vector_table_tab: VectorTableTab, 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. // TODO: We simply try all these types sequentially. Find a better strategy.
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> { fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
// `Table<NodeId>` 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::<IORecord<Context, Table<NodeId>>>() {
return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data));
}
generate_layout_downcast!(introspected_data, data, [ generate_layout_downcast!(introspected_data, data, [
Table<Artboard>, Table<Artboard>,
Table<Graphic>, Table<Graphic>,
@ -170,7 +190,6 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
Table<Color>, Table<Color>,
Table<GradientStops>, Table<GradientStops>,
Table<String>, Table<String>,
Table<NodeId>,
Table<f64>, Table<f64>,
Table<u8>, Table<u8>,
GradientStops, GradientStops,
@ -203,10 +222,12 @@ trait TableRowLayout {
} }
/// Renders this value as a single inline widget inside a row of a Vec/Table. /// 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. /// `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 /// 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 /// inline (colors, transforms, primitives, etc.) override this to ignore `target` and
/// return a richer non-navigating widget. /// 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()) TextButton::new(self.identifier())
.on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into()) .on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into())
.narrow(true) .narrow(true)
@ -258,10 +279,10 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
let mut rows = (0..self.len()) let mut rows = (0..self.len())
.map(|index| { .map(|index| {
let element = self.element(index).unwrap(); 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 { for key in &attribute_keys {
let target = PathStep::Attribute { row: index, key: key.clone() }; 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()); let text = self.attribute_display_value(key, index, |_| None).unwrap_or_else(|| "-".to_string());
TextLabel::new(text).narrow(true).widget_instance() TextLabel::new(text).narrow(true).widget_instance()
}); });
@ -531,7 +552,7 @@ impl TableRowLayout for Color {
fn identifier(&self) -> String { fn identifier(&self) -> String {
format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb()) 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)) ColorInput::new(FillChoice::Solid(*self))
.disabled(true) .disabled(true)
.menu_direction(Some(MenuDirection::Top)) .menu_direction(Some(MenuDirection::Top))
@ -539,7 +560,7 @@ impl TableRowLayout for Color {
.widget_instance() .widget_instance()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.cell_widget(PathStep::Element(0))]; let widgets = vec![self.cell_widget(PathStep::Element(0), _data)];
vec![LayoutGroup::row(widgets)] vec![LayoutGroup::row(widgets)]
} }
} }
@ -551,7 +572,7 @@ impl TableRowLayout for GradientStops {
fn identifier(&self) -> String { fn identifier(&self) -> String {
format!("Gradient ({} stops)", self.len()) 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())) ColorInput::new(FillChoice::Gradient(self.clone()))
.menu_direction(Some(MenuDirection::Top)) .menu_direction(Some(MenuDirection::Top))
.disabled(true) .disabled(true)
@ -559,7 +580,7 @@ impl TableRowLayout for GradientStops {
.widget_instance() .widget_instance()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.cell_widget(PathStep::Element(0))]; let widgets = vec![self.cell_widget(PathStep::Element(0), _data)];
vec![LayoutGroup::row(widgets)] vec![LayoutGroup::row(widgets)]
} }
} }
@ -569,13 +590,13 @@ impl TableRowLayout for f64 {
"Number (f64)" "Number (f64)"
} }
fn identifier(&self) -> String { fn identifier(&self) -> String {
"Number (f64)".to_string() format!("{self}")
}
fn cell_widget(&self, _target: PathStep) -> WidgetInstance {
NumberInput::new(Some(*self)).disabled(true).max_width(220).display_decimal_places(20).widget_instance()
} }
// 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<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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 { fn identifier(&self) -> String {
format!("{self:02X}") format!("{self:02X}")
} }
fn cell_widget(&self, _target: PathStep) -> WidgetInstance { // 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.
TextLabel::new(self.identifier()).narrow(true).widget_instance()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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)" "Number (u32)"
} }
fn identifier(&self) -> String { fn identifier(&self) -> String {
"Number (u32)".to_string() format!("{self}")
}
fn cell_widget(&self, _target: PathStep) -> WidgetInstance {
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance()
} }
// 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<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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)" "Number (u64)"
} }
fn identifier(&self) -> String { 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. // 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<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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 { fn identifier(&self) -> String {
"Bool".to_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() TextLabel::new(self.to_string()).narrow(true).widget_instance()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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) format!("\"{}\"", first_line)
} }
} }
fn cell_widget(&self, _target: PathStep) -> WidgetInstance { // 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`.
TextLabel::new(self.identifier()).narrow(true).widget_instance()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![TextAreaInput::new(self.to_string()).monospace(true).disabled(true).widget_instance()])] vec![LayoutGroup::row(vec![TextAreaInput::new(self.to_string()).monospace(true).disabled(true).widget_instance()])]
} }
@ -668,11 +685,11 @@ impl TableRowLayout for Option<f64> {
fn identifier(&self) -> String { fn identifier(&self) -> String {
"Option<f64>".to_string() "Option<f64>".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() TextLabel::new(format!("{self:?}")).narrow(true).widget_instance()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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 { fn identifier(&self) -> String {
"Vec2".to_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() TextLabel::new(format_dvec2(*self)).narrow(true).widget_instance()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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 { fn identifier(&self) -> String {
"Vec2".to_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() 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<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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 { fn identifier(&self) -> String {
"Transform".to_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() TextLabel::new(format_transform_matrix(*self)).narrow(true).widget_instance()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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 { fn identifier(&self) -> String {
"Transform".to_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)); 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() TextLabel::new(format_transform_matrix(matrix)).narrow(true).widget_instance()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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 { fn identifier(&self) -> String {
format_alpha_blending(*self) 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() TextLabel::new(format_alpha_blending(*self)).narrow(true).widget_instance()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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 { fn identifier(&self) -> String {
format!("Node {self}") format!("Node {self}")
} }
fn cell_widget(&self, _target: PathStep) -> WidgetInstance { // 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<LayoutGroup> {
data.breadcrumbs.push(node_id_display_label(*self, data.network_interface, &data.node_lookup_network_path));
self.element_page(data)
}
// 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<NodeId>` 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<LayoutGroup> {
let node_id = *self; let node_id = *self;
TextButton::new("Go to Node") let network_path = data.node_lookup_network_path.clone();
.tooltip_description("Click to select the node with this ID in the graph.") let known = data.network_interface.node_metadata(&node_id, &network_path).is_some();
.on_update(move |_| NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into()) let is_layer = known && data.network_interface.is_layer(&node_id, &network_path);
.narrow(true) 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() .widget_instance()
} } else {
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { TextLabel::new(name).widget_instance()
vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] };
}
}
impl TableRowLayout for Option<NodeId> { let mut header = vec![kind_widget, Separator::new(SeparatorStyle::Related).widget_instance(), name_widget];
fn type_name() -> &'static str {
"NodeId" 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(),
} }
fn identifier(&self) -> String { .into()
match self { })
Some(node_id) => format!("Node {}", node_id),
None => "-".to_string(),
}
}
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.")
.on_update(move |_| NodeGraphMessage::SelectedNodesSet { nodes: vec![node_id] }.into())
.narrow(true)
.widget_instance(), .widget_instance(),
None => TextLabel::new("-").narrow(true).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 element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![self.cell_widget(PathStep::Element(0))])] 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())
.widget_instance(),
);
vec![LayoutGroup::row(header)]
} }
} }
@ -817,7 +911,6 @@ macro_rules! known_table_row_types {
GradientStops, GradientStops,
Color, Color,
NodeId, NodeId,
Option<NodeId>,
AlphaBlending, AlphaBlending,
DAffine2, DAffine2,
DVec2, DVec2,
@ -843,12 +936,12 @@ macro_rules! known_table_row_types {
/// Delegates to [`TableRowLayout::cell_widget`] so the same widget code is shared between /// 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 /// element-column rendering and attribute-column rendering. Returns `None` for unrecognized types so the
/// caller can fall back to a debug-formatted [`TextLabel`]. /// caller can fall back to a debug-formatted [`TextLabel`].
fn dispatch_cell_widget(any: &dyn Any, target: PathStep) -> Option<WidgetInstance> { fn dispatch_cell_widget(any: &dyn Any, target: PathStep, data: &LayoutData) -> Option<WidgetInstance> {
macro_rules! check { macro_rules! check {
( $($ty:ty),* $(,)? ) => { ( $($ty:ty),* $(,)? ) => {
$( $(
if let Some(value) = any.downcast_ref::<$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<WidgetInstanc
None None
} }
/// Renders a `Table<NodeId>` 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<NodeId>, data: &mut LayoutData) -> Vec<LayoutGroup> {
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<NodeId> = 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<NodeId> = 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::<Vec<_>>();
rows.insert(0, column_headings(&["", "element"]));
vec![LayoutGroup::table(rows, false)]
}
/// Type-dispatched recursion into an attribute value for the data panel breadcrumb navigation. /// Type-dispatched recursion into an attribute value for the data panel breadcrumb navigation.
/// Mirrors [`dispatch_cell_widget`] but routes to [`TableRowLayout::layout_with_breadcrumb`]. /// Mirrors [`dispatch_cell_widget`] but routes to [`TableRowLayout::layout_with_breadcrumb`].
/// Returns `None` for unrecognized types. /// Returns `None` for unrecognized types.
fn drilldown_attribute_layout(any: &dyn Any, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> { fn drilldown_attribute_layout(any: &dyn Any, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
// `Table<NodeId>` 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<T>` blanket impl.
if let Some(path) = any.downcast_ref::<Table<NodeId>>() {
return Some(table_node_id_path_layout_with_breadcrumb(path, data));
}
macro_rules! check { macro_rules! check {
( $($ty:ty),* $(,)? ) => { ( $($ty:ty),* $(,)? ) => {
$( $(

View File

@ -699,6 +699,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
if let Some(name) = name { if let Some(name) = name {
responses.add(NodeGraphMessage::SetDisplayName { responses.add(NodeGraphMessage::SetDisplayName {
node_id: layer.to_node(), node_id: layer.to_node(),
network_path: Vec::new(),
alias: name, alias: name,
skip_adding_history_step: false, skip_adding_history_step: false,
}); });
@ -756,6 +757,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
if let Some(name) = name { if let Some(name) = name {
responses.add(NodeGraphMessage::SetDisplayName { responses.add(NodeGraphMessage::SetDisplayName {
node_id: layer.to_node(), node_id: layer.to_node(),
network_path: Vec::new(),
alias: name, alias: name,
skip_adding_history_step: false, skip_adding_history_step: false,
}); });

View File

@ -197,10 +197,12 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
responses.add(NodeGraphMessage::SetDisplayNameImpl { responses.add(NodeGraphMessage::SetDisplayNameImpl {
node_id: id, node_id: id,
network_path: Vec::new(),
alias: layer_alias.to_string(), alias: layer_alias.to_string(),
}); });
responses.add(NodeGraphMessage::SetDisplayNameImpl { responses.add(NodeGraphMessage::SetDisplayNameImpl {
node_id: control_path_id, node_id: control_path_id,
network_path: Vec::new(),
alias: path_alias.to_string(), alias: path_alias.to_string(),
}); });
} }
@ -245,6 +247,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
responses.add(NodeGraphMessage::SetDisplayNameImpl { responses.add(NodeGraphMessage::SetDisplayNameImpl {
node_id: id, node_id: id,
network_path: Vec::new(),
alias: "Boolean Operation".to_string(), alias: "Boolean Operation".to_string(),
}); });
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
@ -343,6 +346,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
responses.add(NodeGraphMessage::SetDisplayName { responses.add(NodeGraphMessage::SetDisplayName {
node_id, node_id,
network_path: Vec::new(),
alias: network_interface.display_name(&artboard.to_node(), &[]), alias: network_interface.display_name(&artboard.to_node(), &[]),
skip_adding_history_step: true, skip_adding_history_step: true,
}); });

View File

@ -236,7 +236,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
// Derive the parent layer's NodeId from the document path // Derive the parent layer's NodeId from the document path
DocumentNode { DocumentNode {
inputs: vec![NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)], inputs: vec![NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::parent_layer::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphic::path_of_subgraph::IDENTIFIER),
..Default::default() ..Default::default()
}, },
// Stamp each row of the content with the parent layer's NodeId via the `editor:layer` attribute, // Stamp each row of the content with the parent layer's NodeId via the `editor:layer` attribute,
@ -299,7 +299,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
..Default::default() ..Default::default()
}, },
// 2: parent_layer // 2: path_of_subgraph
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, 1)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, 1)),
@ -371,7 +371,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
// Derive the parent layer's NodeId from the document path // Derive the parent layer's NodeId from the document path
DocumentNode { DocumentNode {
inputs: vec![NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)], inputs: vec![NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::parent_layer::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphic::path_of_subgraph::IDENTIFIER),
..Default::default() ..Default::default()
}, },
// Stamp each row of the content with the parent layer's NodeId via the `editor:layer` attribute, // Stamp each row of the content with the parent layer's NodeId via the `editor:layer` attribute,
@ -461,7 +461,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
..Default::default() ..Default::default()
}, },
// 1: parent_layer // 1: path_of_subgraph
DocumentNodeMetadata { DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata { persistent_metadata: DocumentNodePersistentMetadata {
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, 3)), node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, 3)),

View File

@ -157,11 +157,15 @@ pub enum NodeGraphMessage {
}, },
SetDisplayName { SetDisplayName {
node_id: NodeId, node_id: NodeId,
/// The path to the network containing `node_id`. Empty for nodes at the root document network.
/// Lets the rename target a node at any nesting depth, independent of the current selection network.
network_path: Vec<NodeId>,
alias: String, alias: String,
skip_adding_history_step: bool, skip_adding_history_step: bool,
}, },
SetDisplayNameImpl { SetDisplayNameImpl {
node_id: NodeId, node_id: NodeId,
network_path: Vec<NodeId>,
alias: String, alias: String,
}, },
SetToNodeOrLayer { SetToNodeOrLayer {
@ -199,15 +203,22 @@ pub enum NodeGraphMessage {
ToggleSelectedLocked, ToggleSelectedLocked,
ToggleLocked { ToggleLocked {
node_id: NodeId, 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<NodeId>,
}, },
SetLocked { SetLocked {
node_id: NodeId, node_id: NodeId,
network_path: Vec<NodeId>,
locked: bool, locked: bool,
}, },
ToggleSelectedIsPinned, ToggleSelectedIsPinned,
ToggleSelectedVisibility, ToggleSelectedVisibility,
ToggleVisibility { ToggleVisibility {
node_id: NodeId, 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<NodeId>,
}, },
SetPinned { SetPinned {
node_id: NodeId, node_id: NodeId,
@ -215,10 +226,12 @@ pub enum NodeGraphMessage {
}, },
SetVisibility { SetVisibility {
node_id: NodeId, node_id: NodeId,
network_path: Vec<NodeId>,
visible: bool, visible: bool,
}, },
SetLockedOrVisibilitySideEffects { SetLockedOrVisibilitySideEffects {
node_ids: Vec<NodeId>, node_ids: Vec<NodeId>,
network_path: Vec<NodeId>,
}, },
UpdateEdges, UpdateEdges,
UpdateBoxSelection, UpdateBoxSelection,

View File

@ -659,6 +659,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}); });
responses.add(NodeGraphMessage::SetDisplayNameImpl { responses.add(NodeGraphMessage::SetDisplayNameImpl {
node_id: encapsulating_node_id, node_id: encapsulating_node_id,
network_path: selection_network_path.to_vec(),
alias: "Untitled Node".to_string(), alias: "Untitled Node".to_string(),
}); });
@ -909,13 +910,19 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
// Toggle visibility of clicked node and return // 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) { 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; return;
} }
// Toggle lock of clicked node and 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) { 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; return;
} }
@ -1816,13 +1823,14 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
} }
NodeGraphMessage::SetDisplayName { NodeGraphMessage::SetDisplayName {
node_id, node_id,
network_path,
alias, alias,
skip_adding_history_step, skip_adding_history_step,
} => { } => {
if !skip_adding_history_step { if !skip_adding_history_step {
responses.add(DocumentMessage::StartTransaction); 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 { if !skip_adding_history_step {
// Does not add a history step if the name was not changed // Does not add a history step if the name was not changed
responses.add(DocumentMessage::EndTransaction); responses.add(DocumentMessage::EndTransaction);
@ -1831,9 +1839,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(DocumentMessage::RenderScrollbars); responses.add(DocumentMessage::RenderScrollbars);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(OverlaysMessage::Draw); // Redraw overlays to update artboard names responses.add(OverlaysMessage::Draw); // Redraw overlays to update artboard names
responses.add(DataPanelMessage::Refresh);
} }
NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => { NodeGraphMessage::SetDisplayNameImpl { node_id, network_path, alias } => {
network_interface.set_display_name(&node_id, alias, selection_network_path); network_interface.set_display_name(&node_id, alias, &network_path);
} }
NodeGraphMessage::SetImportExportName { name, index } => { NodeGraphMessage::SetImportExportName { name, index } => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
@ -1872,25 +1881,34 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
for node_id in &node_ids { 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 } => { NodeGraphMessage::ToggleLocked { node_id, network_path } => {
let Some(node_metadata) = network_interface.document_network_metadata().persistent_metadata.node_metadata.get(&node_id) else { let locked = !network_interface.is_locked(&node_id, &network_path);
log::error!("Cannot get node {node_id:?} in NodeGraphMessage::ToggleLocked");
return;
};
let locked = !node_metadata.persistent_metadata.locked;
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::SetLocked { node_id, locked }); responses.add(NodeGraphMessage::SetLocked {
responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids: vec![node_id] }) node_id,
network_path: network_path.clone(),
locked,
});
responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects {
node_ids: vec![node_id],
network_path,
});
} }
NodeGraphMessage::SetLocked { node_id, locked } => { NodeGraphMessage::SetLocked { node_id, network_path, locked } => {
network_interface.set_locked(&node_id, selection_network_path, locked); network_interface.set_locked(&node_id, &network_path, locked);
} }
NodeGraphMessage::ToggleSelectedIsPinned => { NodeGraphMessage::ToggleSelectedIsPinned => {
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
@ -1906,7 +1924,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
for node_id in &node_ids { for node_id in &node_ids {
responses.add(NodeGraphMessage::SetPinned { node_id: *node_id, pinned }); 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 => { NodeGraphMessage::ToggleSelectedVisibility => {
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else { let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
@ -1920,31 +1941,46 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
for node_id in &node_ids { 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 } => { NodeGraphMessage::ToggleVisibility { node_id, network_path } => {
let visible = !network_interface.is_visible(&node_id, selection_network_path); let visible = !network_interface.is_visible(&node_id, &network_path);
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::SetVisibility { node_id, visible }); responses.add(NodeGraphMessage::SetVisibility {
responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids: vec![node_id] }); node_id,
network_path: network_path.clone(),
visible,
});
responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects {
node_ids: vec![node_id],
network_path,
});
} }
NodeGraphMessage::SetPinned { node_id, pinned } => { NodeGraphMessage::SetPinned { node_id, pinned } => {
network_interface.set_pinned(&node_id, selection_network_path, pinned); network_interface.set_pinned(&node_id, selection_network_path, pinned);
} }
NodeGraphMessage::SetVisibility { node_id, visible } => { NodeGraphMessage::SetVisibility { node_id, network_path, visible } => {
network_interface.set_visibility(&node_id, selection_network_path, visible); network_interface.set_visibility(&node_id, &network_path, visible);
} }
NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids } => { NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids, network_path } => {
if node_ids.iter().any(|node_id| network_interface.connected_to_output(node_id, selection_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::RunDocumentGraph);
} }
responses.add(NodeGraphMessage::UpdateActionButtons); responses.add(NodeGraphMessage::UpdateActionButtons);
responses.add(NodeGraphMessage::SendGraph); responses.add(NodeGraphMessage::SendGraph);
responses.add(PropertiesPanelMessage::Refresh); responses.add(PropertiesPanelMessage::Refresh);
responses.add(DataPanelMessage::Refresh);
} }
NodeGraphMessage::UpdateBoxSelection => { NodeGraphMessage::UpdateBoxSelection => {
if let Some((box_selection_start, _)) = self.box_selection_start { if let Some((box_selection_start, _)) = self.box_selection_start {
@ -2397,6 +2433,7 @@ impl NodeGraphMessageHandler {
let mut properties = Vec::new(); let mut properties = Vec::new();
if let [node_id] = *nodes.as_slice() { if let [node_id] = *nodes.as_slice() {
let network_path = context.selection_network_path.to_vec();
properties.push(LayoutGroup::row(vec![ properties.push(LayoutGroup::row(vec![
Separator::new(SeparatorStyle::Related).widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(),
IconLabel::new("Node").tooltip_description("Name of the selected node.").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| { .on_update(move |text_input| {
NodeGraphMessage::SetDisplayName { NodeGraphMessage::SetDisplayName {
node_id, node_id,
network_path: network_path.clone(),
alias: text_input.value.clone(), alias: text_input.value.clone(),
skip_adding_history_step: false, skip_adding_history_step: false,
} }
@ -2468,6 +2506,7 @@ impl NodeGraphMessageHandler {
return Vec::new(); return Vec::new();
} }
let layer_network_path = context.selection_network_path.to_vec();
let mut layer_properties = vec![LayoutGroup::row(vec![ let mut layer_properties = vec![LayoutGroup::row(vec![
Separator::new(SeparatorStyle::Related).widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(),
IconLabel::new("Layer").tooltip_description("Name of the selected layer.").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| { .on_update(move |text_input| {
NodeGraphMessage::SetDisplayName { NodeGraphMessage::SetDisplayName {
node_id: layer, node_id: layer,
network_path: layer_network_path.clone(),
alias: text_input.value.clone(), alias: text_input.value.clone(),
skip_adding_history_step: false, skip_adding_history_step: false,
} }

View File

@ -1016,7 +1016,7 @@ pub fn document_migration_reset_node_definition(document_serialized_content: &st
return true; 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 // 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. // reset to the current default so the new internal plumbing replaces the obsolete node.
if document_serialized_content.contains("graphic_nodes::graphic::SourceNodeIdNode") if document_serialized_content.contains("graphic_nodes::graphic::SourceNodeIdNode")

View File

@ -775,6 +775,7 @@ impl EditorWrapper {
let layer = LayerNodeIdentifier::new_unchecked(NodeId(id)); let layer = LayerNodeIdentifier::new_unchecked(NodeId(id));
let message = NodeGraphMessage::SetDisplayName { let message = NodeGraphMessage::SetDisplayName {
node_id: layer.to_node(), node_id: layer.to_node(),
network_path: Vec::new(),
alias: name, alias: name,
skip_adding_history_step: false, skip_adding_history_step: false,
}; };
@ -912,7 +913,7 @@ impl EditorWrapper {
#[wasm_bindgen(js_name = toggleNodeVisibilityLayerPanel)] #[wasm_bindgen(js_name = toggleNodeVisibilityLayerPanel)]
pub fn toggle_node_visibility_layer(&self, id: u64) { pub fn toggle_node_visibility_layer(&self, id: u64) {
let node_id = NodeId(id); let node_id = NodeId(id);
let message = NodeGraphMessage::ToggleVisibility { node_id }; let message = NodeGraphMessage::ToggleVisibility { node_id, network_path: Vec::new() };
self.dispatch(message); self.dispatch(message);
} }
@ -931,7 +932,10 @@ impl EditorWrapper {
/// Toggle lock state of a layer from the layer list /// Toggle lock state of a layer from the layer list
#[wasm_bindgen(js_name = toggleLayerLock)] #[wasm_bindgen(js_name = toggleLayerLock)]
pub fn toggle_layer_lock(&self, node_id: u64) { 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); self.dispatch(message);
} }

View File

@ -95,7 +95,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, 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::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 => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<f64>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<f64>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<String>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<String>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<NodeId>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<f64>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Table<f64>]),
@ -173,7 +172,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Raster<GPU>>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Raster<GPU>>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<f64>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<Color>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<NodeId>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Graphic]), 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::Vec2]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]),

View File

@ -141,7 +141,7 @@ fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic
fn flatten_recursive<T>(output: &mut Table<T>, current_graphic_table: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) { fn flatten_recursive<T>(output: &mut Table<T>, current_graphic_table: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) {
for current_graphic_row in current_graphic_table.into_iter() { for current_graphic_row in current_graphic_table.into_iter() {
let layer: Option<NodeId> = current_graphic_row.attribute_cloned_or_default("editor:layer"); let layer_path: Table<NodeId> = current_graphic_row.attribute_cloned_or_default("editor:layer");
let current_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default("transform"); 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"); let current_alpha_blending: AlphaBlending = current_graphic_row.attribute_cloned_or_default("alpha_blending");
@ -168,7 +168,7 @@ fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic
attributes.insert("transform", current_transform * row_transform); attributes.insert("transform", current_transform * row_transform);
attributes.insert("alpha_blending", compose_alpha_blending(current_alpha_blending, row_alpha_blending)); 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)); output.push(TableRow::from_parts(element, attributes));
} }

View File

@ -412,7 +412,8 @@ impl Render for Graphic {
metadata.upstream_footprints.insert(element_id, footprint); metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than the first row // TODO: Find a way to handle more than the first row
if !table.is_empty() { if !table.is_empty() {
let layer: Option<NodeId> = table.attribute_cloned_or_default("editor:layer", 0); let layer_path: Table<NodeId> = 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); let transform: DAffine2 = table.attribute_cloned_or_default("transform", 0);
metadata.first_element_source_id.insert(element_id, layer); metadata.first_element_source_id.insert(element_id, layer);
@ -655,7 +656,8 @@ impl Render for Table<Artboard> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
for index in 0..self.len() { for index in 0..self.len() {
let layer: Option<NodeId> = self.attribute_cloned_or_default("editor:layer", index); let layer_path: Table<NodeId> = 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); self.element(index).unwrap().collect_metadata(metadata, footprint, layer);
} }
} }
@ -805,7 +807,8 @@ impl Render for Table<Graphic> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
for index in 0..self.len() { for index in 0..self.len() {
let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index); let row_transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let layer: Option<NodeId> = self.attribute_cloned_or_default("editor:layer", index); let layer_path: Table<NodeId> = 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 element = self.element(index).unwrap();
let mut footprint = footprint; let mut footprint = footprint;
@ -860,9 +863,9 @@ impl Render for Table<Graphic> {
} }
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) { fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
let (elements, layers) = self.element_and_attribute_slices_mut::<Option<NodeId>>("editor:layer"); let (elements, layers) = self.element_and_attribute_slices_mut::<Table<NodeId>>("editor:layer");
for (element, layer) in elements.iter_mut().zip(layers.iter()) { 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<Vector> {
for index in 0..self.len() { for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue }; let Some(vector) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default("transform", index); let transform: DAffine2 = self.attribute_cloned_or_default("transform", index);
let layer: Option<NodeId> = self.attribute_cloned_or_default("editor:layer", index); let layer_path: Table<NodeId> = 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) { if let Some(element_id) = caller_element_id.or(layer) {
// When recovering element_id from the row's editor:layer tag (because the caller // When recovering element_id from the row's editor:layer tag (because the caller

View File

@ -314,7 +314,7 @@ async fn brush(
let transform: DAffine2 = actual_image.attribute_cloned_or_default("transform"); let transform: DAffine2 = actual_image.attribute_cloned_or_default("transform");
let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default("alpha_blending"); let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default("alpha_blending");
let layer: Option<NodeId> = actual_image.attribute_cloned_or_default("editor:layer"); let layer: Table<NodeId> = actual_image.attribute_cloned_or_default("editor:layer");
*image.element_mut(0).unwrap() = actual_image.into_element(); *image.element_mut(0).unwrap() = actual_image.into_element();
image.set_attribute("transform", 0, transform); image.set_attribute("transform", 0, transform);

View File

@ -26,7 +26,6 @@ async fn context_modification<T>(
Context -> DAffine2, Context -> DAffine2,
Context -> Footprint, Context -> Footprint,
Context -> DVec2, Context -> DVec2,
Context -> Option<NodeId>,
Context -> Table<String>, Context -> Table<String>,
Context -> Table<NodeId>, Context -> Table<NodeId>,
Context -> Table<f64>, Context -> Table<f64>,

View File

@ -209,14 +209,16 @@ where
result_table result_table
} }
/// Returns the NodeId of the user-facing parent layer node that encapsulates this sub-network. /// Returns the path identifying the subgraph (network) that contains this proto node — i.e. the input `node_path`
/// Used as the value source for stamping the `editor:layer` attribute on each row of a layer's output, /// with its own trailing entry dropped. The terminating element of the returned path is the document node whose
/// which lets editor tools (e.g. selection, click target routing) trace data back to its owning layer. /// encapsulated network we live in, so the path doubles as a unique reference to that node at any nesting depth.
#[node_macro::node(category(""))] /// Used as the value source for stamping the `editor:layer` attribute on each row of a layer's output, which lets
pub fn parent_layer(_: impl Ctx, node_path: Table<NodeId>) -> Option<NodeId> { /// editor tools (e.g. selection, click target routing) trace data back to its owning layer regardless of whether
// Get the penultimate element of the node path, or None if the path is too short /// the layer is at the root document network or nested inside a custom subgraph.
let index = node_path.len().wrapping_sub(2); #[node_macro::node(name("Path of Subgraph"), category(""))]
node_path.element(index).copied() pub fn path_of_subgraph(_: impl Ctx, node_path: Table<NodeId>) -> Table<NodeId> {
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, /// 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<T: AnyHash + Clone + Send + Sync + core_types::CacheHas
name: String, name: String,
/// The node that produces the per-row value. Called once per row with the row index in context. /// The node that produces the per-row value. Called once per row with the row index in context.
#[implementations( #[implementations(
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>, Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Table<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>, Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Table<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>, Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Table<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>, Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Table<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>, Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Table<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>, Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Table<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Option<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>, Context -> f64, Context -> u32, Context -> bool, Context -> String, Context -> Table<String>, Context -> DVec2, Context -> DAffine2, Context -> Table<NodeId>, Context -> Table<Color>, Context -> Table<GradientStops>,
)] )]
value: impl Node<'n, Context<'static>, Output = U>, value: impl Node<'n, Context<'static>, Output = U>,
) -> Table<T> { ) -> Table<T> {

View File

@ -197,7 +197,7 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
(0..image.len()) (0..image.len())
.map(|i| { .map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i); let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i);
let layer: Option<NodeId> = image.attribute_cloned_or_default("editor:layer", i); let layer: Table<NodeId> = image.attribute_cloned_or_default("editor:layer", i);
let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i); let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i);
make_row(parent_transform * row_transform, layer, alpha_blending) make_row(parent_transform * row_transform, layer, alpha_blending)
}) })
@ -223,7 +223,7 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
(0..image.len()) (0..image.len())
.map(|i| { .map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i); let row_transform: DAffine2 = image.attribute_cloned_or_default("transform", i);
let layer: Option<NodeId> = image.attribute_cloned_or_default("editor:layer", i); let layer: Table<NodeId> = image.attribute_cloned_or_default("editor:layer", i);
let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i); let alpha_blending: AlphaBlending = image.attribute_cloned_or_default("alpha_blending", i);
make_row(parent_transform * row_transform, layer, alpha_blending) make_row(parent_transform * row_transform, layer, alpha_blending)
}) })

View File

@ -15,13 +15,14 @@ async fn path_modify(_ctx: impl Ctx, mut vector: Table<Vector>, modification: Bo
} }
modification.apply(vector.element_mut(0).expect("push should give one item")); 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) // Set the path to the encapsulating subgraph (drop our own trailing entry from `node_path`),
let this_node_path = { // matching the `path_of_subgraph` proto so editor tools can route data back to the parent layer.
let index = node_path.len().wrapping_sub(2); let subgraph_path: Table<NodeId> = {
node_path.element(index).copied() let len = node_path.len();
node_path.into_iter().take(len.saturating_sub(1)).collect()
}; };
let existing: Option<NodeId> = vector.attribute_cloned_or_default("editor:layer", 0); let existing: Table<NodeId> = vector.attribute_cloned_or_default("editor:layer", 0);
vector.set_attribute("editor:layer", 0, existing.or(this_node_path)); vector.set_attribute("editor:layer", 0, if existing.is_empty() { subgraph_path } else { existing });
if vector.len() > 1 { if vector.len() > 1 {
warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len()); warn!("The path modify ran on {} vector rows. Only the first can be modified.", vector.len());

View File

@ -1296,8 +1296,8 @@ pub async fn flatten_path<T: IntoGraphicTable + 'n + Send>(_: impl Ctx, #[implem
// Concatenate every vector element's subpaths into the single output compound path // Concatenate every vector element's subpaths into the single output compound path
for index in 0..flattened.len() { for index in 0..flattened.len() {
let Some(element) = flattened.element(index) else { continue }; let Some(element) = flattened.element(index) else { continue };
let node_id: Option<NodeId> = flattened.attribute_cloned_or_default("editor:layer", index); let layer_path: Table<NodeId> = flattened.attribute_cloned_or_default("editor:layer", index);
let node_id = node_id.map(|node_id| node_id.0).unwrap_or_default(); let node_id = layer_path.iter_element_values().next_back().map(|node_id| node_id.0).unwrap_or_default();
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
(index, node_id).hash(&mut hasher); (index, node_id).hash(&mut hasher);
@ -1318,8 +1318,8 @@ pub async fn flatten_path<T: IntoGraphicTable + 'n + Send>(_: impl Ctx, #[implem
// Adopt the last input row's layer so the editor can also bucket clicks under a contributing child layer // Adopt the last input row's layer so the editor can also bucket clicks under a contributing child layer
if !flattened.is_empty() { if !flattened.is_empty() {
let primary = flattened.len() - 1; let primary = flattened.len() - 1;
let layer: Option<NodeId> = flattened.attribute_cloned_or_default("editor:layer", primary); let layer_path: Table<NodeId> = flattened.attribute_cloned_or_default("editor:layer", primary);
output_table.set_attribute("editor:layer", 0, layer); output_table.set_attribute("editor:layer", 0, layer_path);
} }
output_table output_table
@ -2529,13 +2529,13 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
// The result is a synthesis of source and target, so adopt whichever endpoint the result is closer to as // 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) // 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 primary_index = if time < 0.5 { source_index } else { target_index };
let layer: Option<NodeId> = content.attribute_cloned_or_default("editor:layer", primary_index); let layer_path: Table<NodeId> = content.attribute_cloned_or_default("editor:layer", primary_index);
Table::new_from_row( Table::new_from_row(
TableRow::new_from_element(vector) TableRow::new_from_element(vector)
.with_attribute("transform", lerped_transform) .with_attribute("transform", lerped_transform)
.with_attribute("alpha_blending", vector_alpha_blending) .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), .with_attribute("editor:merged_layers", graphic_table_content),
) )
} }