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

@@ -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::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<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<NodeId>]),
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 => 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<NodeId>]),
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]),

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>>) {
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_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("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));
}

View File

@@ -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<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);
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>) {
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);
}
}
@@ -805,7 +807,8 @@ impl Render for Table<Graphic> {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
for index in 0..self.len() {
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 mut footprint = footprint;
@@ -860,9 +863,9 @@ impl Render for Table<Graphic> {
}
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()) {
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() {
let Some(vector) = self.element(index) else { continue };
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) {
// 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 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.set_attribute("transform", 0, transform);

View File

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

View File

@@ -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<NodeId>) -> Option<NodeId> {
// 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<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,
@@ -241,13 +243,13 @@ async fn write_attribute<T: AnyHash + Clone + Send + Sync + core_types::CacheHas
name: String,
/// The node that produces the per-row value. Called once per row with the row index in context.
#[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 -> Option<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 -> Option<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 -> Option<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 -> Table<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 -> Table<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 -> Table<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>,
) -> Table<T> {

View File

@@ -197,7 +197,7 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
(0..image.len())
.map(|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);
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())
.map(|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);
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"));
// 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<NodeId> = {
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);
vector.set_attribute("editor:layer", 0, existing.or(this_node_path));
let existing: Table<NodeId> = 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());

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
for index in 0..flattened.len() {
let Some(element) = flattened.element(index) else { continue };
let node_id: Option<NodeId> = 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<NodeId> = 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<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
if !flattened.is_empty() {
let primary = flattened.len() - 1;
let layer: Option<NodeId> = flattened.attribute_cloned_or_default("editor:layer", primary);
output_table.set_attribute("editor:layer", 0, layer);
let layer_path: Table<NodeId> = 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<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 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<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(
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),
)
}