Eliminate bare Graphic and Artboard graph data by making Merge and Artboard nodes internally use tables (#2996)

* Eliminate bare Graphic and Artboard graph data by making Merge and Artboard nodes internally use tables

* Make the Extend node user-facing
This commit is contained in:
Keavon Chambers
2025-08-05 02:24:12 -07:00
committed by GitHub
parent 836a110c72
commit 2e1396462c
22 changed files with 292 additions and 200 deletions
@@ -227,25 +227,32 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(3), 0)],
exports: vec![NodeInput::node(NodeId(4), 0)],
nodes: [
// Secondary (left) input type coercion
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 1)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::to_element::IDENTIFIER),
manual_composition: Some(concrete!(Context)),
..Default::default()
},
// Primary (bottom) input type coercion
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::to_group::IDENTIFIER),
implementation: DocumentNodeImplementation::ProtoNode(graphic::to_graphic::IDENTIFIER),
manual_composition: Some(concrete!(Context)),
..Default::default()
},
// Secondary (left) input type coercion
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 1)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::wrap_graphic::IDENTIFIER),
manual_composition: Some(concrete!(Context)),
..Default::default()
},
// Store the ID of the parent node (which encapsulates this sub-network) in each row we are extending the table with.
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER),
manual_composition: Some(concrete!(Context)),
..Default::default()
},
// The monitor node is used to display a thumbnail in the UI
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
manual_composition: Some(concrete!(Context)),
skip_deduplication: true,
@@ -253,12 +260,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNode {
manual_composition: Some(generic!(T)),
inputs: vec![
NodeInput::node(NodeId(1), 0),
NodeInput::node(NodeId(2), 0),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
],
implementation: DocumentNodeImplementation::ProtoNode(graphic::layer::IDENTIFIER),
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(3), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default()
},
]
@@ -275,7 +278,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Graphical Data", "TODO").into(), ("Over", "TODO").into()],
input_metadata: vec![("Base", "TODO").into(), ("Content", "TODO").into()],
output_names: vec!["Out".to_string()],
node_type_metadata: NodeTypePersistentMetadata::layer(IVec2::new(0, 0)),
network_metadata: Some(NodeNetworkMetadata {
@@ -283,16 +286,24 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "To Element".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -1)),
display_name: "To Graphic".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "To Group".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)),
display_name: "Wrap Graphic".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -1)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Source Node ID".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -1)),
..Default::default()
},
..Default::default()
@@ -307,7 +318,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Layer".to_string(),
display_name: "Extend".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -3)),
..Default::default()
},
@@ -334,12 +345,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(2), 0)],
exports: vec![NodeInput::node(NodeId(3), 0)],
nodes: [
// Ensure this ID is kept in sync with the ID in set_alias so that the name input is kept in sync with the alias
DocumentNode {
manual_composition: Some(generic!(T)),
implementation: DocumentNodeImplementation::ProtoNode(artboard::to_artboard::IDENTIFIER),
implementation: DocumentNodeImplementation::ProtoNode(artboard::create_artboard::IDENTIFIER),
inputs: vec![
NodeInput::network(concrete!(TaggedValue), 1),
NodeInput::value(TaggedValue::String(String::from("Artboard")), false),
@@ -350,10 +361,17 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
],
..Default::default()
},
// Store the ID of the parent node (which encapsulates this sub-network) in each row we are extending the table with.
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER),
manual_composition: Some(concrete!(Context)),
..Default::default()
},
// The monitor node is used to display a thumbnail in the UI.
// TODO: Check if thumbnail is reversed
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
manual_composition: Some(generic!(T)),
skip_deduplication: true,
@@ -363,10 +381,10 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
manual_composition: Some(concrete!(Context)),
inputs: vec![
NodeInput::network(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0),
NodeInput::node(NodeId(1), 0),
NodeInput::node(NodeId(2), 0),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
],
implementation: DocumentNodeImplementation::ProtoNode(artboard::append_artboard::IDENTIFIER),
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default()
},
]
@@ -388,8 +406,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![
("Artboards", "TODO").into(),
InputMetadata::with_name_description_override("Contents", "TODO", WidgetOverride::Hidden),
("Base", "TODO").into(),
InputMetadata::with_name_description_override("Content", "TODO", WidgetOverride::Hidden),
InputMetadata::with_name_description_override(
"Location",
"TODO",
@@ -422,7 +440,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "To Artboard".to_string(),
display_name: "Create Artboard".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Source Node ID".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)),
..Default::default()
},
@@ -438,7 +464,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Append Artboards".to_string(),
display_name: "Extend".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -4)),
..Default::default()
},
@@ -28,30 +28,48 @@ pub struct NodeReplacement<'a> {
}
const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
// graphic element
// artboard
NodeReplacement {
node: graphene_std::artboard::append_artboard::IDENTIFIER,
aliases: &["graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"],
node: graphene_std::artboard::create_artboard::IDENTIFIER,
aliases: &[
"graphene_core::ConstructArtboardNode",
"graphene_core::graphic_element::ToArtboardNode",
"graphene_core::artboard::ToArtboardNode",
],
},
// graphic
NodeReplacement {
node: graphene_std::graphic::to_graphic::IDENTIFIER,
aliases: &[
"graphene_core::ToGraphicGroupNode",
"graphene_core::graphic_element::ToGroupNode",
"graphene_core::graphic::ToGroupNode",
],
},
NodeReplacement {
node: graphene_std::artboard::to_artboard::IDENTIFIER,
aliases: &["graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"],
node: graphene_std::graphic::wrap_graphic::IDENTIFIER,
aliases: &[
// Converted from "To Element"
"graphene_core::ToGraphicElementNode",
"graphene_core::graphic_element::ToElementNode",
"graphene_core::graphic::ToElementNode",
],
},
NodeReplacement {
node: graphene_std::graphic::to_element::IDENTIFIER,
aliases: &["graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"],
node: graphene_std::graphic::legacy_layer_extend::IDENTIFIER,
aliases: &[
"graphene_core::graphic_element::LayerNode",
"graphene_core::graphic::LayerNode",
// Converted from "Append Artboard"
"graphene_core::AddArtboardNode",
"graphene_core::graphic_element::AppendArtboardNode",
"graphene_core::graphic::AppendArtboardNode",
"graphene_core::artboard::AppendArtboardNode",
],
},
NodeReplacement {
node: graphene_std::graphic::to_group::IDENTIFIER,
aliases: &["graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode"],
},
NodeReplacement {
node: graphene_std::graphic::layer::IDENTIFIER,
aliases: &["graphene_core::graphic_element::LayerNode"],
},
NodeReplacement {
node: graphene_std::graphic::flatten_group::IDENTIFIER,
aliases: &["graphene_core::graphic_element::FlattenGroupNode"],
node: graphene_std::graphic::flatten_graphic::IDENTIFIER,
aliases: &["graphene_core::graphic_element::FlattenGroupNode", "graphene_core::graphic::FlattenGroupNode"],
},
NodeReplacement {
node: graphene_std::graphic::flatten_vector::IDENTIFIER,
@@ -245,8 +263,8 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
aliases: &["graphene_core::ops::SomeNode"],
},
NodeReplacement {
node: graphene_std::debug::unwrap::IDENTIFIER,
aliases: &["graphene_core::ops::UnwrapNode"],
node: graphene_std::debug::unwrap_option::IDENTIFIER,
aliases: &["graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"],
},
NodeReplacement {
node: graphene_std::debug::clone::IDENTIFIER,
@@ -812,7 +830,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path);
}
// Upgrade artboard name being passed as hidden value input to "To Artboard"
// Upgrade artboard name being passed as hidden value input to "Create Artboard"
if reference == "Artboard" && reset_node_definitions_on_open {
let label = document.network_interface.display_name(node_id, network_path);
document
@@ -179,7 +179,13 @@ impl TableRowLayout for Vector {
"Vector"
}
fn identifier(&self) -> String {
format!("Vector ({} points, {} segments)", self.point_domain.ids().len(), self.segment_domain.ids().len())
format!(
"Vector ({} point{}, {} segment{})",
self.point_domain.ids().len(),
if self.point_domain.ids().len() == 1 { "" } else { "s" },
self.segment_domain.ids().len(),
if self.segment_domain.ids().len() == 1 { "" } else { "s" }
)
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
@@ -247,10 +253,10 @@ impl TableRowLayout for Image<Color> {
"Image"
}
fn identifier(&self) -> String {
format!("Image (width={}, height={})", self.width, self.height)
format!("Image ({}x{})", self.width, self.height)
}
fn compute_layout(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let rows = vec![vec![TextLabel::new(format!("Image (width={}, height={})", self.width, self.height)).widget_holder()]];
let rows = vec![vec![TextLabel::new(format!("Image ({}x{})", self.width, self.height)).widget_holder()]];
vec![LayoutGroup::Table { rows }]
}
}
@@ -272,7 +278,7 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
"Table"
}
fn identifier(&self) -> String {
format!("Table<{}> (length={})", T::type_name(), self.len())
format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" })
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
@@ -295,7 +301,7 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
let rotation = if angle == -0. { 0. } else { angle.to_degrees() };
let round = |x: f64| (x * 1e3).round() / 1e3;
vec![
TextLabel::new(format!("{}", index)).widget_holder(),
TextLabel::new(format!("{index}")).widget_holder(),
TextButton::new(row.element.identifier())
.on_update(move |_| SpreadsheetMessage::PushToElementPath { index }.into())
.widget_holder(),
@@ -315,7 +321,6 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
let table = vec![TextLabel::new("Table:").widget_holder()];
vec![LayoutGroup::Row { widgets: table }, LayoutGroup::Table { rows }]
vec![LayoutGroup::Table { rows }]
}
}
@@ -564,13 +564,17 @@ impl Fsm for ArtboardToolFsmState {
#[cfg(test)]
mod test_artboard {
pub use crate::test_utils::test_prelude::*;
use graphene_std::table::Table;
async fn get_artboards(editor: &mut EditorTestUtils) -> Vec<graphene_std::Artboard> {
async fn get_artboards(editor: &mut EditorTestUtils) -> Table<graphene_std::Artboard> {
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {}", e),
};
instrumented.grab_all_input::<graphene_std::artboard::append_artboard::ArtboardInput>(&editor.runtime).collect()
instrumented
.grab_all_input::<graphene_std::graphic::extend::NewInput<graphene_std::Artboard>>(&editor.runtime)
.flatten()
.collect()
}
#[tokio::test]
@@ -582,8 +586,8 @@ mod test_artboard {
let artboards = get_artboards(&mut editor).await;
assert_eq!(artboards.len(), 1);
assert_eq!(artboards[0].location, IVec2::new(10, 0));
assert_eq!(artboards[0].dimensions, IVec2::new(10, 11));
assert_eq!(artboards.get(0).unwrap().element.location, IVec2::new(10, 0));
assert_eq!(artboards.get(0).unwrap().element.dimensions, IVec2::new(10, 11));
}
#[tokio::test]
@@ -594,8 +598,8 @@ mod test_artboard {
let artboards = get_artboards(&mut editor).await;
assert_eq!(artboards.len(), 1);
assert_eq!(artboards[0].location, IVec2::new(-10, 10));
assert_eq!(artboards[0].dimensions, IVec2::new(20, 20));
assert_eq!(artboards.get(0).unwrap().element.location, IVec2::new(-10, 10));
assert_eq!(artboards.get(0).unwrap().element.dimensions, IVec2::new(20, 20));
}
#[tokio::test]
@@ -613,9 +617,9 @@ mod test_artboard {
let artboards = get_artboards(&mut editor).await;
assert_eq!(artboards.len(), 1);
assert_eq!(artboards[0].location, IVec2::new(0, 0));
assert_eq!(artboards.get(0).unwrap().element.location, IVec2::new(0, 0));
let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 10.);
assert_eq!(artboards[0].dimensions, desired_size.round().as_ivec2());
assert_eq!(artboards.get(0).unwrap().element.dimensions, desired_size.round().as_ivec2());
}
#[tokio::test]
@@ -634,9 +638,9 @@ mod test_artboard {
let artboards = get_artboards(&mut editor).await;
assert_eq!(artboards.len(), 1);
assert_eq!(artboards[0].location, DVec2::splat(f64::consts::FRAC_1_SQRT_2 * -10.).as_ivec2());
assert_eq!(artboards.get(0).unwrap().element.location, DVec2::splat(f64::consts::FRAC_1_SQRT_2 * -10.).as_ivec2());
let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 20.);
assert_eq!(artboards[0].dimensions, desired_size.round().as_ivec2());
assert_eq!(artboards.get(0).unwrap().element.dimensions, desired_size.round().as_ivec2());
}
#[tokio::test]
+3 -3
View File
@@ -7,7 +7,6 @@ use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences;
use graph_craft::{ProtoNodeIdentifier, concrete};
use graphene_std::Context;
use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_std::memo::IORecord;
use graphene_std::renderer::{Render, RenderParams, SvgRender};
@@ -17,6 +16,7 @@ use graphene_std::text::FontCache;
use graphene_std::vector::Vector;
use graphene_std::vector::style::ViewMode;
use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi};
use graphene_std::{Artboard, Context, Graphic};
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::util::wrap_network_in_scope;
use once_cell::sync::Lazy;
@@ -308,9 +308,9 @@ impl NodeRuntime {
continue;
};
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, graphene_std::Graphic>>() {
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() {
Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, graphene_std::Artboard>>() {
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
// Insert the vector modify if we are dealing with vector data
} else if let Some(record) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {