Add the Spreadsheet panel to inspect node output data (#2442)

* Inspect node ouput stub

* Fix compile error in tests

* Create a table

* Clickable tables

* Add vector data support

* Checkbox to enable the panel

* Remove Instances table ID column; style the spreadsheet

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
James Lindsay 2025-03-19 06:06:05 +00:00 committed by GitHub
parent 6292dea103
commit 43275b7a1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 771 additions and 108 deletions

View file

@ -150,6 +150,15 @@ pub enum FrontendMessage {
UpdateGraphViewOverlay {
open: bool,
},
UpdateSpreadsheetState {
open: bool,
node: Option<NodeId>,
},
UpdateSpreadsheetLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdateImportReorderIndex {
#[serde(rename = "importIndex")]
index: Option<usize>,

View file

@ -40,6 +40,29 @@ impl LayoutMessageHandler {
LayoutGroup::Section { layout, .. } => {
stack.extend(layout.iter().enumerate().map(|(index, val)| ([widget_path.as_slice(), &[index]].concat(), val)));
}
LayoutGroup::Table { rows } => {
for (row_index, cell) in rows.iter().enumerate() {
for (cell_index, entry) in cell.iter().enumerate() {
// Return if this is the correct ID
if entry.widget_id == widget_id {
widget_path.push(row_index);
widget_path.push(cell_index);
return Some((entry, widget_path));
}
if let Widget::PopoverButton(popover) = &entry.widget {
stack.extend(
popover
.popover_layout
.iter()
.enumerate()
.map(|(child, val)| ([widget_path.as_slice(), &[row_index, cell_index, child]].concat(), val)),
);
}
}
}
}
}
}
None
@ -405,6 +428,7 @@ impl LayoutMessageHandler {
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff },
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },
LayoutTarget::Spreadsheet => FrontendMessage::UpdateSpreadsheetLayout { layout_target, diff },
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff },
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff },

View file

