diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 7e1eaa9512..d5988bbf74 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -150,6 +150,15 @@ pub enum FrontendMessage { UpdateGraphViewOverlay { open: bool, }, + UpdateSpreadsheetState { + open: bool, + node: Option, + }, + UpdateSpreadsheetLayout { + #[serde(rename = "layoutTarget")] + layout_target: LayoutTarget, + diff: Vec, + }, UpdateImportReorderIndex { #[serde(rename = "importIndex")] index: Option, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 88b4c7c5d7..a35861d680 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -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 }, diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index c21c61fea6..b1d9550b02 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -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 { - 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 { - 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, }, + #[serde(rename = "table")] + Table { + #[serde(rename = "tableWidgets")] + rows: Vec>, + }, // 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() } } } diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index a8fb69e57a..a0f32139b7 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -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 for MenuBarMessageHandler { - fn process_message(&mut self, message: MenuBarMessage, responses: &mut VecDeque, 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 for MenuBarMessageHandler { + fn process_message(&mut self, message: MenuBarMessage, responses: &mut VecDeque, _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( diff --git a/editor/src/messages/portfolio/menu_bar/mod.rs b/editor/src/messages/portfolio/menu_bar/mod.rs index ef7ec558a5..27e741b208 100644 --- a/editor/src/messages/portfolio/menu_bar/mod.rs +++ b/editor/src/messages/portfolio/menu_bar/mod.rs @@ -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; diff --git a/editor/src/messages/portfolio/mod.rs b/editor/src/messages/portfolio/mod.rs index 689971d7d8..39b25404df 100644 --- a/editor/src/messages/portfolio/mod.rs +++ b/editor/src/messages/portfolio/mod.rs @@ -3,6 +3,7 @@ mod portfolio_message_handler; pub mod document; pub mod menu_bar; +pub mod spreadsheet; pub mod utility_types; #[doc(inline)] diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 4bdf98089c..5a2ca18c85 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -15,6 +15,8 @@ pub enum PortfolioMessage { MenuBar(MenuBarMessage), #[child] Document(DocumentMessage), + #[child] + Spreadsheet(SpreadsheetMessage), // Messages DocumentPassMessage { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 5dec445b3a..9b46bb9753 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -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> for PortfolioMessageHandler { @@ -58,38 +61,32 @@ impl MessageHandler> 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> 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> 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 { + 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() + } } diff --git a/editor/src/messages/portfolio/spreadsheet/mod.rs b/editor/src/messages/portfolio/spreadsheet/mod.rs new file mode 100644 index 0000000000..1523ea0d2c --- /dev/null +++ b/editor/src/messages/portfolio/spreadsheet/mod.rs @@ -0,0 +1,7 @@ +mod spreadsheet_message; +mod spreadsheet_message_handler; + +#[doc(inline)] +pub use spreadsheet_message::*; +#[doc(inline)] +pub use spreadsheet_message_handler::*; diff --git a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message.rs b/editor/src/messages/portfolio/spreadsheet/spreadsheet_message.rs new file mode 100644 index 0000000000..af579cc17f --- /dev/null +++ b/editor/src/messages/portfolio/spreadsheet/spreadsheet_message.rs @@ -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, +} diff --git a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs b/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs new file mode 100644 index 0000000000..d4f6d214d2 --- /dev/null +++ b/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs @@ -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, + introspected_data: Option>, + instances_path: Vec, + viewing_vector_data_domain: VectorDataDomain, +} + +impl MessageHandler for SpreadsheetMessageHandler { + fn process_message(&mut self, message: SpreadsheetMessage, responses: &mut VecDeque, _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) { + 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, + breadcrumbs: Vec, + vector_data_domain: VectorDataDomain, +} + +fn generate_layout(introspected_data: &Arc, data: &mut LayoutData) -> Option> { + // We simply try random types. TODO: better strategy. + #[allow(clippy::manual_map)] + if let Some(io) = introspected_data.downcast_ref::>() { + Some(io.output.layout_with_breadcrumb(data)) + } else if let Some(io) = introspected_data.downcast_ref::>() { + Some(io.output.layout_with_breadcrumb(data)) + } else if let Some(io) = introspected_data.downcast_ref::>() { + Some(io.output.layout_with_breadcrumb(data)) + } else if let Some(io) = introspected_data.downcast_ref::>() { + Some(io.output.layout_with_breadcrumb(data)) + } else if let Some(io) = introspected_data.downcast_ref::>() { + Some(io.output.layout_with_breadcrumb(data)) + } else if let Some(io) = introspected_data.downcast_ref::>() { + Some(io.output.layout_with_breadcrumb(data)) + } else { + None + } +} + +fn column_headings(value: &[&str]) -> Vec { + value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect() +} + +fn label(x: impl Into) -> Vec { + 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 { + data.breadcrumbs.push(self.identifier()); + self.compute_layout(data) + } + fn compute_layout(&self, data: &mut LayoutData) -> Vec; +} + +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 { + self.compute_layout(data) + } + fn compute_layout(&self, data: &mut LayoutData) -> Vec { + 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 { + 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 { + self.graphic_group.compute_layout(data) + } +} + +impl InstanceLayout for Instances { + 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 { + 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::>(); + + 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 }] + } +} diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index 73203dbaca..bbb8424070 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -45,6 +45,7 @@ pub enum PanelType { Document, Layers, Properties, + Spreadsheet, } impl From for PanelType { @@ -53,6 +54,7 @@ impl From for PanelType { "Document" => PanelType::Document, "Layers" => PanelType::Layers, "Properties" => PanelType::Properties, + "Spreadsheet" => PanelType::Spreadsheet, _ => panic!("Unknown panel type: {}", value), } } diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 9218e06df7..0fa4ca600a 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -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}; diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 32e22276a9..f97ed3c87f 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -41,6 +41,9 @@ pub struct NodeRuntime { node_graph_errors: GraphErrors, monitor_nodes: Vec>, + /// Which node is inspected and which monitor node is used (if any) for the current execution + inspect_state: Option, + // TODO: Remove, it doesn't need to be persisted anymore /// The current renders of the thumbnails for layer nodes. thumbnail_renders: HashMap>, @@ -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, +} + pub struct ExecutionRequest { execution_id: u64, render_config: RenderConfig, @@ -76,6 +85,8 @@ pub struct ExecutionResponse { responses: VecDeque, transform: DAffine2, vector_modify: HashMap, + /// The resulting value from the temporary inspected during execution + inspect_result: Option, } 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, futures: HashMap, node_graph_hash: u64, + old_inspect_node: Option, +} + +/// 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>, + 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 { + 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, 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, 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; diff --git a/frontend/src/components/panels/Spreadsheet.svelte b/frontend/src/components/panels/Spreadsheet.svelte new file mode 100644 index 0000000000..9d54f7df63 --- /dev/null +++ b/frontend/src/components/panels/Spreadsheet.svelte @@ -0,0 +1,50 @@ + + + + + Spreadsheet Data for Node ID {$portfolio.spreadsheetNode}: + + + + + + + diff --git a/frontend/src/components/widgets/WidgetLayout.svelte b/frontend/src/components/widgets/WidgetLayout.svelte index c0bf84c20f..970680a01f 100644 --- a/frontend/src/components/widgets/WidgetLayout.svelte +++ b/frontend/src/components/widgets/WidgetLayout.svelte @@ -1,8 +1,9 @@ + + + {#each widgetData.tableWidgets as row} + + {#each row as cell} + + {/each} + + {/each} +
+ +
+ + diff --git a/frontend/src/components/window/workspace/Panel.svelte b/frontend/src/components/window/workspace/Panel.svelte index ea99294c55..b4b43cb8ce 100644 --- a/frontend/src/components/window/workspace/Panel.svelte +++ b/frontend/src/components/window/workspace/Panel.svelte @@ -2,11 +2,13 @@ import Document from "@graphite/components/panels/Document.svelte"; import Layers from "@graphite/components/panels/Layers.svelte"; import Properties from "@graphite/components/panels/Properties.svelte"; + import Spreadsheet from "@graphite/components/panels/Spreadsheet.svelte"; const PANEL_COMPONENTS = { Document, Layers, Properties, + Spreadsheet, }; type PanelType = keyof typeof PANEL_COMPONENTS; diff --git a/frontend/src/components/window/workspace/Workspace.svelte b/frontend/src/components/window/workspace/Workspace.svelte index 8123b27cf7..4d22dd5634 100644 --- a/frontend/src/components/window/workspace/Workspace.svelte +++ b/frontend/src/components/window/workspace/Workspace.svelte @@ -15,7 +15,8 @@ const PANEL_SIZES = { /**/ root: 100, /* ├─ */ content: 80, - /* │ └─ */ document: 100, + /* │ ├─ */ document: 70, + /* │ └─ */ spreadsheet: 30, /* └─ */ details: 20, /* ├─ */ properties: 45, /* └─ */ layers: 55, @@ -146,6 +147,12 @@ bind:this={documentPanel} /> + {#if $portfolio.spreadsheetOpen} + resizePanel(e)} /> + + + + {/if} resizePanel(e)} /> diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 6f3ee10b20..cf9b2f3e13 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -771,6 +771,12 @@ export class UpdateGraphFadeArtwork extends JsMessage { readonly percentage!: number; } +export class UpdateSpreadsheetState extends JsMessage { + readonly open!: boolean; + + readonly node!: bigint | undefined; +} + export class UpdateMouseCursor extends JsMessage { @Transform(({ value }: { value: MouseCursor }) => mouseCursorIconCSSNames[value] || "alias") readonly cursor!: MouseCursorIcon; @@ -1432,7 +1438,7 @@ export class WidgetDiffUpdate extends JsMessage { diff!: WidgetDiff[]; } -type UIItem = LayoutGroup[] | LayoutGroup | Widget | MenuBarEntry[] | MenuBarEntry; +type UIItem = LayoutGroup[] | LayoutGroup | Widget | Widget[] | MenuBarEntry[] | MenuBarEntry; type WidgetDiff = { widgetPath: number[]; newValue: UIItem }; export function defaultWidgetLayout(): WidgetLayout { @@ -1451,6 +1457,7 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg const diffObject = update.widgetPath.reduce((targetLayout, index) => { if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index]; if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index]; + if ("tableWidgets" in targetLayout) return targetLayout.tableWidgets[index]; if ("layout" in targetLayout) return targetLayout.layout[index]; if (targetLayout instanceof Widget) { if (targetLayout.props.kind === "PopoverButton" && targetLayout.props instanceof PopoverButton && targetLayout.props.popoverLayout) { @@ -1481,7 +1488,7 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg }); } -export type LayoutGroup = WidgetSpanRow | WidgetSpanColumn | WidgetSection; +export type LayoutGroup = WidgetSpanRow | WidgetSpanColumn | WidgetTable | WidgetSection; export type WidgetSpanColumn = { columnWidgets: Widget[] }; export function isWidgetSpanColumn(layoutColumn: LayoutGroup): layoutColumn is WidgetSpanColumn { @@ -1493,6 +1500,11 @@ export function isWidgetSpanRow(layoutRow: LayoutGroup): layoutRow is WidgetSpan return Boolean((layoutRow as WidgetSpanRow)?.rowWidgets); } +export type WidgetTable = { tableWidgets: Widget[][] }; +export function isWidgetTable(layoutTable: LayoutGroup): layoutTable is WidgetTable { + return Boolean((layoutTable as WidgetTable)?.tableWidgets); +} + export type WidgetSection = { name: string; visible: boolean; pinned: boolean; id: bigint; layout: LayoutGroup[] }; export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSection { return Boolean((layoutRow as WidgetSection)?.layout); @@ -1543,6 +1555,13 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup { return result; } + if (layoutGroup.table) { + const result: WidgetTable = { + tableWidgets: layoutGroup.table.tableWidgets.map(hoistWidgetHolders), + }; + return result; + } + throw new Error("Layout row type does not exist"); } @@ -1573,6 +1592,8 @@ export class UpdateNodeGraphControlBarLayout extends WidgetDiffUpdate {} export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {} +export class UpdateSpreadsheetLayout extends WidgetDiffUpdate {} + export class UpdateToolOptionsLayout extends WidgetDiffUpdate {} export class UpdateToolShelfLayout extends WidgetDiffUpdate {} @@ -1649,6 +1670,7 @@ export const messageMakers: Record = { UpdateEyedropperSamplingState, UpdateGraphFadeArtwork, UpdateGraphViewOverlay, + UpdateSpreadsheetState, UpdateImportReorderIndex, UpdateImportsExports, UpdateInputHints, @@ -1664,6 +1686,7 @@ export const messageMakers: Record = { UpdateNodeThumbnail, UpdateOpenDocumentsList, UpdatePropertyPanelSectionsLayout, + UpdateSpreadsheetLayout, UpdateToolOptionsLayout, UpdateToolShelfLayout, UpdateWirePathInProgress, diff --git a/frontend/src/state-providers/portfolio.ts b/frontend/src/state-providers/portfolio.ts index 1518e9f32a..1cf63a2811 100644 --- a/frontend/src/state-providers/portfolio.ts +++ b/frontend/src/state-providers/portfolio.ts @@ -13,6 +13,10 @@ import { TriggerUpgradeDocumentToVectorManipulationFormat, UpdateActiveDocument, UpdateOpenDocumentsList, + UpdateSpreadsheetState, + defaultWidgetLayout, + patchWidgetLayout, + UpdateSpreadsheetLayout, } from "@graphite/messages"; import { downloadFileText, downloadFileBlob, upload } from "@graphite/utility-functions/files"; import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization"; @@ -23,6 +27,9 @@ export function createPortfolioState(editor: Editor) { unsaved: false, documents: [] as FrontendDocumentDetails[], activeDocumentIndex: 0, + spreadsheetOpen: false, + spreadsheetNode: BigInt(0) as bigint | undefined, + spreadsheetWidgets: defaultWidgetLayout(), }); // Set up message subscriptions on creation @@ -103,6 +110,21 @@ export function createPortfolioState(editor: Editor) { editor.handle.triggerUpgradeDocumentToVectorManipulationFormat(documentId, documentName, documentIsAutoSaved, documentIsSaved, documentSerializedContent); }); + editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetState, async (updateSpreadsheetState) => { + update((state) => { + state.spreadsheetOpen = updateSpreadsheetState.open; + state.spreadsheetNode = updateSpreadsheetState.node; + return state; + }); + }); + + editor.subscriptions.subscribeJsMessage(UpdateSpreadsheetLayout, (updateSpreadsheetLayout) => { + update((state) => { + patchWidgetLayout(state.spreadsheetWidgets, updateSpreadsheetLayout); + return state; + }); + }); + return { subscribe, }; diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 8bc7b6c3a4..e23822fc5a 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -826,7 +826,7 @@ impl EditorHandle { let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler; portfolio .executor - .submit_node_graph_evaluation(portfolio.documents.get_mut(&portfolio.active_document_id().unwrap()).unwrap(), glam::UVec2::ONE, true) + .submit_node_graph_evaluation(portfolio.documents.get_mut(&portfolio.active_document_id().unwrap()).unwrap(), glam::UVec2::ONE, None, true) .unwrap(); editor::node_graph_executor::run_node_graph().await; diff --git a/node-graph/gcore/src/instances.rs b/node-graph/gcore/src/instances.rs index c11e296273..fe456554ab 100644 --- a/node-graph/gcore/src/instances.rs +++ b/node-graph/gcore/src/instances.rs @@ -3,7 +3,7 @@ use crate::raster::Pixel; use crate::raster::image::{Image, ImageFrameTable}; use crate::transform::{Transform, TransformMut}; use crate::uuid::NodeId; -use crate::vector::{InstanceId, VectorDataTable}; +use crate::vector::VectorDataTable; use crate::{AlphaBlending, GraphicElement, RasterFrame}; use dyn_any::StaticType; use glam::DAffine2; @@ -11,7 +11,6 @@ use std::hash::Hash; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Instances { - id: Vec, #[serde(alias = "instances")] instance: Vec, #[serde(default = "one_daffine2_default")] @@ -25,7 +24,6 @@ pub struct Instances { impl Instances { pub fn new(instance: T) -> Self { Self { - id: vec![InstanceId::generate()], instance: vec![instance], transform: vec![DAffine2::IDENTITY], alpha_blending: vec![AlphaBlending::default()], @@ -35,7 +33,6 @@ impl Instances { pub fn empty() -> Self { Self { - id: Vec::new(), instance: Vec::new(), transform: Vec::new(), alpha_blending: Vec::new(), @@ -44,14 +41,12 @@ impl Instances { } pub fn push(&mut self, instance: T) -> InstanceMut { - self.id.push(InstanceId::generate()); self.instance.push(instance); self.transform.push(DAffine2::IDENTITY); self.alpha_blending.push(AlphaBlending::default()); self.source_node_id.push(None); InstanceMut { - id: self.id.last_mut().expect("Shouldn't be empty"), instance: self.instance.last_mut().expect("Shouldn't be empty"), transform: self.transform.last_mut().expect("Shouldn't be empty"), alpha_blending: self.alpha_blending.last_mut().expect("Shouldn't be empty"), @@ -63,14 +58,12 @@ impl Instances { where T: Clone, { - self.id.push(*instance.id); self.instance.push(instance.instance.clone()); self.transform.push(*instance.transform); self.alpha_blending.push(*instance.alpha_blending); self.source_node_id.push(*instance.source_node_id); InstanceMut { - id: self.id.last_mut().expect("Shouldn't be empty"), instance: self.instance.last_mut().expect("Shouldn't be empty"), transform: self.transform.last_mut().expect("Shouldn't be empty"), alpha_blending: self.alpha_blending.last_mut().expect("Shouldn't be empty"), @@ -80,7 +73,6 @@ impl Instances { pub fn one_instance(&self) -> Instance { Instance { - id: self.id.first().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", self.instance.len())), instance: self.instance.first().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", self.instance.len())), transform: self.transform.first().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", self.instance.len())), alpha_blending: self.alpha_blending.first().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", self.instance.len())), @@ -92,7 +84,6 @@ impl Instances { let length = self.instance.len(); InstanceMut { - id: self.id.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", length)), instance: self.instance.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", length)), transform: self.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", length)), alpha_blending: self.alpha_blending.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", length)), @@ -101,14 +92,12 @@ impl Instances { } pub fn instances(&self) -> impl DoubleEndedIterator> { - self.id + self.instance .iter() - .zip(self.instance.iter()) .zip(self.transform.iter()) .zip(self.alpha_blending.iter()) .zip(self.source_node_id.iter()) - .map(|((((id, instance), transform), alpha_blending), source_node_id)| Instance { - id, + .map(|(((instance, transform), alpha_blending), source_node_id)| Instance { instance, transform, alpha_blending, @@ -117,20 +106,52 @@ impl Instances { } pub fn instances_mut(&mut self) -> impl DoubleEndedIterator> { - self.id + self.instance .iter_mut() - .zip(self.instance.iter_mut()) .zip(self.transform.iter_mut()) .zip(self.alpha_blending.iter_mut()) .zip(self.source_node_id.iter_mut()) - .map(|((((id, instance), transform), alpha_blending), source_node_id)| InstanceMut { - id, + .map(|(((instance, transform), alpha_blending), source_node_id)| InstanceMut { instance, transform, alpha_blending, source_node_id, }) } + + pub fn get(&self, index: usize) -> Option> { + if index >= self.instance.len() { + return None; + } + + Some(Instance { + instance: &self.instance[index], + transform: &self.transform[index], + alpha_blending: &self.alpha_blending[index], + source_node_id: &self.source_node_id[index], + }) + } + + pub fn get_mut(&mut self, index: usize) -> Option> { + if index >= self.instance.len() { + return None; + } + + Some(InstanceMut { + instance: &mut self.instance[index], + transform: &mut self.transform[index], + alpha_blending: &mut self.alpha_blending[index], + source_node_id: &mut self.source_node_id[index], + }) + } + + pub fn len(&self) -> usize { + self.instance.len() + } + + pub fn is_empty(&self) -> bool { + self.instance.is_empty() + } } impl Default for Instances { @@ -150,7 +171,6 @@ impl Default for Instances { impl core::hash::Hash for Instances { fn hash(&self, state: &mut H) { - self.id.hash(state); for instance in &self.instance { instance.hash(state); } @@ -159,7 +179,7 @@ impl core::hash::Hash for Instances { impl PartialEq for Instances { fn eq(&self, other: &Self) -> bool { - self.id == other.id && self.instance.len() == other.instance.len() && { self.instance.iter().zip(other.instance.iter()).all(|(a, b)| a == b) } + self.instance.len() == other.instance.len() && { self.instance.iter().zip(other.instance.iter()).all(|(a, b)| a == b) } } } @@ -180,7 +200,6 @@ fn one_source_node_id_default() -> Vec> { #[derive(Copy, Clone, Debug)] pub struct Instance<'a, T> { - pub id: &'a InstanceId, pub instance: &'a T, pub transform: &'a DAffine2, pub alpha_blending: &'a AlphaBlending, @@ -188,7 +207,6 @@ pub struct Instance<'a, T> { } #[derive(Debug)] pub struct InstanceMut<'a, T> { - pub id: &'a mut InstanceId, pub instance: &'a mut T, pub transform: &'a mut DAffine2, pub alpha_blending: &'a mut AlphaBlending, diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index bb7ed2ae88..3b84eb8193 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -410,7 +410,7 @@ impl SegmentDomain { /// Iterates over segments in the domain. /// /// Tuple is: (id, start point, end point, handles) - pub(crate) fn iter(&self) -> impl Iterator + '_ { + pub fn iter(&self) -> impl Iterator + '_ { let ids = self.id.iter().copied(); let start_point = self.start_point.iter().copied(); let end_point = self.end_point.iter().copied(); @@ -532,6 +532,16 @@ impl RegionDomain { .iter_mut() .for_each(|range| *range = *id_map.segment_map.get(range.start()).unwrap_or(range.start())..=*id_map.segment_map.get(range.end()).unwrap_or(range.end())); } + + /// Iterates over regions in the domain. + /// + /// Tuple is: (id, segment_range, fill) + pub fn iter(&self) -> impl Iterator, FillId)> + '_ { + let ids = self.id.iter().copied(); + let segment_range = self.segment_range.iter().cloned(); + let fill = self.fill.iter().copied(); + zip(ids, zip(segment_range, fill)).map(|(id, (segment_range, fill))| (id, segment_range, fill)) + } } impl VectorData {