@ -39,6 +39,8 @@ pub enum LayoutTarget {
NodeGraphControlBar,
/// The body of the Properties panel containing many collapsable sections.
PropertiesSections,
/// The spredsheet panel allows for the visualisation of data in the graph.
Spreadsheet,
/// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown.
ToolOptions,
/// The vertical buttons for all of the tools on the left of the canvas.
@ -166,14 +168,14 @@ impl WidgetLayout {
pub fn iter(&self) -> WidgetIter<'_> {
WidgetIter {
stack: self.layout.iter().collect(),
current_slice: None,
..Default::default()
}
}
pub fn iter_mut(&mut self) -> WidgetIterMut<'_> {
WidgetIterMut {
stack: self.layout.iter_mut().collect(),
current_slice: None,
..Default::default()
}
}
@ -205,6 +207,7 @@ impl WidgetLayout {
#[derive(Debug, Default)]
pub struct WidgetIter<'a> {
pub stack: Vec<&'a LayoutGroup>,
pub table: Vec<&'a WidgetHolder>,
pub current_slice: Option<&'a [WidgetHolder]>,
}
@ -212,9 +215,13 @@ impl<'a> Iterator for WidgetIter<'a> {
type Item = &'a WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
if let Some(item) = self.current_slice.and_then(|slice| slice.first()) {
self.current_slice = Some(&self.current_slice.unwrap()[1..]);
let widget = self.table.pop().or_else(|| {
let (first, rest) = self.current_slice.take()?.split_first()?;
self.current_slice = Some(rest);
Some(first)
});
if let Some(item) = widget {
if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = item {
self.stack.extend(p.popover_layout.iter());
return self.next();
@ -232,6 +239,10 @@ impl<'a> Iterator for WidgetIter<'a> {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutGroup::Table { rows }) => {
self.table.extend(rows.iter().flatten().rev());
self.next()
}
Some(LayoutGroup::Section { layout, .. }) => {
for layout_row in layout {
self.stack.push(layout_row);
@ -246,6 +257,7 @@ impl<'a> Iterator for WidgetIter<'a> {
#[derive(Debug, Default)]
pub struct WidgetIterMut<'a> {
pub stack: Vec<&'a mut LayoutGroup>,
pub table: Vec<&'a mut WidgetHolder>,
pub current_slice: Option<&'a mut [WidgetHolder]>,
}
@ -253,16 +265,20 @@ impl<'a> Iterator for WidgetIterMut<'a> {
type Item = &'a mut WidgetHolder;
fn next(&mut self) -> Option<Self::Item> {
if let Some((first, rest)) = self.current_slice.take().and_then(|slice| slice.split_first_mut()) {
let widget = self.table.pop().or_else(|| {
let (first, rest) = self.current_slice.take()?.split_first_mut()?;
self.current_slice = Some(rest);
Some(first)
});
if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = first {
if let Some(widget) = widget {
if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = widget {
self.stack.extend(p.popover_layout.iter_mut());
return self.next();
}
return Some(first);
};
return Some(widget);
}
match self.stack.pop() {
Some(LayoutGroup::Column { widgets }) => {
@ -273,6 +289,10 @@ impl<'a> Iterator for WidgetIterMut<'a> {
self.current_slice = Some(widgets);
self.next()
}
Some(LayoutGroup::Table { rows }) => {
self.table.extend(rows.iter_mut().flatten().rev());
self.next()
}
Some(LayoutGroup::Section { layout, .. }) => {
for layout_row in layout {
self.stack.push(layout_row);
@ -298,6 +318,11 @@ pub enum LayoutGroup {
#[serde(rename = "rowWidgets")]
widgets: Vec<WidgetHolder>,
},
#[serde(rename = "table")]
Table {
#[serde(rename = "tableWidgets")]
rows: Vec<Vec<WidgetHolder>>,
},
// TODO: Move this from being a child of `enum LayoutGroup` to being a child of `enum Layout`
#[serde(rename = "section")]
Section { name: String, visible: bool, pinned: bool, id: u64, layout: SubLayout },
@ -432,7 +457,7 @@ impl LayoutGroup {
pub fn iter_mut(&mut self) -> WidgetIterMut<'_> {
WidgetIterMut {
stack: vec![self],
current_slice: None,
..Default::default()
}
}
}

View file

@ -6,46 +6,20 @@ use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate,
use crate::messages::prelude::*;
use graphene_std::vector::misc::BooleanOperation;
pub struct MenuBarMessageData {
#[derive(Debug, Clone, Default)]
pub struct MenuBarMessageHandler {
pub has_active_document: bool,
pub rulers_visible: bool,
pub node_graph_open: bool,
pub has_selected_nodes: bool,
pub has_selected_layers: bool,
pub has_selection_history: (bool, bool),
pub spreadsheet_view_open: bool,
pub message_logging_verbosity: MessageLoggingVerbosity,
}
#[derive(Debug, Clone, Default)]
pub struct MenuBarMessageHandler {
has_active_document: bool,
rulers_visible: bool,
node_graph_open: bool,
has_selected_nodes: bool,
has_selected_layers: bool,
has_selection_history: (bool, bool),
message_logging_verbosity: MessageLoggingVerbosity,
}
impl MessageHandler<MenuBarMessage, MenuBarMessageData> for MenuBarMessageHandler {
fn process_message(&mut self, message: MenuBarMessage, responses: &mut VecDeque<Message>, data: MenuBarMessageData) {
let MenuBarMessageData {
has_active_document,
rulers_visible,
node_graph_open,
has_selected_nodes,
has_selected_layers,
has_selection_history,
message_logging_verbosity,
} = data;
self.has_active_document = has_active_document;
self.rulers_visible = rulers_visible;
self.node_graph_open = node_graph_open;
self.has_selected_nodes = has_selected_nodes;
self.has_selected_layers = has_selected_layers;
self.has_selection_history = has_selection_history;
self.message_logging_verbosity = message_logging_verbosity;
impl MessageHandler<MenuBarMessage, ()> for MenuBarMessageHandler {
fn process_message(&mut self, message: MenuBarMessage, responses: &mut VecDeque<Message>, _data: ()) {
match message {
MenuBarMessage::SendLayout => self.send_layout(responses, LayoutTarget::MenuBar),
}
@ -590,6 +564,13 @@ impl LayoutHolder for MenuBarMessageHandler {
disabled: no_active_document,
..MenuBarEntry::default()
}],
vec![MenuBarEntry {
label: "Window: Spreadsheet".into(),
icon: Some(if self.spreadsheet_view_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
action: MenuBarEntry::create_action(|_| SpreadsheetMessage::ToggleOpen.into()),
disabled: no_active_document,
..MenuBarEntry::default()
}],
]),
),
MenuBarEntry::new_root(

View file

@ -4,4 +4,4 @@ mod menu_bar_message_handler;
#[doc(inline)]
pub use menu_bar_message::{MenuBarMessage, MenuBarMessageDiscriminant};
#[doc(inline)]
pub use menu_bar_message_handler::{MenuBarMessageData, MenuBarMessageHandler};
pub use menu_bar_message_handler::MenuBarMessageHandler;

View file

@ -3,6 +3,7 @@ mod portfolio_message_handler;
pub mod document;
pub mod menu_bar;
pub mod spreadsheet;
pub mod utility_types;
#[doc(inline)]

View file

@ -15,6 +15,8 @@ pub enum PortfolioMessage {
MenuBar(MenuBarMessage),
#[child]
Document(DocumentMessage),
#[child]
Spreadsheet(SpreadsheetMessage),
// Messages
DocumentPassMessage {

View file

@ -1,5 +1,6 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface::{self, InputConnector, OutputConnector};
use super::spreadsheet::SpreadsheetMessageHandler;
use super::utility_types::{PanelType, PersistentData};
use crate::application::generate_uuid;
use crate::consts::DEFAULT_DOCUMENT_NAME;
@ -44,6 +45,8 @@ pub struct PortfolioMessageHandler {
pub persistent_data: PersistentData,
pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode,
/// The spreadsheet UI allows for instance data to be previewed.
pub spreadsheet: SpreadsheetMessageHandler,
}
impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMessageHandler {
@ -58,38 +61,32 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
match message {
// Sub-messages
PortfolioMessage::MenuBar(message) => {
let mut has_active_document = false;
let mut rulers_visible = false;
let mut node_graph_open = false;
let mut has_selected_nodes = false;
let mut has_selected_layers = false;
let mut has_selection_history = (false, false);
self.menu_bar_message_handler.has_active_document = false;
self.menu_bar_message_handler.rulers_visible = false;
self.menu_bar_message_handler.node_graph_open = false;
self.menu_bar_message_handler.has_selected_nodes = false;
self.menu_bar_message_handler.has_selected_layers = false;
self.menu_bar_message_handler.has_selection_history = (false, false);
self.menu_bar_message_handler.spreadsheet_view_open = self.spreadsheet.spreadsheet_view_open;
self.menu_bar_message_handler.message_logging_verbosity = message_logging_verbosity;
if let Some(document) = self.active_document_id.and_then(|document_id| self.documents.get_mut(&document_id)) {
has_active_document = true;
rulers_visible = document.rulers_visible;
node_graph_open = document.is_graph_overlay_open();
self.menu_bar_message_handler.has_active_document = true;
self.menu_bar_message_handler.rulers_visible = document.rulers_visible;
self.menu_bar_message_handler.node_graph_open = document.is_graph_overlay_open();
let selected_nodes = document.network_interface.selected_nodes();
has_selected_nodes = selected_nodes.selected_nodes().next().is_some();
has_selected_layers = selected_nodes.selected_visible_layers(&document.network_interface).next().is_some();
has_selection_history = {
self.menu_bar_message_handler.has_selected_nodes = selected_nodes.selected_nodes().next().is_some();
self.menu_bar_message_handler.has_selected_layers = selected_nodes.selected_visible_layers(&document.network_interface).next().is_some();
self.menu_bar_message_handler.has_selection_history = {
let metadata = &document.network_interface.document_network_metadata().persistent_metadata;
(!metadata.selection_undo_history.is_empty(), !metadata.selection_redo_history.is_empty())
};
}
self.menu_bar_message_handler.process_message(
message,
responses,
MenuBarMessageData {
has_active_document,
rulers_visible,
node_graph_open,
has_selected_nodes,
has_selected_layers,
has_selection_history,
message_logging_verbosity,
},
);
self.menu_bar_message_handler.process_message(message, responses, ());
}
PortfolioMessage::Spreadsheet(message) => {
self.spreadsheet.process_message(message, responses, ());
}
PortfolioMessage::Document(message) => {
if let Some(document_id) = self.active_document_id {
@ -305,9 +302,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
self.persistent_data.font_cache.insert(font, preview_url, data);
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
for document_id in self.document_ids.iter() {
let inspect_node = self.inspect_node_id();
let _ = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
ipp.viewport_bounds.size().as_uvec2(),
inspect_node,
true,
);
}
@ -1074,9 +1073,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
}
PortfolioMessage::SubmitGraphRender { document_id, ignore_hash } => {
let inspect_node = self.inspect_node_id();
let result = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(&document_id).expect("Tried to render non-existent document"),
ipp.viewport_bounds.size().as_uvec2(),
inspect_node,
ignore_hash,
);
@ -1261,4 +1262,19 @@ impl PortfolioMessageHandler {
}
result
}
/// Get the id of the node that should be used as the target for the spreadsheet
pub fn inspect_node_id(&self) -> Option<NodeId> {
if !self.spreadsheet.spreadsheet_view_open {
warn!("Spreadsheet not open, skipping…");
return None;
}
let document = self.documents.get(&self.active_document_id?)?;
let selected_nodes = document.network_interface.selected_nodes().0;
if selected_nodes.len() != 1 {
warn!("selected nodes != 1, skipping…");
return None;
}
selected_nodes.first().copied()
}
}

View file

@ -0,0 +1,7 @@
mod spreadsheet_message;
mod spreadsheet_message_handler;
#[doc(inline)]
pub use spreadsheet_message::*;
#[doc(inline)]
pub use spreadsheet_message_handler::*;

View file

@ -0,0 +1,33 @@
use crate::messages::prelude::*;
use crate::node_graph_executor::InspectResult;
/// The spreadsheet UI allows for instance data to be previewed.
#[impl_message(Message, PortfolioMessage, Spreadsheet)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum SpreadsheetMessage {
ToggleOpen,
UpdateLayout {
#[serde(skip)]
inspect_result: InspectResult,
},
PushToInstancePath {
index: usize,
},
TruncateInstancePath {
len: usize,
},
ViewVectorDataDomain {
domain: VectorDataDomain,
},
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum VectorDataDomain {
#[default]
Points,
Segments,
Regions,
}

View file

@ -0,0 +1,281 @@
use super::VectorDataDomain;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout};
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::tool_prelude::*;
use graph_craft::document::NodeId;
use graphene_core::Context;
use graphene_core::GraphicGroupTable;
use graphene_core::instances::Instances;
use graphene_core::memo::IORecord;
use graphene_core::vector::{VectorData, VectorDataTable};
use graphene_core::{Artboard, ArtboardGroupTable, GraphicElement};
use std::any::Any;
use std::sync::Arc;
/// The spreadsheet UI allows for instance data to be previewed.
#[derive(Default, Debug, Clone)]
pub struct SpreadsheetMessageHandler {
/// Sets whether or not the spreadsheet is drawn.
pub spreadsheet_view_open: bool,
inspect_node: Option<NodeId>,
introspected_data: Option<Arc<dyn Any + Send + Sync>>,
instances_path: Vec<usize>,
viewing_vector_data_domain: VectorDataDomain,
}
impl MessageHandler<SpreadsheetMessage, ()> for SpreadsheetMessageHandler {
fn process_message(&mut self, message: SpreadsheetMessage, responses: &mut VecDeque<Message>, _data: ()) {
match message {
SpreadsheetMessage::ToggleOpen => {
self.spreadsheet_view_open = !self.spreadsheet_view_open;
// Run the graph to grab the data
if self.spreadsheet_view_open {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
// Update checked UI state for open
responses.add(MenuBarMessage::SendLayout);
self.update_layout(responses);
}
SpreadsheetMessage::UpdateLayout { inspect_result } => {
self.inspect_node = Some(inspect_result.inspect_node);
self.introspected_data = inspect_result.introspected_data;
self.update_layout(responses)
}
SpreadsheetMessage::PushToInstancePath { index } => {
self.instances_path.push(index);
self.update_layout(responses);
}
SpreadsheetMessage::TruncateInstancePath { len } => {
self.instances_path.truncate(len);
self.update_layout(responses);
}
SpreadsheetMessage::ViewVectorDataDomain { domain } => {
self.viewing_vector_data_domain = domain;
self.update_layout(responses);
}
}
}
fn actions(&self) -> ActionList {
actions!(SpreadsheetMessage;)
}
}
impl SpreadsheetMessageHandler {
fn update_layout(&mut self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateSpreadsheetState {
node: self.inspect_node,
open: self.spreadsheet_view_open,
});
if !self.spreadsheet_view_open {
return;
}
let mut layout_data = LayoutData {
current_depth: 0,
desired_path: &mut self.instances_path,
breadcrumbs: Vec::new(),
vector_data_domain: self.viewing_vector_data_domain,
};
let mut layout = self
.introspected_data
.as_ref()
.map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data))
.unwrap_or_else(|| Some(label("No data")))
.unwrap_or_else(|| label("Failed to downcast data"));
if layout_data.breadcrumbs.len() > 1 {
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
.on_update(|&len| SpreadsheetMessage::TruncateInstancePath { len: len as usize }.into())
.widget_holder();
layout.insert(0, LayoutGroup::Row { widgets: vec![breadcrumb] });
}
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout { layout }),
layout_target: LayoutTarget::Spreadsheet,
});
}
}
struct LayoutData<'a> {
current_depth: usize,
desired_path: &'a mut Vec<usize>,
breadcrumbs: Vec<String>,
vector_data_domain: VectorDataDomain,
}
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
// We simply try random types. TODO: better strategy.
#[allow(clippy::manual_map)]
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, ArtboardGroupTable>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), ArtboardGroupTable>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, VectorDataTable>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), VectorDataTable>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, GraphicGroupTable>>() {
Some(io.output.layout_with_breadcrumb(data))
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), GraphicGroupTable>>() {
Some(io.output.layout_with_breadcrumb(data))
} else {
None
}
}
fn column_headings(value: &[&str]) -> Vec<WidgetHolder> {
value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect()
}
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
let error = vec![TextLabel::new(x).widget_holder()];
vec![LayoutGroup::Row { widgets: error }]
}
trait InstanceLayout {
fn type_name() -> &'static str;
fn identifier(&self) -> String;
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
data.breadcrumbs.push(self.identifier());
self.compute_layout(data)
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup>;
}
impl InstanceLayout for GraphicElement {
fn type_name() -> &'static str {
"GraphicElement"
}
fn identifier(&self) -> String {
match self {
Self::GraphicGroup(instances) => instances.identifier(),
Self::VectorData(instances) => instances.identifier(),
Self::RasterFrame(_) => "RasterFrame".to_string(),
}
}
// Don't put a breadcrumb for GraphicElement
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.compute_layout(data)
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
match self {
Self::GraphicGroup(instances) => instances.layout_with_breadcrumb(data),
Self::VectorData(instances) => instances.layout_with_breadcrumb(data),
Self::RasterFrame(_) => label("Raster frame not supported"),
}
}
}
impl InstanceLayout for VectorData {
fn type_name() -> &'static str {
"VectorData"
}
fn identifier(&self) -> String {
format!("Vector Data (points={}, segments={})", self.point_domain.ids().len(), self.segment_domain.ids().len())
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
let mut rows = Vec::new();
match data.vector_data_domain {
VectorDataDomain::Points => {
rows.push(column_headings(&["", "position"]));
rows.extend(
self.point_domain
.iter()
.map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{}", position)).widget_holder()]),
);
}
VectorDataDomain::Segments => {
rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{}", start)).widget_holder(),
TextLabel::new(format!("{}", end)).widget_holder(),
TextLabel::new(format!("{:?}", handles)).widget_holder(),
]
}));
}
VectorDataDomain::Regions => {
rows.push(column_headings(&["", "segment_range", "fill"]));
rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{:?}", segment_range)).widget_holder(),
TextLabel::new(format!("{}", fill.inner())).widget_holder(),
]
}));
}
}
let entries = [VectorDataDomain::Points, VectorDataDomain::Segments, VectorDataDomain::Regions]
.into_iter()
.map(|domain| {
RadioEntryData::new(format!("{domain:?}"))
.label(format!("{domain:?}"))
.on_update(move |_| SpreadsheetMessage::ViewVectorDataDomain { domain }.into())
})
.collect();
let domain = vec![RadioInput::new(entries).selected_index(Some(data.vector_data_domain as u32)).widget_holder()];
vec![LayoutGroup::Row { widgets: domain }, LayoutGroup::Table { rows }]
}
}
impl InstanceLayout for Artboard {
fn type_name() -> &'static str {
"Artboard"
}
fn identifier(&self) -> String {
self.label.clone()
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.graphic_group.compute_layout(data)
}
}
impl<T: InstanceLayout> InstanceLayout for Instances<T> {
fn type_name() -> &'static str {
"Instances"
}
fn identifier(&self) -> String {
format!("Instances<{}> (length={})", T::type_name(), self.len())
}
fn compute_layout(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
if let Some(instance) = self.get(index) {
data.current_depth += 1;
let result = instance.instance.layout_with_breadcrumb(data);
data.current_depth -= 1;
return result;
} else {
warn!("Desired path truncated");
data.desired_path.truncate(data.current_depth);
}
}
let mut rows = self
.instances()
.enumerate()
.map(|(index, instance)| {
vec![
TextLabel::new(format!("{}", index)).widget_holder(),
TextButton::new(instance.instance.identifier())
.on_update(move |_| SpreadsheetMessage::PushToInstancePath { index }.into())
.widget_holder(),
TextLabel::new(format!("{}", instance.transform)).widget_holder(),
TextLabel::new(format!("{:?}", instance.alpha_blending)).widget_holder(),
TextLabel::new(instance.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
]
})
.collect::<Vec<_>>();
rows.insert(0, column_headings(&["", "instance", "transform", "alpha_blending", "source_node_id"]));
let instances = vec![TextLabel::new("Instances:").widget_holder()];
vec![LayoutGroup::Row { widgets: instances }, LayoutGroup::Table { rows }]
}
}

View file

@ -45,6 +45,7 @@ pub enum PanelType {
Document,
Layers,
Properties,
Spreadsheet,
}
impl From<String> for PanelType {
@ -53,6 +54,7 @@ impl From<String> for PanelType {
"Document" => PanelType::Document,
"Layers" => PanelType::Layers,
"Properties" => PanelType::Properties,
"Spreadsheet" => PanelType::Spreadsheet,
_ => panic!("Unknown panel type: {}", value),
}
}

View file

@ -20,7 +20,8 @@ pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, Nod
pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, OverlaysMessageData, OverlaysMessageDiscriminant, OverlaysMessageHandler};
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageData, DocumentMessageDiscriminant, DocumentMessageHandler};
pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageData, MenuBarMessageDiscriminant, MenuBarMessageHandler};
pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler};
pub use crate::messages::portfolio::spreadsheet::{SpreadsheetMessage, SpreadsheetMessageDiscriminant};
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageData, PortfolioMessageDiscriminant, PortfolioMessageHandler};
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};

View file

@ -41,6 +41,9 @@ pub struct NodeRuntime {
node_graph_errors: GraphErrors,
monitor_nodes: Vec<Vec<NodeId>>,
/// Which node is inspected and which monitor node is used (if any) for the current execution
inspect_state: Option<InspectState>,
// TODO: Remove, it doesn't need to be persisted anymore
/// The current renders of the thumbnails for layer nodes.
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
@ -49,7 +52,7 @@ pub struct NodeRuntime {
/// Messages passed from the editor thread to the node runtime thread.
pub enum NodeRuntimeMessage {
GraphUpdate(NodeNetwork),
GraphUpdate(GraphUpdate),
ExecutionRequest(ExecutionRequest),
FontCacheUpdate(FontCache),
EditorPreferencesUpdate(EditorPreferences),
@ -65,6 +68,12 @@ pub struct ExportConfig {
pub size: DVec2,
}
pub struct GraphUpdate {
network: NodeNetwork,
/// The node that should be temporary inspected during execution
inspect_node: Option<NodeId>,
}
pub struct ExecutionRequest {
execution_id: u64,
render_config: RenderConfig,
@ -76,6 +85,8 @@ pub struct ExecutionResponse {
responses: VecDeque<FrontendMessage>,
transform: DAffine2,
vector_modify: HashMap<NodeId, VectorData>,
/// The resulting value from the temporary inspected during execution
inspect_result: Option<InspectResult>,
}
pub struct CompilationResponse {
@ -132,6 +143,8 @@ impl NodeRuntime {
node_graph_errors: Vec::new(),
monitor_nodes: Vec::new(),
inspect_state: None,
thumbnail_renders: Default::default(),
vector_modify: Default::default(),
}
@ -191,10 +204,13 @@ impl NodeRuntime {
let _ = self.update_network(graph).await;
}
}
NodeRuntimeMessage::GraphUpdate(graph) => {
self.old_graph = Some(graph.clone());
NodeRuntimeMessage::GraphUpdate(GraphUpdate { mut network, inspect_node }) => {
// Insert the monitor node to manage the inspection
self.inspect_state = inspect_node.map(|inspect| InspectState::monitor_inspect_node(&mut network, inspect));
self.old_graph = Some(network.clone());
self.node_graph_errors.clear();
let result = self.update_network(graph).await;
let result = self.update_network(network).await;
self.update_thumbnails = true;
self.sender.send_generation_response(CompilationResponse {
result,
@ -210,12 +226,16 @@ impl NodeRuntime {
self.process_monitor_nodes(&mut responses, self.update_thumbnails);
self.update_thumbnails = false;
// Resolve the result from the inspection by accessing the monitor node
let inspect_result = self.inspect_state.and_then(|state| state.access(&self.executor));
self.sender.send_execution_response(ExecutionResponse {
execution_id,
result,
responses,
transform,
vector_modify: self.vector_modify.clone(),
inspect_result,
});
}
}
@ -269,6 +289,10 @@ impl NodeRuntime {
self.thumbnail_renders.retain(|id, _| self.monitor_nodes.iter().any(|monitor_node_path| monitor_node_path.contains(id)));
for monitor_node_path in &self.monitor_nodes {
// Skip the inspect monitor node
if self.inspect_state.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) {
continue;
}
// The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID
let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else {
warn!("Monitor node has invalid node id");
@ -371,6 +395,70 @@ pub struct NodeGraphExecutor {
receiver: Receiver<NodeGraphUpdate>,
futures: HashMap<u64, ExecutionContext>,
node_graph_hash: u64,
old_inspect_node: Option<NodeId>,
}
/// Which node is inspected and which monitor node is used (if any) for the current execution
#[derive(Debug, Clone, Copy)]
struct InspectState {
inspect_node: NodeId,
monitor_node: NodeId,
}
/// The resulting value from the temporary inspected during execution
#[derive(Clone, Debug, Default)]
pub struct InspectResult {
pub introspected_data: Option<Arc<dyn std::any::Any + Send + Sync + 'static>>,
pub inspect_node: NodeId,
}
// This is very ugly but is required to be inside a message
impl PartialEq for InspectResult {
fn eq(&self, other: &Self) -> bool {
self.inspect_node == other.inspect_node
}
}
impl InspectState {
/// Insert the monitor node to manage the inspection
pub fn monitor_inspect_node(network: &mut NodeNetwork, inspect_node: NodeId) -> Self {
let monitor_id = NodeId::new();
// It is necessary to replace the inputs before inserting the monitor node to avoid changing the input of the new monitor node
for input in network.nodes.values_mut().flat_map(|node| node.inputs.iter_mut()).chain(&mut network.exports) {
let NodeInput::Node { node_id, output_index, .. } = input else { continue };
// We only care about the primary output of our inspect node
if *output_index != 0 || *node_id != inspect_node {
continue;
}
*node_id = monitor_id;
}
let monitor_node = DocumentNode {
inputs: vec![NodeInput::node(inspect_node, 0)], // Connect to the primary output of the inspect node
implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"),
manual_composition: Some(graph_craft::generic!(T)),
skip_deduplication: true,
..Default::default()
};
network.nodes.insert(monitor_id, monitor_node);
Self {
inspect_node,
monitor_node: monitor_id,
}
}
/// Resolve the result from the inspection by accessing the monitor node
fn access(&self, executor: &DynamicExecutor) -> Option<InspectResult> {
let introspected_data = executor.introspect(&[self.monitor_node]).inspect_err(|e| warn!("Failed to introspect monitor node {e}")).ok();
Some(InspectResult {
inspect_node: self.inspect_node,
introspected_data,
})
}
}
#[derive(Debug, Clone)]
@ -389,6 +477,7 @@ impl Default for NodeGraphExecutor {
sender: request_sender,
receiver: response_receiver,
node_graph_hash: 0,
old_inspect_node: None,
}
}
}
@ -406,6 +495,7 @@ impl NodeGraphExecutor {
sender: request_sender,
receiver: response_receiver,
node_graph_hash: 0,
old_inspect_node: None,
};
(node_runtime, node_executor)
}
@ -461,18 +551,22 @@ impl NodeGraphExecutor {
let mut network = document.network_interface.document_network().clone();
let instrumented = Instrumented::new(&mut network);
self.sender.send(NodeRuntimeMessage::GraphUpdate(network)).map_err(|e| e.to_string())?;
self.sender
.send(NodeRuntimeMessage::GraphUpdate(GraphUpdate { network, inspect_node: None }))
.map_err(|e| e.to_string())?;
Ok(instrumented)
}
/// Update the cached network if necessary.
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, ignore_hash: bool) -> Result<(), String> {
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, inspect_node: Option<NodeId>, ignore_hash: bool) -> Result<(), String> {
let network_hash = document.network_interface.document_network().current_hash();
if network_hash != self.node_graph_hash || ignore_hash {
// Refresh the graph when it changes or the inspect node changes
if network_hash != self.node_graph_hash || self.old_inspect_node != inspect_node || ignore_hash {
let network = document.network_interface.document_network().clone();
self.old_inspect_node = inspect_node;
self.node_graph_hash = network_hash;
self.sender
.send(NodeRuntimeMessage::GraphUpdate(document.network_interface.document_network().clone()))
.map_err(|e| e.to_string())?;
self.sender.send(NodeRuntimeMessage::GraphUpdate(GraphUpdate { network, inspect_node })).map_err(|e| e.to_string())?;
}
Ok(())
}
@ -502,8 +596,8 @@ impl NodeGraphExecutor {
}
/// Evaluates a node graph, computing the entire graph
pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, ignore_hash: bool) -> Result<(), String> {
self.update_node_graph(document, ignore_hash)?;
pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, inspect_node: Option<NodeId>, ignore_hash: bool) -> Result<(), String> {
self.update_node_graph(document, inspect_node, ignore_hash)?;
self.submit_current_node_graph_evaluation(document, viewport_resolution)?;
Ok(())
@ -537,7 +631,9 @@ impl NodeGraphExecutor {
export_config.size = size;
// Execute the node graph
self.sender.send(NodeRuntimeMessage::GraphUpdate(network)).map_err(|e| e.to_string())?;
self.sender
.send(NodeRuntimeMessage::GraphUpdate(GraphUpdate { network, inspect_node: None }))
.map_err(|e| e.to_string())?;
let execution_id = self.queue_execution(render_config);
let execution_context = ExecutionContext { export_config: Some(export_config) };
self.futures.insert(execution_id, execution_context);
@ -589,6 +685,7 @@ impl NodeGraphExecutor {
responses: existing_responses,
transform,
vector_modify,
inspect_result,
} = execution_response;
responses.add(OverlaysMessage::Draw);
@ -613,6 +710,13 @@ impl NodeGraphExecutor {
} else {
self.process_node_graph_output(node_graph_output, transform, responses)?
}
// Update the spreadsheet on the frontend using the value of the inspect result.
if self.old_inspect_node.is_some() {
if let Some(inspect_result) = inspect_result {
responses.add(SpreadsheetMessage::UpdateLayout { inspect_result });
}
}
}
NodeGraphUpdate::CompilationResponse(execution_response) => {
let CompilationResponse { node_graph_errors, result } = execution_response;